mirror of
https://github.com/copy/v86.git
synced 2025-12-31 12:33:15 +00:00
300 lines
9.7 KiB
JavaScript
300 lines
9.7 KiB
JavaScript
import { LOG_FETCH } from "../const.js";
|
|
import { h } from "../lib.js";
|
|
import { dbg_log } from "../log.js";
|
|
|
|
import {
|
|
create_eth_encoder_buf,
|
|
handle_fake_networking,
|
|
TCPConnection,
|
|
TCP_STATE_SYN_RECEIVED,
|
|
fake_tcp_connect,
|
|
fake_tcp_probe
|
|
} from "./fake_network.js";
|
|
|
|
// For Types Only
|
|
import { BusConnector } from "../bus.js";
|
|
|
|
/**
|
|
* @constructor
|
|
*
|
|
* @param {BusConnector} bus
|
|
* @param {*=} config
|
|
*/
|
|
export function FetchNetworkAdapter(bus, config)
|
|
{
|
|
config = config || {};
|
|
this.bus = bus;
|
|
this.id = config.id || 0;
|
|
this.router_mac = new Uint8Array((config.router_mac || "52:54:0:1:2:3").split(":").map(function(x) { return parseInt(x, 16); }));
|
|
this.router_ip = new Uint8Array((config.router_ip || "192.168.86.1").split(".").map(function(x) { return parseInt(x, 10); }));
|
|
this.vm_ip = new Uint8Array((config.vm_ip || "192.168.86.100").split(".").map(function(x) { return parseInt(x, 10); }));
|
|
this.masquerade = config.masquerade === undefined || !!config.masquerade;
|
|
this.vm_mac = new Uint8Array(6);
|
|
this.dns_method = config.dns_method || "static";
|
|
this.doh_server = config.doh_server;
|
|
this.tcp_conn = {};
|
|
this.eth_encoder_buf = create_eth_encoder_buf();
|
|
this.fetch = (...args) => fetch(...args);
|
|
|
|
// Ex: 'https://corsproxy.io/?'
|
|
this.cors_proxy = config.cors_proxy;
|
|
|
|
this.local_http = !!config.local_http;
|
|
|
|
this.bus.register("net" + this.id + "-mac", function(mac) {
|
|
this.vm_mac = new Uint8Array(mac.split(":").map(function(x) { return parseInt(x, 16); }));
|
|
}, this);
|
|
this.bus.register("net" + this.id + "-send", function(data)
|
|
{
|
|
this.send(data);
|
|
}, this);
|
|
}
|
|
|
|
FetchNetworkAdapter.prototype.destroy = function()
|
|
{
|
|
};
|
|
|
|
FetchNetworkAdapter.prototype.on_tcp_connection = function(packet, tuple)
|
|
{
|
|
if(packet.tcp.dport === 80) {
|
|
let conn = new TCPConnection();
|
|
conn.state = TCP_STATE_SYN_RECEIVED;
|
|
conn.net = this;
|
|
conn.on("data", on_data_http);
|
|
conn.tuple = tuple;
|
|
conn.accept(packet);
|
|
this.tcp_conn[tuple] = conn;
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
FetchNetworkAdapter.prototype.connect = function(port)
|
|
{
|
|
return fake_tcp_connect(port, this);
|
|
};
|
|
|
|
FetchNetworkAdapter.prototype.tcp_probe = function(port)
|
|
{
|
|
return fake_tcp_probe(port, this);
|
|
};
|
|
|
|
/**
|
|
* @this {TCPConnection}
|
|
* @param {!ArrayBuffer} data
|
|
*/
|
|
async function on_data_http(data)
|
|
{
|
|
this.read = this.read || "";
|
|
this.read += new TextDecoder().decode(data);
|
|
if(this.read && this.read.indexOf("\r\n\r\n") !== -1) {
|
|
let offset = this.read.indexOf("\r\n\r\n");
|
|
let headers = this.read.substring(0, offset).split(/\r\n/);
|
|
let data = this.read.substring(offset + 4);
|
|
this.read = "";
|
|
|
|
let first_line = headers[0].split(" ");
|
|
let target;
|
|
if(/^https?:/.test(first_line[1])) {
|
|
// HTTP proxy
|
|
target = new URL(first_line[1]);
|
|
}
|
|
else {
|
|
target = new URL("http://host" + first_line[1]);
|
|
}
|
|
if(typeof window !== "undefined" && target.protocol === "http:" && window.location.protocol === "https:") {
|
|
// fix "Mixed Content" errors
|
|
target.protocol = "https:";
|
|
}
|
|
|
|
let req_headers = new Headers();
|
|
for(let i = 1; i < headers.length; ++i) {
|
|
const header = this.net.parse_http_header(headers[i]);
|
|
if(!header) {
|
|
console.warn('The request contains an invalid header: "%s"', headers[i]);
|
|
const body = new TextEncoder().encode(`Invalid header in request: ${headers[i]}`);
|
|
const resp_headers = new Headers({
|
|
"content-type": "text/plain",
|
|
"content-length": body.length.toString(10),
|
|
"connection": "close"
|
|
});
|
|
this.writev([new TextEncoder().encode(this.net.form_response_head(400, "Bad Request", resp_headers)), body]);
|
|
this.close();
|
|
return;
|
|
}
|
|
if( header.key.toLowerCase() === "host" ) target.host = header.value;
|
|
else req_headers.append(header.key, header.value);
|
|
}
|
|
|
|
if(this.net.local_http && !this.net.cors_proxy && (/\d+\.v86local\.http/).test(target.hostname)) {
|
|
dbg_log("Request to localhost: " + target.href, LOG_FETCH);
|
|
const localport = parseInt(target.hostname.split(".")[0], 10);
|
|
if(!isNaN(localport) && localport > 0 && localport < 65536) {
|
|
target.protocol = "http:";
|
|
target.hostname = "localhost";
|
|
target.port = localport.toString(10);
|
|
} else {
|
|
console.warn('Unknown port for localhost: "%s"', target.href);
|
|
const body = new TextEncoder().encode(`Unknown port for localhost: ${target.href}`);
|
|
const resp_headers = new Headers({
|
|
"content-type": "text/plain",
|
|
"content-length": body.length.toString(10),
|
|
"connection": "close"
|
|
});
|
|
this.writev([new TextEncoder().encode(this.net.form_response_head(400, "Bad Request", resp_headers)), body]);
|
|
this.close();
|
|
return;
|
|
}
|
|
}
|
|
|
|
dbg_log("HTTP Dispatch: " + target.href, LOG_FETCH);
|
|
this.name = target.href;
|
|
let opts = {
|
|
method: first_line[0],
|
|
headers: req_headers,
|
|
};
|
|
if(["put", "post"].indexOf(opts.method.toLowerCase()) !== -1) {
|
|
opts.body = data;
|
|
}
|
|
|
|
const fetch_url = this.net.cors_proxy ? this.net.cors_proxy + encodeURIComponent(target.href) : target.href;
|
|
const encoder = new TextEncoder();
|
|
let response_started = false;
|
|
this.net.fetch(fetch_url, opts).then((resp) => {
|
|
let resp_headers = new Headers(resp.headers);
|
|
resp_headers.delete("content-encoding");
|
|
resp_headers.delete("keep-alive");
|
|
resp_headers.delete("content-length");
|
|
resp_headers.delete("transfer-encoding");
|
|
|
|
resp_headers.set("x-was-fetch-redirected", `${!!resp.redirected}`);
|
|
resp_headers.set("x-fetch-resp-url", resp.url);
|
|
resp_headers.set("connection", "close");
|
|
|
|
this.write(encoder.encode(this.net.form_response_head(resp.status, resp.statusText, resp_headers)));
|
|
response_started = true;
|
|
|
|
if(resp.body && resp.body.getReader) {
|
|
const resp_reader = resp.body.getReader();
|
|
const pump = ({ value, done }) => {
|
|
if(value) {
|
|
this.write(value);
|
|
}
|
|
if(done) {
|
|
this.close();
|
|
}
|
|
else {
|
|
return resp_reader.read().then(pump);
|
|
}
|
|
};
|
|
resp_reader.read().then(pump);
|
|
} else {
|
|
resp.arrayBuffer().then(buffer => {
|
|
this.write(new Uint8Array(buffer));
|
|
this.close();
|
|
});
|
|
}
|
|
})
|
|
.catch((e) => {
|
|
console.warn("Fetch Failed: " + fetch_url + "\n" + e);
|
|
if(!response_started) {
|
|
const body = encoder.encode(`Fetch ${fetch_url} failed:\n\n${e.stack || e.message}`);
|
|
const resp_headers = new Headers({
|
|
"Content-Type": "text/plain",
|
|
"Content-Length": body.length.toString(10),
|
|
"Connection": "close"
|
|
});
|
|
this.writev([encoder.encode(this.net.form_response_head(502, "Fetch Error", resp_headers)), body]);
|
|
}
|
|
this.close();
|
|
});
|
|
}
|
|
}
|
|
|
|
FetchNetworkAdapter.prototype.fetch = async function(url, options)
|
|
{
|
|
if(this.cors_proxy) url = this.cors_proxy + encodeURIComponent(url);
|
|
|
|
try
|
|
{
|
|
const resp = await fetch(url, options);
|
|
const ab = await resp.arrayBuffer();
|
|
return [resp, ab];
|
|
}
|
|
catch(e)
|
|
{
|
|
console.warn("Fetch Failed: " + url + "\n" + e);
|
|
return [
|
|
{
|
|
status: 502,
|
|
statusText: "Fetch Error",
|
|
headers: new Headers({ "Content-Type": "text/plain" }),
|
|
},
|
|
new TextEncoder().encode(`Fetch ${url} failed:\n\n${e.stack}`).buffer
|
|
];
|
|
}
|
|
};
|
|
|
|
FetchNetworkAdapter.prototype.form_response_head = function(status_code, status_text, headers)
|
|
{
|
|
let lines = [
|
|
`HTTP/1.1 ${status_code} ${status_text}`
|
|
];
|
|
|
|
for(const [key, value] of headers.entries()) {
|
|
lines.push(`${key}: ${value}`);
|
|
}
|
|
|
|
return lines.join("\r\n") + "\r\n\r\n";
|
|
};
|
|
|
|
FetchNetworkAdapter.prototype.parse_http_header = function(header)
|
|
{
|
|
const parts = header.match(/^([^:]*):(.*)$/);
|
|
if(!parts) {
|
|
dbg_log("Unable to parse HTTP header", LOG_FETCH);
|
|
return;
|
|
}
|
|
|
|
const key = parts[1];
|
|
const value = parts[2].trim();
|
|
|
|
if(key.length === 0)
|
|
{
|
|
dbg_log("Header key is empty, raw header", LOG_FETCH);
|
|
return;
|
|
}
|
|
if(value.length === 0)
|
|
{
|
|
dbg_log("Header value is empty", LOG_FETCH);
|
|
return;
|
|
}
|
|
if(!/^[\w-]+$/.test(key))
|
|
{
|
|
dbg_log("Header key contains forbidden characters", LOG_FETCH);
|
|
return;
|
|
}
|
|
if(!/^[\x20-\x7E]+$/.test(value))
|
|
{
|
|
dbg_log("Header value contains forbidden characters", LOG_FETCH);
|
|
return;
|
|
}
|
|
|
|
return { key, value };
|
|
};
|
|
|
|
/**
|
|
* @param {Uint8Array} data
|
|
*/
|
|
FetchNetworkAdapter.prototype.send = function(data)
|
|
{
|
|
handle_fake_networking(data, this);
|
|
};
|
|
|
|
/**
|
|
* @param {Uint8Array} data
|
|
*/
|
|
FetchNetworkAdapter.prototype.receive = function(data)
|
|
{
|
|
this.bus.send("net" + this.id + "-receive", new Uint8Array(data));
|
|
};
|