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()); } } }