v0.9.1
- fix: throw proper error if wtv-update:/sync called without arguments
- feature: Support to route all HTTP proxied requests over a SOCKS proxy (eg Tor or VPN)
- feature: Psuedo-HTTPS (WebTV can now visit HTTPS URLs via proxy, but
we do not use SSL encryption when sending back to the WebTV)
- fix: header issue with login-stage-two.js
- fix: encrypted request headers were shown despite verbosity level
- update: wtv-update/sync: allow multiple groups in sync diskmap, fix md5 comparsion
- update: wtv-home:/home: added connection speed
- Renamed processSSID to filterSSID
- Documented and rewrote some functions
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
const fs = require('fs');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const strftime = require('strftime');
|
||||
const strftime = require('strftime'); // used externally by service scripts
|
||||
const net = require('net');
|
||||
const CryptoJS = require('crypto-js');
|
||||
const mime = require('mime-types');
|
||||
@@ -11,8 +11,14 @@ const { crc16 } = require('easy-crc');
|
||||
var WTVSec = require('./wtvsec.js');
|
||||
var ClientSessionData = require('./session_data.js');
|
||||
|
||||
// Where we store our session information
|
||||
var ssid_sessions = new Array();
|
||||
var socket_sessions = new Array();
|
||||
|
||||
var ports = [];
|
||||
|
||||
// add .reverse() feature to all JavaScript Strings in this application
|
||||
// works for service vault scripts too.
|
||||
String.prototype.reverse = function () {
|
||||
var splitString = this.split("");
|
||||
var reverseArray = splitString.reverse();
|
||||
@@ -20,9 +26,8 @@ String.prototype.reverse = function () {
|
||||
return joinArray;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getServiceString(service) {
|
||||
// used externally by service scripts
|
||||
if (service === "all") {
|
||||
var out = "";
|
||||
Object.keys(minisrv_config.services).forEach(function (k) {
|
||||
@@ -38,12 +43,6 @@ function getServiceString(service) {
|
||||
}
|
||||
}
|
||||
|
||||
var ssid_sessions = new Array();
|
||||
var socket_buffer = new Array();
|
||||
var socket_sessions = new Array();
|
||||
|
||||
var script_processing_timeout = 10; // seconds
|
||||
|
||||
function getFileExt(path) {
|
||||
return path.reverse().split(".")[0].reverse();
|
||||
}
|
||||
@@ -71,45 +70,45 @@ function doErrorPage(code, data = null) {
|
||||
return new Array(headers, data);
|
||||
}
|
||||
|
||||
|
||||
function getConType(path) {
|
||||
// custom contype for flashrom
|
||||
if (path.indexOf("wtv-flashrom") && (getFileExt(path).toLowerCase() == "rom" || getFileExt(path).toLowerCase() == "brom")) {
|
||||
return "binary/x-wtv-flashblock";
|
||||
} else if (getFileExt(path).toLowerCase() == "rmf") {
|
||||
return "audio/x-rmf";
|
||||
}
|
||||
}
|
||||
return mime.lookup(path);
|
||||
}
|
||||
|
||||
async function processPath(socket, path, request_headers = new Array(), service_name) {
|
||||
async function processPath(socket, service_vault_file_path, request_headers = new Array(), service_name) {
|
||||
var headers, data = null;
|
||||
var request_is_direct_file = false;
|
||||
var request_is_async = false;
|
||||
var service_vault_found = false;
|
||||
var service_path = path;
|
||||
var service_path = service_vault_file_path;
|
||||
try {
|
||||
service_vaults.forEach(function (service_vault_dir) {
|
||||
if (service_vault_found) return;
|
||||
path = service_vault_dir.path + "/" + service_path.replace(/\\/g, "/");
|
||||
service_vault_file_path = service_vault_dir.path + "/" + service_path.replace(/\\/g, "/");
|
||||
|
||||
|
||||
if (fs.existsSync(path)) {
|
||||
if (fs.existsSync(service_vault_file_path)) {
|
||||
// file exists, read it and return it
|
||||
service_vault_found = true;
|
||||
request_is_async = true;
|
||||
if (!zquiet) console.log(" * Found " + path + " in " + service_vault_dir.name +" to handle request (Direct File Mode) [Socket " + socket.id + "]");
|
||||
var contype = getConType(path);
|
||||
if (!zquiet) console.log(" * Found " + service_vault_file_path + " in " + service_vault_dir.name +" to handle request (Direct File Mode) [Socket " + socket.id + "]");
|
||||
var contype = getConType(service_vault_file_path);
|
||||
headers = "200 OK\n"
|
||||
headers += "Content-Type: " + contype;
|
||||
fs.readFile(path, null, function (err, data) {
|
||||
fs.readFile(service_vault_file_path, null, function (err, data) {
|
||||
sendToClient(socket, headers, data);
|
||||
});
|
||||
} else if (fs.existsSync(path + ".txt")) {
|
||||
service_vault_found = true;
|
||||
} else if (fs.existsSync(service_vault_file_path + ".txt")) {
|
||||
// raw text format, entire payload expected (headers and content)
|
||||
if (!zquiet) console.log(" * Found " + path + ".txt in " + service_vault_dir.name +" to handle request (Raw TXT Mode) [Socket " + socket.id + "]");
|
||||
service_vault_found = true;
|
||||
if (!zquiet) console.log(" * Found " + service_vault_file_path + ".txt in " + service_vault_dir.name +" to handle request (Raw TXT Mode) [Socket " + socket.id + "]");
|
||||
request_is_async = true;
|
||||
fs.readFile(path + ".txt", 'Utf-8', function (err, file_raw) {
|
||||
fs.readFile(service_vault_file_path + ".txt", 'Utf-8', function (err, file_raw) {
|
||||
if (file_raw.indexOf("\n\n") > 0) {
|
||||
// split headers and data by newline (unix format)
|
||||
var file_raw_split = file_raw.split("\n\n");
|
||||
@@ -129,32 +128,35 @@ async function processPath(socket, path, request_headers = new Array(), service_
|
||||
}
|
||||
sendToClient(socket, headers, data);
|
||||
});
|
||||
} else if (fs.existsSync(path + ".js")) {
|
||||
service_vault_found = true;
|
||||
} else if (fs.existsSync(service_vault_file_path + ".js")) {
|
||||
// synchronous js scripting, process with vars, must set 'headers' and 'data' appropriately.
|
||||
// loaded script will have r/w access to any JavaScript vars this function does.
|
||||
// request headers are in an array named `request_headers`.
|
||||
// Query arguments in `request_headers.query`
|
||||
if (!zquiet) console.log(" * Found " + path + ".js in " + service_vault_dir.name + " to handle request (JS Interpreter mode) [Socket " + socket.id + "]");
|
||||
// Can upgrade to asynchronous by setting `request_is_async` to `true`
|
||||
// In Asynchronous mode, you are expected to call sendToClient(socket,headers,data) by the end of your script
|
||||
// `socket` is already defined and should be passed-through.
|
||||
service_vault_found = true;
|
||||
if (!zquiet) console.log(" * Found " + service_vault_file_path + ".js in " + service_vault_dir.name + " to handle request (JS Interpreter mode) [Socket " + socket.id + "]");
|
||||
// expose var service_dir for script path to the root of the wtv-service
|
||||
var service_dir = service_vault_dir.path.replace(/\\/g, "/") + "/" + service_name;
|
||||
socket_sessions[socket.id].starttime = Math.floor(new Date().getTime() / 1000);
|
||||
var jscript_eval = fs.readFileSync(path + ".js").toString();
|
||||
var jscript_eval = fs.readFileSync(service_vault_file_path + ".js").toString();
|
||||
eval(jscript_eval);
|
||||
if (request_is_async && !zquiet) console.log(" * Script requested Asynchronous mode");
|
||||
}
|
||||
else if (fs.existsSync(path + ".html")) {
|
||||
service_vault_found = true;
|
||||
else if (fs.existsSync(service_vault_file_path + ".html")) {
|
||||
// Standard HTML with no headers, WTV Style
|
||||
if (!zquiet) console.log(" * Found " + path + ".html in " + service_vault_dir.name +" to handle request (HTML Mode) [Socket " + socket.id + "]");
|
||||
service_vault_found = true;
|
||||
if (!zquiet) console.log(" * Found " + service_vault_file_path + ".html in " + service_vault_dir.name +" to handle request (HTML Mode) [Socket " + socket.id + "]");
|
||||
request_is_async = true;
|
||||
headers = "200 OK\n"
|
||||
headers += "Content-Type: text/html"
|
||||
fs.readFile(path + ".html", null, function (err, data) {
|
||||
fs.readFile(service_vault_file_path + ".html", null, function (err, data) {
|
||||
sendToClient(socket, headers, data);
|
||||
});
|
||||
}
|
||||
// 'headers' and 'data' should both be set with content by this point!
|
||||
// either `request_is_async`, or `headers` and `data` MUST be defined by this point!
|
||||
});
|
||||
} catch (e) {
|
||||
var errpage = doErrorPage(400);
|
||||
@@ -183,7 +185,7 @@ async function processPath(socket, path, request_headers = new Array(), service_
|
||||
}
|
||||
}
|
||||
|
||||
function processSSID(obj) {
|
||||
function filterSSID(obj) {
|
||||
if (minisrv_config.config.hide_ssid_in_logs) {
|
||||
if (typeof (obj) == "string") {
|
||||
if (obj.substr(0, 8) == "MSTVSIMU") {
|
||||
@@ -243,14 +245,14 @@ async function processURL(socket, request_headers) {
|
||||
reqverb = "Psuedo-encrypted " + reqverb;
|
||||
}
|
||||
if (ssid != null) {
|
||||
console.log(" * " + reqverb + " for " + request_headers.request_url + " from WebTV SSID " + (await processSSID(ssid)), 'on', socket.id);
|
||||
console.log(" * " + reqverb + " for " + request_headers.request_url + " from WebTV SSID " + (await filterSSID(ssid)), 'on', socket.id);
|
||||
} else {
|
||||
console.log(" * " + reqverb + " for " + request_headers.request_url, 'on', socket.id);
|
||||
}
|
||||
// assume webtv since there is a :/ in the GET
|
||||
var service_name = shortURL.split(':/')[0];
|
||||
var urlToPath = service_name + "/" + shortURL.split(':/')[1];
|
||||
if (zshowheaders) console.log(" * Incoming headers on socket ID", socket.id, (await processSSID(request_headers)));
|
||||
if (zshowheaders) console.log(" * Incoming headers on socket ID", socket.id, (await filterSSID(request_headers)));
|
||||
processPath(socket, urlToPath, request_headers, service_name);
|
||||
} else if (shortURL.indexOf('http://') >= 0 || shortURL.indexOf('https://') >= 0) {
|
||||
doHTTPProxy(socket, request_headers);
|
||||
@@ -266,8 +268,8 @@ async function processURL(socket, request_headers) {
|
||||
}
|
||||
|
||||
async function doHTTPProxy(socket, request_headers) {
|
||||
if (zshowheaders) console.log("HTTP Proxy: Client Request Headers on socket ID", socket.id, (await processSSID(request_headers)));
|
||||
var request_type = request_headers.request.indexOf('https://') ? 'http' : 'https'
|
||||
var request_type = (request_headers.request_url.substring(0,5) == 'https') ? 'https' : 'http'
|
||||
if (zshowheaders) console.log(request_type.toUpperCase() +" Proxy: Client Request Headers on socket ID", socket.id, (await filterSSID(request_headers)));
|
||||
switch (request_type) {
|
||||
case "https":
|
||||
var proxy_agent = https;
|
||||
@@ -309,10 +311,15 @@ async function doHTTPProxy(socket, request_headers) {
|
||||
}
|
||||
|
||||
if (minisrv_config.services[request_type].use_external_proxy && minisrv_config.services[request_type].external_proxy_port) {
|
||||
options.host = minisrv_config.services[request_type].external_proxy_host;
|
||||
options.port = minisrv_config.services[request_type].external_proxy_port;
|
||||
options.path = request_headers.request.split(' ')[1];
|
||||
options.headers.Host = request_data.host;
|
||||
if (minisrv_config.services[request_type].external_proxy_is_socks) {
|
||||
var ProxyAgent = require('proxy-agent');
|
||||
options.agent = new ProxyAgent("socks://" + (minisrv_config.services[request_type].external_proxy_host || "127.0.0.1") + ":" + minisrv_config.services[request_type].external_proxy_port);
|
||||
} else {
|
||||
options.host = minisrv_config.services[request_type].external_proxy_host;
|
||||
options.port = minisrv_config.services[request_type].external_proxy_port;
|
||||
options.path = request_headers.request.split(' ')[1];
|
||||
options.headers.Host = request_data.host;
|
||||
}
|
||||
}
|
||||
const req = proxy_agent.request(options, function (res) {
|
||||
var data = [];
|
||||
@@ -367,7 +374,7 @@ async function doHTTPProxy(socket, request_headers) {
|
||||
}
|
||||
}
|
||||
|
||||
async function headerStringToObj(headers, response = false) {
|
||||
function headerStringToObj(headers, response = false) {
|
||||
var inc_headers = 0;
|
||||
var headers_obj = new Array();
|
||||
var headers_obj_pre = headers.split("\n");
|
||||
@@ -403,7 +410,7 @@ async function sendToClient(socket, headers_obj, data) {
|
||||
if (typeof (data) === 'undefined') data = '';
|
||||
if (typeof (headers_obj) === 'string') {
|
||||
// string to header object
|
||||
headers_obj = await headerStringToObj(headers_obj, true);
|
||||
headers_obj = headerStringToObj(headers_obj, true);
|
||||
}
|
||||
|
||||
// add Connection header if missing, default to Keep-Alive
|
||||
@@ -448,7 +455,7 @@ async function sendToClient(socket, headers_obj, data) {
|
||||
|
||||
|
||||
// header object to string
|
||||
if (zshowheaders) console.log(" * Outgoing headers on socket ID", socket.id, (await processSSID(headers_obj)));
|
||||
if (zshowheaders) console.log(" * Outgoing headers on socket ID", socket.id, (await filterSSID(headers_obj)));
|
||||
Object.keys(headers_obj).forEach(function (k) {
|
||||
if (k == "http_response") {
|
||||
headers += headers_obj[k] + "\r\n";
|
||||
@@ -520,10 +527,11 @@ function moveObjectElement(currentKey, afterKey, obj) {
|
||||
}
|
||||
|
||||
function headersAreStandard(string, verbose = false) {
|
||||
// the test will see the binary compressed/enrypted data as ASCII, so a generic "isAscii"
|
||||
// is not suffuicent. This checks for characters expected in unecrypted headers, and returns
|
||||
// true only if every character in the string matches the regex. Once we know the string is binary
|
||||
// we can better process it with the raw base64 data in processRequest() below.
|
||||
// a generic "isAscii" check is not sufficient, as the test will see the binary
|
||||
// compressed / encrypted data as ASCII. This function checks for characters expected
|
||||
// in unencrypted headers, and returns true only if every character in the string matches
|
||||
// the regex. Once we know the string is binary, we can better process it with the
|
||||
// raw base64 or hex data in processRequest() below.
|
||||
return /^([A-Za-z0-9\+\/\=\-\.\,\ \"\;\:\?\&\r\n\(\)\%\<\>\_]{8,})$/.test(string);
|
||||
}
|
||||
|
||||
@@ -540,7 +548,7 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals
|
||||
data = data.split("\n\n")[0];
|
||||
}
|
||||
if (headersAreStandard(data)) {
|
||||
headers = await headerStringToObj(data);
|
||||
headers = headerStringToObj(data);
|
||||
} else if (!returnHeadersBeforeSecure) {
|
||||
// if its a POST request, assume its a binary blob and not encrypted (dangerous)
|
||||
if (!encryptedRequest) {
|
||||
@@ -615,7 +623,7 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals
|
||||
}
|
||||
if (socket_sessions[socket.id].secure != true) {
|
||||
// first time so reroll sessions
|
||||
if (zdebug) console.log(" # [ SECURE ON BLOCK (" + socket.id + ")]");
|
||||
if (zdebug) console.log(" # [ SECURE ON BLOCK (" + socket.id + ") ]");
|
||||
socket_sessions[socket.id].secure = true;
|
||||
}
|
||||
if (!headers.request_url) {
|
||||
@@ -642,7 +650,8 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals
|
||||
ssid_sessions[socket.ssid].set("box-does-psuedo-encryption", false);
|
||||
var dec_data = CryptoJS.lib.WordArray.create(socket_sessions[socket.id].wtvsec.Decrypt(0, enc_data))
|
||||
var secure_headers = await processRequest(socket, dec_data.toString(CryptoJS.enc.Hex), true);
|
||||
if (zdebug) console.log(" # Encrypted Request (SECURE ON)", "on", socket.id, secure_headers);
|
||||
if (zdebug) console.log(" # Encrypted Request (SECURE ON)", "on", socket.id);
|
||||
if (zshowheaders) console.log(secure_headers);
|
||||
if (!secure_headers.request) {
|
||||
socket_sessions[socket.id].secure = false;
|
||||
var errpage = doErrorPage(400);
|
||||
@@ -675,8 +684,9 @@ wtv-visit: client:relog
|
||||
Content-type: text/html`;
|
||||
data = '';
|
||||
*/
|
||||
delete socket_sessions[socket.id].wtvsec;
|
||||
socket_sessions[socket.id].secure = false
|
||||
socket_sessions[socket.id].close_me = true;
|
||||
delete socket_sessions[socket.id].wtvsec;
|
||||
sendToClient(socket, headers, data);
|
||||
} else {
|
||||
processURL(socket, headers);
|
||||
@@ -723,11 +733,10 @@ async function cleanupSocket(socket) {
|
||||
var fuckyou = ssid_sessions[socket.ssid].sockets;
|
||||
if (ssid_sessions[socket.ssid].sockets.length === 0 && ssid_sessions[socket.ssid].get("wtvsec_login")) {
|
||||
// if last socket for SSID disconnected, destroy login session
|
||||
if (!zquiet) console.log(" * Last socket from WebTV SSID", processSSID(socket.ssid),"disconnected, destroying initial WTVSec instance");
|
||||
if (!zquiet) console.log(" * Last socket from WebTV SSID", filterSSID(socket.ssid),"disconnected, destroying initial WTVSec instance");
|
||||
ssid_sessions[socket.ssid].delete("wtvsec_login");
|
||||
}
|
||||
}
|
||||
;
|
||||
socket.end();
|
||||
} catch (e) {
|
||||
console.log(" # Could not clean up socket data for socket ID", socket.id, e);
|
||||
@@ -737,12 +746,22 @@ async function cleanupSocket(socket) {
|
||||
|
||||
async function handleSocket(socket) {
|
||||
// create unique socket id with client address and port
|
||||
|
||||
socket.id = parseInt(crc16('CCITT-FALSE', Buffer.from(String(socket.remoteAddress) + String(socket.remotePort), "utf8")).toString(16), 16);
|
||||
socket_sessions[socket.id] = [];
|
||||
socket.setEncoding('hex'); //set data encoding (either 'ascii', 'utf8', or 'base64')
|
||||
socket.setEncoding('hex'); //set data encoding (Text: 'ascii', 'utf8' ~ Binary: 'hex', 'base64' (do not trust 'binary' encoding))
|
||||
|
||||
// NOTE: As it stands we use a 'timeout' to start processing data when we have not recieved any data
|
||||
// from the client in X time (defined in config, in milliseconds). The problem with this is in the case of
|
||||
// a modem retrain during a request.
|
||||
|
||||
// TODO: Properly know when client is done sending data, by parsing headers.
|
||||
// Caveat of this is that sometimes the Content-length header does not exist, or will be encrypted.
|
||||
|
||||
socket.on('data', function (data_hex) {
|
||||
socket.setTimeout(minisrv_config.config.socket_timeout);
|
||||
socket.setTimeout(minisrv_config.config.socket_timeout); // the timeout mentioned above
|
||||
|
||||
// Store all received data into a buffer. Kind of misleading as its not a true JS Buffer
|
||||
// but instead a CryptoJS WordArray
|
||||
if (socket_sessions[socket.id].buffer) {
|
||||
socket_sessions[socket.id].buffer.concat(CryptoJS.enc.Hex.parse(data_hex));
|
||||
} else {
|
||||
@@ -753,6 +772,7 @@ async function handleSocket(socket) {
|
||||
socket.on('timeout', async function () {
|
||||
// start the async chain
|
||||
if (socket_sessions[socket.id].buffer) {
|
||||
// process the request if the buffer exists
|
||||
processRequest(this, socket_sessions[socket.id].buffer.toString(CryptoJS.enc.Hex));
|
||||
}
|
||||
});
|
||||
@@ -762,6 +782,7 @@ async function handleSocket(socket) {
|
||||
});
|
||||
|
||||
socket.on('end', function () {
|
||||
// Attempt to clean up all of our WTVSec instances
|
||||
cleanupSocket(socket);
|
||||
});
|
||||
}
|
||||
@@ -793,6 +814,9 @@ function returnAbsolsutePath(path) {
|
||||
return path;
|
||||
}
|
||||
|
||||
|
||||
// SERVER START
|
||||
|
||||
var z_title = "zefie's wtv minisrv v" + require('./package.json').version;
|
||||
console.log("**** Welcome to " + z_title + " ****");
|
||||
console.log(" *** Reading global configuration...");
|
||||
|
||||
Reference in New Issue
Block a user