BililiveRecorder/BililiveRecorder.Core/Scripting/Runtime/JintFetchSync.cs

259 lines
9.9 KiB
C#

using System;
using System.Net;
using System.Net.Http;
using System.Text;
using Jint;
using Jint.Native;
using Jint.Native.Array;
using Jint.Native.Function;
using Jint.Native.Object;
using Jint.Runtime;
namespace BililiveRecorder.Core.Scripting.Runtime
{
internal class JintFetchSync : FunctionInstance
{
private static readonly JsString functionName = new JsString("fetchSync");
public JintFetchSync(Engine engine) : base(engine, engine.Realm, functionName)
{
}
protected override JsValue Call(JsValue thisObject, JsValue[] arguments)
{
if (arguments.Length == 0)
throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, "1 argument required, but only 0 present.");
if (arguments[0] is not JsString urlString)
throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, "Only url string is supported as the 1st argument.");
ObjectInstance? initObject = null;
if (arguments.Length > 1)
initObject = arguments[1] is not ObjectInstance arg1
? throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, "The provided value is not of type 'RequestInit'.")
: arg1;
try
{
return this.Run(urlString, initObject);
}
catch (Exception ex)
{
var b = new StringBuilder("Request failed: ");
this.FormatClrException(b, ex);
throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, b.ToString());
}
}
private void FormatClrException(StringBuilder b, Exception ex)
{
start:
if (ex is AggregateException ae)
{
if (ae.InnerExceptions.Count == 0)
{
goto treatAsNormalException;
}
if (ae.InnerExceptions.Count == 1)
{
// the following is equivalent of calling
// this.FormatClrException(b, ae.InnerExceptions[0]); return;
ex = ae.InnerExceptions[0];
goto start;
}
// there are at least 2 exceptions
b.Append(ae.Message);
b.Append(": [");
this.FormatClrException(b, ae.InnerExceptions[0]);
for (var i = 1; i < ae.InnerExceptions.Count; i++)
{
b.Append(',');
this.FormatClrException(b, ae.InnerExceptions[i]);
}
b.Append(']');
return;
}
treatAsNormalException:
b.Append(ex.Message);
if (ex.InnerException != null)
{
b.Append(" (");
this.FormatClrException(b, ex.InnerException);
b.Append(')');
}
}
private JsObject Run(JsString urlString, ObjectInstance? initObject)
{
var handler = new HttpClientHandler
{
UseCookies = false,
UseDefaultCredentials = false,
UseProxy = false,
};
var httpClient = new HttpClient(handler);
var requestMessage = new HttpRequestMessage(HttpMethod.Get, urlString.ToString());
var throwOnRedirect = false;
if (initObject is not null)
{
foreach (var kv in initObject.GetOwnProperties())
{
var key = kv.Key;
var value = kv.Value;
if (!key.IsString())
continue;
switch (key.AsString())
{
case "body":
this.SetRequestBody(requestMessage, value.Value);
break;
case "headers":
this.SetRequestHeader(requestMessage, value.Value);
break;
case "method":
this.SetRequestMethod(requestMessage, value.Value);
break;
case "redirect":
{
var redirect = value.Value;
if (redirect is JsNull or JsUndefined)
break;
switch (redirect.ToString())
{
case "follow":
handler.AllowAutoRedirect = true;
break;
case "manual":
handler.AllowAutoRedirect = false;
break;
case "error":
handler.AllowAutoRedirect = false;
throwOnRedirect = true;
break;
default:
throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, $"'{redirect}' is not a valid value for 'redirect'.");
}
break;
}
case "referrer":
{
var referrer = value.Value;
if (referrer is JsNull or JsUndefined)
break;
requestMessage.Headers.Referrer = new System.Uri(referrer.ToString());
break;
}
case "cache":
case "credentials":
case "integrity":
case "keepalive":
case "mode":
case "referrerPolicy":
case "signal":
default:
break;
}
}
}
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
var resp = httpClient.SendAsync(requestMessage).Result;
if (throwOnRedirect && (resp.StatusCode is (HttpStatusCode)301 or (HttpStatusCode)302 or (HttpStatusCode)303 or (HttpStatusCode)307 or (HttpStatusCode)308))
{
throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, $"'Failed to fetch, Status code: {(int)resp.StatusCode}.");
}
var respString = resp.Content.ReadAsStringAsync().Result;
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
var respHeaders = new JsObject(this._engine);
foreach (var respHeader in resp.Headers)
respHeaders.Set(respHeader.Key, string.Join(", ", respHeader.Value));
var result = new JsObject(this._engine);
result.Set("body", respString);
result.Set("headers", respHeaders);
result.Set("ok", resp.IsSuccessStatusCode);
result.Set("status", (int)resp.StatusCode);
result.Set("statusText", resp.ReasonPhrase);
return result;
}
private void SetRequestMethod(HttpRequestMessage requestMessage, JsValue value)
{
if (value is JsNull or JsUndefined)
return;
var method = value.ToString();
requestMessage.Method = method.ToUpperInvariant() switch
{
"HEAD" => HttpMethod.Head,
"GET" => HttpMethod.Get,
"POST" => HttpMethod.Post,
"PUT" => HttpMethod.Put,
"DELETE" => HttpMethod.Delete,
"OPTIONS" => HttpMethod.Options,
"TRACE" => HttpMethod.Trace,
_ => new HttpMethod(method),
};
}
private void SetRequestHeader(HttpRequestMessage requestMessage, JsValue value)
{
if (value is JsNull or JsUndefined)
return;
if (value is ObjectInstance objectInstance)
{
foreach (var header in objectInstance.GetOwnProperties())
{
var headerName = header.Key.ToString();
var headerValue = header.Value.Value.ToString();
requestMessage.Headers.Remove(headerName);
requestMessage.Headers.TryAddWithoutValidation(headerName, headerValue);
}
}
else if (value is ArrayInstance arrayInstance)
{
foreach (ArrayInstance header in arrayInstance)
{
if (header.Length != 2)
throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, "The header object must contain exactly two elements.");
var headerName = header[0].ToString();
var headerValue = header[1].ToString();
requestMessage.Headers.Remove(headerName);
requestMessage.Headers.TryAddWithoutValidation(headerName, headerValue);
}
}
else
{
throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, "Only object or array is supported for 'header'.");
}
}
private void SetRequestBody(HttpRequestMessage requestMessage, JsValue value)
{
if (value is JsNull or JsUndefined)
return;
if (value is not JsString jsString)
throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, "Only string is supported for 'body'.");
requestMessage.Content = new StringContent(jsString.ToString());
}
}
}