- wtv-cookie support
 - further development will be in dev branch (may rebase alot)
 - compression not yet ready, leave it disabled
 - update: do not delete WTVSec on last socket, instead recreate on prereg
 - update: clean up SSID session data only if client is not seen for 3 minutes
 - update: add shouldWeCompress() function
 - update: tweak lzpf (still corrupted)
 - update: rename wtv-setup:/get to wtv-setup:/get-settings
 - update: add additional headers to wtv-setup:/get-settings
 - update: add initial blank wtv-music:/get-playlist
 - update wtv-tricks system
   - Info now shows Guest Mode or Subscriber Info
   - Implemented wtv-tricks:/unregister
   - Implemented wtv-tricks:/register
   - Show correct link in wtv-tricks:/tricks based on Guest Mode status
 - config.json: enable compression by default
 - WTVP does not use \r, so swapping the internal header's usage for now. May remove internal header in future update
 - lzpf: this doesn't fix anything but doesn't break it more either :)
  - renamed some functions
  - fixed some param documentation
  - added ConvertToBuffer function
 - WTVSec Updates
  - optimize WordArray to Buffer functions
  - update documentation in WTVSec
  - update WTVSec barrowed function in WTVLzpf
  - removed NewRC4Session, was a pointless alias to SecureOn
This commit is contained in:
zefie
2021-08-07 17:04:17 -04:00
parent 622afce5c7
commit df472ab91f
39 changed files with 794 additions and 387 deletions

View File

@@ -99,12 +99,68 @@ function doErrorPage(code, data = null) {
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";
var file_ext = getFileExt(path).toLowerCase();
// process WebTV overrides, fall back to generic mime lookup
switch (file_ext) {
case "aif":
return "audio/x-aif";
case "aifc":
return "audio/x-aifc";
case "aiff":
return "audio/x-aiff";
case "ani":
return "x-wtv-animation";
case "brom":
return "binary/x-wtv-bootrom";
case "cdf":
return "application/netcdf";
case "dat":
return "binary/cache-data";
case "dl":
return "wtv/download-list";
case "gsm":
return "audio/x-gsm";
case "gz":
return "application/gzip";
case "ini":
return "wtv/jack-configuration";
case "mips-code":
return "code/x-wtv-code-mips";
case "o":
return "binary/x-wtv-approm";
case "ram":
return "audio/x-pn-realaudio";
case "rom":
return "binary/x-wtv-flashblock";
case "rsp":
return "wtv/jack-response";
case "swa":
case "swf":
return "application/x-shockwave-flash";
case "srf":
case "spl":
return "wtv/jack-data";
case "ttf":
return "wtv/jack-fonts";
case "tvch":
return "wtv/tv-channels";
case "tvl":
return "wtv/tv-listings";
case "tvsl":
return "wtv/tv-smartlinks";
case "wad":
return "binary/doom-data";
case "mp2":
case "hsb":
case "rmf":
case "s3m":
case "mod":
case "xm":
return "application/Music";
}
// if we reach here, its not a WebTV specific override
// or we are not yet aware of said override
return mime.lookup(path);
}
@@ -260,9 +316,10 @@ async function processURL(socket, request_headers) {
if (qraw.length > 0) {
qraw = qraw.split("&");
for (let i = 0; i < qraw.length; i++) {
var k = qraw[i].split("=")[0];
if (k) {
request_headers.query[k] = qraw[i].split("=")[1];
var qraw_split = qraw[i].split("=");
if (qraw_split.length == 2) {
var k = qraw_split[0];
request_headers.query[k] = unescape(qraw[i].split("=")[1].replace(/\+/g,"%20"));
}
}
}
@@ -271,23 +328,25 @@ async function processURL(socket, request_headers) {
}
if (request_headers.post_data) {
if (headersAreStandard(request_headers.post_data.toString(CryptoJS.enc.Utf8))) {
if (request_headers.post_data.toString(CryptoJS.enc.Utf8).indexOf('=')) {
if (request_headers.post_data.toString(CryptoJS.enc.Utf8).indexOf('&')) {
var qraw = request_headers.post_data.toString(CryptoJS.enc.Utf8).split('&');
var post_data_string = request_headers.post_data.toString(CryptoJS.enc.Utf8).replace("\0", "");
if (isUnencryptedString(post_data_string)) {
if (post_data_string.indexOf('=')) {
if (post_data_string.indexOf('&')) {
var qraw = post_data_string.split('&');
if (qraw.length > 0) {
for (let i = 0; i < qraw.length; i++) {
var k = qraw[i].split("=")[0];
if (k) {
request_headers.query[k] = qraw[i].split("=")[1];
var qraw_split = qraw[i].split("=");
if (qraw_split.length == 2) {
var k = qraw_split[0];
request_headers.query[k] = unescape(qraw[i].split("=")[1].replace(/\+/g, "%20"));
}
}
}
} else {
var qraw = request_headers.post_data.toString(CryptoJS.enc.Utf8);
var k = qraw[i].split("=")[0];
if (k) {
request_headers.query[k] = qraw[i].split("=")[1];
var qraw_split = post_data_string.split("=");
if (qraw_split.length == 2) {
var k = qraw_split[0];
request_headers.query[k] = unescape(qraw_split[1].replace(/\+/g, "%20"));
}
}
}
@@ -495,10 +554,10 @@ function headerStringToObj(headers, response = false) {
return headers_obj;
}
async function sendToClient(socket, headers_obj, data, compress_data = false) {
var headers = "";
var wni_style_content_length = false;
async function sendToClient(socket, headers_obj, data) {
var compress_data = false;
var headers = "";
var content_length = 0;
if (typeof (data) === 'undefined') data = '';
if (typeof (headers_obj) === 'string') {
// string to header object
@@ -517,72 +576,44 @@ async function sendToClient(socket, headers_obj, data, compress_data = false) {
headers_obj = moveObjectElement('Connection', 'http_response', headers_obj);
}
if (headers_obj['minisrv-already-compressed'] && wni_style_content_length) {
content_length = headers_obj["Content-length"];
} else {
var content_length = 0;
if (typeof data.length !== 'undefined') {
content_length = data.length;
} else if (typeof data.byteLength !== 'undefined') {
content_length = data.byteLength;
}
var clen = 0;
if (typeof data.length !== 'undefined') {
clen = data.length;
} else if (typeof data.byteLength !== 'undefined') {
clen = data.byteLength;
}
// fix captialization of Content-Type header. May be unnecessary.
// fix captialization
if (headers_obj["Content-type"]) {
headers_obj["Content-Type"] = headers_obj["Content-type"];
delete headers_obj["Content-type"];
}
/*
// check if client reports it supports compressed data
// if box can do compression, see if its worth enabling
if (ssid_sessions[socket.ssid].capabilities) {
if (ssid_sessions[socket.ssid].capabilities['client-can-receive-compressed-data']) {
// if the client reports it supports compression, check the Content-Type
// of the file we are sending to see if its worth compressing
if (ssid_sessions[socket.ssid].capabilities['client-can-receive-compressed-data'] && minisrv_config.config.enable_lzpf_compression) {
compress_data = shouldWeCompress(headers_obj["Content-Type"]);
}
}
*/
// compress if needed, and if not already compressed
if (compress_data && content_length > 0 && !headers_obj['minisrv-already-compressed']) {
if (zdebug) console.log(" # Uncompressed data length:", content_length);
// compress if needed
if (compress_data && clen > 0) {
content_length = clen;
headers_obj["wtv-lzpf"] = 0;
var wtvcomp = new WTVLzpf();
// we expect the compressed data to be smaller or at most equal to the source size
// so we set our initial buffer size to the source size
var compressed_data = new Buffer.alloc(content_length);
wtvcomp.on('data', (data, length, offset, complete) => {
// put data received into buffer
data.copy(compressed_data, offset, 0, length);
if (complete !== false) {
if (zdebug) console.log(" # Compressed data length:", complete);
// now that we have all of the compressed data, copy it to a new buffer
// of the correct length, and clean up the original buffer.
data = new Buffer.alloc(complete);
compressed_data.copy(data, 0, 0, compressed_data.byteLength);
compressed_data, wtvcomp = null;
// internal header to tell ourselves to not compress again
headers_obj['minisrv-already-compressed'] = true;
if (wni_style_content_length) headers_obj["Content-length"] = content_length;
sendToClient(socket, headers_obj, data);
}
});
wtvcomp.Compress(data);
return;
data = wtvcomp.Compress(data);
wtvcomp = null; // Makes the garbage gods happy so it cleans up our mess
}
// clean up internal header for compression
if (headers_obj['minisrv-already-compressed']) delete headers_obj['minisrv-already-compressed'];
// encrypt if needed
if (socket_sessions[socket.id].secure == true) {
headers_obj["wtv-encrypted"] = 'true';
headers_obj = moveObjectElement('wtv-encrypted', 'Connection', headers_obj);
if (content_length > 0 && socket_sessions[socket.id].wtvsec) {
if (clen > 0 && socket_sessions[socket.id].wtvsec) {
if (!zquiet) console.log(" * Encrypting response to client ...")
var enc_data = socket_sessions[socket.id].wtvsec.Encrypt(1, data);
data = enc_data;
@@ -594,8 +625,14 @@ async function sendToClient(socket, headers_obj, data, compress_data = false) {
if (headers_obj["Content-Length"]) delete headers_obj["Content-Length"];
if (headers_obj["Content-length"]) delete headers_obj["Content-length"];
// On the WNI server this is the length before compression but we're using the length after compression.
// It matches the HTTP spec anyway so leaving.
if (content_length == 0) {
if (typeof data.length !== 'undefined') {
content_length = data.length;
} else if (typeof data.byteLength !== 'undefined') {
content_length = data.byteLength;
}
}
headers_obj["Content-length"] = content_length;
if (ssid_sessions[socket.ssid]) {
@@ -610,15 +647,13 @@ async function sendToClient(socket, headers_obj, data, compress_data = false) {
}
}
// internal header to determine EOL type. bf0app upgrader does not like \r, while the rest of the WebTV world does.
// set header 'minisrv-use-carriage-return' to true to disable \r for this specific transfer.
var end_of_line = "\n";
if (!headers_obj['minisrv-use-carriage-return'] || headers_obj['minisrv-use-carriage-return'] != "false") end_of_line = "\r\n";
if (headers_obj['minisrv-use-carriage-return'] == "true") end_of_line = "\r\n";
if (headers_obj['minisrv-use-carriage-return']) delete headers_obj['minisrv-use-carriage-return'];
if (end_of_line == "\n" && zdebug) console.log(" * Script requested to send headers without carriage return (bf0app hack)");
if (end_of_line == "\r\n" && zdebug) console.log(" * Script requested to send headers with carriage return (out of WTVP Spec)");
// convert header object back to string
// header object to string
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") {
@@ -643,7 +678,7 @@ async function sendToClient(socket, headers_obj, data, compress_data = false) {
if (zquiet) var verbosity_mod = (headers_obj["wtv-encrypted"] == 'true') ? " encrypted response" : "";
if (socket_sessions[socket.id].secure_headers == true) {
// encrypt headers
if (zquiet)verbosity_mod += " with encrypted headers";
if (zquiet) verbosity_mod += " with encrypted headers";
var enc_headers = socket_sessions[socket.id].wtvsec.Encrypt(1, headers + end_of_line);
socket.write(new Uint8Array(concatArrayBuffer(enc_headers, data)));
} else {
@@ -660,7 +695,7 @@ async function sendToClient(socket, headers_obj, data, compress_data = false) {
if (socket_sessions[socket.id].post_data) delete socket_sessions[socket.id].post_data;
if (socket_sessions[socket.id].post_data_length) delete socket_sessions[socket.id].post_data_length;
if (socket_sessions[socket.id].post_data_percents_shown) delete socket_sessions[socket.id].post_data_percents_shown;
if (socket_sessions[socket.id].close_me) socket.end();
if (headers_obj["Connection"]) {
if (headers_obj["Connection"].toLowerCase() == "close" || wtv_connection_close == "true") {
@@ -712,7 +747,7 @@ function moveObjectElement(currentKey, afterKey, obj) {
if (next !== -1) return result; else return obj;
}
function headersAreStandard(string, verbose = false) {
function isUnencryptedString(string, verbose = false) {
// 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
@@ -745,7 +780,7 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq
} else {
data = data.split("\n\n")[0];
}
if (headersAreStandard(data)) {
if (isUnencryptedString(data)) {
if (headers.length != 0) {
var new_header_obj = headerStringToObj(data);
Object.keys(new_header_obj).forEach(function (k, v) {
@@ -758,7 +793,7 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq
} else if (!skipSecure) {
// if its a POST request, assume its a binary blob and not encrypted (dangerous)
if (!encryptedRequest) {
// its not a POST and it failed the headersAreStandard test, so we think this is an encrypted blob
// its not a POST and it failed the isUnencryptedString test, so we think this is an encrypted blob
if (socket_sessions[socket.id].secure != true) {
// first time so reroll sessions
if (zdebug) console.log(" # [ UNEXPECTED BINARY BLOCK ] First sign of encryption, re-creating RC4 sessions for socket id", socket.id);
@@ -917,6 +952,7 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq
if (zdebug) console.log(" # New ticket from client");
ssid_sessions[socket.ssid].data_store.wtvsec_login.ticket_b64 = headers["wtv-ticket"];
ssid_sessions[socket.ssid].data_store.wtvsec_login.DecodeTicket(ssid_sessions[socket.ssid].data_store.wtvsec_login.ticket_b64);
ssid_sessions[socket.ssid].data_store.wtvsec_login.set_incarnation(headers["wtv-incarnation"]);
}
}
}
@@ -951,7 +987,7 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq
}
var enc_data = CryptoJS.enc.Hex.parse(data_hex.substring(header_length * 2));
if (enc_data.sigBytes > 0) {
if (headersAreStandard(enc_data.toString(CryptoJS.enc.Latin1), (!skipSecure && !encryptedRequest))) {
if (isUnencryptedString(enc_data.toString(CryptoJS.enc.Latin1), (!skipSecure && !encryptedRequest))) {
// some builds (like our targeted 3833), send SECURE ON but then unencrypted headers
if (zdebug) console.log(" # Psuedo-encrypted Request (SECURE ON)", "on", socket.id);
// don't actually encrypt output
@@ -1028,33 +1064,34 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq
var post_string = "POST";
if (socket_sessions[socket.id].secure == true) {
post_string = "Encrypted " + post_string;
} else {
// if the request is not encrypted, the client may have just sent the data with the primary headers, so lets look for that.
if (data_hex.indexOf("0d0a0d0a") != -1) socket_sessions[socket.id].post_data = data_hex.substring(data_hex.indexOf("0d0a0d0a") + 8);
if (data_hex.indexOf("0a0a") != -1) socket_sessions[socket.id].post_data = data_hex.substring(data_hex.indexOf("0a0a") + 4);
}
if (socket_sessions[socket.id].post_data.length == (socket_sessions[socket.id].post_data_length * 2)) {
// got all expected data
if (socket_sessions[socket.id].expecting_post_data) delete socket_sessions[socket.id].expecting_post_data;
console.log(" * Incoming", post_string, "request on", socket.id, "from", filterSSID(socket.ssid), "to", headers['request_url'], "(got all expected", socket_sessions[socket.id].post_data_length, "bytes of data from client already)");
headers.post_data = CryptoJS.enc.Hex.parse(socket_sessions[socket.id].post_data);
if (socket_sessions[socket.id].headers) delete socket_sessions[socket.id].headers;
processURL(socket, headers);
} else {
// expecting more data (see below)
socket_sessions[socket.id].expecting_post_data = true;
console.log(" * Incoming", post_string, "request on", socket.id, "from", filterSSID(socket.ssid), "to", headers['request_url'], "(expecting", socket_sessions[socket.id].post_data_length, "bytes of data from client...)");
}
if (socket_sessions[socket.id].post_data.length > (socket_sessions[socket.id].post_data_length * 2)) {
// got too much data ? ... should not ever reach this code
var errpage = doErrorPage(400, "Received too much data in POST request<br>Got " + (socket_sessions[socket.id].post_data.length / 2) + ", expected " + socket_sessions[socket.id].post_data_length);
headers = errpage[0];
data = errpage[1];
sendToClient(socket, headers, data);
return;
}
// the client may have just sent the data with the primary headers, so lets look for that.
if (data_hex.indexOf("0d0a0d0a") != -1) socket_sessions[socket.id].post_data = data_hex.substring(data_hex.indexOf("0d0a0d0a") + 8);
if (data_hex.indexOf("0a0a") != -1) socket_sessions[socket.id].post_data = data_hex.substring(data_hex.indexOf("0a0a") + 4);
}
if (socket_sessions[socket.id].post_data.length == (socket_sessions[socket.id].post_data_length * 2)) {
// got all expected data
if (socket_sessions[socket.id].expecting_post_data) delete socket_sessions[socket.id].expecting_post_data;
console.log(" * Incoming", post_string, "request on", socket.id, "from", filterSSID(socket.ssid), "to", headers['request_url'], "(got all expected", socket_sessions[socket.id].post_data_length, "bytes of data from client already)");
headers.post_data = CryptoJS.enc.Hex.parse(socket_sessions[socket.id].post_data);
if (socket_sessions[socket.id].headers) delete socket_sessions[socket.id].headers;
processURL(socket, headers);
} else {
// expecting more data (see below)
socket_sessions[socket.id].expecting_post_data = true;
console.log(" * Incoming", post_string, "request on", socket.id, "from", filterSSID(socket.ssid), "to", headers['request_url'], "(expecting", socket_sessions[socket.id].post_data_length, "bytes of data from client...)");
}
if (socket_sessions[socket.id].post_data.length > (socket_sessions[socket.id].post_data_length * 2)) {
// got too much data ? ... should not ever reach this code
var errpage = doErrorPage(400, "Received too much data in POST request<br>Got " + (socket_sessions[socket.id].post_data.length / 2) + ", expected " + socket_sessions[socket.id].post_data_length);
headers = errpage[0];
data = errpage[1];
sendToClient(socket, headers, data);
return;
}
return;
} else {
delete socket_sessions[socket.id].headers;
delete socket_sessions[socket.id].post_data;
@@ -1150,7 +1187,7 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq
return;
}
var str_test = enc_data.toString(CryptoJS.enc.Latin1);
if (headersAreStandard(str_test)) {
if (isUnencryptedString(str_test)) {
var dec_data = enc_data;
} else {
var dec_data = CryptoJS.lib.WordArray.create(socket_sessions[socket.id].wtvsec.Decrypt(0, enc_data));
@@ -1303,7 +1340,7 @@ function getGitRevision() {
if (rev.indexOf(':') === -1) {
return rev;
} else {
return fs.readFileSync(__dirname + path.sep + ".." + path.sep + ".git" + path.sep + rev.substring(5)).toString().trim();
return fs.readFileSync(__dirname + path.sep + ".." + path.sep + ".git" + path.sep + rev.substring(5)).toString().trim().substring(0,8) + "-" + rev.split('/').pop();
}
} catch (e) {
return null;
@@ -1312,12 +1349,9 @@ function getGitRevision() {
// SERVER START
var git_commit = getGitRevision()
if (git_commit) {
var z_title = "zefie's wtv minisrv v" + require('./package.json').version + " (git " + git_commit.substring(0,8) + ")";
} else {
var z_title = "zefie's wtv minisrv v" + require('./package.json').version;
}
console.log("**** Welcome to " + z_title + " ****");
var z_title = "zefie's wtv minisrv v" + require('./package.json').version;
if (git_commit) console.log("**** Welcome to " + z_title + " (git " + git_commit + ") ****");
else console.log("**** Welcome to " + z_title + " ****");
console.log(" *** Reading global configuration...");
try {
var minisrv_config = JSON.parse(fs.readFileSync(__dirname + path.sep + "config.json"));