Fix script fetch binary response (#407)

This commit is contained in:
wanghongenpin
2025-04-07 03:38:55 +08:00
parent a893430b3c
commit e46d2d179f
5 changed files with 518 additions and 6 deletions

66
assets/js/fetch.js Normal file
View File

@@ -0,0 +1,66 @@
function fetch(url, options) {
options = options || {};
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
const keys = [];
const all = [];
const headers = {};
const response = () => ({
ok: (request.status / 100 | 0) === 2, // 200-299
statusText: request.statusText,
status: request.status,
url: request.responseURL,
body: request.response.body,
text: () => Promise.resolve(request.responseText),
json: () => {
// TODO: review this handle because it may discard \n from json attributes
try {
// console.log('RESPONSE TEXT IN FETCH: ' + request.responseText);
return Promise.resolve(JSON.parse(request.responseText));
} catch (e) {
// console.log('ERROR on fetch parsing JSON: ' + e.message);
return Promise.resolve(request.responseText);
}
},
blob: () => Promise.resolve(request.response.body),
clone: response,
headers: {
keys: () => keys,
entries: () => all,
get: n => headers[n.toLowerCase()],
has: n => n.toLowerCase() in headers
}
});
request.open(options.method || 'get', url, true);
request.onload = () => {
request.getAllResponseHeaders().replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm, (m, key, value) => {
keys.push(key = key.toLowerCase());
all.push([key, value]);
headers[key] = headers[key] ? `${headers[key]},${value}` : value;
});
resolve(response());
};
request.onerror = reject;
request.withCredentials = options.credentials == 'include';
if (options.headers) {
if (options.headers.constructor.name == 'Object') {
for (const i in options.headers) {
request.setRequestHeader(i, options.headers[i]);
}
} else { // if it is some Headers pollyfill, the way to iterate is through for of
for (const header of options.headers) {
request.setRequestHeader(header[0], header[1]);
}
}
}
request.send(options.body || null);
});
}

View File

@@ -0,0 +1,437 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter_js/javascript_runtime.dart';
import 'package:http/http.dart' as http;
import 'package:proxypin/network/util/file_read.dart';
import 'package:proxypin/network/util/logger.dart';
/*
* Based on bits and pieces from different OSS sources
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ignore: non_constant_identifier_names
var _XHR_DEBUG = false;
setXhrDebug(bool value) => _XHR_DEBUG = value;
const HTTP_GET = "get";
const HTTP_POST = "post";
const HTTP_PATCH = "patch";
const HTTP_DELETE = "delete";
const HTTP_PUT = "put";
const HTTP_HEAD = "head";
enum HttpMethod { put, get, post, delete, patch, head }
String _debugSendNativeCallback() {
if (_XHR_DEBUG) {
return """console.log("XMLHttpRequest._send_native_callback");
console.log("arguments");
console.log(arguments);
console.log(responseInfo);
console.log(responseText);
console.log(error);""";
} else
return "";
}
final String xhrJsCode = """
function XMLHttpRequest() {
this._send_native = XMLHttpRequestExtension_send_native;
this._httpMethod = null;
this._url = null;
this._requestHeaders = [];
this._responseHeaders = [];
this.response = null;
this.responseText = null;
this.responseXML = null;
this.onreadystatechange = null;
this.onloadstart = null;
this.onprogress = null;
this.onabort = null;
this.onerror = null;
this.onload = null;
this.onloadend = null;
this.ontimeout = null;
this.readyState = 0;
this.status = 0;
this.statusText = "";
this.withCredentials = null;
};
// readystate enum
XMLHttpRequest.UNSENT = 0;
XMLHttpRequest.OPENED = 1;
XMLHttpRequest.HEADERS = 2;
XMLHttpRequest.LOADING = 3;
XMLHttpRequest.DONE = 4;
XMLHttpRequest.prototype.constructor = XMLHttpRequest;
XMLHttpRequest.prototype.open = function(httpMethod, url) {
this._httpMethod = httpMethod;
this._url = url;
this.readyState = XMLHttpRequest.OPENED;
if (typeof this.onreadystatechange === "function") {
//console.log("Calling onreadystatechange(OPENED)...");
this.onreadystatechange();
}
};
XMLHttpRequest.prototype.send = function(data) {
this.readyState = XMLHttpRequest.LOADING;
if (typeof this.onreadystatechange === "function") {
//console.log("Calling onreadystatechange(LOADING)...");
this.onreadystatechange();
}
if (typeof this.onloadstart === "function") {
//console.log("Calling onloadstart()...");
this.onloadstart();
}
var that = this;
this._send_native(this._httpMethod, this._url, this._requestHeaders, data || null, function(responseInfo, responseText, error) {
that._send_native_callback(responseInfo, responseText, error);
}, this);
};
XMLHttpRequest.prototype.abort = function() {
this.readyState = XMLHttpRequest.UNSENT;
// Note: this.onreadystatechange() is not supposed to be called according to the XHR specs
}
// responseInfo: {statusCode, statusText, responseHeaders}
XMLHttpRequest.prototype._send_native_callback = function(responseInfo, responseText, error) {
${_debugSendNativeCallback()}
if (this.readyState === XMLHttpRequest.UNSENT) {
console.log("XHR native callback ignored because the request has been aborted");
if (typeof this.onabort === "function") {
//console.log("Calling onabort()...");
this.onabort();
}
return;
}
if (this.readyState != XMLHttpRequest.LOADING) {
// Request was not expected
console.log("XHR native callback ignored because the current state is not LOADING");
return;
}
// Response info
// TODO: responseXML?
this.responseURL = this._url;
this.status = responseInfo.statusCode;
this.statusText = responseInfo.statusText;
this.responseBody = responseInfo.body;
this._responseHeaders = responseInfo.responseHeaders || [];
this.readyState = XMLHttpRequest.DONE;
// Response
this.response = null;
this.responseText = null;
this.responseXML = null;
if (error) {
this.responseText = error;
} else {
this.responseText = responseText;
this.response = {
body: responseInfo.body,
}
// console.log('RESPONSE TEXT: ' + responseText);
}
this.readyState = XMLHttpRequest.DONE;
if (typeof this.onreadystatechange === "function") {
//console.log("Calling onreadystatechange(DONE)...");
this.onreadystatechange();
}
if (error === "timeout") {
// Timeout
console.warn("Got XHR timeout");
if (typeof this.ontimeout === "function") {
//console.log("Calling ontimeout()...");
this.ontimeout();
}
} else if (error) {
// Error
console.warn("Got XHR error:", error);
if (typeof this.onerror === "function") {
//console.log("Calling onerror()...");
this.onerror();
}
} else {
// Success
//console.log("XHR success: response =", this.response);
if (typeof this.onload === "function") {
//console.log("Calling onload()...");
this.onload();
}
}
if (typeof this.onloadend === "function") {
//console.log("Calling onloadend()...");
this.onloadend();
}
};
XMLHttpRequest.prototype.setRequestHeader = function(header, value) {
this._requestHeaders.push([header, value]);
};
XMLHttpRequest.prototype.getAllResponseHeaders = function() {
var ret = "";
for (var i = 0; i < this._responseHeaders.length; i++) {
var keyValue = this._responseHeaders[i];
ret += keyValue[0] + ": " + keyValue[1] + "\\r\\n";
}
return ret;
};
XMLHttpRequest.prototype.getResponseHeader = function(name) {
var ret = "";
for (var i = 0; i < this._responseHeaders.length; i++) {
var keyValue = this._responseHeaders[i];
if (keyValue[0] !== name) continue;
if (ret === "") ret += ", ";
ret += keyValue[1];
}
return ret;
};
// XMLHttpRequest.prototype.overrideMimeType = function() {
// // TODO
// };
this.XMLHttpRequest = XMLHttpRequest;""";
RegExp regexpHeader = RegExp("^([\\w-])+:(?!\\s*\$).+\$");
class XhrPendingCall {
int? idRequest;
String? method;
String? url;
Map<String, String> headers;
String? body;
XhrPendingCall({
required this.idRequest,
required this.method,
required this.url,
required this.headers,
required this.body,
});
}
const XHR_PENDING_CALLS_KEY = "xhrPendingCalls";
http.Client? httpClient;
xhrSetHttpClient(http.Client client) {
httpClient = client;
}
extension JavascriptRuntimeXhrExtension on JavascriptRuntime {
List<dynamic>? getPendingXhrCalls() {
return dartContext[XHR_PENDING_CALLS_KEY];
}
bool hasPendingXhrCalls() => getPendingXhrCalls()!.length > 0;
void clearXhrPendingCalls() {
dartContext[XHR_PENDING_CALLS_KEY] = [];
}
Future<void> enableFetch2() async {
enableXhr2();
final fetchPolyfill = await FileRead.readAsString('assets/js/fetch.js');
final evalFetchResult = evaluate(fetchPolyfill);
logger.d('Eval Fetch Result: $evalFetchResult');
}
JavascriptRuntime enableXhr2() {
httpClient = httpClient ?? http.Client();
dartContext[XHR_PENDING_CALLS_KEY] = [];
Timer.periodic(Duration(milliseconds: 40), (timer) {
// exits if there is no pending call to remote
if (!hasPendingXhrCalls()) return;
// collect the pending calls into a local variable making copies
List<dynamic> pendingCalls = List<dynamic>.from(getPendingXhrCalls()!);
// clear the global pending calls list
clearXhrPendingCalls();
// for each pending call, calls the remote http service
pendingCalls.forEach((element) async {
XhrPendingCall pendingCall = element as XhrPendingCall;
HttpMethod eMethod = HttpMethod.values
.firstWhere((e) => e.toString().toLowerCase() == ("HttpMethod.${pendingCall.method}".toLowerCase()));
late http.Response response;
switch (eMethod) {
case HttpMethod.head:
response = await httpClient!.head(
Uri.parse(pendingCall.url!),
headers: pendingCall.headers,
);
break;
case HttpMethod.get:
response = await httpClient!.get(
Uri.parse(pendingCall.url!),
headers: pendingCall.headers,
);
break;
case HttpMethod.post:
response = await httpClient!.post(
Uri.parse(pendingCall.url!),
body: (pendingCall.body is String) ? pendingCall.body : jsonEncode(pendingCall.body),
headers: pendingCall.headers,
);
break;
case HttpMethod.put:
response = await httpClient!.put(
Uri.parse(pendingCall.url!),
body: (pendingCall.body is String) ? pendingCall.body : jsonEncode(pendingCall.body),
headers: pendingCall.headers,
);
break;
case HttpMethod.patch:
response = await httpClient!.patch(
Uri.parse(pendingCall.url!),
body: (pendingCall.body is String) ? pendingCall.body : jsonEncode(pendingCall.body),
headers: pendingCall.headers,
);
break;
case HttpMethod.delete:
response = await httpClient!.delete(
Uri.parse(pendingCall.url!),
headers: pendingCall.headers,
);
break;
}
// assuming request was successfully executed
String? responseText;
List<int>? body;
try {
responseText = utf8.decode(response.bodyBytes);
responseText = jsonEncode(json.decode(responseText));
} on Exception {
// responseText = response.body;
body = response.bodyBytes;
}
// logger.d('RESPONSE TEXT: $responseText');
final xhrResult = XmlHttpRequestResponse(
responseText: responseText,
responseInfo: XhtmlHttpResponseInfo(statusCode: 200, statusText: "OK", body: body),
);
final responseInfo = jsonEncode(xhrResult.responseInfo);
//final responseText = xhrResult.responseText; //.replaceAll("\\n", "\\\n");
final error = xhrResult.error;
// send back to the javascript environment the
// response for the http pending callback
this.evaluate(
"globalThis.xhrRequests[${pendingCall.idRequest}].callback($responseInfo, `$responseText`, $error);",
);
});
});
this.evaluate("""
var xhrRequests = {};
var idRequest = -1;
function XMLHttpRequestExtension_send_native() {
idRequest += 1;
var cb = arguments[4];
var context = arguments[5];
xhrRequests[idRequest] = {
callback: function(responseInfo, responseText, error) {
cb(responseInfo, responseText, error);
}
};
var args = [];
args[0] = arguments[0];
args[1] = arguments[1];
args[2] = arguments[2];
args[3] = arguments[3];
args[4] = idRequest;
sendMessage('SendNative', JSON.stringify(args));
}
""");
final evalXhrResult = this.evaluate(xhrJsCode);
if (_XHR_DEBUG) print('RESULT evalXhrResult: $evalXhrResult');
this.onMessage('SendNative', (arguments) {
try {
String? method = arguments[0];
String? url = arguments[1];
dynamic headersList = arguments[2];
String? body = arguments[3];
int? idRequest = arguments[4];
Map<String, String> headers = {};
headersList.forEach((header) {
// final headerMatch = regexpHeader.allMatches(value).first;
// String? headerName = headerMatch.group(0);
// String? headerValue = headerMatch.group(1);
// if (headerName != null) {
// headers[headerName] = headerValue ?? '';
// }
String headerKey = header[0];
headers[headerKey] = header[1];
});
(dartContext[XHR_PENDING_CALLS_KEY] as List<dynamic>).add(
XhrPendingCall(
idRequest: idRequest,
method: method,
url: url,
headers: headers,
body: body,
),
);
} on Error catch (e) {
if (_XHR_DEBUG) print('ERROR calling sendNative on Dart: >>>> $e');
} on Exception catch (e) {
if (_XHR_DEBUG) print('Exception calling sendNative on Dart: >>>> $e');
}
});
return this;
}
}
class XhtmlHttpResponseInfo {
final int? statusCode;
final String? statusText;
final List<int>? body;
final List<List<String>> responseHeaders = [];
XhtmlHttpResponseInfo({
this.body,
this.statusCode,
this.statusText,
});
void addResponseHeaders(String name, String value) {
responseHeaders.add([name, value]);
}
Map<String, Object?> toJson() {
return {
"statusCode": statusCode,
"statusText": statusText,
"body": body,
"responseHeaders": jsonEncode(responseHeaders)
};
}
}
class XmlHttpRequestResponse {
final String? responseText;
final String? error; // should be timeout in case of timeout
final XhtmlHttpResponseInfo? responseInfo;
XmlHttpRequestResponse({this.responseText, this.responseInfo, this.error});
Map<String, Object?> toJson() {
return {'responseText': responseText, 'responseInfo': responseInfo!.toJson(), 'error': error};
}
}

View File

@@ -21,7 +21,9 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter_js/flutter_js.dart';
import 'package:proxypin/network/components/js/file.dart';
import 'package:proxypin/network/components/js/md5.dart';
import 'package:proxypin/network/components/js/xhr.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/http.dart' as http;
import 'package:proxypin/network/http/http_headers.dart';
import 'package:proxypin/network/util/lang.dart';
import 'package:proxypin/network/util/logger.dart';
@@ -69,7 +71,7 @@ async function onResponse(context, request, response) {
final Map<ScriptItem, String> _scriptMap = {};
static JavascriptRuntime flutterJs = getJavascriptRuntime();
static JavascriptRuntime flutterJs = getJavascriptRuntime(xhr: false);
static String? deviceId;
@@ -89,6 +91,9 @@ async function onResponse(context, request, response) {
deviceId = await DeviceUtils.deviceId();
Md5Bridge.registerMd5(flutterJs);
FileBridge.registerFile(flutterJs);
flutterJs.enableFetch2();
logger.d('init script manager $deviceId');
}
return _instance!;
@@ -347,7 +352,7 @@ async function onResponse(context, request, response) {
//http request
HttpRequest convertHttpRequest(HttpRequest request, Map<dynamic, dynamic> map) {
request.headers.clear();
request.method = HttpMethod.values.firstWhere((element) => element.name == map['method']);
request.method = http.HttpMethod.values.firstWhere((element) => element.name == map['method']);
String query = UriUtils.mapToQuery(map['queries']);
var requestUri = request.requestUri!.replace(path: map['path'], query: query);
@@ -362,7 +367,7 @@ async function onResponse(context, request, response) {
request.headers.addValues(key, value.map((e) => e.toString()).toList());
return;
}
request.headers.add(key, value);
request.headers.set(key, value);
});
//判断是否是二进制
@@ -389,7 +394,7 @@ async function onResponse(context, request, response) {
return;
}
response.headers.add(key, value);
response.headers.set(key, value);
});
response.headers.remove(HttpHeaders.CONTENT_ENCODING);

View File

@@ -11,6 +11,7 @@ import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:highlight/languages/javascript.dart';
import 'package:proxypin/network/components/js/file.dart';
import 'package:proxypin/network/components/js/md5.dart';
import 'package:proxypin/network/components/js/xhr.dart';
class JavaScript extends StatefulWidget {
const JavaScript({super.key});
@@ -40,13 +41,14 @@ class _JavaScriptState extends State<JavaScript> {
void initState() {
super.initState();
if (resetEnvironment || flutterJs == null) {
flutterJs = getJavascriptRuntime();
flutterJs = getJavascriptRuntime(xhr: false);
}
// register channel callback
final channelCallbacks = JavascriptRuntime.channelFunctionsRegistered[flutterJs!.getEngineInstanceId()];
channelCallbacks!["ConsoleLog"] = consoleLog;
Md5Bridge.registerMd5(flutterJs!);
FileBridge.registerFile(flutterJs!);
flutterJs?.enableFetch2();
code = CodeController(language: javascript, text: 'console.log("Hello, World!")');
}
@@ -145,7 +147,8 @@ class _JavaScriptState extends State<JavaScript> {
))))),
const SizedBox(height: 10),
Row(children: [
Text("${localizations.output}:", style: TextStyle(fontSize: 16, color: primaryColor, fontWeight: FontWeight.w500)),
Text("${localizations.output}:",
style: TextStyle(fontSize: 16, color: primaryColor, fontWeight: FontWeight.w500)),
const SizedBox(width: 15),
//copy
IconButton(

View File

@@ -59,3 +59,4 @@ flutter:
- assets/certs/ca.crt
- assets/certs/ca_key.pem
- assets/icon.png
- assets/js/