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.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]); this.net.respond_text_and_close(this, 400, "Bad Request", `Invalid header in request: ${headers[i]}`); return; } if( header.key.toLowerCase() === "host" ) target.host = header.value; else req_headers.append(header.key, header.value); } if(!this.net.cors_proxy && /^\d+\.external$/.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); this.net.respond_text_and_close(this, 400, "Bad Request", `Unknown port for localhost: ${target.href}`); 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(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) { this.net.respond_text_and_close(this, 502, "Fetch Error", `Fetch ${fetch_url} failed:\n\n${e.stack || e.message}`); } 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 new TextEncoder().encode(lines.join("\r\n") + "\r\n\r\n"); }; FetchNetworkAdapter.prototype.respond_text_and_close = function(conn, status_code, status_text, body) { const headers = new Headers({ "content-type": "text/plain", "content-length": body.length.toString(10), "connection": "close" }); conn.writev([this.form_response_head(status_code, status_text, headers), new TextEncoder().encode(body)]); conn.close(); }; 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)); };