mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-12-25 19:56:10 +08:00
259 lines
9.9 KiB
C#
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());
|
|
}
|
|
}
|
|
}
|