v0.9.18
- numerous bug fixes
- too much to remember
- rewrote sync system yet again
- more classes
- WTVShared class for shared functions
- clientShowAlert class for easy client:showalert urls
- User File Store
- Can upload with PUT commands in wtv-disk
- Programmically access files with new functions in WTVClientSessionData
- TODO: file browser
- other stuff I can't remember
- work on post data bug
- proper gzip download for disk system (aka WNI reinventing the Content-Encoding: gzip wheel)
- send Last-Modified for static files
- send wtv-checksum for all disk system downloads
- update to v90 modem firmware
- offer kflex with `Old` diskmap
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -367,3 +367,4 @@ FodyWeavers.xsd
|
||||
/zefie_wtvp_minisrv/UserServiceVault/*-*/
|
||||
/zefie_wtvp_minisrv/ServiceLogPost/*.log
|
||||
/zefie_wtvp_minisrv/SessionStore/*.json
|
||||
/zefie_wtvp_minisrv/SessionStore/*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
if (socket.ssid != null && !ssid_sessions[socket.ssid].get("wtvsec_login")) {
|
||||
var wtvsec_login = new WTVSec();
|
||||
var wtvsec_login = new WTVSec(minisrv_config);
|
||||
wtvsec_login.IssueChallenge();
|
||||
wtvsec_login.set_incarnation(request_headers["wtv-incarnation"]);
|
||||
ssid_sessions[socket.ssid].set("wtvsec_login", wtvsec_login);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
if (socket.ssid) {
|
||||
if (ssid_sessions[socket.ssid].loadSessionData() == true) {
|
||||
console.log(" * Loaded session data from disk for", filterSSID(socket.ssid))
|
||||
console.log(" * Loaded session data from disk for", wtvshared.filterSSID(socket.ssid))
|
||||
ssid_sessions[socket.ssid].setSessionData("registered", (ssid_sessions[socket.ssid].getSessionData("registered") == true) ? true : false);
|
||||
} else {
|
||||
ssid_sessions[socket.ssid].session_data = {};
|
||||
@@ -20,15 +20,15 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
if (i > 0 && zdebug) console.log(" # Closed", i, "previous sockets for", filterSSID(socket.ssid));
|
||||
if (i > 0 && minisrv_config.config.debug_flags.debug) console.log(" # Closed", i, "previous sockets for", wtvshared.filterSSID(socket.ssid));
|
||||
}
|
||||
}
|
||||
if (ssid_sessions[socket.ssid].data_store.wtvsec_login) {
|
||||
if (zdebug) console.log(" # Recreating primary WTVSec login instance for", filterSSID(socket.ssid));
|
||||
if (minisrv_config.config.debug_flags.debug) console.log(" # Recreating primary WTVSec login instance for", wtvshared.filterSSID(socket.ssid));
|
||||
delete ssid_sessions[socket.ssid].data_store.wtvsec_login;
|
||||
}
|
||||
|
||||
ssid_sessions[socket.ssid].data_store.wtvsec_login = new WTVSec();
|
||||
ssid_sessions[socket.ssid].data_store.wtvsec_login = new WTVSec(minisrv_config);
|
||||
ssid_sessions[socket.ssid].data_store.wtvsec_login.IssueChallenge();
|
||||
ssid_sessions[socket.ssid].data_store.wtvsec_login.set_incarnation(request_headers["wtv-incarnation"] || 1);
|
||||
} else {
|
||||
@@ -115,24 +115,23 @@ if (ssid_sessions[socket.ssid].data_store.wtvsec_login) {
|
||||
gourl = "wtv-head-waiter:/login-stage-two?relogin=true";
|
||||
}
|
||||
|
||||
if (request_headers.query.reconnect) {
|
||||
gourl = null;
|
||||
}
|
||||
|
||||
if (!file_path != null && !zquiet) console.log(" * Sending TellyScript", file_path, "on socket", socket.id);
|
||||
if (request_headers.query.reconnect) gourl = null;
|
||||
|
||||
if (request_headers.query.guest_login) {
|
||||
send_tellyscript = false;
|
||||
gourl += "&guest_login=true"
|
||||
if (gourl != null) gourl += "&guest_login=true"
|
||||
if (request_headers.query.skip_splash) gourl += "&skip_splash=true";
|
||||
}
|
||||
|
||||
if (!file_path != null && send_tellyscript && !minisrv_config.config.debug_flags.quiet) console.log(" * Sending TellyScript", file_path, "on socket", socket.id);
|
||||
|
||||
|
||||
headers = "200 OK\n"
|
||||
if (bf0app_update) headers += "minisrv-use-carriage-return: false\n";
|
||||
headers += "Connection: Keep-Alive\n";
|
||||
headers += "wtv-initial-key: " + ssid_sessions[socket.ssid].data_store.wtvsec_login.challenge_key.toString(CryptoJS.enc.Base64) + "\n";
|
||||
headers += "Content-Type: " + prereg_contype + "\n";
|
||||
headers += "wtv-service: reset\n";
|
||||
if (!request_headers.query.reconnect) headers += "wtv-service: reset\n";
|
||||
if (!bf0app_update) headers += getServiceString('wtv-1800') + "\n";
|
||||
|
||||
if (bf0app_update) headers += getServiceString('wtv-head-waiter', { "flags": "0x00000001" }) + "\n";
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"ModemFirmware": {
|
||||
"base": "file://Disk/Browser/Modem_Firmware/",
|
||||
"location": "content/Modem_Firmware/",
|
||||
"execute": "client:ModemReload",
|
||||
"execute_when": "atEnd",
|
||||
"service_owned": true,
|
||||
"files": [
|
||||
{
|
||||
"file": "file://Disk/Browser/Modem_Firmware/Locale/en-US/modem_firmware.dat.gz",
|
||||
"location": "content/Modem_Firmware/Locale/en-US/v90/modem_firmware.dat.gz"
|
||||
},
|
||||
{
|
||||
"file": "file://Disk/Browser/Modem_Firmware/Locale/ja-JP/modem_firmware.dat.gz"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"ModemFirmware": {
|
||||
"base": "file://Disk/Browser/Modem_Firmware/",
|
||||
"location": "content/Modem_Firmware/",
|
||||
"execute": "client:ModemReload",
|
||||
"execute_when": "atEnd",
|
||||
"service_owned": true,
|
||||
"files": [
|
||||
{
|
||||
"file": "file://Disk/Browser/Modem_Firmware/Locale/en-US/modem_firmware.dat.gz",
|
||||
"location": "content/Modem_Firmware/Locale/en-US/kflex/modem_firmware.dat.gz"
|
||||
},
|
||||
{
|
||||
"file": "file://Disk/Browser/Modem_Firmware/Locale/ja-JP/modem_firmware.dat.gz"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,88 +1,104 @@
|
||||
// todo: async
|
||||
const WTVDownloadList = require("./WTVDownloadList.js");
|
||||
var wtvdl = new WTVDownloadList(minisrv_config, service_name);
|
||||
|
||||
var force_update = (request_headers.query.force == "true") ? true : false;
|
||||
console.log(force_update);
|
||||
if (request_headers['wtv-request-type'] == 'download') {
|
||||
var path = require("path");
|
||||
|
||||
var content_dir = "content/"
|
||||
var diskmap_dir = content_dir + "diskmaps/";
|
||||
|
||||
function generateDownloadList(diskmap_group_data, update_list, diskmap_data) {
|
||||
// create WebTV Download List
|
||||
var newest_file_epoch = 0;
|
||||
var download_list = '';
|
||||
|
||||
if (diskmap_data.execute && diskmap_data.execute_when == "atStart") {
|
||||
download_list += "EXECUTE " + diskmap_data.execute + "\n\n";
|
||||
}
|
||||
|
||||
if (diskmap_data.partition_size) {
|
||||
download_list += "CREATE " + diskmap_data.base + "\n";
|
||||
download_list += "partition-size: " + diskmap_data.partition_size + "\n\n";
|
||||
}
|
||||
|
||||
download_list += "CREATE-GROUP " + diskmap_group_data + "-UPDATE\n";
|
||||
download_list += "state: invalid\n";
|
||||
download_list += "base: " + diskmap_data.base + ".GROUP-UPDATE/\n\n";
|
||||
|
||||
download_list += "CREATE-GROUP " + diskmap_group_data + "\n";
|
||||
download_list += "state: invalid\n";
|
||||
download_list += "service-owned: " + (diskmap_data.service_owned || false) + "\n";
|
||||
download_list += "base: " + diskmap_data.base + "\n\n";
|
||||
|
||||
Object.keys(update_list).forEach(function (k) {
|
||||
if (!update_list[k].invalid) return;
|
||||
download_list += "DELETE " + update_list[k].file.replace(diskmap_data.base, "") + "\n";
|
||||
download_list += "group: " + diskmap_group_data + "\n\n";
|
||||
});
|
||||
|
||||
wtvdl.reset();
|
||||
var files_to_send = 0;
|
||||
Object.keys(update_list).forEach(function (k) {
|
||||
if (update_list[k].checksum_match && !force_update) return;
|
||||
if (!update_list[k].invalid && !force_update) return;
|
||||
download_list += "DISPLAY " + update_list[k].display + "\n\n";
|
||||
download_list += "GET " + update_list[k].file.replace(diskmap_data.base, "") + "\n";
|
||||
download_list += "group: " + diskmap_group_data + "-UPDATE\n";
|
||||
download_list += "location: " + service_name + ":/" + update_list[k].location + "\n";
|
||||
download_list += "file-permission: r\n"
|
||||
download_list += "wtv-checksum: " + update_list[k].checksum + "\n";
|
||||
download_list += "service-source-location: /webtv/content/" + service_name.replace("wtv-", "") + "d/" + update_list[k].location + "\n";
|
||||
download_list += "client-dest-location: " + update_list[k].file + "\n\n";
|
||||
files_to_send++;
|
||||
});
|
||||
|
||||
download_list += "CREATE-GROUP " + diskmap_group_data + "\n";
|
||||
download_list += "state: invalid\n";
|
||||
download_list += "service-owned: " + (diskmap_data.service_owned || false) + "\n";
|
||||
download_list += "base: " + diskmap_data.base + "\n\n";
|
||||
|
||||
|
||||
Object.keys(update_list).forEach(function (k) {
|
||||
if (!update_list[k].invalid) return;
|
||||
download_list += "RENAME " + update_list[k].file.replace(diskmap_data.base, "") + "\n";
|
||||
download_list += "group: " + diskmap_group_data + "-UPDATE\n";
|
||||
download_list += "destination-group: " + diskmap_group_data + "\n";
|
||||
download_list += "location: " + update_list[k].file.replace(diskmap_data.base, "") + "\n\n";
|
||||
});
|
||||
|
||||
download_list += "SET-GROUP " + diskmap_group_data + "\n";
|
||||
download_list += "state: ok\n";
|
||||
download_list += "version: " + diskmap_data.version + "\n";
|
||||
download_list += "last-checkup-time: " + new Date().toUTCString().replace("GMT", "+0000") + "\n\n";
|
||||
|
||||
if (diskmap_data.execute && diskmap_data.execute_when == "atEnd") {
|
||||
download_list += "EXECUTE " + diskmap_data.execute + "\n\n";
|
||||
// create WebTV Download List
|
||||
if (diskmap_data.execute && diskmap_data.execute_when) {
|
||||
if (diskmap_data.execute_when.toLowerCase().match(/start/)) {
|
||||
wtvdl.execute(diskmap_data.execute);
|
||||
}
|
||||
}
|
||||
|
||||
download_list += "DELETE-GROUP " + diskmap_group_data + "-UPDATE\n\n";
|
||||
download_list += "DELETE " + diskmap_data.base + ".GROUP-UPDATE/\n\n";
|
||||
console.log(download_list);
|
||||
if (diskmap_group_data.display) wtvdl.display(diskmap_group_data.display);
|
||||
|
||||
if (files_to_send > 0) {
|
||||
|
||||
if (diskmap_data.partition_size) {
|
||||
wtvdl.createPartition(diskmap_data.base, diskmap_data.partition_size);
|
||||
}
|
||||
|
||||
if (!diskmap_data.nogroup) {
|
||||
// only send group commands if group mode is enable
|
||||
// useful to disable for PUT
|
||||
wtvdl.createUpdateGroup(diskmap_group_data, diskmap_data.base, "invalid", (diskmap_data.service_owned || false));
|
||||
}
|
||||
|
||||
Object.keys(update_list).forEach(function (k) {
|
||||
// file { "action": "delete" }
|
||||
// Useful to purge files we no longer want on the client
|
||||
if (update_list[k].action != "DELETE") {
|
||||
// skip deleting valid files if we aren't specifically requesting their deletion
|
||||
if (update_list[k].checksum_match && !force_update) return;
|
||||
if (!update_list[k].invalid && !force_update) return;
|
||||
}
|
||||
wtvdl.delete(update_list[k].file.replace(diskmap_data.base, ""), diskmap_group_data);
|
||||
});
|
||||
|
||||
Object.keys(update_list).forEach(function (k) {
|
||||
if (update_list[k].checksum_match && !force_update) return;
|
||||
if (!update_list[k].invalid && !force_update) return;
|
||||
if (update_list[k].display) wtvdl.display(update_list[k].display);
|
||||
switch (update_list[k].action) {
|
||||
case "PUT":
|
||||
wtvdl.put(update_list[k].file.replace(diskmap_data.base, ""), service_name + ":/" + update_list[k].location, update_list[k].display);
|
||||
break;
|
||||
|
||||
case "GET":
|
||||
wtvdl.get(update_list[k].file.replace(diskmap_data.base, ""), update_list[k].file, service_name + ":/" + update_list[k].location, diskmap_group_data, update_list[k].checksum, update_list[k].uncompressed_size || null, update_list[k].original_filename)
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (!diskmap_data.nogroup) {
|
||||
wtvdl.createGroup(diskmap_group_data, diskmap_data.base, "invalid", (diskmap_data.service_owned || false));
|
||||
|
||||
|
||||
// this rename loop is a part of the group system
|
||||
Object.keys(update_list).forEach(function (k) {
|
||||
if (update_list[k].checksum_match && !force_update) return;
|
||||
if (!update_list[k].invalid && !force_update) return;
|
||||
wtvdl.rename(update_list[k].file.replace(diskmap_data.base, ""), update_list[k].file.replace(diskmap_data.base, ""), diskmap_group_data, diskmap_group_data);
|
||||
});
|
||||
|
||||
wtvdl.setGroup(diskmap_group_data, 'ok', diskmap_data.version);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (diskmap_data.execute && diskmap_data.execute_when) {
|
||||
if (diskmap_data.execute_when.toLowerCase().match(/end/)) {
|
||||
wtvdl.execute(diskmap_data.execute);
|
||||
}
|
||||
}
|
||||
|
||||
if (files_to_send > 0) {
|
||||
if (!diskmap_data.nogroup) {
|
||||
wtvdl.deleteGroupUpdate(diskmap_group_data, diskmap_data.base);
|
||||
}
|
||||
}
|
||||
var download_list = wtvdl.getDownloadList();
|
||||
if (minisrv_config.config.show_diskmap) console.log(download_list);
|
||||
return download_list;
|
||||
}
|
||||
|
||||
function processGroup(diskmap_primary_group, diskmap_group_data, diskmap_subgroup = null) {
|
||||
// parse webtv post
|
||||
var output_data = '';
|
||||
var post_data = request_headers.post_data.toString(CryptoJS.enc.Latin1).split("\n");
|
||||
var post_data = new Array();
|
||||
if (request_headers.post_data) post_data = request_headers.post_data.toString(CryptoJS.enc.Latin1).split("\n");
|
||||
var post_data_current_directory = '';
|
||||
var post_data_current_file = false;
|
||||
var post_data_current_group = '';
|
||||
@@ -172,6 +188,8 @@ if (request_headers['wtv-request-type'] == 'download') {
|
||||
if (!fs.existsSync(post_match_file)) post_match_file = null;
|
||||
});
|
||||
|
||||
|
||||
|
||||
var post_match_file_lstat = fs.lstatSync(post_match_file);
|
||||
var post_match_file_data = new Buffer.from(fs.readFileSync(post_match_file, {
|
||||
encoding: null,
|
||||
@@ -180,10 +198,23 @@ if (request_headers['wtv-request-type'] == 'download') {
|
||||
diskmap_group_data.files[k].base = diskmap_group_data.base;
|
||||
diskmap_group_data.files[k].last_modified = (new Date(new Date(post_match_file_lstat.mtime).toUTCString()) / 1000);
|
||||
diskmap_group_data.files[k].content_length = post_match_file_lstat.size;
|
||||
diskmap_group_data.files[k].checksum = CryptoJS.MD5(CryptoJS.lib.WordArray.create(post_match_file_data)).toString(CryptoJS.enc.Hex).toLowerCase();
|
||||
diskmap_group_data.files[k].action = (diskmap_group_data.files[k].action) ? diskmap_group_data.files[k].action.toUpperCase() : "GET";
|
||||
|
||||
if (wtvshared.getFileExt(post_match_file).toLowerCase() == "gz") {
|
||||
// we need the checksum of the uncompressed data
|
||||
var gunzipped = zlib.gunzipSync(post_match_file_data);
|
||||
diskmap_group_data.files[k].checksum = CryptoJS.MD5(CryptoJS.lib.WordArray.create(gunzipped)).toString(CryptoJS.enc.Hex).toLowerCase();
|
||||
var gzip_fn_end = post_match_file_data.indexOf("\0", 10);
|
||||
if (!diskmap_group_data.files[k].dont_extract_filename) {
|
||||
diskmap_group_data.files[k].original_filename = post_match_file_data.toString('utf8', 10, gzip_fn_end);
|
||||
}
|
||||
//diskmap_group_data.files[k].uncompressed_size = gunzipped.byteLength;
|
||||
gunzipped = null;
|
||||
} else {
|
||||
diskmap_group_data.files[k].checksum = CryptoJS.MD5(CryptoJS.lib.WordArray.create(post_match_file_data)).toString(CryptoJS.enc.Hex).toLowerCase();
|
||||
}
|
||||
|
||||
if (parseInt(diskmap_group_data.files[k].last_modified) > newest_file_epoch) newest_file_epoch = parseInt(diskmap_group_data.files[k].last_modified);
|
||||
if (!diskmap_group_data.files[k].display) diskmap_group_data.files[k].display = diskmap_group_data.display;
|
||||
|
||||
diskmap_group_data.files[k].invalid = true;
|
||||
wtv_download_list.push(diskmap_group_data.files[k]);
|
||||
@@ -205,7 +236,7 @@ if (request_headers['wtv-request-type'] == 'download') {
|
||||
return output_data;
|
||||
}
|
||||
|
||||
if (request_headers.query.diskmap && request_headers.query.group && request_headers.post_data) {
|
||||
if (request_headers.query.diskmap && request_headers.query.group) {
|
||||
var diskmap_json_file = null;
|
||||
Object.keys(service_vaults).forEach(function (g) {
|
||||
if (diskmap_json_file != null) return;
|
||||
@@ -243,69 +274,17 @@ if (request_headers['wtv-request-type'] == 'download') {
|
||||
var errpage = doErrorPage(404, "The requested DiskMap does not exist.");
|
||||
headers = errpage[0];
|
||||
data = errpage[1];
|
||||
if (zdebug) console.error(" # " + service_name +":/sync error", "could not find diskmap");
|
||||
if (minisrv_config.config.debug_flags.debug) console.error(" # " + service_name +":/sync error", "could not find diskmap");
|
||||
}
|
||||
} else {
|
||||
var errpage = doErrorPage(400);
|
||||
headers = errpage[0];
|
||||
data = errpage[1];
|
||||
if (zdebug) console.error(" # " + service_name + ":/sync error", "missing query arguments");
|
||||
if (minisrv_config.config.debug_flags.debug) console.error(" # " + service_name + ":/sync error", "missing query arguments");
|
||||
}
|
||||
} else if (request_headers.query.group && request_headers.query.diskmap) {
|
||||
var message = request_headers.query.message || "Retrieving files...";
|
||||
var main_message = request_headers.query.main_message || "Your receiver is downloading files.";
|
||||
headers = `200 OK
|
||||
Content-Type: text/html`;
|
||||
|
||||
data = `
|
||||
<html>
|
||||
<head>
|
||||
<meta
|
||||
http-equiv=refresh
|
||||
content="0;url=client:Fetch?group=${escape(request_headers.query.group)}&source=${service_name}:/sync%3Fdiskmap%3D${escape(escape(request_headers.query.diskmap))}%26force%3D${force_update}&message=${escape(message)}"
|
||||
>
|
||||
<display downloadsuccess="client:ShowAlert?message=Download%20successful%21&buttonlabel1=Okay&buttonaction1=client:goback&image=${minisrv_config.config.service_logo}&noback=true" downloadfail="client:ShowAlert?message=Download%20failed...&buttonlabel1=Okay...&buttonaction1=client:goback&image=${minisrv_config.config.service_logo}&noback=true">
|
||||
<title>Retrieving files...</title>
|
||||
</head>
|
||||
<body bgcolor=#0 text=#42CC55 fontsize=large hspace=0 vspace=0>
|
||||
<table cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td width=104 height=74 valign=middle align=center bgcolor=3B3A4D>
|
||||
<img src="${minisrv_config.config.service_logo}" width=86 height=64>
|
||||
<td width=20 valign=top align=left bgcolor=3B3A4D>
|
||||
<spacer>
|
||||
<td colspan=2 width=436 valign=middle align=left bgcolor=3B3A4D>
|
||||
<font color=D6DFD0 size=+2><blackface><shadow>
|
||||
<spacer type=block width=1 height=4>
|
||||
<br>
|
||||
${message}
|
||||
</shadow>
|
||||
</blackface>
|
||||
</font>
|
||||
<tr>
|
||||
<td width=104 height=20>
|
||||
<td width=20>
|
||||
<td width=416>
|
||||
<td width=20>
|
||||
<tr>
|
||||
<td colspan=2>
|
||||
<td>
|
||||
<font size=+1>
|
||||
${main_message}
|
||||
<p>This may take a while.
|
||||
</font>
|
||||
<tr>
|
||||
<td colspan=2>
|
||||
<td>
|
||||
<br><br>
|
||||
<font color=white>
|
||||
<progressindicator name="downloadprogress"
|
||||
message="Preparing..."
|
||||
height=40 width=250>
|
||||
</font>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
headers = "200 OK\nwtv-connection-close: close\nConnection: close\nContent-Type: text/html";
|
||||
data = wtvdl.getSyncPage(message, request_headers.query.group, request_headers.query.diskmap, main_message, message, force_update)
|
||||
}
|
||||
21
zefie_wtvp_minisrv/ServiceVault/wtv-disk/userstore.js
Normal file
21
zefie_wtvp_minisrv/ServiceVault/wtv-disk/userstore.js
Normal file
@@ -0,0 +1,21 @@
|
||||
if (request_headers.post_data) {
|
||||
if (request_headers.query.partialPath || request_headers.query.path) {
|
||||
if (socket.ssid) {
|
||||
if (ssid_sessions[socket.ssid]) {
|
||||
if (ssid_sessions[socket.ssid].isRegistered()) {
|
||||
var result = ssid_sessions[socket.ssid].storeUserStoreFile(request_headers.query.path || request_headers.query.partialPath, new Buffer.from(request_headers.post_data.toString(CryptoJS.enc.Hex), 'hex'), request_headers.query['last-modified-seconds'] || null, (request_headers.query.no_overwrite) ? false : true);
|
||||
if (result) {
|
||||
headers = "200 OK\n";
|
||||
headers += "Content-Type: text/plain";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!headers) {
|
||||
var errpage = doErrorPage(400)
|
||||
headers = errpage[0];
|
||||
data = errpage[1];
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
const WTVFlashrom = require("./WTVFlashrom.js");
|
||||
request_is_async = true;
|
||||
console.log(request_headers);
|
||||
|
||||
var bf0app_update = false;
|
||||
var request_path = request_headers.request_url.replace(service_name + ":/", "");
|
||||
@@ -15,7 +14,7 @@ if ((romtype == "bf0app" || !romtype) && (bootver == "105" || !bootver)) {
|
||||
}
|
||||
|
||||
if (!ssid_sessions[socket.ssid].data_store.WTVFlashrom) {
|
||||
ssid_sessions[socket.ssid].data_store.WTVFlashrom = new WTVFlashrom(service_vaults, service_name, minisrv_config.services[service_name].use_zefie_server, bf0app_update, minisrv_config.services[service_name].debug);
|
||||
ssid_sessions[socket.ssid].data_store.WTVFlashrom = new WTVFlashrom(minisrv_config, service_vaults, service_name, minisrv_config.services[service_name].use_zefie_server, bf0app_update);
|
||||
}
|
||||
|
||||
ssid_sessions[socket.ssid].data_store.WTVFlashrom.getFlashRom(request_path, function (data, headers) {
|
||||
|
||||
@@ -22,7 +22,7 @@ if (ssid_sessions[socket.ssid].get("wtv-client-rom-type") == "bf0app" && ssid_se
|
||||
}
|
||||
|
||||
if (!ssid_sessions[socket.ssid].data_store.WTVFlashrom) {
|
||||
ssid_sessions[socket.ssid].data_store.WTVFlashrom = new WTVFlashrom(service_vaults, service_name, minisrv_config.services[service_name].use_zefie_server, bf0app_update, minisrv_config.services[service_name].debug);
|
||||
ssid_sessions[socket.ssid].data_store.WTVFlashrom = new WTVFlashrom(minisrv_config, service_vaults, service_name, minisrv_config.services[service_name].use_zefie_server, bf0app_update);
|
||||
}
|
||||
|
||||
ssid_sessions[socket.ssid].data_store.WTVFlashrom.getFlashRom(request_path, function (data, headers) {
|
||||
|
||||
@@ -15,7 +15,7 @@ if ((romtype == "bf0app" || !romtype) && (bootver == "105" || !bootver)) {
|
||||
|
||||
if (request_headers.query.raw || bf0app_update) {
|
||||
if (!ssid_sessions[socket.ssid].data_store.WTVFlashrom) {
|
||||
ssid_sessions[socket.ssid].data_store.WTVFlashrom = new WTVFlashrom(service_vaults, service_name, minisrv_config.services[service_name].use_zefie_server, bf0app_update, minisrv_config.services[service_name].debug);
|
||||
ssid_sessions[socket.ssid].data_store.WTVFlashrom = new WTVFlashrom(minisrv_config, service_vaults, service_name, minisrv_config.services[service_name].use_zefie_server, bf0app_update);
|
||||
}
|
||||
|
||||
ssid_sessions[socket.ssid].data_store.WTVFlashrom.getFlashRom(request_path, function (data, headers) {
|
||||
|
||||
@@ -8,7 +8,7 @@ if (!request_headers.query.path) {
|
||||
headers = errpage[0];
|
||||
data = errpage[1];
|
||||
} else {
|
||||
var wtvflashrom = new WTVFlashrom(service_vaults, service_name, minisrv_config.services[service_name].use_zefie_server, minisrv_config.services[service_name].debug);
|
||||
var wtvflashrom = new WTVFlashrom(minisrv_config, service_vaults, service_name, minisrv_config.services[service_name].use_zefie_server, false, true);
|
||||
var request_path = request_headers.query.path;
|
||||
|
||||
// read flashrom header info into array using WTVFlashrom class
|
||||
|
||||
@@ -5,8 +5,9 @@ var romtype = ssid_sessions[socket.ssid].get("wtv-client-rom-type");
|
||||
url = "client:updateflash?ipaddr=" + minisrv_config.services[service_name].host + "&port=" + minisrv_config.services[service_name].port + "&path=" + escape(service_name + ":/" +request_headers.query.path);
|
||||
if (request_headers.query.numparts) url += escape("&numparts=" + request_headers.query.numparts);
|
||||
}
|
||||
headers = "200 OK\n";
|
||||
headers = "300 OK\n";
|
||||
headers += "wtv-visit: " + url + "\n";
|
||||
headers += "Location: " + url + "\n";
|
||||
headers += "Content-type: text/html";
|
||||
data = '';
|
||||
} else {
|
||||
|
||||
@@ -22,7 +22,7 @@ if (ssid_sessions[socket.ssid].get("wtv-client-rom-type") == "bf0app" && ssid_se
|
||||
}
|
||||
|
||||
if (!ssid_sessions[socket.ssid].data_store.WTVFlashrom) {
|
||||
ssid_sessions[socket.ssid].data_store.WTVFlashrom = new WTVFlashrom(service_vaults, service_name, 0, minisrv_config.services[service_name].use_zefie_server, bf0app_update, minisrv_config.services[service_name].debug);
|
||||
ssid_sessions[socket.ssid].data_store.WTVFlashrom = new WTVFlashrom(minisrv_config, service_vaults, service_name, 0, minisrv_config.services[service_name].use_zefie_server, bf0app_update, minisrv_config.services[service_name].debug);
|
||||
}
|
||||
|
||||
ssid_sessions[socket.ssid].data_store.WTVFlashrom.getFlashRom(request_path, function (data, headers) {
|
||||
|
||||
@@ -32,7 +32,7 @@ const req = https.request(options, function (res) {
|
||||
});
|
||||
|
||||
res.on('error', function (e) {
|
||||
if (!zquiet) console.log(" * Upstream Ultra Willies HTTP Error:", e);
|
||||
if (!minisrv_config.config.debug_flags.quiet) console.log(" * Upstream Ultra Willies HTTP Error:", e);
|
||||
var errpage = doErrorPage(400)
|
||||
headers = errpage[0];
|
||||
data = errpage[1];
|
||||
@@ -40,7 +40,7 @@ const req = https.request(options, function (res) {
|
||||
});
|
||||
|
||||
res.on('end', function () {
|
||||
if (!zquiet) console.log(" * Upstream Ultra Willies HTTP Response:", res.statusCode, res.statusMessage);
|
||||
if (!minisrv_config.config.debug_flags.quiet) console.log(" * Upstream Ultra Willies HTTP Response:", res.statusCode, res.statusMessage);
|
||||
if (request_headers.query.clear_cache) {
|
||||
headers += "\nwtv-expire-all: "+service_name;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ var challenge_response, challenge_header = '';
|
||||
var gourl;
|
||||
|
||||
if (socket.ssid != null && !ssid_sessions[socket.ssid].get("wtvsec_login")) {
|
||||
var wtvsec_login = new WTVSec(1,zdebug);
|
||||
var wtvsec_login = new WTVSec(minisrv_config);
|
||||
wtvsec_login.IssueChallenge();
|
||||
wtvsec_login.set_incarnation(request_headers["wtv-incarnation"]);
|
||||
ssid_sessions[socket.ssid].set("wtvsec_login", wtvsec_login);
|
||||
@@ -16,13 +16,13 @@ if (socket.ssid !== null) {
|
||||
var client_challenge_response = request_headers["wtv-challenge-response"] || null;
|
||||
if (challenge_response && client_challenge_response) {
|
||||
if (challenge_response.toString(CryptoJS.enc.Base64) == client_challenge_response) {
|
||||
console.log(" * wtv-challenge-response success for " + filterSSID(socket.ssid));
|
||||
console.log(" * wtv-challenge-response success for " + wtvshared.filterSSID(socket.ssid));
|
||||
wtvsec_login.PrepareTicket();
|
||||
|
||||
} else {
|
||||
console.log(" * wtv-challenge-response FAILED for " + filterSSID(socket.ssid));
|
||||
if (zdebug) console.log("Response Expected:", challenge_response.toString(CryptoJS.enc.Base64));
|
||||
if (zdebug) console.log("Response Received:", client_challenge_response)
|
||||
console.log(" * wtv-challenge-response FAILED for " + wtvshared.filterSSID(socket.ssid));
|
||||
if (minisrv_config.config.debug_flags.debug) console.log("Response Expected:", challenge_response.toString(CryptoJS.enc.Base64));
|
||||
if (minisrv_config.config.debug_flags.debug) console.log("Response Received:", client_challenge_response)
|
||||
gourl = "wtv-head-waiter:/login?reissue_challenge=true";
|
||||
}
|
||||
} else {
|
||||
@@ -147,6 +147,7 @@ wtv-open-isp-disabled: false
|
||||
wtv-offline-mail-enable: false
|
||||
wtv-demo-mode: 0
|
||||
wtv-wink-deferrer-retries: 3
|
||||
wtv-name-server: 8.8.8.8
|
||||
wtv-visit: ${home_url}
|
||||
Content-Type: text/html`;
|
||||
}
|
||||
@@ -54,7 +54,7 @@ wtv-log-url: wtv-log:/log`;
|
||||
if (challenge_header != "") headers += "\n" + challenge_header;
|
||||
headers += `
|
||||
wtv-relogin-url: wtv-head-waiter:/relogin?relogin=true
|
||||
wtv-reconnect-url: wwtv-head-waiter:/relogin?reconnect=true
|
||||
wtv-reconnect-url: wtv-head-waiter:/relogin?reconnect=true
|
||||
wtv-visit: ${gourl}
|
||||
Content-type: text/html`;
|
||||
data = '';
|
||||
|
||||
@@ -5,14 +5,9 @@ wtv-expire-all: wtv-flashrom:
|
||||
Content-type: text/html`
|
||||
|
||||
if (request_headers.query.url) headers += "\nwtv-visit: " + request_headers.query.url;
|
||||
var cryptstatus = ((socket_sessions[socket.id].secure === true) ? "Encrypted" : "Not Encrypted")
|
||||
|
||||
if (ssid_sessions[socket.ssid].get('box-does-psuedo-encryption')) {
|
||||
var cryptstatus = "<a href='client:showalert?message=Your%20WebTV%20Unit%20sent%20us%20a%20request%20for%20SECURE%20ON%2C%20but%20did%20not%20encrypt%20any%20data%2C%20nor%20will%20accept%20it.%20However%2C%20we%20send%20the%20wtv-encryption%20flag%20to%20roll%20with%20it%2C%20enabling%20%27psuedo-encryption%27.%20Nothing%20is%20encrypted%2C%20but%20the%20box%20trusts%20us.%20This%20will%20probably%20go%20away%20if%20you%20reload%20or%20change%20pages.&buttonaction1=client:donothing&buttonlabel1=Oh%2C%20okay...'>Psuedo-encrypted</a>";
|
||||
} else {
|
||||
var cryptstatus = ((socket_sessions[socket.id].secure === true) ? "Encrypted" : "Not Encrypted")
|
||||
}
|
||||
|
||||
var comp_type = shouldWeCompress(socket.ssid,'text/html');
|
||||
var comp_type = wtvmime.shouldWeCompress(ssid_sessions[socket.ssid],'text/html');
|
||||
var compstatus = "uncompressed";
|
||||
switch (comp_type) {
|
||||
case 1:
|
||||
@@ -58,7 +53,7 @@ if (ssid_sessions[socket.ssid].hasCap("client-has-disk")) {
|
||||
data += "<li><a href=\"client:diskhax\">DiskHax</a> ~ <a href=\"client:vfathax\">VFatHax</a></li>\n";
|
||||
if (ssid_sessions[socket.ssid].hasCap("client-can-do-macromedia-flash2")) {
|
||||
// only show demo if client can do flash2
|
||||
data += "<li>Old MSNTV DealerDemo: <a href=\"wtv-disk:/sync?group=DealerDemo&diskmap=DealerDemo\">Download</a> ~ <a href=\"file://Disk/Demo/index.html\"> Access (after Download)</a></li>\n";
|
||||
data += "<li>Old DealerDemo: <a href=\"wtv-disk:/sync?group=DealerDemo&diskmap=DealerDemo\">Download</a> ~ <a href=\"file://Disk/Demo/index.html\">Access</a></li>\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Content-length: 0`;
|
||||
logdata_outstring_hex += request_headers.post_data.toString(CryptoJS.enc.Hex);
|
||||
if (minisrv_config.services[service_name].write_logs_to_disk) {
|
||||
fs.writeFile(fullpath, logdata_outstring_hex, "Hex", function () {
|
||||
if (!zquiet) console.log(" * Wrote POST log data from", filterSSID(socket.ssid), "for", socket.id);
|
||||
if (!minisrv_config.config.debug_flags.quiet) console.log(" * Wrote POST log data from", wtvshared.filterSSID(socket.ssid), "for", socket.id);
|
||||
sendToClient(socket, headers, data);
|
||||
});
|
||||
} else {
|
||||
@@ -41,7 +41,7 @@ Content-length: 0`;
|
||||
var logdata_outstring_hex = Buffer.from(logdata_outstring, 'utf8').toString('hex');
|
||||
if (minisrv_config.services[service_name].write_logs_to_disk) {
|
||||
fs.writeFile(fullpath, logdata_outstring_hex, "Hex", function () {
|
||||
if (!zquiet) console.log(" * Wrote GET log data from", filterSSID(socket.ssid), "for", socket.id);
|
||||
if (!minisrv_config.config.debug_flags.quiet) console.log(" * Wrote GET log data from", wtvshared.filterSSID(socket.ssid), "for", socket.id);
|
||||
sendToClient(socket, headers, data);
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -17,7 +17,7 @@ if (!request_headers.query.registering ||
|
||||
ssid_sessions[socket.ssid].setSessionData("subscriber_contact_method", request_headers.query.subscriber_contact_method);
|
||||
ssid_sessions[socket.ssid].setSessionData("subscriber_userid", '1' + Math.floor(Math.random() * 1000000000000000000));
|
||||
ssid_sessions[socket.ssid].setSessionData("registered", true);
|
||||
if (!ssid_sessions[socket.ssid].storeSessionData()) {
|
||||
if (!ssid_sessions[socket.ssid].storeSessionData(true)) {
|
||||
var errpage = doErrorPage(400);
|
||||
headers = errpage[0];
|
||||
data = errpage[1];
|
||||
|
||||
@@ -18,7 +18,7 @@ Content-Type: text/html`
|
||||
|
||||
var wtv_system_version = ssid_sessions[socket.ssid].get("wtv-system-version");
|
||||
var wtv_client_bootrom_version = ssid_sessions[socket.ssid].get("wtv-client-bootrom-version");
|
||||
var wtv_client_serial_number = filterSSID(ssid_sessions[socket.ssid].get("wtv-client-serial-number"));
|
||||
var wtv_client_serial_number = wtvshared.filterSSID(ssid_sessions[socket.ssid].get("wtv-client-serial-number"));
|
||||
var wtv_client_rom_type = ssid_sessions[socket.ssid].get("wtv-client-rom-type");
|
||||
var wtv_system_chipversion_str = ssid_sessions[socket.ssid].get("wtv-system-chipversion");
|
||||
var wtv_system_sysconfig_hex = parseInt(ssid_sessions[socket.ssid].get("wtv-system-sysconfig")).toString(16);
|
||||
|
||||
@@ -2,7 +2,9 @@ headers = `200 OK
|
||||
Content-Type: text/html`;
|
||||
|
||||
if (!ssid_sessions[socket.ssid].getSessionData("registered")) {
|
||||
var redirect = [10, "client:goback?"];
|
||||
headers += "\nwtv-noback-all: wtv-";
|
||||
headers += "\nwtv-expire-all: wtv-";
|
||||
var redirect = [5, "client:relogin?"];
|
||||
var message = "Error: Your box is not registered. You are accessing " + minisrv_config.config.service_name + " in Guest Mode. There is nothing to delete!";
|
||||
} else if (request_headers.query.confirm_unregister) {
|
||||
if (ssid_sessions[socket.ssid].unregisterBox()) {
|
||||
@@ -14,7 +16,7 @@ if (!ssid_sessions[socket.ssid].getSessionData("registered")) {
|
||||
} else {
|
||||
var redirect = [10, "client:goback?"];
|
||||
var message = "There was an error deleting your account data. Please try again later. If the problem persists, please contact " + minisrv_config.config.service_owner + " to request manual deletion.";
|
||||
message += "SSID verifcation may be required to perform a manual deletion.< br > <br>Returning from whence you came...<br><br>";
|
||||
message += "SSID verifcation may be required to perform a manual deletion.<br><br>Returning from whence you came...<br><br>";
|
||||
message += `<a href="${redirect[1]}">Click here if you are not automatically redirected.</a>`;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -4,55 +4,109 @@ class WTVClientSessionData {
|
||||
|
||||
fs = require('fs');
|
||||
path = require('path');
|
||||
|
||||
ssid = null;
|
||||
data_store = null;
|
||||
session_store = null;
|
||||
login_security = null;
|
||||
capabilities = null;
|
||||
session_storage = "";
|
||||
hide_ssid_in_logs = true;
|
||||
minisrv_config = [];
|
||||
wtvshared = null;
|
||||
wtvmime = null;
|
||||
|
||||
filterSSID(obj) {
|
||||
if (this.hide_ssid_in_logs === true) {
|
||||
if (typeof (obj) == "string") {
|
||||
if (obj.substr(0, 8) == "MSTVSIMU") {
|
||||
return obj.substr(0, 10) + ('*').repeat(10) + obj.substr(20);
|
||||
} else if (obj.substr(0, 5) == "1SEGA") {
|
||||
return obj.substr(0, 6) + ('*').repeat(6) + obj.substr(13);
|
||||
} else {
|
||||
return obj.substr(0, 6) + ('*').repeat(9);
|
||||
}
|
||||
} else {
|
||||
if (obj["wtv-client-serial-number"]) {
|
||||
var ssid = obj["wtv-client-serial-number"];
|
||||
if (ssid.substr(0, 8) == "MSTVSIMU") {
|
||||
obj["wtv-client-serial-number"] = ssid.substr(0, 10) + ('*').repeat(10) + ssid.substr(20);
|
||||
} else if (ssid.substr(0, 5) == "1SEGA") {
|
||||
obj["wtv-client-serial-number"] = ssid.substr(0, 6) + ('*').repeat(6) + ssid.substr(13);
|
||||
} else {
|
||||
obj["wtv-client-serial-number"] = ssid.substr(0, 6) + ('*').repeat(9);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
constructor(minisrv_config, ssid) {
|
||||
if (!minisrv_config) throw ("minisrv_config required");
|
||||
var WTVShared = require('./WTVShared.js')['WTVShared'];
|
||||
var WTVMime = require('./WTVMime.js');
|
||||
this.minisrv_config = minisrv_config;
|
||||
this.wtvshared = new WTVShared(minisrv_config);
|
||||
this.wtvmime = new WTVMime(minisrv_config);
|
||||
|
||||
constructor(ssid, hide_ssid_in_logs, session_storage_directory) {
|
||||
this.ssid = ssid;
|
||||
if (hide_ssid_in_logs) this.hide_ssid_in_logs = hide_ssid_in_logs;
|
||||
if (!session_storage_directory) session_storage_directory = __dirname + "/SessionStore";
|
||||
this.session_storage = session_storage_directory;
|
||||
this.data_store = new Array();
|
||||
this.session_store = {};
|
||||
}
|
||||
|
||||
getUTCTime(offset = 0) {
|
||||
return new Date((new Date).getTime() + offset).toUTCString();
|
||||
/**
|
||||
* Returns the absolute path to the user's file store, or false if unregistered
|
||||
* @returns {string|boolean} Absolute path to the user's file store, or false if unregistered
|
||||
*/
|
||||
getUserStoreDirectory() {
|
||||
if (!this.isRegistered()) return false;
|
||||
return this.minisrv_config.config.SessionStore + this.path.sep + this.ssid + this.path.sep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a file in the user's file store
|
||||
* @param {string} path Relative path to User's file store
|
||||
* @param {Buffer} data File data
|
||||
* @param {number|null} last_modified Unix timestamp to set last modified date to
|
||||
* @param {boolean} overwrite Overwrite if file exists
|
||||
* @returns {boolean} Whether or not the file was written
|
||||
*/
|
||||
storeUserStoreFile(path, data, last_modified = null, overwrite = true) {
|
||||
var store_dir = this.getUserStoreDirectory();
|
||||
if (!store_dir) return false; // unregistered
|
||||
var result = false;
|
||||
var path_split = path.split('/');
|
||||
var file_name = path_split.pop();
|
||||
var store_dir_path = this.wtvshared.makeSafePath(store_dir, path_split.join('/').replace('/', this.path.sep));
|
||||
var store_full_path = this.wtvshared.makeSafePath(store_dir_path, file_name);
|
||||
|
||||
try {
|
||||
if (!this.fs.existsSync(store_dir_path)) this.fs.mkdirSync(store_dir_path, { recursive: true });
|
||||
var file_exists = this.fs.existsSync(store_full_path);
|
||||
if (!file_exists || (file_exists && overwrite)) result = this.fs.writeFileSync(store_full_path, data);
|
||||
if (result !== false && last_modified) {
|
||||
var file_timestamp = new Date(last_modified * 1000);
|
||||
fs.utimesSync(store_full_path, Date.now(), file_timestamp)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(" # User File Store failed", e);
|
||||
}
|
||||
return (result === false) ? false : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a file from the user store
|
||||
* @param {string} path Path relative to the User File Store
|
||||
* @returns {Buffer|false} Buffer data, or false if could not open file
|
||||
*/
|
||||
getUserStoreFile(path) {
|
||||
var store_dir = this.getUserStoreDirectory();
|
||||
if (!store_dir) return false; // unregistered
|
||||
var store_dir_path = this.wtvshared.makeSafePath(store_dir, path.replace('/', this.path.sep));
|
||||
if (this.fs.existsSync(store_dir_path)) return this.fs.readFileSync(store_dir_path);
|
||||
else return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a file from the user store with a file://Disk/ url
|
||||
* @param {string} url file://Disk/ base url
|
||||
* @returns {Buffer|false} Buffer data, or false if could not open file
|
||||
*/
|
||||
getUserStoreFileByURL(url) {
|
||||
var path_split = url.split('/');
|
||||
path_split.shift();
|
||||
path_split.shift();
|
||||
var store_dir_path = path_split.join('/').replace('/', this.path.sep);
|
||||
return this.getUserStoreFile(store_dir_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Content-Type of a User Store File
|
||||
* @param {string} path Path relative to the User File Store
|
||||
* @returns {string|false} Content-Type, or false if could not open file
|
||||
*/
|
||||
getUserStoreContentType(path) {
|
||||
return this.wtvmime.getSimpleContentType(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of user cookies
|
||||
* @returns {number} Number of cookies
|
||||
*/
|
||||
countCookies() {
|
||||
return Object.keys(this.session_store.cookies).length || 0;
|
||||
}
|
||||
@@ -60,7 +114,7 @@ class WTVClientSessionData {
|
||||
resetCookies() {
|
||||
this.session_store.cookies = {};
|
||||
// webtv likes to have at least one cookie in the list, set a dummy cookie for zefie's site expiring in 1 year.
|
||||
this.addCookie("wtv.zefie.com", "/", this.getUTCTime(365 * 86400000), "cookie_type=chocolatechip");
|
||||
this.addCookie("wtv.zefie.com", "/", this.wtvshared.getUTCTime(365 * 86400000), "cookie_type=chocolatechip");
|
||||
}
|
||||
|
||||
addCookie(domain, path = null, expires = null, data = null) {
|
||||
@@ -174,8 +228,8 @@ class WTVClientSessionData {
|
||||
|
||||
loadSessionData(raw_data = false) {
|
||||
try {
|
||||
if (this.fs.lstatSync(this.session_storage + this.path.sep + this.ssid + ".json")) {
|
||||
var json_data = this.fs.readFileSync(this.session_storage + this.path.sep + this.ssid + ".json", 'Utf8')
|
||||
if (this.fs.lstatSync(this.minisrv_config.config.SessionStore + this.path.sep + this.ssid + ".json")) {
|
||||
var json_data = this.fs.readFileSync(this.minisrv_config.config.SessionStore + this.path.sep + this.ssid + ".json", 'Utf8')
|
||||
if (raw_data) return json_data;
|
||||
|
||||
var session_data = JSON.parse(json_data);
|
||||
@@ -184,12 +238,12 @@ class WTVClientSessionData {
|
||||
}
|
||||
} catch (e) {
|
||||
// Don't log error 'file not found', it just means the client isn't registered yet
|
||||
if (e.code != "ENOENT") console.error(" # Error loading session data for", this.filterSSID(this.ssid), e);
|
||||
if (e.code != "ENOENT") console.error(" # Error loading session data for", this.wtvshared.filterSSID(this.ssid), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
saveSessionData() {
|
||||
saveSessionData(force_write = false) {
|
||||
if (this.isRegistered()) {
|
||||
// load data from disk and merge new data
|
||||
var temp_store = this.session_store;
|
||||
@@ -198,17 +252,18 @@ class WTVClientSessionData {
|
||||
temp_store = null;
|
||||
} else {
|
||||
// do not write file if user is not registered, return true because this is not an error
|
||||
return true;
|
||||
// force write needed to set the initial reg
|
||||
if (!force_write) return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// only save if file has changed
|
||||
var json_save_data = JSON.stringify(this.session_store);
|
||||
var json_load_data = this.loadSessionData(true);
|
||||
if (json_save_data != json_load_data) this.fs.writeFileSync(this.session_storage + this.path.sep + this.ssid + ".json", JSON.stringify(this.session_store), "Utf8");
|
||||
if (json_save_data != json_load_data) this.fs.writeFileSync(this.minisrv_config.config.SessionStore + this.path.sep + this.ssid + ".json", JSON.stringify(this.session_store), "Utf8");
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(" # Error saving session data for", this.filterSSID(this.ssid), e);
|
||||
console.error(" # Error saving session data for", this.wtvshared.filterSSID(this.ssid), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -218,9 +273,9 @@ class WTVClientSessionData {
|
||||
return this.loadSessionData();
|
||||
}
|
||||
|
||||
storeSessionData() {
|
||||
storeSessionData(force_write = false) {
|
||||
// alias
|
||||
return this.saveSessionData();
|
||||
return this.saveSessionData(force_write);
|
||||
}
|
||||
|
||||
SaveIfRegistered() {
|
||||
@@ -231,7 +286,7 @@ class WTVClientSessionData {
|
||||
isRegistered() {
|
||||
var self = this;
|
||||
var ssid_match = false;
|
||||
this.fs.readdirSync(this.session_storage).forEach(file => {
|
||||
this.fs.readdirSync(this.minisrv_config.config.SessionStore).forEach(file => {
|
||||
if (!file.match(/.*\.json/ig)) return;
|
||||
if (ssid_match) return;
|
||||
if (file.split('.')[0] == self.ssid) ssid_match = true;
|
||||
@@ -240,15 +295,19 @@ class WTVClientSessionData {
|
||||
}
|
||||
|
||||
unregisterBox() {
|
||||
var user_store_base = this.wtvshared.makeSafePath(this.wtvshared.getAbsolutePath(this.minisrv_config.config.SessionStore), this.path.sep + this.ssid);
|
||||
try {
|
||||
if (this.fs.lstatSync(this.session_storage + this.path.sep + this.ssid + ".json")) {
|
||||
this.fs.unlinkSync(this.session_storage + this.path.sep + this.ssid + ".json");
|
||||
if (this.fs.existsSync(user_store_base + ".json")) {
|
||||
this.fs.unlinkSync(user_store_base + ".json");
|
||||
this.session_store = {};
|
||||
return true;
|
||||
}
|
||||
if (this.fs.existsSync(user_store_base)) {
|
||||
this.fs.rmdirSync(user_store_base, { recursive: true });
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
// Don't log error 'file not found', it just means the client isn't registered yet
|
||||
console.error(" # Error deleting session data for", this.filterSSID(this.ssid), e);
|
||||
console.error(" # Error deleting session data for", this.wtvshared.filterSSID(this.ssid), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
310
zefie_wtvp_minisrv/WTVDownloadList.js
Normal file
310
zefie_wtvp_minisrv/WTVDownloadList.js
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* wtv/download-list creation helper class
|
||||
* By: zefie
|
||||
*/
|
||||
class WTVDownloadList {
|
||||
|
||||
download_list = "";
|
||||
service_name = "";
|
||||
content_type = "wtv/download-list";
|
||||
wtvshared = null;
|
||||
clientShowAlert = null;
|
||||
minisrv_config = [];
|
||||
|
||||
/**
|
||||
* Constructs the WTVDownloadList Class
|
||||
* @param {string} service_name Service name to use in wtv-urls
|
||||
*/
|
||||
constructor(minisrv_config, service_name = "wtv-disk") {
|
||||
var { WTVShared, clientShowAlert } = require('./WTVShared.js');
|
||||
this.minisrv_config = minisrv_config;
|
||||
this.wtvshared = new WTVShared(minisrv_config);
|
||||
this.clientShowAlert = clientShowAlert;
|
||||
this.service_name = service_name
|
||||
this.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the download list
|
||||
*/
|
||||
clear() {
|
||||
this.download_list = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias to clear() (clears the download list)
|
||||
*/
|
||||
reset() {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the download list.
|
||||
* @returns {string} Download list for client;
|
||||
*/
|
||||
getDownloadList() {
|
||||
return this.download_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a DISPLAY command to the download list
|
||||
* @param {string} message Message to display to the client
|
||||
*/
|
||||
display(message) {
|
||||
this.download_list += "DISPLAY " + message + "\n\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an EXECUTE command to the download list
|
||||
* @param {string} command client command to execute
|
||||
*/
|
||||
execute(command) {
|
||||
this.download_list += "EXECUTE " + command + "\n\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a CREATE partition command to the download list
|
||||
* @param {string} path file://Disk/ path to desired partition
|
||||
* @param {string} size Size of the desired partition
|
||||
*/
|
||||
createPartition(path, size) {
|
||||
this.download_list += "CREATE " + path + "\n";
|
||||
this.download_list += "partition-size: " + size + "\n\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a CREATE-GROUP command to the download list
|
||||
* @param {string} name Group name
|
||||
* @param {string} path file://Disk/ path of desired group
|
||||
* @param {string} state Group state
|
||||
* @param {boolean|null} service_owned Sets service owned flag. (null = don't set)
|
||||
*/
|
||||
createGroup(name, path, state = 'invalid', service_owned = null) {
|
||||
this.download_list += "CREATE-GROUP " + name + "\n";
|
||||
this.download_list += "state: " + state + "\n";
|
||||
if (service_owned !== null) this.download_list += "service-owned: " + service_owned + "\n";
|
||||
this.download_list += "base: " + path + "\n\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* An alias for createGroup() that handles creating the '-UPDATE' group for you
|
||||
* @param {string} name Group name
|
||||
* @param {string} path file://Disk/ path of desired group
|
||||
* @param {string} state Group state
|
||||
* @param {boolean} service_owned Sets service owned flag.
|
||||
*/
|
||||
createUpdateGroup(name, path, state = 'invalid', service_owned = false) {
|
||||
this.createGroup(name + "-UPDATE", path, state);
|
||||
this.createGroup(name, path, state, service_owned);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a DELETE command to the download list
|
||||
* @param {string} path Non-absolute path of client destination file (relative to group base) if group defined, otherwise absolute file://Disk/ path to delete
|
||||
* @param {string} group Group to which it belongs
|
||||
*/
|
||||
delete(path, group = null) {
|
||||
path = this.wtvshared.stripGzipFromPath(path);
|
||||
this.download_list += "DELETE " + path + "\n";
|
||||
if (group !== null) this.download_list += "group: " + group + "\n\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a PUT command to the download list
|
||||
* @param {string} path Absolute file://Disk/ path of a file to upload to the service
|
||||
* @param {string} destination Destination address (wtv url on service) in which to POST upload the file to
|
||||
*/
|
||||
put(path, destination) {
|
||||
this.download_list += "PUT " + path + "\n";
|
||||
this.download_list += "location: " + destination + "\n\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias to put() for User Store
|
||||
* @param {string} path Absolute file://Disk/ path of a file to upload to the service
|
||||
* @param {string} destination Destination file path in the User Store
|
||||
*/
|
||||
putUserStoreDest(path, destination) {
|
||||
this.put(path, this.service_name + ":/userstore?partialPath=" + escape(destination));
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias to putUserStoreDest() that generates the destination
|
||||
* @param {any} path
|
||||
*/
|
||||
putUserStore(path) {
|
||||
var destination = path.replace("file://", "");
|
||||
this.putUserStoreDest(path, destination);
|
||||
}
|
||||
/**
|
||||
* Adds a GET command to the download list
|
||||
* @param {string} file Non-absolute path of client destination file (relative to group base)
|
||||
* @param {string} path Absolute file://Disk/ path of destination
|
||||
* @param {string} source wtv-url to fetch file from
|
||||
* @param {string} group Group this file belongs to
|
||||
* @param {string} display Message to display while working on this file
|
||||
* @param {string} checksum md5sum of the file
|
||||
* @param {string} file_permission File permissions
|
||||
*/
|
||||
get(file, path, source, group, checksum = null, uncompressed_size = null, original_filename = null, file_permission = 'r') {
|
||||
if (original_filename) {
|
||||
file = file.split('/');
|
||||
var file_name = file[file.length - 1];
|
||||
path = path.replace(file_name, original_filename);
|
||||
file.pop();
|
||||
if (file.length > 0) file = file.join('/') + '/' + original_filename;
|
||||
else file = original_filename;
|
||||
}
|
||||
this.download_list += "GET " + file + "\n";
|
||||
|
||||
this.download_list += "group: " + group + "-UPDATE\n";
|
||||
this.download_list += "location: " + source + "\n";
|
||||
this.download_list += "file-permission: " + file_permission + "\n";
|
||||
if (checksum != null) this.download_list += "wtv-checksum: " + checksum + "\n";
|
||||
if (uncompressed_size != null) this.download_list += "wtv-uncompressed-filesize: " + uncompressed_size + "\n";
|
||||
this.download_list += "service-source-location: /webtv/content/" + source.substr(source.indexOf('-') + 1, source.indexOf(':/') - source.indexOf('-') - 1) + "d/" + source.substr(source.indexOf(':/') + 2) + "\n";
|
||||
this.download_list += "client-dest-location: " + path + "\n\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a RENAME command to the download list
|
||||
* @param {string} srcfile Non-absolute path of client source file (relative to source group base)
|
||||
* @param {string} destfile Non-absolute path of client destination file (relative to destination group base)
|
||||
* @param {string} srcgroup Source Group
|
||||
* @param {string} destgroup Destination Group
|
||||
*/
|
||||
rename(srcfile, destfile, srcgroup, destgroup) {
|
||||
srcfile = this.wtvshared.stripGzipFromPath(srcfile);
|
||||
destfile = this.wtvshared.stripGzipFromPath(destfile);
|
||||
this.download_list += "RENAME " + srcfile + "\n";
|
||||
this.download_list += "group: " + srcgroup + "-UPDATE\n";
|
||||
this.download_list += "destination-group: " + destgroup + "\n";
|
||||
this.download_list += "location: " + destfile + "\n\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a SET-GROUP command to the download list
|
||||
* @param {string} group Group to set state of
|
||||
* @param {string} state State to set group to
|
||||
* @param {string} version Version to set group to
|
||||
*/
|
||||
setGroup(group, state, version) {
|
||||
this.download_list += "SET-GROUP " + group + "\n";
|
||||
this.download_list += "state: " + state + "\n";
|
||||
this.download_list += "version: " + version + "\n";
|
||||
this.download_list += "last-checkup-time: " + new Date().toUTCString().replace("GMT", "+0000") + "\n\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a DELETE-GROUP command to the download list
|
||||
* @param {string} group Group to delete
|
||||
*/
|
||||
deleteGroup(group) {
|
||||
this.download_list += "DELETE-GROUP " + group + "\n\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* An alias for deleteGroup() that handles deleting the '-UPDATE' group files for you
|
||||
* @param {string} group Group to delete
|
||||
* @param {string} path Group base path
|
||||
*/
|
||||
deleteGroupUpdate(group, path) {
|
||||
this.deleteGroup(group + "-UPDATE");
|
||||
this.delete(path + ".GROUP-UPDATE/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the Download page
|
||||
* @param {object} minisrv_config minisrv config object
|
||||
* @param {string} title Page title
|
||||
* @param {string} group
|
||||
* @param {string|null} diskmap
|
||||
* @param {string|null} main_message Message displayed in the center of the page
|
||||
* @param {string|null} message Initial progress bar message
|
||||
* @param {boolean|null} force_update Force this update even if the client reports the files are synced
|
||||
* @param {string|null} success_url Where the client goes when the process succeeds
|
||||
* @param {string|null} fail_url Where the client goes when the process fails.
|
||||
* @param {string|null} url Use your own URL for client:fetch?source= instead of our generated one
|
||||
* @returns {string} HTML Download Page
|
||||
*/
|
||||
getSyncPage(title, group, diskmap = null, main_message = null, message = null, force_update = null, success_url = null, fail_url = null, url = null) {
|
||||
// Begin Set defaults
|
||||
if (main_message === null) main_message = "Your receiver is downloading files.";
|
||||
|
||||
if (message === null) message = "Retrieving files";
|
||||
|
||||
if (force_update === null) force_update = false;
|
||||
|
||||
if (url === null) url = this.service_name + ":/sync?diskmap=" + escape(diskmap) + "&force=" + force_update;
|
||||
|
||||
if (success_url === null) success_url = new this.clientShowAlert({
|
||||
'image': this.minisrv_config.config.service_logo,
|
||||
'message': "Download successful!",
|
||||
'buttonlabel1': "Okay",
|
||||
'buttonaction1': "client:goback",
|
||||
'noback': true,
|
||||
}).getURL();
|
||||
|
||||
if (fail_url === null) fail_url = new this.clientShowAlert({
|
||||
'image': this.minisrv_config.config.service_logo,
|
||||
'message': "Download failed...",
|
||||
'buttonlabel1': "Fuck!",
|
||||
'buttonaction1': "client:goback",
|
||||
'noback': true,
|
||||
}).getURL();
|
||||
// End set defaults
|
||||
return `<html>
|
||||
<head>
|
||||
<meta
|
||||
http-equiv=refresh
|
||||
content="0;url=client:Fetch?group=${escape(group)}&source=${escape(url)}&message=${escape(message)}"
|
||||
>
|
||||
<display downloadsuccess="${success_url}" downloadfail="${fail_url}">
|
||||
<title>${title}</title>
|
||||
</head>
|
||||
<body bgcolor=#0 text=#42CC55 fontsize=large hspace=0 vspace=0>
|
||||
<table cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td width=104 height=74 valign=middle align=center bgcolor=3B3A4D>
|
||||
<img src="${this.minisrv_config.config.service_logo}" width=86 height=64>
|
||||
<td width=20 valign=top align=left bgcolor=3B3A4D>
|
||||
<spacer>
|
||||
<td colspan=2 width=436 valign=middle align=left bgcolor=3B3A4D>
|
||||
<font color=D6DFD0 size=+2><blackface><shadow>
|
||||
<spacer type=block width=1 height=4>
|
||||
<br>
|
||||
${message}
|
||||
</shadow>
|
||||
</blackface>
|
||||
</font>
|
||||
<tr>
|
||||
<td width=104 height=20>
|
||||
<td width=20>
|
||||
<td width=416>
|
||||
<td width=20>
|
||||
<tr>
|
||||
<td colspan=2>
|
||||
<td>
|
||||
<font size=+1>
|
||||
${main_message}
|
||||
<p>This may take a while.
|
||||
</font>
|
||||
<tr>
|
||||
<td colspan=2>
|
||||
<td>
|
||||
<br><br>
|
||||
<font color=white>
|
||||
<progressindicator name="downloadprogress"
|
||||
message="Preparing..."
|
||||
height=40 width=250>
|
||||
</font>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = WTVDownloadList;
|
||||
@@ -5,16 +5,18 @@ class WTVFlashrom {
|
||||
use_zefie_server = true;
|
||||
bf0app_update = false;
|
||||
service_vaults = new Array();
|
||||
no_debug = false;
|
||||
service_name = "";
|
||||
zdebug = false;
|
||||
minisrv_config = [];
|
||||
|
||||
|
||||
constructor(service_vaults, service_name, use_zefie_server = true, bf0app_update = false, debug = false) {
|
||||
constructor(minisrv_config, service_vaults, service_name, use_zefie_server = true, bf0app_update = false, no_debug = false) {
|
||||
this.service_vaults = service_vaults;
|
||||
this.service_name = service_name;
|
||||
this.use_zefie_server = use_zefie_server;
|
||||
this.bf0app_update = bf0app_update;
|
||||
this.zdebug = true;
|
||||
this.no_debug = no_debug;
|
||||
this.minisrv_config = minisrv_config;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,31 +97,37 @@ class WTVFlashrom {
|
||||
if (flashrom_info.magic == flashrom_magic) flashrom_info.valid_flashrom = true;
|
||||
if (!flashrom_info.valid_flashrom) console.error(" * Warning! FlashROM File Magic (" + flashrom_info.magic + ") did not match expected magic (" + flashrom_magic + ")...");
|
||||
|
||||
if (this.zdebug) console.log(" # FlashROM File Magic (" + flashrom_info.magic + "), expected magic (" + flashrom_magic + "), OK = " + flashrom_info.valid_flashrom + "...");
|
||||
//if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # FlashROM File Magic (" + flashrom_info.magic + "), expected magic (" + flashrom_magic + "), OK = " + flashrom_info.valid_flashrom + "...");
|
||||
flashrom_info.byte_progress = data.readUInt32BE(68);
|
||||
if (this.zdebug) console.log(" # Flashrom Part Bytes Sent:", flashrom_info.byte_progress);
|
||||
flashrom_info.compression_type = parseInt(part_header[16], 16);
|
||||
if (this.zdebug) console.log(" # Flashrom Part Compression Type:", flashrom_info.compression_type);
|
||||
//if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Part Compression Type:", flashrom_info.compression_type);
|
||||
flashrom_info.part_data_size = data.readUInt32BE(4);
|
||||
if (this.zdebug) console.log(" # Flashrom Part Data Size:", flashrom_info.part_data_size);
|
||||
//if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Part Data Size:", flashrom_info.part_data_size);
|
||||
flashrom_info.part_total_size = flashrom_info.part_data_size + flashrom_info.header_length;
|
||||
if (this.zdebug) console.log(" # Flashrom Part Total Size:", flashrom_info.part_total_size);
|
||||
|
||||
flashrom_info.total_parts_size = data.readUInt32BE(32);
|
||||
if (this.zdebug) console.log(" # Flashrom All Parts Total Size:", flashrom_info.total_parts_size);
|
||||
flashrom_info.percent_complete = ((((flashrom_info.byte_progress + flashrom_info.part_total_size) / flashrom_info.total_parts_size)) * 100).toFixed(1);
|
||||
|
||||
if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Part Size :", flashrom_info.part_total_size);
|
||||
if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Bytes Sent :", flashrom_info.byte_progress);
|
||||
if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Bytes Sent+:", flashrom_info.byte_progress + flashrom_info.part_total_size, "(" + flashrom_info.percent_complete + "% complete)");
|
||||
if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Total Size :", flashrom_info.total_parts_size);
|
||||
|
||||
// read current part number bit from part header
|
||||
flashrom_info.part_number = data.readUInt16BE(28);
|
||||
|
||||
if (this.zdebug) console.log(" # Flashrom Current Part Number:", flashrom_info.part_number);
|
||||
if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Curr Part Number :", flashrom_info.part_number);
|
||||
flashrom_info.is_last_part = ((flashrom_info.byte_progress + flashrom_info.part_total_size) == flashrom_info.total_parts_size) ? true : false;
|
||||
|
||||
if (flashrom_info.is_last_part) {
|
||||
if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Curr Part is Last:", flashrom_info.is_last_part);
|
||||
} else {
|
||||
flashrom_info.next_part_number = flashrom_info.part_number + 1;
|
||||
if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Next Part Number :", flashrom_info.next_part_number);
|
||||
}
|
||||
|
||||
// read current part display message from part header
|
||||
flashrom_info.message = new Buffer.from(part_header.toString('hex').substring(36 * 2, 68 * 2), 'hex').toString('ascii').replace(/[^0-9a-z\ \.\-]/gi, "");
|
||||
|
||||
flashrom_info.is_last_part = ((flashrom_info.byte_progress + flashrom_info.part_total_size) == flashrom_info.total_parts_size) ? true : false;
|
||||
flashrom_info.rompath = `wtv-flashrom:/${path}`;
|
||||
if (this.zdebug) console.log(" # Flashrom Part Bytes Sent (after this part):", flashrom_info.byte_progress + flashrom_info.part_total_size);
|
||||
if (this.zdebug) console.log(" # Flashrom Part is Last Part", flashrom_info.is_last_part);
|
||||
|
||||
if (flashrom_info.is_last_part && this.bf0app_update) {
|
||||
flashrom_info.next_rompath = null;
|
||||
@@ -138,7 +146,7 @@ class WTVFlashrom {
|
||||
var flashrom_info = this.getFlashromInfo(data, request_path)
|
||||
if (flashrom_info.is_bootrom) headers += "Content-Type: binary/x-wtv-bootrom"; // maybe?
|
||||
else headers += "Content-Type: binary/x-wtv-flashblock";
|
||||
if (flashrom_info.next_rompath != null) headers += "\nwtv-visit: " + flashrom_info.next_rompath;
|
||||
if (flashrom_info.next_rompath != null && this.bf0app_update) headers += "\nwtv-visit: " + flashrom_info.next_rompath;
|
||||
callback(data, headers);
|
||||
}
|
||||
|
||||
@@ -179,7 +187,7 @@ class WTVFlashrom {
|
||||
})
|
||||
|
||||
res.on('end', function () {
|
||||
console.log(` * Zefie's FlashROM Server HTTP Status: ${res.statusCode} ${res.statusMessage}`)
|
||||
if (self.minisrv_config.config.debug_flags.debug) console.log(` * Zefie's FlashROM Server HTTP Status: ${res.statusCode} ${res.statusMessage}`)
|
||||
if (res.statusCode == 200) {
|
||||
var data = Buffer.from(data_hex, 'hex');
|
||||
} else if (res.statusCode == 206) {
|
||||
|
||||
@@ -400,8 +400,6 @@ class WTVLzpf {
|
||||
this.EncodeLiteral(code_length, code);
|
||||
}
|
||||
}
|
||||
|
||||
return Buffer.from(this.encoded_data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -442,32 +440,26 @@ class WTVLzpf {
|
||||
// End
|
||||
this.AddByte((this.current_literal >>> 0x18) & 0xFF);
|
||||
this.AddByte(0x20);
|
||||
|
||||
return Buffer.from(this.encoded_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the data to a Javascript Buffer object
|
||||
*
|
||||
* @param data {String|Buffer|CryptoJS.lib.WordArray} Data to convert
|
||||
* @param data {String|Buffer} Data to convert
|
||||
*
|
||||
* @returns {Buffer} Javascript Buffer object
|
||||
*/
|
||||
ConvertToBuffer(data) {
|
||||
if (data.words) {
|
||||
var WTVSec = require("./WTVSec.js");
|
||||
wtvsec = new WTVSec(1);
|
||||
data = wtvsec.wordArrayToBuffer(data);
|
||||
WTVSec, wtvsec = null;
|
||||
} else if (!data.byteLength) {
|
||||
// otherwise if its not already a Buffer, convert it to one
|
||||
data = new Buffer.from(data);
|
||||
}
|
||||
data = new Buffer.from(data.toString('binary'));
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress data using WebTV's Lzpf compression algorithm and adds the footer to the end.
|
||||
*
|
||||
* @param uncompressed_data {String|Buffer|CryptoJS.lib.WordArray} data to compress
|
||||
* @param uncompressed_data {String|Buffer} data to compress
|
||||
*
|
||||
* @returns {Buffer} Lzpf compression data
|
||||
*/
|
||||
@@ -475,9 +467,7 @@ class WTVLzpf {
|
||||
uncompressed_data = this.ConvertToBuffer(uncompressed_data);
|
||||
this.Begin();
|
||||
this.EncodeBlock(uncompressed_data, true);
|
||||
this.Finish();
|
||||
|
||||
return Buffer.from(this.encoded_data);
|
||||
return this.Finish();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
198
zefie_wtvp_minisrv/WTVMime.js
Normal file
198
zefie_wtvp_minisrv/WTVMime.js
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Simple class for WebTV Mime Types and overrides
|
||||
*/
|
||||
|
||||
|
||||
class WTVMime {
|
||||
|
||||
mime = require('mime-types');
|
||||
wtvshared = null;
|
||||
minisrv_config = [];
|
||||
|
||||
|
||||
constructor(minisrv_config) {
|
||||
var WTVShared = require('./WTVShared.js')['WTVShared'];
|
||||
this.minisrv_config = minisrv_config;
|
||||
this.wtvshared = new WTVShared(minisrv_config);
|
||||
if (!String.prototype.reverse) {
|
||||
String.prototype.reverse = function () {
|
||||
var splitString = this.split("");
|
||||
var reverseArray = splitString.reverse();
|
||||
var joinArray = reverseArray.join("");
|
||||
return joinArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
shouldWeCompress(ssid_session, headers_obj) {
|
||||
var compress_data = false;
|
||||
var compression_type = 0; // no compression
|
||||
if (ssid_session) {
|
||||
if (ssid_session.capabilities) {
|
||||
if (ssid_session.capabilities['client-can-receive-compressed-data']) {
|
||||
|
||||
if (this.minisrv_config.config.enable_lzpf_compression || this.minisrv_config.config.force_compression_type) {
|
||||
compression_type = 1; // lzpf
|
||||
}
|
||||
|
||||
if (ssid_session) {
|
||||
// if gzip is enabled...
|
||||
if (this.minisrv_config.config.enable_gzip_compression || this.minisrv_config.config.force_compression_type) {
|
||||
var is_bf0app = ssid_session.get("wtv-client-rom-type") == "bf0app";
|
||||
var is_minibrowser = (ssid_session.get("wtv-needs-upgrade") || ssid_session.get("wtv-used-8675309"));
|
||||
var is_softmodem = ssid_session.get("wtv-client-rom-type").match(/softmodem/);
|
||||
if (!is_bf0app && ((!is_softmodem && !is_minibrowser) || (is_softmodem && !is_minibrowser))) {
|
||||
// softmodem boxes do not appear to support gzip in the minibrowser
|
||||
// LC2 appears to support gzip even in the MiniBrowser
|
||||
// LC2 and newer approms appear to support gzip
|
||||
// bf0app does not appear to support gzip
|
||||
compression_type = 2; // gzip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// mostly for debugging
|
||||
if (this.minisrv_config.config.force_compression_type == "lzpf") compression_type = 1;
|
||||
if (this.minisrv_config.config.force_compression_type == "gzip") compression_type = 2;
|
||||
|
||||
// do not compress if already encoded
|
||||
if (headers_obj["Content-Encoding"]) return 0;
|
||||
|
||||
// should we bother to compress?
|
||||
var content_type = "";
|
||||
if (typeof (headers_obj) == 'string') content_type = headers_obj;
|
||||
else content_type = (typeof (headers_obj["wtv-modern-content-type"]) != 'undefined') ? headers_obj["wtv-modern-content-type"] : headers_obj["Content-Type"];
|
||||
|
||||
if (content_type) {
|
||||
// both lzpf and gzip
|
||||
if (content_type.match(/^text\//) && content_type != "text/tellyscript") compress_data = true;
|
||||
else if (content_type.match(/^application\/(x-?)javascript$/)) compress_data = true;
|
||||
else if (content_type == "application/json") compress_data = true;
|
||||
if (compression_type == 2) {
|
||||
// gzip only
|
||||
if (content_type.match(/^audio\/(x-)?[s3m|mod|xm]$/)) compress_data = true; // s3m, mod, xm
|
||||
if (content_type.match(/^audio\/(x-)?[midi|wav|wave]$/)) compress_data = true; // midi & wav
|
||||
if (content_type.match(/^binary\/x-wtv-approm$/)) compress_data = true; // approms
|
||||
if (content_type.match(/^binary\/doom-data$/)) compress_data = true; // DOOM WADs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return compression_type if compress_data = true
|
||||
return (compress_data) ? compression_type : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the WebTV Content-Type
|
||||
* @param {string} path Path to a file
|
||||
* @returns {string} Content-Type
|
||||
*/
|
||||
getSimpleContentType(path) {
|
||||
return this.getContentType(path)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets both the WebTV Content-Type and the Modern Content-Type
|
||||
* @param {string} path Path to a file
|
||||
* @returns {Array} (WebTV Content-Type, Modern Content-Type)
|
||||
*/
|
||||
getContentType(path) {
|
||||
var file_ext = this.wtvshared.getFileExt(path).toLowerCase();
|
||||
var wtv_mime_type = "";
|
||||
var modern_mime_type = "";
|
||||
// process WebTV overrides, fall back to generic mime lookup
|
||||
switch (file_ext) {
|
||||
case "aif":
|
||||
wtv_mime_type = "audio/x-aif";
|
||||
break;
|
||||
case "aifc":
|
||||
wtv_mime_type = "audio/x-aifc";
|
||||
break;
|
||||
case "aiff":
|
||||
wtv_mime_type = "audio/x-aiff";
|
||||
break;
|
||||
case "ani":
|
||||
wtv_mime_type = "x-wtv-animation";
|
||||
break;
|
||||
case "brom":
|
||||
wtv_mime_type = "binary/x-wtv-bootrom";
|
||||
break;
|
||||
case "cdf":
|
||||
wtv_mime_type = "application/netcdf";
|
||||
break;
|
||||
case "dat":
|
||||
wtv_mime_type = "binary/cache-data";
|
||||
break;
|
||||
case "dl":
|
||||
wtv_mime_type = "wtv/download-list";
|
||||
break;
|
||||
case "gsm":
|
||||
wtv_mime_type = "audio/x-gsm";
|
||||
break;
|
||||
case "gz":
|
||||
wtv_mime_type = "application/gzip";
|
||||
break;
|
||||
case "ini":
|
||||
wtv_mime_type = "wtv/jack-configuration";
|
||||
break;
|
||||
case "mips-code":
|
||||
wtv_mime_type = "code/x-wtv-code-mips";
|
||||
break;
|
||||
case "o":
|
||||
wtv_mime_type = "binary/x-wtv-approm";
|
||||
break;
|
||||
case "ram":
|
||||
wtv_mime_type = "audio/x-pn-realaudio";
|
||||
break;
|
||||
case "rom":
|
||||
wtv_mime_type = "binary/x-wtv-flashblock";
|
||||
break;
|
||||
case "rsp":
|
||||
wtv_mime_type = "wtv/jack-response";
|
||||
break;
|
||||
case "swa":
|
||||
case "swf":
|
||||
wtv_mime_type = "application/x-shockwave-flash";
|
||||
break;
|
||||
case "srf":
|
||||
case "spl":
|
||||
wtv_mime_type = "wtv/jack-data";
|
||||
break;
|
||||
case "ttf":
|
||||
wtv_mime_type = "wtv/jack-fonts";
|
||||
break;
|
||||
case "tvch":
|
||||
wtv_mime_type = "wtv/tv-channels";
|
||||
break;
|
||||
case "tvl":
|
||||
wtv_mime_type = "wtv/tv-listings";
|
||||
break;
|
||||
case "tvsl":
|
||||
wtv_mime_type = "wtv/tv-smartlinks";
|
||||
break;
|
||||
case "wad":
|
||||
wtv_mime_type = "binary/doom-data";
|
||||
break;
|
||||
case "mp2":
|
||||
case "hsb":
|
||||
case "rmf":
|
||||
case "s3m":
|
||||
case "mod":
|
||||
case "xm":
|
||||
wtv_mime_type = "application/Music";
|
||||
break;
|
||||
}
|
||||
|
||||
modern_mime_type = this.mime.lookup(path);
|
||||
if (wtv_mime_type == "") wtv_mime_type = modern_mime_type;
|
||||
return new Array(wtv_mime_type, modern_mime_type);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = WTVMime;
|
||||
@@ -31,19 +31,18 @@ class WTVSec {
|
||||
hRC4_Key1 = null;
|
||||
hRC4_Key2 = null;
|
||||
RC4Session = new Array();
|
||||
zdebug = false;
|
||||
|
||||
minisrv_config = [];
|
||||
|
||||
/**
|
||||
*
|
||||
* Initialize the WTVSec class.
|
||||
*
|
||||
* @param {Number} wtv_incarnation Sets the wtv-incarnation for this instance
|
||||
* @param {Boolean} zdebug Enable debugging
|
||||
* @param {Boolean} minisrv_config.config.debug_flags.debug Enable debugging
|
||||
*
|
||||
*/
|
||||
constructor(wtv_incarnation = 1, zdebug = false) {
|
||||
this.zdebug = zdebug;
|
||||
constructor(minisrv_config, wtv_incarnation = 1) {
|
||||
this.minisrv_config = minisrv_config;
|
||||
this.initial_shared_key = CryptoJS.enc.Base64.parse(this.initial_shared_key_b64);
|
||||
|
||||
if (this.initial_shared_key.sigBytes === 8) {
|
||||
@@ -238,7 +237,8 @@ class WTVSec {
|
||||
* #returns {Buffer} JS Buffer object
|
||||
*/
|
||||
wordArrayToBuffer(wordArray) {
|
||||
return new Buffer.from(wordArray.toString(CryptoJS.enc.Hex), 'hex');
|
||||
if (wordArray) return new Buffer.from(wordArray.toString(CryptoJS.enc.Hex), 'hex');
|
||||
else return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,7 +247,7 @@ class WTVSec {
|
||||
*
|
||||
*/
|
||||
SecureOn(rc4session = null) {
|
||||
if (this.zdebug) console.log(" # Generating RC4 sessions with wtv-incarnation: " + this.incarnation);
|
||||
if (this.minisrv_config.config.debug_flags.debug) console.log(" # Generating RC4 sessions with wtv-incarnation: " + this.incarnation);
|
||||
|
||||
var buf = new Uint8Array([0xff & this.incarnation, 0xff & (this.incarnation >> 8), 0xff & (this.incarnation >> 16), 0xff & (this.incarnation >> 24)]);
|
||||
endianness(buf, 4);
|
||||
|
||||
195
zefie_wtvp_minisrv/WTVShared.js
Normal file
195
zefie_wtvp_minisrv/WTVShared.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Shared functions across all classes and apps
|
||||
*/
|
||||
|
||||
class WTVShared {
|
||||
|
||||
path = require('path');
|
||||
fs = require('fs');
|
||||
minisrv_config = [];
|
||||
|
||||
constructor(minisrv_config) {
|
||||
this.minisrv_config = minisrv_config;
|
||||
if (!String.prototype.reverse) {
|
||||
String.prototype.reverse = function () {
|
||||
var splitString = this.split("");
|
||||
var reverseArray = splitString.reverse();
|
||||
var joinArray = reverseArray.join("");
|
||||
return joinArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Last-Modified date in Unix Timestamp format
|
||||
* @param {string} file Path to a file
|
||||
*/
|
||||
getFileLastModified(file) {
|
||||
var stats = this.fs.lstatSync(file);
|
||||
if (stats) return new Date(stats.mtimeMs);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Last-Modified date in a RFC7231 compliant UTC Date String
|
||||
* @param {string} file Path to a file
|
||||
*/
|
||||
getFileLastModifiedUTCString(file) {
|
||||
return this.getFileLastModified(file).toUTCString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a RFC7231 compliant UTC Date String from the current time
|
||||
* @param {Number} offset Offset from current time (+/-)
|
||||
* @returns {string} A RFC7231 compliant UTC Date String from the current time
|
||||
*/
|
||||
getUTCTime(offset = 0) {
|
||||
return new Date((new Date).getTime() + offset).toUTCString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a censored SSID
|
||||
* @param {string|Array} obj SSID String or Headers Object
|
||||
*/
|
||||
filterSSID(obj) {
|
||||
if (this.minisrv_config.config.hide_ssid_in_logs === true) {
|
||||
if (typeof (obj) == "string") {
|
||||
if (obj.substr(0, 8) == "MSTVSIMU") {
|
||||
return obj.substr(0, 10) + ('*').repeat(10) + obj.substr(20);
|
||||
} else if (obj.substr(0, 5) == "1SEGA") {
|
||||
return obj.substr(0, 6) + ('*').repeat(6) + obj.substr(13);
|
||||
} else {
|
||||
return obj.substr(0, 6) + ('*').repeat(9);
|
||||
}
|
||||
} else {
|
||||
if (obj["wtv-client-serial-number"]) {
|
||||
var ssid = obj["wtv-client-serial-number"];
|
||||
if (ssid.substr(0, 8) == "MSTVSIMU") {
|
||||
obj["wtv-client-serial-number"] = ssid.substr(0, 10) + ('*').repeat(10) + ssid.substr(20);
|
||||
} else if (ssid.substr(0, 5) == "1SEGA") {
|
||||
obj["wtv-client-serial-number"] = ssid.substr(0, 6) + ('*').repeat(6) + ssid.substr(13);
|
||||
} else {
|
||||
obj["wtv-client-serial-number"] = ssid.substr(0, 6) + ('*').repeat(9);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an absolute path
|
||||
* @param {string} path
|
||||
* @param {string} directory Root directory
|
||||
*/
|
||||
getAbsolutePath(path, directory = __dirname) {
|
||||
if (path.substring(0, 1) != this.path.sep && path.substring(1, 1) != ":") {
|
||||
// non-absolute path, so use current directory as base
|
||||
path = (directory + this.path.sep + path);
|
||||
} else {
|
||||
// already absolute path
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a percentage
|
||||
* @param {number} partialValue
|
||||
* @param {number} totalValue
|
||||
* @returns {number} percentage
|
||||
*/
|
||||
getPercentage = function (partialValue, totalValue) {
|
||||
return Math.floor((100 * partialValue) / totalValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the file ends with .gz, remove it
|
||||
* @param {string} path
|
||||
* @return {string} path without gz, or unmodified path if it isnt a gz
|
||||
*/
|
||||
stripGzipFromPath(path) {
|
||||
var path_split = path.split('.');
|
||||
if (path_split[path_split.length - 1].toLowerCase() == "gz") {
|
||||
path_split.pop();
|
||||
path = path_split.join(".");
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file extension from a path
|
||||
* @param {string} path
|
||||
* @returns {String} File Extension (without dot)
|
||||
*/
|
||||
getFileExt(path) {
|
||||
return path.reverse().split(".")[0].reverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips bad things from paths
|
||||
* @param {string} base Base path
|
||||
* @param {string} target Sub path
|
||||
*/
|
||||
makeSafePath(base, target) {
|
||||
target.replace(/[\|\&\;\$\%\@\"\<\>\+\,\\]/g, "");
|
||||
if (this.path.sep != "/") target = target.replace(/\//g, this.path.sep);
|
||||
var targetPath = this.path.posix.normalize(target)
|
||||
return base + this.path.sep + targetPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure an SSID is clean, and doesn't contain any exploitable characters
|
||||
* @param {string} ssid
|
||||
* @returns {string} Sanitized SSID
|
||||
*/
|
||||
makeSafeSSID(ssid = "") {
|
||||
ssid = ssid.replace(/[^a-zA-Z0-9]/g, "");
|
||||
if (ssid.length == 0) ssid = null;
|
||||
return ssid;
|
||||
}
|
||||
}
|
||||
|
||||
class clientShowAlert {
|
||||
message = null;
|
||||
buttonlabel1 = null;
|
||||
buttonlabel2 = null;
|
||||
buttonaction1 = null;
|
||||
buttonaction2 = null;
|
||||
noback = null;
|
||||
image = null;
|
||||
|
||||
constructor(image = null, message = null, buttonlabel1 = null, buttonaction1 = null, buttonlabel2 = null, buttonaction2 = null, noback = null) {
|
||||
this.message = message;
|
||||
this.buttonlabel1 = buttonlabel1;
|
||||
this.buttonlabel2 = buttonlabel2;
|
||||
this.buttonaction1 = buttonaction1;
|
||||
this.buttonaction2 = buttonaction2;
|
||||
this.message = message;
|
||||
this.noback = noback;
|
||||
if (typeof image === 'object') {
|
||||
this.image = null;
|
||||
Object.keys(image).forEach(function (k) {
|
||||
if (this[k] === null) this[k] = image[k];
|
||||
}, this);
|
||||
} else {
|
||||
this.image = image;
|
||||
}
|
||||
}
|
||||
|
||||
getURL() {
|
||||
var url = "client:ShowAlert?";
|
||||
if (this.message) url += "message=" + escape(this.message) + "&";
|
||||
if (this.buttonlabel1) url += "buttonlabel1=" + escape(this.buttonlabel1) + "&";
|
||||
if (this.buttonaction1) url += "buttonaction1=" + escape(this.buttonaction1) + "&";
|
||||
if (this.buttonlabel2) url += "buttonlabel2=" + escape(this.buttonlabel2) + "&";
|
||||
if (this.buttonaction2) url += "buttonaction2=" + escape(this.buttonaction2) + "&";
|
||||
if (this.image) url += "image=" + escape(this.image) + "&";
|
||||
if (this.noback) url += "noback=true&";
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.WTVShared = WTVShared;
|
||||
module.exports.clientShowAlert = clientShowAlert;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,12 +13,15 @@
|
||||
"hide_ssid_in_logs": true,
|
||||
"post_percentages": [ 0, 25, 50, 100 ],
|
||||
"verbosity": 2,
|
||||
"socket_timeout": 86400,
|
||||
"post_data_socket_timeout": 30,
|
||||
"error_log_file": "errors.log",
|
||||
"catchall_file_name": "catchall.js",
|
||||
"enable_lzpf_compression": false,
|
||||
"enable_gzip_compression": true,
|
||||
"pc_server_hidden_service": "http_pc",
|
||||
"pc_server_hidden_service_enabled": false,
|
||||
"show_diskmap": false,
|
||||
"allow_guests": true
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zefie_wtvp_minisrv",
|
||||
"version": "0.9.16",
|
||||
"version": "0.9.18",
|
||||
"description": "WebTV Service (WTVP) Emulation Server",
|
||||
"main": "app.js",
|
||||
"homepage": "https://github.com/zefie/zefie_wtvp_minisrv",
|
||||
|
||||
@@ -52,6 +52,10 @@
|
||||
<Content Include="ServiceVault\wtv-cookie\reset.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="ServiceVault\wtv-disk\content\diskmaps\ModemFirmware.json" />
|
||||
<Content Include="ServiceVault\wtv-disk\userstore.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="ServiceVault\wtv-flashrom\content\content-serve.js" />
|
||||
<Content Include="ServiceVault\wtv-flashrom\current-noflash.js">
|
||||
<SubType>Code</SubType>
|
||||
@@ -275,12 +279,18 @@
|
||||
<Content Include="WTVClientCapabilities.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="WTVDownloadList.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="WTVFlashrom.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="WTVLzpf.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="WTVMime.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="WTVRegister.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
@@ -288,6 +298,9 @@
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
<Content Include="package.json" />
|
||||
<Content Include="WTVShared.js">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="ServiceVault\" />
|
||||
|
||||
Reference in New Issue
Block a user