292 lines
15 KiB
JavaScript
292 lines
15 KiB
JavaScript
class WTVHTTP {
|
|
constructor(...[minisrv_config, service_name, wtvshared, sendToClient, net, http]) {
|
|
this.minisrv_config = minisrv_config;
|
|
this.service_name = service_name;
|
|
this.wtvshared = wtvshared;
|
|
this.sendToClient = sendToClient;
|
|
this.http = http;
|
|
this.https = require('follow-redirects').https
|
|
this.proxy_agent = null;
|
|
}
|
|
|
|
async doHTTPProxy(socket, request_headers) {
|
|
// detect protocol name
|
|
const idx = request_headers.request_url.indexOf('/') - 1;
|
|
|
|
const request_type = request_headers.request_url.slice(0, idx);
|
|
if (this.minisrv_config.config.debug_flags.show_headers) console.debug(request_type.toUpperCase() + " Proxy: Client Request Headers on socket ID", socket.id, (await this.wtvshared.decodePostData(await this.wtvshared.filterRequestLog(await this.wtvshared.filterSSID(request_headers)))));
|
|
else debug(request_type.toUpperCase() + " Proxy: Client Request Headers on socket ID", socket.id, (await this.wtvshared.decodePostData(await this.wtvshared.filterRequestLog(await this.wtvshared.filterSSID(request_headers)))));
|
|
|
|
switch (request_type) {
|
|
case "https":
|
|
this.proxy_agent = this.https;
|
|
break;
|
|
default:
|
|
this.proxy_agent = this.http;
|
|
break;
|
|
}
|
|
|
|
const request_data = [];
|
|
const data = [];
|
|
|
|
request_data.method = request_headers.request.split(' ')[0];
|
|
const request_url_split = request_headers.request.split(' ')[1].split('/');
|
|
request_data.host = request_url_split[2];
|
|
if (request_data.host.indexOf(':') > 0) {
|
|
request_data.port = request_data.host.split(':')[1];
|
|
request_data.host = request_data.host.split(':')[0];
|
|
} else {
|
|
if (request_type === "https") request_data.port = 443;
|
|
else request_data.port = 80;
|
|
}
|
|
for (let i = 0; i < 3; i++) request_url_split.shift();
|
|
request_data.path = "/" + request_url_split.join('/');
|
|
if (request_data.method && request_data.host && request_data.path) {
|
|
|
|
const options = {
|
|
host: request_data.host,
|
|
port: request_data.port,
|
|
path: request_data.path,
|
|
method: request_data.method,
|
|
followAllRedirects: true,
|
|
headers: {
|
|
"User-Agent": request_headers["User-Agent"] || "WebTV",
|
|
"Connection": "Keep-Alive"
|
|
}
|
|
}
|
|
|
|
// RFC7239
|
|
if (socket.remoteAddress !== "127.0.0.1") {
|
|
options.headers["X-Forwarded-For"] = socket.remoteAddress;
|
|
}
|
|
|
|
if (request_headers.post_data) {
|
|
if (request_headers["Content-type"]) options.headers["Content-type"] = request_headers["Content-type"];
|
|
if (request_headers["Content-length"]) options.headers["Content-length"] = request_headers["Content-length"];
|
|
}
|
|
|
|
if (request_type == "https" && this.minisrv_config.services[request_type].allow_self_signed_ssl) {
|
|
options.rejectUnauthorized = false;
|
|
}
|
|
|
|
if (this.minisrv_config.services[request_type].use_external_proxy && minisrv_config.services[request_type].external_proxy_port) {
|
|
// configure connection to an external proxy
|
|
if (this.minisrv_config.services[request_type].external_proxy_is_socks) {
|
|
// configure connection to remote socks proxy
|
|
const { SocksProxyAgent }= require('socks-proxy-agent');
|
|
options.agent = new SocksProxyAgent("socks://" + (minisrv_config.services[request_type].external_proxy_host || "127.0.0.1") + ":" + minisrv_config.services[request_type].external_proxy_port);
|
|
} else {
|
|
// configure connection to remote http proxy
|
|
this.proxy_agent = this.http;
|
|
options.host = this.minisrv_config.services[request_type].external_proxy_host;
|
|
options.port = this.minisrv_config.services[request_type].external_proxy_port;
|
|
options.path = request_headers.request.split(' ')[1];
|
|
options.headers.Host = request_data.host + ":" + request_data.port;
|
|
if (this.minisrv_config.services[request_type].replace_protocol) {
|
|
options.path = options.path.replace(request_type, this.minisrv_config.services[request_type].replace_protocol);
|
|
}
|
|
}
|
|
if (this.minisrv_config.services[request_type].external_proxy_is_http1) {
|
|
options.insecureHTTPParser = true;
|
|
options.headers.Connection = 'close'
|
|
}
|
|
}
|
|
if (this.minisrv_config.services[request_type].support_bitdefender_self_signed_proxy) {
|
|
try {
|
|
const WTVSSL = require('./WTVSSL.js');
|
|
const ssl = new WTVSSL();
|
|
const bitdefenderCACert = ssl.getBitdefenderCACert();
|
|
if (bitdefenderCACert) {
|
|
options.ca = [bitdefenderCACert];
|
|
// this sucks, but bitdefender's cert is weird and doesn't seem to work properly with Node's TLS implementation
|
|
// even when added to the trusted store, so we have to disable rejection of unauthorized certs
|
|
// when the Bitdefender CA cert is present. At least this way we can still allow it without
|
|
// completely breaking SSL proxying for Bitdefender users.
|
|
// This will only trigger on Windows if support_bitdefender_self_signed_proxy is true, and the Bitdefender CA file exists
|
|
options.rejectUnauthorized = false;
|
|
}
|
|
} catch (err) {
|
|
console.warn(" * Failed to load Bitdefender CA certificate:", err.message);
|
|
}
|
|
}
|
|
const req = this.proxy_agent.request(options, (res) => {
|
|
let total_data = 0;
|
|
let abortedBySize = false;
|
|
const maxBytes = 1024 * 1024 * parseFloat(this.minisrv_config.services[request_type].max_response_size || 16);
|
|
const responseContentLength = parseInt(res.headers['content-length'] || res.headers['Content-Length'] || '0', 10) || 0;
|
|
if (responseContentLength > 0 && responseContentLength > maxBytes) {
|
|
console.warn(` * Proxy response contains Content-Length ${responseContentLength} bytes > limit ${maxBytes} bytes, destroying...`);
|
|
abortedBySize = true;
|
|
res.destroy();
|
|
const errpage = this.wtvshared.doErrorPage(400, "The item chosen is too large to be used.");
|
|
this.sendToClient(socket, errpage[0], errpage[1]);
|
|
return;
|
|
}
|
|
|
|
res.on('data', d => {
|
|
if (abortedBySize) return;
|
|
data.push(d);
|
|
total_data += d.length;
|
|
if (total_data > maxBytes) {
|
|
abortedBySize = true;
|
|
console.warn(` * Response data exceeded ${this.minisrv_config.services[request_type].max_response_size || 16}MB limit, destroying...`);
|
|
res.destroy();
|
|
const errpage = this.wtvshared.doErrorPage(400, "The item chosen is too large to be used.");
|
|
this.sendToClient(socket, errpage[0], errpage[1]);
|
|
}
|
|
})
|
|
|
|
res.on('error', (err) => {
|
|
// hack for Protoweb ECONNRESET
|
|
if (this.minisrv_config.services[request_type].external_proxy_is_http1 && data.length > 0) {
|
|
this.handleProxy(socket, request_type, request_headers, res, data);
|
|
} else {
|
|
console.error(" * Unhandled Proxy Request Error:", err);
|
|
}
|
|
});
|
|
|
|
res.on('end', () => {
|
|
if (abortedBySize) return;
|
|
// For when http proxies behave correctly
|
|
if (!this.minisrv_config.services[request_type].external_proxy_is_http1 || data.length > 0) {
|
|
this.handleProxy(socket, request_type, request_headers, res, data);
|
|
}
|
|
});
|
|
}).on('error', (err) => {
|
|
// severe errors, such as unable to connect.
|
|
if (err.code === "ENOTFOUND" || err.message.indexOf("HostUnreachable") > 0) {
|
|
const errpage = this.wtvshared.doErrorPage(400, `The publisher <b>${request_data.host}</b> is unknown.`);
|
|
this.sendToClient(socket, errpage[0], errpage[1]);
|
|
} else {
|
|
console.error(" * Unhandled Proxy Request Error:", err);
|
|
const errpage = this.wtvshared.doErrorPage(400);
|
|
this.sendToClient(socket, errpage[0], errpage[1]);
|
|
}
|
|
|
|
});
|
|
if (request_headers.post_data) {
|
|
req.write(Buffer.from(request_headers.post_data.toString(CryptoJS.enc.Hex), 'hex'), () => {
|
|
req.end();
|
|
});
|
|
} else {
|
|
req.end();
|
|
}
|
|
}
|
|
}
|
|
|
|
handleProxy(socket, request_type, request_headers, res, data) {
|
|
console.log(` * Proxy Request ${request_type.toUpperCase()} ${res.statusCode} for ${request_headers.request}`)
|
|
// an http response error is not a request error, and will come here under the 'end' event rather than an 'error' event.
|
|
switch (res.statusCode) {
|
|
case 404:
|
|
res.headers.Status = res.statusCode + " The publisher can’t find the page requested.";
|
|
break;
|
|
|
|
case 401:
|
|
case 403:
|
|
res.headers.Status = res.statusCode + " The publisher of that page has not authorized you to use it.";
|
|
break;
|
|
|
|
case 500:
|
|
res.headers.Status = res.statusCode + " The publisher of that page can’t be reached.";
|
|
break;
|
|
|
|
default:
|
|
res.headers.Status = res.statusCode + " " + res.statusMessage;
|
|
break;
|
|
}
|
|
|
|
if (res.headers['Content-type']) {
|
|
res.headers['Content-Type'] = res.headers['Content-type'];
|
|
delete (res.headers['Content-type'])
|
|
}
|
|
|
|
if (res.headers['content-type']) {
|
|
res.headers['Content-Type'] = res.headers['content-type'];
|
|
delete (res.headers['content-type'])
|
|
}
|
|
|
|
// header pass-through whitelist, case insensitive comparsion to server, however, you should
|
|
// specify the header case as you intend for the client
|
|
const headers = this.wtvshared.stripHeaders(res.headers, [
|
|
'Connection',
|
|
'Server',
|
|
'Date',
|
|
'Content-Type',
|
|
'Cookie',
|
|
'Location',
|
|
'Accept-Ranges',
|
|
'Last-Modified'
|
|
]);
|
|
headers["wtv-http-proxy"] = true;
|
|
headers["wtv-trusted"] = false;
|
|
|
|
if (typeof res.headers['Content-Type'] === 'string' && res.headers['Content-Type'].startsWith("text")) {
|
|
// Get the original URL for relative link fixing
|
|
const originalUrl = request_headers.request.split(' ')[1];
|
|
|
|
// Transform HTML content for WebTV compatibility
|
|
if (res.headers['Content-Type'].includes('html') &&
|
|
this.minisrv_config.services[request_type]?.use_minifying_proxy === true) {
|
|
try {
|
|
const WTVMinifyingProxy = require('./WTVMinifyingProxy.js');
|
|
const proxy = new WTVMinifyingProxy(this.minisrv_config);
|
|
|
|
let htmlContent = Buffer.concat(data).toString();
|
|
|
|
// Apply WebTV-specific transformations
|
|
const transformOptions = {
|
|
removeImages: this.minisrv_config.services[request_type]?.remove_images || false,
|
|
maxImageWidth: this.minisrv_config.services[request_type]?.max_image_width || 400,
|
|
simplifyTables: this.minisrv_config.services[request_type]?.simplify_tables !== false,
|
|
maxWidth: this.minisrv_config.services[request_type]?.max_width || 544,
|
|
preserveJellyScript: this.minisrv_config.services[request_type]?.preserve_jellyscript !== false,
|
|
jellyScriptMaxSize: this.minisrv_config.services[request_type]?.jellyscript_max_size || 8192
|
|
};
|
|
|
|
htmlContent = proxy.transformForWebTV(htmlContent, originalUrl, transformOptions);
|
|
data = [Buffer.from(htmlContent)];
|
|
|
|
if (this.minisrv_config.config.verbosity >= 3) {
|
|
console.log(` * HTML transformed for WebTV compatibility (${originalUrl})`);
|
|
}
|
|
} catch (err) {
|
|
console.warn(` * HTML transformation failed: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
if (request_type !== "http" && request_type !== "https") {
|
|
// replace http and https links on non http/https protocol (for proto:// for example)
|
|
const data_t = Buffer.concat(data).toString().replaceAll("http://", request_type + "://").replaceAll("https://", request_type + "://");
|
|
data = [Buffer.from(data_t)]
|
|
}
|
|
}
|
|
|
|
// if Connection: close header, set our internal variable to close the socket
|
|
if (headers['Connection']) {
|
|
if (headers['Connection'].toLowerCase().includes('close')) {
|
|
headers["wtv-connection-close"] = true;
|
|
}
|
|
}
|
|
|
|
// if a wtv-explaination is defined for an error code (except 200), define the header here to
|
|
// show the 'Explain' button on the client error ShowAlert
|
|
if (this.minisrv_config.services[request_type]['wtv-explanation']) {
|
|
if (this.minisrv_config.services[request_type]['wtv-explanation'][res.statusCode]) {
|
|
headers['wtv-explanation-url'] = this.minisrv_config.services[request_type]['wtv-explanation'][res.statusCode];
|
|
}
|
|
} else if (this.minisrv_config.services['http']['wtv-explanation']) {
|
|
if (this.minisrv_config.services['http']['wtv-explanation'][res.statusCode]) {
|
|
headers['wtv-explanation-url'] = this.minisrv_config.services['http']['wtv-explanation'][res.statusCode];
|
|
}
|
|
}
|
|
let data_hex = Buffer.concat(data).toString('hex');
|
|
if (data_hex.startsWith("0d0a0d0a")) data_hex = data_hex.slice(8);
|
|
if (data_hex.startsWith("0a0d0a")) data_hex = data_hex.slice(6);
|
|
if (data_hex.startsWith("0a0a")) data_hex = data_hex.slice(4);
|
|
this.sendToClient(socket, headers, Buffer.from(data_hex, 'hex'));
|
|
}
|
|
|
|
}
|
|
|
|
module.exports = WTVHTTP; |