Compare commits

..

40 Commits

Author SHA1 Message Date
Ryder
8089bd4439 Remove redundant files 2026-05-08 00:35:58 +01:00
zefie
750435fc83 small cleanup 2026-05-05 14:39:39 -04:00
zefie
fd67132da9 store the msntv2 token so we can look it up later 2026-05-05 14:35:37 -04:00
zefie
43a87347b8 some more msntv2 updates 2026-05-05 09:20:31 -04:00
zefie
e1d2c59ed5 some updates 2026-05-05 07:43:40 -04:00
2d64acaab6 Merge pull request 'Fix TV2 support' (#1) from feature/FixTv2 into dev
Reviewed-on: #1
2026-05-05 02:20:34 +02:00
17e0e6e526 Merge branch 'dev' into feature/FixTv2 2026-05-05 02:15:34 +02:00
Ryder
02a3eef5e7 upload 2026-05-05 01:08:26 +01:00
zefie
11d2ab8c86 some documentation 2026-05-04 20:07:18 -04:00
zefie
778c0a2827 new ssl 2026-05-04 14:01:11 -04:00
zefie
00e385cdbe various fixups, allow FTP max size config 2026-05-04 08:38:22 -04:00
zefie
cf9cc22a1c new depreciation 2026-05-03 15:26:38 -04:00
zefie
0c5dc17ae6 create depreciation scanning tool 2026-05-03 15:23:23 -04:00
zefie
4347543ef7 don't expose version on pc services if hide_minisrv_config is true 2026-05-03 15:15:27 -04:00
zefie
9d51abd9ab documentation is hard 2026-05-03 15:10:44 -04:00
zefie
118443305b fix wrong key in quicksetup 2026-05-03 15:09:35 -04:00
zefie
ab4453487e more quicksetup 2026-05-03 15:07:30 -04:00
zefie
e003d9795b even more accurate QuickSetup.md 2026-05-03 14:59:14 -04:00
zefie
e88dbd98cc more accurate QuickSetup.md 2026-05-03 14:58:17 -04:00
zefie
eba447cd06 buncha stuff, v0.9.74 2026-05-03 14:51:59 -04:00
zefie
9aec2d3150 Update WTVClientSessionData
- Hash passwords before encrypting them for slightly extra security
  - Backwards compatible with previous 2 methods
- findAccountByUsername & setSSID - for MSNTV2 stuff
2026-05-03 14:14:43 -04:00
zefie
f2e11f827f switch mail to domain_name 2026-05-03 13:30:30 -04:00
zefie
32b6129ae3 set domain name to minisrv.local, check content-length on proxies 2026-05-03 13:07:12 -04:00
zefie
94e8ecb60a protect MSNTV2 proxy a little bit 2026-05-03 09:33:46 -04:00
zefie
1cf8032be4 remove comma 2026-05-02 18:08:52 -04:00
zefie
47033f737d passport service 2026-05-02 18:07:30 -04:00
zefie
0f85d8da56 stuff and things 2026-05-02 18:06:11 -04:00
zefie
31c2d94e0b idfk 2026-05-02 15:38:27 -04:00
zefie
0848f4a015 update readme 2026-05-02 11:51:16 -04:00
zefie
92958e4c64 somewhat guest login 2026-05-02 10:15:24 -04:00
zefie
4f20c08ed0 use UUID that can decode back to SSID 2026-05-01 19:21:19 -04:00
zefie
9400c1f917 actually create tv2 account 2026-05-01 19:07:47 -04:00
zefie
ddbcb4be5e msntv2 initial work 2026-05-01 18:10:25 -04:00
zefie
20357809a7 move 'hide_minisrv_version' from services to config 2026-04-30 02:15:58 -04:00
zefie
c1d730dffa image encoder/audio proxy only for webtv clients 2026-04-30 02:11:13 -04:00
zefie
1dfcf710ff experimental AudioProxy 2026-04-30 00:59:29 -04:00
zefie
d3ffd6c678 Smoother ALF/ALP 2026-04-29 14:45:04 -04:00
zefie
b294771061 Allow throttling sendToClient() 2026-04-28 23:11:06 -04:00
zefie
a058aa5095 ALP works now 2026-04-28 17:39:15 -04:00
zefie
fa459026ff LC2 telly-gen for minibrowser seems broken, at least on MAME, so bypass tellyscript for now 2026-04-27 15:41:53 -04:00
290 changed files with 20719 additions and 377 deletions

87
QuickSetup.md Normal file
View File

@@ -0,0 +1,87 @@
# Quick Setup
## user_config.json
`user_config.json` (in the same folder as `app.js`) is where you put your local configuration overrides. It merges on top of `includes/config.json`**do not edit `includes/config.json` directly**.
You only need to include keys you want to override. Copy `user_config.example.json` as a starting point, or start with a minimal file:
```json
{
"config": {
"service_ip": "192.168.1.x"
}
}
```
The file supports `// line comments` and `/* block comments */`.
---
## configurator.js
`tools/configurator.js` is a command-line tool that sets or deletes individual keys in `user_config.json` without manually editing JSON.
**Usage:**
```
node tools/configurator.js <dot.path.key> <value> [--overwrite]
node tools/configurator.js <dot.path.key> --delete [--overwrite]
```
- Use `--overwrite` to replace a key that already exists.
- Keys are expressed as dot-separated paths (e.g. `config.keys.user_data_key`).
---
## Setting service_ip
`service_ip` tells the box where to connect, this CANNOT be `0.0.0.0`, and must be an address reachable by your box when it connects via your setup. Can be `127.0.0.1` if you are running TouchPPP or WebTV Viewer on the same machine as minisrv.
```
node tools/configurator.js config.service_ip 192.168.1.x --overwrite
```
---
## Setting user_data_key
`user_data_key` is used to encrypt user data. It should be a random secret string and **must be set before registering any users**.
Changing it after users have registered will require updating the userdata with `tools/update_user_data_key.js`. Making a backup
of `SessionStore/accounts` is recommended before running `tools/update_user_data_key.js`, it is pretty resilent against corruption, but just in case.
```
node tools/configurator.js config.keys.user_data_key YOUR_RANDOM_SECRET --overwrite
```
To generate a random key:
```
openssl rand -base64 32
```
## Disabling a standard service
You can disable a configured service by setting the `disabled: true` flag for that service. For example, to disable `wtv-admin`:
```
node tools/configurator.js services.wtv-admin.disabled true
```
## Enabling a disabled service
You can disable a configured service by setting the `disabled: false` flag for that service. For example, to enable `pc_services`:
```
node tools/configurator.js services.pc_services.disabled false
```
## Custom service pages
You can place your custom pages in `UserServiceVault/servicename/page.js`. For example, to override `wtv-home:/home`, you would create
`UserServiceVault/wtv-home/home.js`, and the server will automatically prioritize your page. You can mix and match service vaults, accessing
resources in the standard service vault within your custom pages.
## Updating minisrv
You can `git pull`, or extract a new archive over the existing folder. If you followed the directions and kept your changes in `user_config.json` and `UserServiceVault`,
then you can update minisrv without worrying about breakage or losing data. Do pay attention to the console, and if any deprecreations appear, fix them before updating to the version listed in the notice.

View File

@@ -9,7 +9,7 @@ This open source server is in beta status. Use at your own risk.
### Note: ### Note:
- This has nothing to do with streaming video. - This has nothing to do with streaming video.
- This is a server for the MSNTV (formerly WebTV) set-top box, and its various emulators. - This is a server for the MSNTV (formerly WebTV) set-top box, and its various emulators.
- This does not support MSNTV2. - This does not yet fully support MSNTV2.
### Current status: ### Current status:
- DB-less flat file client session store and registration system - DB-less flat file client session store and registration system

View File

@@ -11,7 +11,7 @@ const process = wtvshared.process;
const util = wtvshared.util; const util = wtvshared.util;
const nunjucks = require('nunjucks'); const nunjucks = require('nunjucks');
const {serialize, unserialize} = require('php-serialize'); const {serialize, unserialize} = require('php-serialize');
const {spawn} = require('child_process'); const {spawn, spawnSync} = require('child_process');
const http = require('follow-redirects').http; const http = require('follow-redirects').http;
const httpx = require(classPath + "/HTTPX.js"); const httpx = require(classPath + "/HTTPX.js");
const { URL } = require('url'); const { URL } = require('url');
@@ -27,6 +27,7 @@ const WTVClientSessionData = require(classPath + "/WTVClientSessionData.js");
const WTVMime = require(classPath + "/WTVMime.js"); const WTVMime = require(classPath + "/WTVMime.js");
const WTVFlashrom = require(classPath + "/WTVFlashrom.js"); const WTVFlashrom = require(classPath + "/WTVFlashrom.js");
const WTVImage = require(classPath + "/WTVImage.js"); const WTVImage = require(classPath + "/WTVImage.js");
const WTVAudioProxy = require(classPath + "/WTVAudioProxy.js");
const vm = require('vm'); const vm = require('vm');
const debug = require('debug')('app'); const debug = require('debug')('app');
const express = require('express'); const express = require('express');
@@ -37,6 +38,30 @@ const protocolServers = [];
const minisrv_config = wtvshared.getMiniSrvConfig(); // snatches minisrv_config const minisrv_config = wtvshared.getMiniSrvConfig(); // snatches minisrv_config
const wtvmime = new WTVMime(minisrv_config); const wtvmime = new WTVMime(minisrv_config);
const wtvAudioProxy = new WTVAudioProxy(minisrv_config);
function validateAudioProxy() {
if (!wtvAudioProxy || !wtvAudioProxy.isEnabled()) return;
try {
const check = spawnSync(wtvAudioProxy.config.ffmpegPath, ['-hide_banner', '-version'], {
stdio: ['ignore', 'ignore', 'ignore'],
timeout: 5000
});
if (check.error || check.status !== 0) {
console.warn(` # AudioProxy disabled: ffmpeg not found or failed to execute at '${wtvAudioProxy.config.ffmpegPath}'.`);
if (check.error) console.warn(` # AudioProxy ffmpeg error: ${check.error.message}`);
wtvAudioProxy.config.enabled = false;
return;
}
console.log(` * AudioProxy enabled: transcoding audio to ${wtvAudioProxy.config.bitrate} ${wtvAudioProxy.config.sampleRate}Hz mono MP3.`);
} catch (error) {
console.warn(` # AudioProxy disabled: ffmpeg validation failed: ${error.message}`);
wtvAudioProxy.config.enabled = false;
}
}
process process
.on('SIGTERM', shutdown('SIGTERM')) .on('SIGTERM', shutdown('SIGTERM'))
@@ -203,24 +228,43 @@ function getServiceString(service_name, overrides = {}) {
} }
const DEPRECIATED_CONFIG_PATH = path.join(__dirname, 'includes', 'depreciated.json');
function loadDepreciatedPatterns() {
try {
if (!fs.existsSync(DEPRECIATED_CONFIG_PATH)) {
return {};
}
const raw = fs.readFileSync(DEPRECIATED_CONFIG_PATH, 'utf8');
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed) || parsed.length === 0) {
return {};
}
const mapped = parsed
.filter((entry) => entry && typeof entry.pattern === 'string')
.map((entry) => ({
id: entry.id || entry.pattern,
pattern: new RegExp(entry.pattern, entry.flags || 'g'),
message: entry.message || 'Deprecated API usage found',
removeVersion: entry.removeVersion || 'unknown',
replacement: entry.replacement || null
}));
return mapped.length > 0 ? mapped : {};
} catch (error) {
console.warn('Failed to load includes/depreciated.json, using fallback deprecation patterns:', error.message);
return {};
}
}
// Deprecation warnings configuration // Deprecation warnings configuration
const deprecationWarnings = { const deprecationWarnings = {
// Array of deprecated patterns with their details // Array of deprecated patterns with their details
patterns: [ patterns: loadDepreciatedPatterns(),
{
// Example deprecations - you can modify these as needed
pattern: /session\_data\.hasCap\s*\(/g,
message: "session_data.hasCap() is deprecated and will be removed",
removeVersion: "0.9.80",
replacement: "Use session_data.capabilities.get() instead"
},
{
pattern: /(?<!wtvshared\.)getServiceString\s*\(/g,
message: "getServiceString() is deprecated and will be removed",
removeVersion: "0.9.80",
replacement: "Use wtvshared.getServiceString() instead"
}
],
// Enable/disable deprecation warnings globally // Enable/disable deprecation warnings globally
enabled: true, enabled: true,
@@ -338,7 +382,7 @@ const runScriptInVM = function (script_data, user_contextObj = {}, privileged =
"service_vaults": service_vaults, "service_vaults": service_vaults,
"service_deps": service_deps, "service_deps": service_deps,
"ssid_sessions": ssid_sessions, "ssid_sessions": ssid_sessions,
"moveArrayKey": wtvshared.moveArrayKey, "moveArrayKey": wtvshared.moveArrayKey, // deprecated - use wtvshared.moveArrayKey() instead
"cwd": (filename) ? path.dirname(filename) : __dirname, // current working directory "cwd": (filename) ? path.dirname(filename) : __dirname, // current working directory
// Our prototype overrides // Our prototype overrides
@@ -468,7 +512,7 @@ async function handleCGI(executable, cgi_file, socket, request_headers, vault, s
env.SERVER_PORT = request_data.port; env.SERVER_PORT = request_data.port;
env.SERVER_ADDR = request_data.host; env.SERVER_ADDR = request_data.host;
env.SERVER_NAME = request_data.host; env.SERVER_NAME = request_data.host;
if ((minisrv_config.services[socket.service_name] && minisrv_config.services[socket.service_name].hide_minisrv_version) || minisrv_config.config.hide_server_version) { if (minisrv_config.config.hide_minisrv_version) {
env.SERVER_SOFTWARE = "NodeJS; minisrv"; env.SERVER_SOFTWARE = "NodeJS; minisrv";
} else { } else {
// Full version // Full version
@@ -1256,7 +1300,7 @@ minisrv-no-mail-count: true`;
} }
} }
async function sendToClient(socket, headers_obj, data = null) { async function sendToClient(socket, headers_obj, data = null, throttle = 0) {
let headers = ""; let headers = "";
let content_length = 0; let content_length = 0;
const eol = "\n"; const eol = "\n";
@@ -1272,6 +1316,7 @@ async function sendToClient(socket, headers_obj, data = null) {
if (socket.destroy) socket.destroy(); if (socket.destroy) socket.destroy();
return; return;
} }
const isWebTVClient = Boolean(socket.ssid && ssid_sessions[socket.ssid]);
if (!socket.res) { if (!socket.res) {
wtv_connection_close = (headers_obj["wtv-connection-close"]) ? true : false; wtv_connection_close = (headers_obj["wtv-connection-close"]) ? true : false;
if (typeof (headers_obj["wtv-connection-close"]) !== 'undefined') delete headers_obj["wtv-connection-close"]; if (typeof (headers_obj["wtv-connection-close"]) !== 'undefined') delete headers_obj["wtv-connection-close"];
@@ -1367,24 +1412,22 @@ async function sendToClient(socket, headers_obj, data = null) {
delete headers_obj['minisrv-no-last-modified']; delete headers_obj['minisrv-no-last-modified'];
} }
if (minisrv_config.config.image_decoder && minisrv_config.config.image_decoder.enabled) { if (isWebTVClient && minisrv_config.config.image_decoder && minisrv_config.config.image_decoder.enabled) {
const contype_key = wtvshared.getCaseInsensitiveKey('content-type', headers_obj); const contype_key = wtvshared.getCaseInsensitiveKey('content-type', headers_obj);
let pngOpts = {};
if (contype_key) { if (contype_key) {
if (minisrv_config.config.image_decoder.image_formats && minisrv_config.config.image_decoder.image_formats.includes(headers_obj[contype_key].toLowerCase())) { if (minisrv_config.config.image_decoder.image_formats && minisrv_config.config.image_decoder.image_formats.includes(headers_obj[contype_key].toLowerCase())) {
const convertOpts = { const convertOpts = {
jpegQuality: minisrv_config.config.image_decoder.jpg_quality, jpegQuality: minisrv_config.config.image_decoder.jpg_quality,
type: imageArtemisType type: imageArtemisType,
imgopts: minisrv_config.config.image_decoder.image_options || null
}; };
if (minisrv_config.config.image_decoder.max_height > 0) convertOpts.maxHeight = minisrv_config.config.image_decoder.max_height; if (minisrv_config.config.image_decoder.max_height > 0) convertOpts.maxHeight = minisrv_config.config.image_decoder.max_height;
if (minisrv_config.config.image_decoder.max_width > 0) convertOpts.maxWidth = minisrv_config.config.image_decoder.max_width; if (minisrv_config.config.image_decoder.max_width > 0) convertOpts.maxWidth = minisrv_config.config.image_decoder.max_width;
if (minisrv_config.config.image_decoder.png_opts) {
pngOpts = minisrv_config.config.image_decoder.png_opts;
}
const sourceData = Buffer.isBuffer(data) ? data : Buffer.from(data); const sourceData = Buffer.isBuffer(data) ? data : Buffer.from(data);
try { try {
const converted = await WTVImage.ImageToWebTV(sourceData, convertOpts, pngOpts); const converted = await WTVImage.ImageToWebTV(sourceData, convertOpts);
data = converted.data; data = converted.data;
content_length = data.length; content_length = data.length;
var i=0; var i=0;
@@ -1392,7 +1435,7 @@ async function sendToClient(socket, headers_obj, data = null) {
// Image is too big, try to reduce quality // Image is too big, try to reduce quality
if (i < minisrv_config.config.image_decoder.max_quality_tries) { if (i < minisrv_config.config.image_decoder.max_quality_tries) {
convertOpts.jpegQuality -= minisrv_config.config.image_decoder.jpeg_interval; convertOpts.jpegQuality -= minisrv_config.config.image_decoder.jpeg_interval;
var converted2 = await WTVImage.ImageToWebTV(sourceData, convertOpts); var converted2 = await WTVImage.ImageToWebTV(sourceData, convertOpts, pngOpts);
data = converted2.data; data = converted2.data;
content_length = data.length; content_length = data.length;
i++; i++;
@@ -1421,6 +1464,27 @@ async function sendToClient(socket, headers_obj, data = null) {
} }
} }
if (isWebTVClient && wtvAudioProxy && wtvAudioProxy.isEnabled()) {
const contype_key = wtvshared.getCaseInsensitiveKey('content-type', headers_obj);
if (contype_key && wtvAudioProxy.shouldProxy(headers_obj[contype_key])) {
try {
const transformResult = await wtvAudioProxy.transformIfNeeded(headers_obj, data);
headers_obj = transformResult.headers;
data = transformResult.data;
content_length = Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data || '');
} catch (e) {
console.error('Audio proxy error:', e.message);
headers_obj = {
"Status": "413 Audio Too Long",
"Content-type": "text/plain"
};
data = (e.code === 'AUDIO_TOO_LONG') ?
`Audio exceeds maximum allowed duration of ${wtvAudioProxy.config.maxDurationSeconds} seconds.` :
`Audio proxy failure: ${e.message}`;
}
}
}
// if client can do compression, see if its worth enabling // if client can do compression, see if its worth enabling
// small files actually get larger, so don't compress them // small files actually get larger, so don't compress them
@@ -1545,7 +1609,7 @@ async function sendToClient(socket, headers_obj, data = null) {
if (!xpower && socket.service_name) { if (!xpower && socket.service_name) {
// add X-Powered-By header if not WebTV and not already set // add X-Powered-By header if not WebTV and not already set
xpower = 'X-Powered-By'; xpower = 'X-Powered-By';
if (minisrv_config.services[socket.service_name].hide_minisrv_version) { if (minisrv_config.config.hide_minisrv_version) {
// Don't report version // Don't report version
if (!socket.ssid) headers_obj[xpower] = "NodeJS; minisrv"; if (!socket.ssid) headers_obj[xpower] = "NodeJS; minisrv";
} else { } else {
@@ -1556,7 +1620,7 @@ async function sendToClient(socket, headers_obj, data = null) {
// delete if webtv // delete if webtv
if (socket.ssid) delete headers_obj[xpower]; if (socket.ssid) delete headers_obj[xpower];
if (socket.service_name) { if (socket.service_name) {
if (minisrv_config.services[socket.service_name].hide_minisrv_version) { if (minisrv_config.config.hide_minisrv_version) {
// Don't report version // Don't report version
if (!socket.ssid) headers_obj[xpower] = headers_obj[xpower] + "; NodeJS; minisrv"; if (!socket.ssid) headers_obj[xpower] = headers_obj[xpower] + "; NodeJS; minisrv";
} else { } else {
@@ -1614,64 +1678,98 @@ async function sendToClient(socket, headers_obj, data = null) {
let toClient = null; let toClient = null;
if (typeof data === 'string') { if (typeof data === 'string') {
toClient = headers + eol + data; toClient = headers + eol + data;
sendToSocket(socket, Buffer.from(toClient)); sendToSocket(socket, Buffer.from(toClient), throttle);
} else if (typeof data === 'object') { } else if (typeof data === 'object') {
let verbosity_mod = (headers_obj["wtv-encrypted"] === 'true') ? " encrypted response" : ""; let verbosity_mod = (headers_obj["wtv-encrypted"] === 'true') ? " encrypted response" : "";
if (socket_sessions[socket.id].secure_headers === true) { if (socket_sessions[socket.id].secure_headers === true) {
// encrypt headers // encrypt headers
if (minisrv_config.config.debug_flags.quiet) verbosity_mod += " with encrypted headers"; if (minisrv_config.config.debug_flags.quiet) verbosity_mod += " with encrypted headers";
const enc_headers = socket_sessions[socket.id].wtvsec.Encrypt(1, headers + eol); const enc_headers = socket_sessions[socket.id].wtvsec.Encrypt(1, headers + eol);
sendToSocket(socket, new Buffer.from(concatArrayBuffer(enc_headers, data))); sendToSocket(socket, new Buffer.from(concatArrayBuffer(enc_headers, data)), throttle);
} else { } else {
sendToSocket(socket, new Buffer.from(concatArrayBuffer(Buffer.from(headers + eol), data))); sendToSocket(socket, new Buffer.from(concatArrayBuffer(Buffer.from(headers + eol), data)), throttle);
} }
if (minisrv_config.config.debug_flags.quiet) console.debug(" * Sent" + verbosity_mod + " " + headers_obj.Status + " to client (Content-Type:", headers_obj['Content-type'], "~", headers_obj['Content-length'], "bytes)"); if (minisrv_config.config.debug_flags.quiet) console.debug(" * Sent" + verbosity_mod + " " + headers_obj.Status + " to client (Content-Type:", headers_obj['Content-type'], "~", headers_obj['Content-length'], "bytes)");
} }
} }
} }
async function sendToSocket(socket, data) { async function sendToSocket(socket, data, throttle = 0) {
const chunk_size = 16384; const chunk_size = 16384;
let can_write = true; if (!throttle || throttle <= 0) {
let close_socket = false; let can_write = true;
let expected_data_out = 0; let close_socket = false;
while ((socket.bytesWritten === 0 || socket.bytesWritten !== expected_data_out) && can_write) { let expected_data_out = 0;
if (expected_data_out === 0) expected_data_out = data.byteLength + (socket_sessions[socket.id].socket_total_written || 0); while ((socket.bytesWritten === 0 || socket.bytesWritten !== expected_data_out) && can_write) {
if (socket.bytesWritten === expected_data_out) break; if (expected_data_out === 0) expected_data_out = data.byteLength + (socket_sessions[socket.id].socket_total_written || 0);
if (socket.bytesWritten === expected_data_out) break;
const data_left = (expected_data_out - socket.bytesWritten); const data_left = (expected_data_out - socket.bytesWritten);
// buffer size = lesser of chunk_size or size remaining // buffer size = lesser of chunk_size or size remaining
const buffer_size = (data_left >= chunk_size) ? chunk_size : data_left; const buffer_size = (data_left >= chunk_size) ? chunk_size : data_left;
if (buffer_size < 0) { if (buffer_size < 0) {
socket.destroy(); socket.destroy();
close_socket = true; close_socket = true;
break; break;
}
const offset = (data.byteLength - data_left);
const chunk = new Buffer.alloc(buffer_size);
data.copy(chunk, 0, offset, (offset + buffer_size));
can_write = socket.write(chunk);
if (!can_write) {
socket.once('drain', function () {
sendToSocket(socket, data, throttle);
});
break;
}
} }
const offset = (data.byteLength - data_left); if (socket.bytesWritten === expected_data_out || close_socket) {
const chunk = new Buffer.alloc(buffer_size); socket_sessions[socket.id].socket_total_written = socket.bytesWritten;
data.copy(chunk, 0, offset, (offset + buffer_size)); if (socket_sessions[socket.id].expecting_post_data) delete socket_sessions[socket.id].expecting_post_data;
can_write = socket.write(chunk); if (socket_sessions[socket.id].header_buffer) delete socket_sessions[socket.id].header_buffer;
if (socket_sessions[socket.id].secure_buffer) delete socket_sessions[socket.id].secure_buffer;
if (socket_sessions[socket.id].buffer) delete socket_sessions[socket.id].buffer;
if (socket_sessions[socket.id].headers) delete socket_sessions[socket.id].headers;
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;
socket.setTimeout(minisrv_config.config.socket_timeout * 1000);
if (socket_sessions[socket.id] && socket_sessions[socket.id].close_me) socket.end();
if (socket_sessions[socket.id].destroy_me) socket.destroy();
}
return;
}
let offset = 0;
while (offset < data.byteLength) {
const buffer_size = Math.min(chunk_size, data.byteLength - offset);
const chunk = Buffer.alloc(buffer_size);
data.copy(chunk, 0, offset, offset + buffer_size);
offset += buffer_size;
const can_write = socket.write(chunk);
if (!can_write) { if (!can_write) {
socket.once('drain', function () { await new Promise(resolve => socket.once('drain', resolve));
sendToSocket(socket, data); }
});
break; if (offset < data.byteLength) {
const delay_ms = Math.max(1, Math.round((buffer_size * 8) / throttle * 1000));
await new Promise(resolve => setTimeout(resolve, delay_ms));
} }
} }
if (socket.bytesWritten === expected_data_out || close_socket) {
socket_sessions[socket.id].socket_total_written = socket.bytesWritten; socket_sessions[socket.id].socket_total_written = socket.bytesWritten;
if (socket_sessions[socket.id].expecting_post_data) delete socket_sessions[socket.id].expecting_post_data; if (socket_sessions[socket.id].expecting_post_data) delete socket_sessions[socket.id].expecting_post_data;
if (socket_sessions[socket.id].header_buffer) delete socket_sessions[socket.id].header_buffer; if (socket_sessions[socket.id].header_buffer) delete socket_sessions[socket.id].header_buffer;
if (socket_sessions[socket.id].secure_buffer) delete socket_sessions[socket.id].secure_buffer; if (socket_sessions[socket.id].secure_buffer) delete socket_sessions[socket.id].secure_buffer;
if (socket_sessions[socket.id].buffer) delete socket_sessions[socket.id].buffer; if (socket_sessions[socket.id].buffer) delete socket_sessions[socket.id].buffer;
if (socket_sessions[socket.id].headers) delete socket_sessions[socket.id].headers; if (socket_sessions[socket.id].headers) delete socket_sessions[socket.id].headers;
if (socket_sessions[socket.id].post_data) delete socket_sessions[socket.id].post_data; 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_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].post_data_percents_shown) delete socket_sessions[socket.id].post_data_percents_shown;
socket.setTimeout(minisrv_config.config.socket_timeout * 1000); socket.setTimeout(minisrv_config.config.socket_timeout * 1000);
if (socket_sessions[socket.id] && socket_sessions[socket.id].close_me) socket.end(); if (socket_sessions[socket.id] && socket_sessions[socket.id].close_me) socket.end();
if (socket_sessions[socket.id].destroy_me) socket.destroy(); if (socket_sessions[socket.id].destroy_me) socket.destroy();
}
} }
function concatArrayBuffer(buffer1, buffer2) { function concatArrayBuffer(buffer1, buffer2) {
@@ -2406,6 +2504,8 @@ else console.log(" * Shenanigans disabled");
if (minisrv_config.config.image_decoder.enabled) console.log(" * WebTV Unsupported images will be processed and converted for WebTV clients"); if (minisrv_config.config.image_decoder.enabled) console.log(" * WebTV Unsupported images will be processed and converted for WebTV clients");
else console.log(" * WebTV Unsupported images will not be processed, and sent to client as-is"); else console.log(" * WebTV Unsupported images will not be processed, and sent to client as-is");
validateAudioProxy();
ports.sort(); ports.sort();
pc_ports.sort(); pc_ports.sort();
@@ -2602,4 +2702,13 @@ if (bind_ports.length > 0) console.log(` * Started WTVP Server on port${bind_por
if (pc_bind_ports.length > 0) console.log(` * Started HTTP Server on port${pc_bind_ports.length !== 1 ? "s" : ""} ` + pc_bind_ports.join(", ") + "..."); if (pc_bind_ports.length > 0) console.log(` * Started HTTP Server on port${pc_bind_ports.length !== 1 ? "s" : ""} ` + pc_bind_ports.join(", ") + "...");
if (protocolHandledPorts.size > 0) console.log(` * Started ${protocolHandledPorts.size} specialized protocol handler${protocolHandledPorts.size !== 1 ? "s" : ""} on port${protocolHandledPorts.size !== 1 ? "s" : ""} ` + [...protocolHandledPorts].map(([sn, sp, pt]) => `${pt} (${sp.toUpperCase()})`).join(", ") + "..."); if (protocolHandledPorts.size > 0) console.log(` * Started ${protocolHandledPorts.size} specialized protocol handler${protocolHandledPorts.size !== 1 ? "s" : ""} on port${protocolHandledPorts.size !== 1 ? "s" : ""} ` + [...protocolHandledPorts].map(([sn, sp, pt]) => `${pt} (${sp.toUpperCase()})`).join(", ") + "...");
const listening_ip_string = (minisrv_config.config.bind_ip !== "0.0.0.0") ? "IP: " + minisrv_config.config.bind_ip : "all interfaces"; const listening_ip_string = (minisrv_config.config.bind_ip !== "0.0.0.0") ? "IP: " + minisrv_config.config.bind_ip : "all interfaces";
console.log(" * Listening on", listening_ip_string, "~", "Service IP:", service_ip); console.log(" * Listening on", listening_ip_string, "~", "Service IP:", service_ip);
// Security warning for default user data encryption key
if (minisrv_config.config.keys.user_data_key === "PNa$WN7gz}!T=t6X7^=|Ii##CEB~p\\EP") {
console.log(" * WARNING: You are using the default user data encryption key. This is not secure, and you should change it in the configuration file before registering any users.");
console.log(" * To generate a random key in bash or PowerShell, you can run: node ./tools/configurator.js config.keys.user_data_key $(openssl rand -base64 32)");
console.log(" * After changing the key in the user_config, please restart the server.");
console.log(" * If you had existing users prior to changing the key, you can run tools/update_user_data_key.js to update existing accounts with the new key.");
console.log(" * Making a backup of your user accounts before doing this is highly recommended, in case something goes wrong during the update process.");
}

View File

@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIIDwzCCAqugAwIBAgIULL+4ofEIwgHwoGgoEJ+mJOmeF4YwDQYJKoZIhvcNAQEF
BQAwcDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk9IMRMwEQYDVQQHDApCdXR0IENy
YWNrMSAwHgYDVQQKDBdVbmRlcndlYXIgSW5zcGVjdG9yICMxMjEdMBsGA1UECwwU
VGhpbmcgTG9va2VyIEV4cGVydHMwIBcNMDAwNDIwMDAwMDAxWhgPMjA2OTA2MDkw
MDAwMDFaMHAxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJPSDETMBEGA1UEBwwKQnV0
dCBDcmFjazEgMB4GA1UECgwXVW5kZXJ3ZWFyIEluc3BlY3RvciAjMTIxHTAbBgNV
BAsMFFRoaW5nIExvb2tlciBFeHBlcnRzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEArN5xb8PqUKNNTitGuxyJJxEsWTIt7t7eobSq1+V3hTpknJEspRpb
JeD5Ep86pWn4aGoWYT0JU9w4FRJFPaBPAMmLFTIYv1bySlFwna+5J67dAtMm59Pr
t3ZSw2dQpads3LGZRSpkY1mv9GC2TVVk/TANWZmWYvEZZ4/2E2YuMJZ6v6gNIY7Y
pfJ6F6Q4+d/Dgrvow4E35lmNuM0Y95sdMhKy8HTJ0ecU/bMLKyoXXZyt7LxezT+T
w3J2EDVV03lDXZhb0h2ZECmpCAgnKsQ/WmLMjEGeojDQnm+QzTaFHqO0148RMbYN
lxURCZVl+keYe1LTHW/K+2f58DeeI0z11wIDAQABo1MwUTAdBgNVHQ4EFgQU0bsp
ZOhtJGP0dLS4sdaA4nmQ5ocwHwYDVR0jBBgwFoAU0bspZOhtJGP0dLS4sdaA4nmQ
5ocwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEANAV1WNeCggjK
eb8kIGkt2R3PMMBy2w+rC4X80xygh+NHU5neJKM1lpq6NLkt5rCBXNCTDb5uPjRv
mcrGeIYJYXMy39htjlYgPtfUiQx7e7eq3g1mMHYN26Gr9p7BwJW0QVvaA6zB3lqt
HGL7hwQyBCD9P/0sR91sjyLF9mPkaO4sBM5EbrV89LXbD+OPqn10X2Mu4trI3bd4
R02Pl0A/rDO/S7bOGc4HGGPSsmgeoapydvkqgnXL497zD1Sv103n96hiuq4Dhurj
gfnGxQvEN7MCc31HE2ufkTHjNc62RatseiCLxJE3sBhMl9aaNEhtM3FNDXZnhMbs
0/7C2b5kSg==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCs3nFvw+pQo01O
K0a7HIknESxZMi3u3t6htKrX5XeFOmSckSylGlsl4PkSnzqlafhoahZhPQlT3DgV
EkU9oE8AyYsVMhi/VvJKUXCdr7knrt0C0ybn0+u3dlLDZ1Clp2zcsZlFKmRjWa/0
YLZNVWT9MA1ZmZZi8Rlnj/YTZi4wlnq/qA0hjtil8noXpDj538OCu+jDgTfmWY24
zRj3mx0yErLwdMnR5xT9swsrKhddnK3svF7NP5PDcnYQNVXTeUNdmFvSHZkQKakI
CCcqxD9aYsyMQZ6iMNCeb5DNNoUeo7TXjxExtg2XFREJlWX6R5h7UtMdb8r7Z/nw
N54jTPXXAgMBAAECggEAN5W7IfX8FPu9csIllyrAyygRe4jQDjvpmFNwj8VAHj29
dj6T/W6xGkfxrtQ2VlPxxRk/ovquUiHAgr4CN6OhY55qs2ENZhD+DXmMXZfQUHtA
o5TjsK2K4M4SJLTG0M06CuisYPoVl4CSPGEQnWM0+tiFombpXV0rDwrSVECJ36Mr
x/HPoMJQPewBSI4lG2ACQMBmQ+7WQVwl0mprJjIFJYW9BXFRkqmFKSXq6G302VcH
VOjiSOF/8BR64worbo9UrpPBPMLZMAW1rwZx50Chwhplben8B2uQNSqrc7Q9ZA3C
2XJ8Wi0KCeYmJy+xng3LT22Sdme1UC8YfC9iCsHfaQKBgQDe3sN97UK+WVIgvA8Q
+4+rpkUWOy/dCoTzPuxHy4qSTHyF723+URyVkNF/P5EtwKwpIuJ/WMwIyUF0EqLQ
0+6HrmqfO5CR+p/2xJhFPHi1qn3uCM8pw5O9i5MjhOr1jM1RQz1xe0rToLiTlq3x
sSrYzDmIs61kBH2NKaVp38Oh5QKBgQDGkObMw8Gyn5JT1ewkS0VGDT5bbVK5sptJ
KTkaikKTuNV5RLya+7SxnR7rOBWWLN+ZpdQygM8IlUh0TJiTZazUkKUSNv3isIbF
rfak10q8Zad6RkaQyyH7GKhBOnMs0z3kqgL0SvNhoIs2KKsE46UoMVE49/C9Ma8Y
zbdEBLTtCwKBgQCRJc934d/IDHAadZ/yVYOaLO1trxpbARDZQq+rinozEbE/oVGn
gbf2CJ0IHKQ2gfcdy2Rrv68SQdBpAgIbswr0Prmd/rMG/4zSr/LjlKCg3+qn7gDN
mFxN4+ruBRDo3syREhOgJsXy0gejx0x9zf6ztz35M9vG+c2y896Q93R3qQKBgA/e
AmN4fSD9+V5zqMQZs7ZuVn5N1R97s0b8YVDKnZlaWsyu/ndQB9dtm8vmFmuCuHd5
teQ3QNQJwdlxSXv10wLFcDttY4pa2tovFZeEkLdPVDvEI91sLhH3nXJg7lU1qCt+
nm/REXPKtXUleM0SN99nWXs47ObhcoGD5tIroh2TAoGBAISazlBBrmnNOavnUzWa
Ka2uXk/2txan8Fh9jul0g6GPPsI7Ej3YMf3ccC9iotVugteHrBUK0cNbesydw3J0
PIs05XLX0curxoTox2Jeynq+ajy2tnmrDHDfilXdxRt2rsQlobZi3qoJskwPFYhf
ssL5LnP78Pv/t2H01jDSpZXu
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIE8TCCA9mgAwIBAgIQhTOF71uduRa0SXk4z+A7ujANBgkqhkiG9w0BAQUFADB0
MRkwFwYDVQQDDBBtaW5pc3J2IHNlcnZpY2VzMREwDwYDVQQIDAhOZXcgWW9yazEL
MAkGA1UEBhMCVVMxHjAcBgkqhkiG9w0BCQEWD3plZmllQHplZmllLm5ldDEXMBUG
A1UECgwOWmVmaWUgTmV0d29ya3MwIBcNMDAwMTAxMTIwMDAwWhgPMjA5OTEyMzEy
MzU5NTlaMFExKTAnBgNVBAMTIGhlYWR3YWl0ZXIudHJ1c3RlZC5tc250di5tc24u
Y29tMRcwFQYDVQQKEw5aZWZpZSBOZXR3b3JrczELMAkGA1UEBhMCVVMwggEiMA0G
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6pNNH2lF7SFx8cEIF1ImA7AI4bv/W
qvbErvUYJOfrOLXOfvXnxWEbEfDk9+XEf+JD8PQo2rvze1cVcXjVO7i2m+c4jdWw
X/VPdRM0NpoppFXbWC7nWNuXhZD/S7f6pUEJez7BYUpEeBFdR9eFb8VPo8+kefMz
inYznvP1UAn9wwoSIFDglX9QbijkJ/ZKtOY3vxCMVBZedWVnMPEJt928NJBNDGcC
VeV1thEAAVbQBf5nyhF9VfblTzEHoxq+d6rvi4rVkd0ZYqQPCcafDFccXf6YNQcz
8cmwzha61bgLbJLPNPiSqbqL8GNfsHbt2vyX6OhYpKwF+Y2CCp0xbGflAgMBAAGj
ggGeMIIBmjAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEF
BQcDATCCAWkGA1UdEQSCAWAwggFcgiBoZWFkd2FpdGVyLnRydXN0ZWQubXNudHYu
bXNuLmNvbYIZc2cxLnRydXN0ZWQubXNudHYubXNuLmNvbYIZc2cyLnRydXN0ZWQu
bXNudHYubXNuLmNvbYIZc2czLnRydXN0ZWQubXNudHYubXNuLmNvbYIZc2c0LnRy
dXN0ZWQubXNudHYubXNuLmNvbYINbXNudHYubXNuLmNvbYIWbWFpbC5zZXJ2aWNl
cy5saXZlLmNvbYIObG9naW4ubGl2ZS5jb22CEXBvcHRpbWl6ZS5tc24uY29tghFm
YXZvcml0ZXMubXNuLmNvbYIRbWVzc2VuZ2VyLm1zbi5jb22CEWxpdmVmaWxlc3Rv
cmUuY29tghZ1c2Vycy5zdG9yYWdlLmxpdmUuY29tgglnLm1zbi5jb22CF21zbmlh
bG9naW4ucGFzc3BvcnQuY29tgg1taW5pc3J2LmxvY2FsMA0GCSqGSIb3DQEBBQUA
A4IBAQAZTy82heE64hCFxEiIFIxglGyPVU14wA2gXrv82mci/U0h/xBHfIfQWY8d
ULM6dO6kEk2DriBbo2ET10rkBwCTqa1iSDRN1eg0umdT2vbEYigjOelZJQqJi3Ua
LGBrPh8PK7juGa37aEdMWFLxmDtfEXE//OmMiliXU6bIi44pqM571X3Q3WPh3C3K
xOCOwQMgTPovLJDwRIJNyTrnb0kI+1s7oOtZ+QUQa7frY0Bgxn4IMEnZoIkOcAkh
R1m/OKyjjqQ8EVM73dTeiNr0yByG64C8dsVhJVXPT3GZOl7p5Pof9VfQg9Qr39Vh
ds7T/BVzQ79G8IXQ+AgZnZHu7pnn
-----END CERTIFICATE-----

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAuqTTR9pRe0hcfHBCBdSJgOwCOG7/1qr2xK71GCTn6zi1zn71
58VhGxHw5PflxH/iQ/D0KNq783tXFXF41Tu4tpvnOI3VsF/1T3UTNDaaKaRV21gu
51jbl4WQ/0u3+qVBCXs+wWFKRHgRXUfXhW/FT6PPpHnzM4p2M57z9VAJ/cMKEiBQ
4JV/UG4o5Cf2SrTmN78QjFQWXnVlZzDxCbfdvDSQTQxnAlXldbYRAAFW0AX+Z8oR
fVX25U8xB6Mavneq74uK1ZHdGWKkDwnGnwxXHF3+mDUHM/HJsM4WutW4C2ySzzT4
kqm6i/BjX7B27dr8l+joWKSsBfmNggqdMWxn5QIDAQABAoIBAEGq0DNNmrF3aiLW
FESc3KwhXT6hvx22FRBqRg1ynq5hy4WVocsj5OBzVYAZwBt8qw0gb6cYHlyyHpeK
zuqnEnwdKiL5tB9UA6krFdCfDWptSU/dHNOEre4HrlZEO7zR+6nsVM4Q/uJMJD/f
kPJ/urokdl/2EB0oMCJFYKwEtx8yrxJriNAjr4he0ibLHKiSTobanpbJDaDrIvOk
3njH8TNxjj/wdIaLJIWP/xPNTgMmUERYT6fDRe9p7gXg8R2+kSuvhpZSCa/fHxp0
6E1UESZyfHQgieUfgD7SB9Teq2gxTBIHGDsRhjgHLAa5R+p0lc8DEIgO+32hOI6v
p3CdThkCgYEA90EtOQZeGt9yVfLOTP7G6WiuiBC/kS4aEJDbr+JqtFJtEOXZWWuh
pbT7M+r/IDS/+TFPRtXe4Xm8eFmUIyW59V+9/jAqhU43zQZye159oKs6lyTmk84j
byQVsarnYTQxa2psqVWDbzfDAR3M01vIPa9pSmeCBoZVCxoi0Cd8OtMCgYEAwT7V
s9K0oajY49Kgeo8RA77/a1tGqlaEuQRX8KR85wcGm3nG7rDMaxbEoGurhwy8n4HW
KigQxFezhjXaTvFonCgTg5Dm0jAaCtHsJw48tGpkXZWrJ+elCZZPSred3Z1hKmvJ
SJ64dGP+cS4icw5NzsoGEpJZHrr9BBVYbDNML2cCgYEAmXi8QEQij12Y056VzRbr
kp+mjdCPh+bsyNGRezf38ZukFTQGWEnFmVyf/BbmazAy5NNlmNtRr/TnNnCr0bEu
Hw9hl/B/xCTL4BgbYVZCdkMyZ/TApofyWJ82VAR4AE7sSfdSIT1yCsu63+uGYr76
qMdDfKqI+9HP4cdESp3nr38CgYAGKOOU9M1vLbukH22gGnlXXjo0CNfKzDE02I+Z
CxU0JAQw5oPRze7mJvajimsQRfapOvFBrL9EEuuVBphr1cQY3iopEnBZGNFrsN9P
K2QB+DY0yXWIMxkOoizq28l7a+3R9VeYKf8FLr7IisjsU/Nk+QmSg/m1Qg6Yl7mW
0VfHVwKBgHT1oKjsQQc+a1dxwgXO1AKTMpUbpy88vRMaGu2emnHcTg7Y0lpwhsMo
4SSh2kN4ijLs8BeDbpxQf4ygWrqxWyeV73c4om/ADo4RWzMBzMm3lEcHiHA8jJSJ
cFCTew4Xiqzkbae5zU+mL6Dsxw8KSrkzSa62P8dgAGWQ1RsjoI3F
-----END RSA PRIVATE KEY-----

View File

@@ -1,15 +1,17 @@
const minisrv_service_file = true; const minisrv_service_file = true;
const title = minisrv_config.config.hide_minisrv_version ? "zefie's minisrv PC Services" : `zefie minisrv v${minisrv_config.version} PC Services`;
headers = `200 OK headers = `200 OK
Content-Type: text/html` Content-Type: text/html`
data = `<html> data = `<html>
<head> <head>
<title>zefie minisrv v${minisrv_config.version}</title> <title>${title}</title>
</head> </head>
<body bgcolor="#000000" text="#449944"> <body bgcolor="#000000" text="#449944">
<p> <p>
Welcome to the zefie minisrv v${minisrv_config.version} PC Services Welcome to ${title}
</p> </p>
<hr> <hr>
<a href="/viewergen/">WebTV Viewer Generator</a><br> <a href="/viewergen/">WebTV Viewer Generator</a><br>

View File

@@ -0,0 +1,76 @@
const minisrv_service_file = true;
headers = `200 OK
Content-type: text/html`;
data = `<HTML>
<HEAD>
<title id="title"></title>
</HEAD>
<body>
<iframe id=checkmail style="display:none"></iframe>
<script language="javascript">
var tvShell = new ActiveXObject("MSNTV.TVShell");
function IsNightlyEnabled() {
var taskScheduler = tvShell.TaskScheduler;
var updateTask = null;
for (var i = 0; ((i < taskScheduler.Count) && (updateTask == null)); i++) {
if (taskScheduler.Item(i).Caller == 'NightlyUpdate') {
updateTask = taskScheduler.Item(i);
}
}
if (updateTask != null) {
return(true);
}
else {
return(false);
}
}
function GotoBoxCheck() {
var url = 'https://sg1.trusted.msntv.msn.com/connection/GatePage.aspx?phase=BoxCheck&purpose=Authorize';
var parms='';
parms += 'BoxId=' + tvShell.SystemInfo.BoxIDService + '&';
parms += 'WANProvider=' + tvShell.ConnectionManager.WANProvider + '&';
parms += 'version=' + encodeURIComponent(tvShell.SystemInfo.LastVersion) + '&';
if ((tvShell.ConnectionManager.MSNIAManager != null) && (tvShell.ConnectionManager.MSNIAManager.CurrentConnector != null)) parms += 'ConnectorName=' + encodeURIComponent(tvShell.ConnectionManager.MSNIAManager.CurrentConnector.Name) + '&';
if (tvShell.UserManager.CurrentUser != null) parms += 'domain=' + encodeURIComponent(tvShell.UserManager.CurrentUser.Domain) + '&';
parms += 'NumRedirects=0&';
parms += 'NightlyEnabled=' + IsNightlyEnabled() + '&';
parms += 'x=y';
var myPanel = tvShell.PanelManager.Item('service');
if (myPanel) myPanel.PostToURL(url, parms);
}
var progressPanel = tvShell.PanelManager.Item('progress');
function SetProgress(text, percent) {
if (progressPanel) {
progressPanel.Document.SetProgressText(text);
progressPanel.Document.SetProgressPercent(percent);
}
}
function IsServicePanel() {
if ((window.name == null) || ((window.name != null) && (window.name.toLowerCase() != 'service'))) {
return(false);
}
return(true);
}
function DontContinue() {
var currentUser = tvShell.UserManager.CurrentUser;
if (currentUser != null && currentUser.IsAuthorized) {
window.location.replace(tvShell.UserManager.CurrentUser.ServiceList.Item('home::home').URL);
}
else {
tvShell.ConnectionManager.ServiceState = 'ReSignIn';
}
}
if (!IsServicePanel()) {
DontContinue();
}
else {
tvShell.MeteringManager.Stop();
SetProgress('Please wait while we sign you into MSN TV.', 10);
GotoBoxCheck();
}
</script>
</body>
</HTML>
`;

View File

@@ -0,0 +1,470 @@
const minisrv_service_file = true;
if (!session_data) {
session_data = new WTVClientSessionData(minisrv_config, (socket.ssid || null))
}
// Sorry Zef :kek
// https://git.computernewb.com/yellows111/msnp-wiki/src/branch/master/docs/services/rst.md
// the RST_ cookie stuff was code that was temp until we had proper token authentication
const NS = {
SOAP: "http://schemas.xmlsoap.org/soap/envelope/",
WSSE: "http://schemas.xmlsoap.org/ws/2003/06/secext",
WSP: "http://schemas.xmlsoap.org/ws/2002/12/policy",
WSU: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
WSA: "http://schemas.xmlsoap.org/ws/2004/03/addressing",
WST: "http://schemas.xmlsoap.org/ws/2004/04/trust",
PSF: "http://schemas.microsoft.com/Passport/SoapServices/SOAPFault",
ENC: "http://www.w3.org/2001/04/xmlenc#",
DS: "http://www.w3.org/2000/09/xmldsig#"
};
function getCookie(cookieString, name) {
if (!cookieString) return null;
const match = cookieString.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : null;
}
function setCookie(name, value, options = {}) {
const cookie = `${name}=${encodeURIComponent(value)}`;
const path = options.path || '/';
const expires = options.expires || '';
return `${cookie}; path=${path}${expires ? `; expires=${expires}` : ''}`;
}
function formatDateTime(dt) {
return dt.toISOString().replace(/\.\d{3}Z$/, 'Z');
}
function getClientIP() {
const forwarded = request_headers['x-forwarded-for'];
if (forwarded) {
const ips = forwarded.split(',');
return ips[0].trim();
}
return request_headers['x-real-ip'] || '127.0.0.1';
}
function generateRandomToken(userId, appliesTo, isLegacy = false) {
const timestamp = Date.now();
const randomPart = crypto.randomBytes(32).toString('hex');
if (isLegacy) {
const tokenData = `${userId}|${appliesTo}|${timestamp}|${randomPart}`;
return crypto.createHash('sha256').update(tokenData).digest('hex');
} else {
const tokenData = {
uid: userId,
app: appliesTo,
ts: timestamp,
rand: randomPart,
ver: '1.0'
};
return Buffer.from(JSON.stringify(tokenData)).toString('base64');
}
}
function extractXmlValue(xml, elementName) {
if (!xml) return null;
const patterns = [
new RegExp(`<${elementName}>([\\s\\S]*?)</${elementName}>`, 'i'),
new RegExp(`<wsse:${elementName}>([\\s\\S]*?)</wsse:${elementName}>`, 'i'),
new RegExp(`<wst:${elementName}>([\\s\\S]*?)</wst:${elementName}>`, 'i'),
new RegExp(`<ps:${elementName}>([\\s\\S]*?)</ps:${elementName}>`, 'i')
];
for (const regex of patterns) {
const match = xml.match(regex);
if (match && match[1]) {
let value = match[1].trim();
value = value.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
return value;
}
}
return null;
}
function extractTokenFromCipherValue(xml) {
if (!xml) return null;
const cipherRegex = /<CipherValue>([\s\S]*?)<\/CipherValue>/gi;
let match;
let token = null;
while ((match = cipherRegex.exec(xml)) !== null) {
let cipherValue = match[1].trim();
if (cipherValue && cipherValue.length > 0) {
token = cipherValue;
debug("Found CipherValue token:", token.substring(0, 50) + "...");
break;
}
}
return token;
}
function validateTokenAndGetUser(token) {
try {
let userId = null;
let email = null;
if (request_headers.cookie) {
userId = getCookie(request_headers.cookie, 'RST_Auth');
email = getCookie(request_headers.cookie, 'RST_Email');
if (!email) email = getCookie(request_headers.cookie, 'rst_email');
if (!email) email = getCookie(request_headers.cookie, 'rst_username');
}
if (!userId) {
userId = crypto.createHash('md5').update(token).digest('hex');
email = `user_${userId.substring(0, 8)}@example.com`;
}
debug(`Token validated - UserId: ${userId}, Email: ${email}`);
return { success: true, userId, email };
} catch (error) {
console.error("Token validation error:", error);
return { success: false, userId: null, email: null };
}
}
function generateErrorResponse(errorCode, errorText) {
const now = formatDateTime(new Date());
headers = `Status: 200 OK
Content-type: text/xml; charset=utf-8`;
return `<?xml version="1.0" encoding="utf-8"?>
<S:Envelope xmlns:S="${NS.SOAP}" xmlns:psf="${NS.PSF}">
<S:Header>
<psf:pp>
<psf:serverVersion>1</psf:serverVersion>
<psf:authstate>0x80048800</psf:authstate>
<psf:reqstatus>${errorCode}</psf:reqstatus>
<psf:serverInfo Path="Live1" RollingUpgradeState="ExclusiveNew" LocVersion="0" ServerTime="${now}">
${minisrv_config.config.service_name} [minisrv ${minisrv_config.config.hide_minisrv_version ? "beta" : minisrv_version_string.replace("zefie's wtv minisrv ","")}]
</psf:serverInfo>
<psf:cookies></psf:cookies>
<psf:response></psf:response>
</psf:pp>
</S:Header>
<S:Body>
<S:Fault>
<S:Code>
<S:Value>S:Sender</S:Value>
<S:Subcode>
<S:Value>wst:FailedAuthentication</S:Value>
</S:Subcode>
</S:Code>
<S:Reason>
<S:Text xml:lang="en-US">Authentication Failure</S:Text>
</S:Reason>
<S:Detail>
<psf:error>
<psf:value>${errorCode}</psf:value>
<psf:internalerror>
<psf:code>0x80041012</psf:code>
<psf:text>${errorText}</psf:text>
</psf:internalerror>
</psf:error>
</S:Detail>
</S:Fault>
</S:Body>
</S:Envelope>`;
}
function generateSuccessResponse(requestBody, userId, email, firstName, lastName) {
const now = new Date();
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const createdTime = formatDateTime(now);
const expiresTime = formatDateTime(tomorrow);
const puid = crypto.randomBytes(16).toString('hex').toUpperCase();
const cid = crypto.randomBytes(8).toString('hex').toUpperCase();
const safeFirstName = firstName || email.split('@')[0] || "User";
const safeLastName = lastName || "User";
const clientIp = getClientIP();
const rstRegex = /<wst:RequestSecurityToken[\s\S]*?<\/wst:RequestSecurityToken>/gi;
const responses = [];
let match;
let foundRst = false;
let rstIndex = 0;
while ((match = rstRegex.exec(requestBody)) !== null) {
foundRst = true;
const rstBlock = match[0];
const addressMatch = rstBlock.match(/<wsa:Address>(.*?)<\/wsa:Address>/i);
let appliesTo = addressMatch ? addressMatch[1] : "urn:passport:compact";
const policyMatch = rstBlock.match(/<wsse:PolicyReference\s+URI="([^"]+)"/i);
const policy = policyMatch ? policyMatch[1] : null;
const isLegacy = appliesTo.includes("Passport.NET");
const tokenType = isLegacy ? "urn:passport:legacy" : "urn:passport:compact";
const needsProofToken = policy === "MBI_KEY_OLD";
const token = generateRandomToken(userId, appliesTo, isLegacy);
wtvshared.storeToken(token, socket.ssid, userId, expiresTime);
const tokenId = isLegacy ? `BinaryDAToken${rstIndex}` : `Compact${rstIndex}`;
const binarySecret = crypto.randomBytes(32).toString('base64');
let requestedSecurityToken;
if (isLegacy) {
requestedSecurityToken = `
<wst:RequestedSecurityToken>
<EncryptedData xmlns="${NS.ENC}" Id="${tokenId}" Type="http://www.w3.org/2001/04/xmlenc#Element">
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc"/>
<ds:KeyInfo xmlns:ds="${NS.DS}">
<ds:KeyName>http://Passport.NET/STS</ds:KeyName>
</ds:KeyInfo>
<CipherData>
<CipherValue>${token}</CipherValue>
</CipherData>
</EncryptedData>
</wst:RequestedSecurityToken>`;
} else {
let tokenValue = `t=${token}`;
if (needsProofToken) {
tokenValue += `&p=profile`;
}
requestedSecurityToken = `
<wst:RequestedSecurityToken>
<wsse:BinarySecurityToken Id="${tokenId}">${tokenValue}</wsse:BinarySecurityToken>
</wst:RequestedSecurityToken>`;
}
let responseXml = `
<wst:RequestSecurityTokenResponse>
<wst:TokenType>${tokenType}</wst:TokenType>
<wsp:AppliesTo xmlns:wsa="${NS.WSA}">
<wsa:EndpointReference>
<wsa:Address>${appliesTo}</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<wst:LifeTime>
<wsu:Created>${createdTime}</wsu:Created>
<wsu:Expires>${expiresTime}</wsu:Expires>
</wst:LifeTime>
${requestedSecurityToken}
<wst:RequestedTokenReference>
<wsse:KeyIdentifier ValueType="${tokenType}"/>
<wsse:Reference URI="#${tokenId}"/>
</wst:RequestedTokenReference>`;
if (needsProofToken || isLegacy) {
responseXml += `
<wst:RequestedProofToken>
<wst:BinarySecret>${binarySecret}</wst:BinarySecret>
</wst:RequestedProofToken>`;
}
responseXml += `
</wst:RequestSecurityTokenResponse>`;
responses.push(responseXml);
rstIndex++;
}
if (!foundRst) {
const defaultToken = generateRandomToken(userId, "urn:passport:compact", false);
wtvshared.storeToken(defaultToken, socket.ssid, userId, expiresTime);
responses.push(`
<wst:RequestSecurityTokenResponse>
<wst:TokenType>urn:passport:compact</wst:TokenType>
<wst:RequestedSecurityToken>
<wsse:BinarySecurityToken Id="Compact0">t=${defaultToken}</wsse:BinarySecurityToken>
</wst:RequestedSecurityToken>
<wst:LifeTime>
<wsu:Created>${createdTime}</wsu:Created>
<wsu:Expires>${expiresTime}</wsu:Expires>
</wst:LifeTime>
</wst:RequestSecurityTokenResponse>`);
}
headers = `Status: 200 OK
Content-type: text/xml; charset=utf-8
Set-Cookie: RST_Auth=${userId}; path=/; HttpOnly
Set-Cookie: RST_Email=${email}; path=/`;
return `<?xml version="1.0" encoding="utf-8"?>
<S:Envelope xmlns:S="${NS.SOAP}">
<S:Header>
<psf:pp xmlns:psf="${NS.PSF}">
<psf:serverVersion>1</psf:serverVersion>
<psf:PUID>${puid}</psf:PUID>
<psf:configVersion>16.000.26889.00</psf:configVersion>
<psf:uiVersion>3.100.2179.0</psf:uiVersion>
<psf:mobileConfigVersion>16.000.26208.0</psf:mobileConfigVersion>
<psf:authstate>0x48803</psf:authstate>
<psf:reqstatus>0x0</psf:reqstatus>
<psf:serverInfo Path="Live1" RollingUpgradeState="ExclusiveNew" LocVersion="0" ServerTime="${now.toISOString()}">
NOBELLIUM 16.0.30846.6
</psf:serverInfo>
<psf:cookies></psf:cookies>
<psf:browserCookies>
<psf:browserCookie Name="MH" URL="http://www.msn.com">MSFT; path=/; domain=.msn.com; expires=Wed, 30-Dec-2037 16:00:00 GMT</psf:browserCookie>
<psf:browserCookie Name="MH" URL="http://www.live.com">MSFT; path=/; domain=.live.com; expires=Wed, 30-Dec-2037 16:00:00 GMT</psf:browserCookie>
</psf:browserCookies>
<psf:credProperties>
<psf:credProperty Name="MainBrandID">MSFT</psf:credProperty>
<psf:credProperty Name="IsWinLiveUser">true</psf:credProperty>
<psf:credProperty Name="CID">${cid}</psf:credProperty>
<psf:credProperty Name="AuthMembername">${email}</psf:credProperty>
<psf:credProperty Name="Country">US</psf:credProperty>
<psf:credProperty Name="Language">1033</psf:credProperty>
<psf:credProperty Name="FirstName">${safeFirstName}</psf:credProperty>
<psf:credProperty Name="LastName">${safeLastName}</psf:credProperty>
<psf:credProperty Name="Flags">40100643</psf:credProperty>
<psf:credProperty Name="IP">${clientIp}</psf:credProperty>
</psf:credProperties>
<psf:extProperties>
<psf:extProperty Name="CID">${cid}</psf:extProperty>
</psf:extProperties>
<psf:response></psf:response>
</psf:pp>
</S:Header>
<S:Body>
<wst:RequestSecurityTokenResponseCollection
xmlns:wst="${NS.WST}"
xmlns:wsse="${NS.WSSE}"
xmlns:wsu="${NS.WSU}"
xmlns:wsp="${NS.WSP}"
xmlns:psf="${NS.PSF}">
${responses.join('\n ')}
</wst:RequestSecurityTokenResponseCollection>
</S:Body>
</S:Envelope>`;
}
function rstHandler() {
try {
// Get POST data
let requestBody = '';
if (request_headers.post_data) {
if (Buffer.isBuffer(request_headers.post_data)) {
requestBody = request_headers.post_data.toString('utf8');
} else if (typeof request_headers.post_data === 'string') {
requestBody = request_headers.post_data;
} else if (typeof request_headers.post_data === 'object') {
requestBody = JSON.stringify(request_headers.post_data);
}
} else {
debug("No post_data found. Available keys:", Object.keys(request_headers));
return generateErrorResponse("0x80048820", "No POST data received");
}
if (!requestBody || requestBody.trim() === '') {
debug("Empty request body");
return generateErrorResponse("0x80048820", "Empty request body");
}
// Authentication
let email = extractXmlValue(requestBody, 'Username');
let password = extractXmlValue(requestBody, 'Password');
let userId = null;
let userEmail = null;
let firstName = "User";
let lastName = "User";
if ((!email || !password) && requestBody.includes('CipherValue')) {
debug("No username/password found, trying token authentication...");
const token = extractTokenFromCipherValue(requestBody);
if (token) {
const tokenValidation = validateTokenAndGetUser(token);
if (tokenValidation.success) {
userId = tokenValidation.userId;
userEmail = tokenValidation.email;
debug(`Token authentication successful for: ${userEmail} (${userId})`);
if (request_headers.cookie) {
const cookieEmail = getCookie(request_headers.cookie, 'RST_Email');
const cookieUsername = getCookie(request_headers.cookie, 'rst_username');
if (cookieEmail) userEmail = cookieEmail;
if (cookieUsername) firstName = cookieUsername;
}
} else {
debug("Token validation failed");
return generateErrorResponse("0x80048821", "Invalid token");
}
} else {
debug("No token found in CipherValue");
return generateErrorResponse("0x80048820", "Missing credentials/token");
}
}
else if (email && password) {
debug(`Extracted - Email: ${email}, Password: ${password ? '***' : 'empty'}`);
if (email && email.indexOf('@') < 0) {
const domain = minisrv_config.config.domain_name || 'minisrv.local';
email = `${email}@${domain}`;
}
userEmail = email;
firstName = email.split('@')[0];
userId = crypto.createHash('md5').update(email).digest('hex');
const validAuth = validateCredentials(email, password);
if (!validAuth) {
debug("Invalid credentials");
return generateErrorResponse("0x80048821", "Invalid credentials");
} else {
debug(`Authentication successful for: ${userEmail} (${userId})`);
}
}
else {
debug("Missing both credentials and token");
return generateErrorResponse("0x80048820", "Missing credentials/token");
}
if (!userId || !userEmail) {
debug("Failed to get user identity");
return generateErrorResponse("0x80048821", "User identity not found");
}
const cookieHeaders = [
setCookie('rst_email', userEmail, { path: '/' }),
setCookie('rst_username', firstName, { path: '/' }),
setCookie('rst_authenticated', 'true', { path: '/', expires: 'Wed, 30-Dec-2037 16:00:00 GMT' })
];
const response = generateSuccessResponse(requestBody, userId, userEmail, firstName, lastName);
for (const cookie of cookieHeaders) {
headers += `\nSet-Cookie: ${cookie}`;
}
return response;
} catch (error) {
console.error("RST Handler Error:", error);
console.error("Error stack:", error.stack);
return generateErrorResponse("0x80048820", `Internal error: ${error.message}`);
}
}
function validateCredentials(email, password) {
username = email.split('@')[0];
result_ary = session_data.findAccountByUsername(username);
if (result_ary[0]) {
if (!socket.ssid) {
socket.ssid = result_ary[1];
// second arg should handle secondary users
session_data.setSSID(socket.ssid, result_ary[2]);
}
return session_data.validateUserPassword(password);
}
return false;
}
let result = rstHandler();
if (result) {
data = result;
}

View File

@@ -0,0 +1,13 @@
const minisrv_service_file = true;
// Wrong email return: <LoginResponse Success="false"><Error Code="e5b"/></LoginResponse>
// Wrong Password return: <LoginResponse Success="false"><Error Code="e5a"/></LoginResponse>
// Example Client request: <LoginRequest><ClientInfo name="MSNTV" version="1.35"/><User><SignInName>example@example.com</SignInName><Password>example</Password><SavePassword>false</SavePassword></User><DAOption>1</DAOption><TargetOption>1</TargetOption></LoginRequest>
data = `<LoginResponse Success="true"><TnP>t=Disabled&amp;p=Disabled</TnP></LoginResponse>`; // T and P cant be nulled they have to have some content in it
headers = `200 OK
Content-Type: text/xml`;
console.log(request_headers.query);

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

View File

@@ -0,0 +1,528 @@
const minisrv_service_file = true;
// Weather placeholder (static defaults — no live API)
const WeatherCity = 'Your City';
const WeatherTemp = '72';
const WeatherDescription = 'Sunny';
const WeatherIcon = '/Home/Weather/26.gif';
// News headlines
const NewsLink1 = '';
const NewsLink2 = '';
const NewsLink3 = '';
const NewsTitle1 = '...';
const NewsTitle2 = '...';
const NewsTitle3 = '...';
headers = `200 OK
Content-type: text/html`;
data = `<html xmlns:msntv>
<?import namespace="msntv" implementation="Shared/Anduril/HTC/en-us/TruncatedHtml.htc"?>
<head><title>Home</title>
<?import namespace="msntv" implementation="HTC/Shared/CustomButton.htc"?>
<?import namespace="msntv" implementation="/Home/Shared/BaseClient/HTCTransforms/en-us/LoopingDIV.htc"?>
<script src="/Include/2.0.261.900/localhost-1700/Shared/BaseClient/JsTransforms/en-us/PaneHelp.js" language="javascript" defer="true"></script>
<link href="/Include/2.0.261.778/localhost-1700/Home/Anduril/CssTransforms/en-us/Home.css" type="text/css" rel="StyleSheet">
<script src="/Include/2.0.261.778/localhost-1700/Home/Anduril/JsTransforms/en-us/Home.js" language="javascript"></script>
<script>
var forceReload = false;
var l = 'd:' + new Date().valueOf() + '|';
function setCookie(name, value) {
{
var now = new Date();
var expires = new Date(now.getFullYear() + 1, now.getMonth(), now.getDate());
document.cookie = escape(name) + '=' + escape(value) + ';expires=' + expires.toGMTString() + ';path=/';
}
}
function getCookie(name) {
{
var str = document.cookie;
var arr = str.split('; ');
for (var i = arr.length - 1; i >= 0; i--) {
{
var c = arr[i].split('=');
if (c.length != 2)
continue;
if (unescape(c[0]) == name)
return unescape(c[1]);
}
}
return null;
}
}
function syncCookie(cookieName, propValue) {
{
var c = getCookie(cookieName);
l += 'g:' + cookieName + ':' + c + '|';
if (c != propValue) {
{
setCookie(cookieName, propValue);
l += 's:' + cookieName + ':' + propValue + '|';
var check = getCookie(cookieName);
if (check == propValue)
forceReload = true;
}
}
}
}
var d = new Date();
var utcOffset = d.getTimezoneOffset();
syncCookie('UserUtcOffset', utcOffset);
var connSpeed;
try { { connSpeed = window.external.ClientCaps.connectionType; } }
catch (e) {
{
connSpeed = "undetected";
}
}
syncCookie('UserConnectionSpeed', connSpeed);
try {
{
top.log(l);
}
}
catch (e) {
{
}
}
if (forceReload)
location.replace(location.href);
</script></head>
<body>
<body onload="initPage();" scroll="no" tabindex="-1">
<div id="focdiv" style="position:absolute;top:314px;left:27px;width:70px;height:70px;" onclick="goToMail();">
</div>
<script>
document.all["focdiv"].focus();
function handleLoopingDivReady()
{
document.all["focdiv"].style.display = "none";
}
</script>
<div class="topdiv">
<div class="textMenu">
<script xmlns:msntvuxp="msntvuxp.microsoft.com">
function goToService(serviceName) {window.location = window.external.SafeGetServiceURL(serviceName);}
function goToCenter(svcURL) {window.location = svcURL;}
</script>
<div style="position:absolute; left:0px; top:0px" xmlns:msntvuxp="msntvuxp.microsoft.com">
<table border="0" class="TextMenuTbl">
<tr height="34" style="background-color: transparent;">
<td class="leftGradientTD"></td><td class="rightGradientTD">
</td>
</tr>
</table>
</div>
<div style="position:absolute; left:0px; top:0px" xmlns:msntvuxp="msntvuxp.microsoft.com">
<table border="0" class="TextMenuTbl">
<tr>
<td style="background-color: transparent;" width="8">
<img height="1" src="/Images/Shared/s.gif" width="8">
</td>
<td style="background-color: transparent;" align="left" width="162">
<a class="TextMenuLink" href="javascript:goToCenter('/Pages/UsingMSNTV/Main');" id="UsingMSNTVLinkID" onblur="umtvHasFocus=false">Using MSN TV</a>
</td>
<div>
<span style="position:absolute;left:0;top:35px;width:100%;height:2px;background-color:#8fc3d6;">
</span>
</div>
<td style="background-color: transparent;" align="left" width="115">
<a class="TextMenuLink" href="javascript:void(window.open(' ', 'signout', 'msntv:panel'));" id="SignoutLinkID">Sign Out</a>
</td>
<div>
<span style="position:absolute;left:0;top:35px;width:100%;height:2px;background-color:#8fc3d6;"></span>
</div>
<td style="background-color: transparent;" align="left" width="106">
<a class="TextMenuLink" href="javascript:goToService('UAM::UAMbase');" id="AccountLinkID">Account</a>
</td>
<div>
<span style="position:absolute;left:0;top:35px;width:100%;height:2px;background-color:#8fc3d6;">
</span>
</div>
<td style="background-color: transparent;" align="left" width="109">
<a class="TextMenuLink" href="javascript:goToService('settings::mainindex');" id="SettingsLinkID">Settings</a>
</td>
<div><span style="position:absolute;left:0;top:35px;width:100%;height:2px;background-color:#8fc3d6;">
</span>
</div>
<td style="background-color: transparent;" align="left" width="78">
<a href='javascript:CallPaneHelp(PH_TOC);' id="HelpLinkID">
<table><tr>
<td valign="middle" class="TextMenuLinkSimulation">Help</td>
<td valign="middle" width="30" height="20">
<span id="helpIcon" style='src:url(msntv:/Shared/Images/Icon_Help_RelatedLink.png);'></span>
</td>
</tr>
</table>
</a>
</td>
<div>
<span style="position:absolute;left:0;top:35px;width:100%;height:2px;background-color:#8fc3d6;">
</span>
</div>
</tr>
</table>
</div>
</div>
<div class="infoPaneDiv">
<div class="promoImgDiv">
<div style="position:absolute; left:0px; top:0px">
<img id="PromoImageID" width="178" height="135" border="0" hspace="0" alt="Promotional Image" src="/Home/ads/webtv3.gif">
</div>
<div style="position:absolute; left:5px; top:0px"><a id="PromoImageLinkID" href="">
<table width="173" height="135"><tr><td></td></tr></table></a>
</div>
</div>
<div id="timerRotatorDiv" class="personalPanelDiv" onclick="ClickRotator()">
<div>
<div style="top:0px; left:0px; width:176px; height:105px;" xmlns:msntvuxp="msntvuxp.microsoft.com">
<div class="PNGImage" style="width:176px;height:105px;src:/images/Home/HomeRotatorBGWeather.png;"></div>
</div>
<div style="position:absolute; top:0px; left:0px; width:178px; height:107px;" xmlns:msntvuxp="msntvuxp.microsoft.com">
<table class="wthrTbl" border="0" cellpadding="1" cellspacing="0">
<tr>
<td height="4" width="4" rowspan="4"><img src="images/Shared/s.gif" height="4" width="4"></td>
<td height="4" width="45"><img src="images/Shared/s.gif" height="4" width="45"></td>
<td height="4" width="10"><img src="images/Shared/s.gif" height="4" width="10"></td>
<td height="4" width="65"><img src="images/Shared/s.gif" height="4" width="65"></td>
</tr>
<tr>
<td id="CityCellID" class="wthrCityCell" colspan="3" valign="top">
<span class="wthrCityText">${WeatherCity}</span>
</td>
</tr>
<tr>
<td id="TRCID" class="wthrTempCond">
<table>
<tr>
<td id="TemperatureCellID" class="wthrTempCell">
<span class="wthrTempTxt">${WeatherTemp}&#176;F</span>
</td>
</tr>
<tr>
<td id="ConditionCellID" class="wthrCondCell">
<span class="wthrCondTxt">${WeatherDescription}</span>
</td>
</tr>
</table>
</td>
<td id="PaddingID" width="10"><img src="images/Shared/s.gif" height="1" width="10"></td>
<td id="ConditionIconID" class="wthrCondIcon">
<img src="${WeatherIcon}" alt="Weather icon" style="width:65px;height:61px;">
</td>
</tr>
<tr>
<td id="ProviderID" class="wthrProvider" colspan="3">
Open-Meteo
</td>
</tr>
</table>
</div>
<script xmlns:msntvuxp="msntvuxp.microsoft.com">
function clickPageRotatePanel() {
location.href = "/Pages/weather/yourcity";
}
</script>
</div>
</div>
<script>
InitRotator(timerRotatorDiv, 5000, new Array("/home/MoneyModule.aspx","/Pages/weather/WeatherModule"));
</script>
<div ID="clockID" class="clockDiv">
</div>
<script>clockID.innerHTML = formClockLink();</script>
<div class="newsHdlnDiv">
<div class="newsHdlnTitleDiv"><span class="newsHdlnTitleText">Today on TV2</span></div>
<table style="top:0px;left:0px;width:365px;height:78px;">
<tr><td><a class="newsHdlnLink" id="newsHdlnLinkID1" href="${NewsLink1}">${NewsTitle1}</a></td></tr>
<tr><td><a class="newsHdlnLink" id="newsHdlnLinkID2" href="${NewsLink2}">${NewsTitle2}</a></td></tr>
<tr><td><a class="newsHdlnLink" id="newsHdlnLinkID3" href="${NewsLink3}">${NewsTitle3}</a></td></tr>
</table>
<div class="moreNewsDiv">
<table><tr>
<td><img src="msntv:/Shared/images/BulletCustom.gif" height="14" width="7"></td>
<td><img src="images/Shared/s.gif" height="1" width="4"></td>
<td style="height:37px"><a id="moreNewsLinkID" class="moreNewsLink" href="../pages/TopStories">More MSN news</a></td>
</tr></table>
</div>
</div>
<div class="promoPanelDiv">
<div style="position:absolute; width:355px; height:70px;">
<table style="width:365px;">
<td class="promoHdlnTitle" style="color:#1D704C">Using MSN TV</td>
<tr>
<td class="promoHdlnCell"><a class="promoHdlnLink" id="promoHdlnLinkID1" href="http://msntv.msn.com/pages/usingmsntv/tippage.aspx?id=set15">Tip: Turn on audible dialing </a></td></tr><tr>
<td class="promoHdlnCell"><a class="promoHdlnLink" id="promoHdlnLinkID2" href="http://msntv.msn.com/pages/UsingMSNTV/Article.aspx?id=feature3">Get better printing results</a></td></tr>
</table>
</div>
<div style="position:absolute; left:161px; top:74px; width:200px;">
<table class="MoreUsingLinkTable"><tr>
<td><img src="msntv:/Shared/Images/BulletCustom.gif" height="14" width="7"></td>
<td><img src="/Images/Shared/s.gif" height="1" width="4"></td>
<td>
<a id="MoreUsingLinkID" class="MoreNewsLink" href="http://headwaiter.trusted.msntv.msn.com/Pages/UsingMSNTV/Main">Go to Using MSN TV</a>
</td></tr>
</table>
</div>
</div>
</div>
<div id="searchID" class="searchDiv">
<div class="searchCenterDiv">
<script>
function doSearch(country){
if (searchFormID.searchFieldID.value == "")
{
window.location = window.external.SafeGetServiceURL('search::search') + "?FORM=WEBTV&cfg=MSTVXML&v=1&c="+country+"&x=26&y=14";
}
else
{
window.location = window.external.SafeGetServiceURL('search::search') + "?FORM=WEBTV&cfg=MSTVXML&v=1&c="+country+"&x=26&y=14&q=" + escape(searchFormID.searchFieldID.value);
}
}
</script>
<div style="position:absolute; z-index:1; left:0; top:0px; width:100%; height:2px; background-color:#0c7faa;">
<table style="width:100%; height:2px;"><tr><td height="2"></td></tr></table>
</div>
<table class="searchTbl">
<form id="searchFormID" action="javascript:doSearch('US')">
<tr>
<td width="10"></td>
<td><a class="searchLink" href="javascript:doSearch('US');">Search</a><span class="searchLabelText"> or type www</span></td>
<td></td>
<td><span class="searchFieldText"><input id="searchFieldID" name="searchFieldName" class="searchField" type="text" size="28"></span></td>
<td></td>
<td valign="center"><msntv:custombutton id="GoButtonID" onClick="doSearch('US');" label="Go" /></td>
</tr></form></table>
<div style="position:absolute; left:0; top:33px; width:100%; height:2px; background-color:#1c4373;">
<table style="width:100%; height:2px;"><tr><td height="2"></td></tr></table>
</div>
</div>
</div>
<div id="iconNavBarID" class="iconNavBar">
<table class="iconNavBarMasterTbl">
<tr>
<td align="center" valign="middle">
<table class="iconNavBarTbl">
<tr>
<td class="iconNavBarTblFrameCell">
<table class="ApolloIcons" xmlns:msntv="http://tv.msn.com">
<tr height="70">
<td>
<msntv:loopingDIV id="navbar" hasInitialFocus="true" divWidthPX="554" onLoopingDivReady="handleLoopingDivReady()" />
</td>
</tr>
<script>
var nIcons;
var ImgURL = new Array();
var ImgWidth = new Array();
var ImgOverURL = new Array();
var IconURL = new Array();
function goToFavorites() {
window.open(" ", "favoritespanel", "msntv:panel");
}
function goToMessenger() {
if (window.external.SafeGetServiceURL('messenger::root') != null && window.external.SafeGetServiceURL('messenger::root') != "") window.open(" ", "impanel", "msntv:panel");
else window.location = "msntv:/OLTK/IMBlock.html";
}
function goToMail() {
if (window.external.SafeGetServiceURL('mail::listmail') != null && window.external.SafeGetServiceURL('mail::listmail') != "") window.location = window.external.SafeGetServiceURL('mail::listmail');
else window.location = "msntv:/OLTK/EmailBlock.html";
}
function goToChat() {
if (window.external.SafeGetServiceURL('chat::home') != null && window.external.SafeGetServiceURL('chat::home') != "") window.location = window.external.SafeGetServiceURL('chat::home');
else window.location = "msntv:/OLTK/chatBlock.html";
}
function goToSearch() { window.location = window.external.SafeGetServiceURL('search::main'); }
function goToMaps() { window.location = window.external.SafeGetServiceURL('maps::main'); }
function goToMusicHome() { window.location = window.external.SafeGetServiceURL('Music::Home'); }
function goToVideoHome() { window.location = window.external.SafeGetServiceURL('Video::Home'); }
function goToNetwork() { window.location = window.external.SafeGetServiceURL('Settings::HomeNetwork'); }
function goToAccount() { window.location = window.external.SafeGetServiceURL('UAM::UAMbase'); }
function goToSettings() { window.location = window.external.SafeGetServiceURL('settings::mainindex'); }
function goToCenter(svcURL) { window.location = svcURL; }
function goToPhotosApp() { window.location = window.external.SafeGetServiceURL('Photos'); }
function goToPhotosHome() { window.location = window.external.SafeGetServiceURL('Photo::Home'); }
function initIcons() {
var dq = '"';
for (index = 0; index < nIcons; index++) {
var realIndex = (index + nIcons - 1) % nIcons;
var cellHTML = '<span' +
' onFocus=' + dq + 'ImgObjs' + realIndex + ".src='" + ImgOverURL[realIndex] + "';" + dq +
' onBlur=' + dq + 'ImgObjs' + realIndex + ".src='" + ImgURL[realIndex] + "';" + dq +
' onClick=' + dq + IconURL[realIndex] + dq +
'><img id=' + dq + 'ImgObjs' + realIndex + dq +
" src='" + ImgURL[realIndex] + "' width=" + ImgWidth[realIndex] + ' height=61 border=0></span>';
navbar.AddCellHTML(cellHTML, ImgWidth[realIndex]);
}
}
ImgURL[0] = "/Images/Home/HomeIconFav.jpg";
ImgOverURL[0] = "/Images/Home/HomeIconFavOver.jpg";
ImgWidth[0] = 87;
IconURL[0] = "javascript:goToFavorites();";
ImgURL[1] = "/Images/Home/HomeIconMail.jpg";
ImgOverURL[1] = "/Images/Home/HomeIconMailOver.jpg";
ImgWidth[1] = 70;
IconURL[1] = "javascript:goToMail();";
ImgURL[2] = "/Images/Home/HomeIconMsgr.jpg";
ImgOverURL[2] = "/Images/Home/HomeIconMsgrOver.jpg";
ImgWidth[2] = 99;
IconURL[2] = "javascript:goToMessenger();";
ImgURL[3] = "/Images/Home/HomeIconMaps.jpg";
ImgOverURL[3] = "/Images/Home/HomeIconMapsOver.jpg";
ImgWidth[3] = 60;
IconURL[3] = "javascript:goToMaps();";
ImgURL[4] = "/Images/Home/HomeIconPhoto.jpg";
ImgOverURL[4] = "/Images/Home/HomeIconPhotoOver.jpg";
ImgWidth[4] = 70;
IconURL[4] = "javascript:goToPhotosApp();";
ImgURL[5] = "/Images/Home/HomeIconVideos.jpg";
ImgOverURL[5] = "/Images/Home/HomeIconVideosOver.jpg";
ImgWidth[5] = 70;
IconURL[5] = "javascript:goToVideoHome();";
ImgURL[6] = "/Images/Home/HomeIconMusic.jpg";
ImgOverURL[6] = "/Images/Home/HomeIconMusicOver.jpg";
ImgWidth[6] = 66;
IconURL[6] = "javascript:goToMusicHome();";
ImgURL[7] = "/Images/Home/HomeIconDiscuss.jpg";
ImgOverURL[7] = "/Images/Home/HomeIconDiscussOver.jpg";
ImgWidth[7] = 72;
IconURL[7] = "javascript:goToCenter('/Pages/Discuss/Home.aspx');";
ImgURL[8] = "/Images/Home/HomeIconMSNBC.jpg";
ImgOverURL[8] = "/Images/Home/HomeIconMSNBCOver.jpg";
ImgWidth[8] = 68;
IconURL[8] = "javascript:goToCenter('/Pages/News/TopStories.aspx');";
ImgURL[9] = "/Images/Home/HomeIconTWC.jpg";
ImgOverURL[9] = "/Images/Home/HomeIconTWCOver.jpg";
ImgWidth[9] = 81;
IconURL[9] = "javascript:goToCenter('/Pages/Weather/MyCity.aspx');";
ImgURL[10] = "/Images/Home/HomeIconEnt.jpg";
ImgOverURL[10] = "/Images/Home/HomeIconEntOver.jpg";
ImgWidth[10] = 125;
IconURL[10] = "javascript:goToCenter('/Pages/Entertainment/Home.aspx');";
ImgURL[11] = "/Images/Home/HomeIconTVListings.jpg";
ImgOverURL[11] = "/Images/Home/HomeIconTVListingsOver.jpg";
ImgWidth[11] = 96;
IconURL[11] = "javascript:goToCenter('http://tv.msn.com/tv/guide');";
ImgURL[12] = "/Images/Home/HomeIconSports.jpg";
ImgOverURL[12] = "/Images/Home/HomeIconSportsOver.jpg";
ImgWidth[12] = 82;
IconURL[12] = "javascript:goToCenter('/Pages/Sports/Main.aspx');";
ImgURL[13] = "/Images/Home/HomeIconMoney.jpg";
ImgOverURL[13] = "/Images/Home/HomeIconMoneyOver.jpg";
ImgWidth[13] = 68;
IconURL[13] = "javascript:goToCenter('/Pages/Money/Home.aspx');";
ImgURL[14] = "/Images/Home/HomeIconShopping.jpg";
ImgOverURL[14] = "/Images/Home/HomeIconShoppingOver.jpg";
ImgWidth[14] = 62;
IconURL[14] = "javascript:goToCenter('/Pages/Shopping/Main.aspx');";
ImgURL[15] = "/Images/Home/HomeIconGames.jpg";
ImgOverURL[15] = "/Images/Home/HomeIconGamesOver.jpg";
ImgWidth[15] = 70;
IconURL[15] = "javascript:goToCenter('/Pages/Games/Home.aspx');";
ImgURL[16] = "/Images/Home/HomeIconEncarta.jpg";
ImgOverURL[16] = "/Images/Home/HomeIconEncartaOver.jpg";
ImgWidth[16] = 74;
IconURL[16] = "javascript:goToCenter('http://g.msn.com/5TVANDURIL/1505')";
ImgURL[17] = "/Images/Home/HomeIconChat.jpg";
ImgOverURL[17] = "/Images/Home/HomeIconChatOver.jpg";
ImgWidth[17] = 55;
IconURL[17] = "javascript:goToChat();";
ImgURL[18] = "/Images/Home/HomeIconUsingMSN.jpg";
ImgOverURL[18] = "/Images/Home/HomeIconUsingMSNOver.jpg";
ImgWidth[18] = 127;
IconURL[18] = "javascript:goToCenter('/Pages/UsingMSNTV/Main');";
ImgURL[19] = "/Images/Home/HomeIconTTT.jpg";
ImgOverURL[19] = "/Images/Home/HomeIconTTTOver.jpg";
ImgWidth[19] = 116;
IconURL[19] = "javascript:goToCenter('/Pages/UsingMSNTV/ThingstoTry');";
ImgURL[20] = "/Images/Home/HomeIconSearch.jpg";
ImgOverURL[20] = "/Images/Home/HomeIconSearchOver.jpg";
ImgWidth[20] = 84;
IconURL[20] = "javascript:goToSearch();";
ImgObjs = new Array();
ImgOverObjs = new Array();
nIcons = ImgURL.length;
ImgOverObjs[0] = new Image();
ImgOverObjs[0].src = ImgOverURL[0];
for (var i = 0; i < nIcons; i++) {
ImgObjs[i] = new Image();
ImgObjs[i].src = ImgURL[i];
}
for (var i = 1; i < nIcons; i++) {
ImgOverObjs[i] = new Image();
ImgOverObjs[i].src = ImgOverURL[i];
}
initIcons();
</script>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<script>
function callCGif()
{
var i = new Image();
i.src = "http://c.msn.com/c.gif?DI=1455&PI=68206&PS=45577&TP=http://msntv.msn.com/HomePage.htm&RF=";
}
window.attachEvent("onload", callCGif);
</script>
</div>
</body></html>`;

View File

@@ -0,0 +1,127 @@
const minisrv_service_file = true;
headers = `200 OK
Content-type: text/html`;
data = `<html>
<head>
<script>
var forceReload = false;
var l = 'd:' + new Date().valueOf() + '|';
function setCookie(name, value) {
var now = new Date();
var expires = new Date(now.getFullYear() + 1, now.getMonth(), now.getDate());
document.cookie = escape(name) + '=' + escape(value) + ';expires=' + expires.toGMTString() + ';path=/';
}
function getCookie(name) {
var str = document.cookie;
var arr = str.split('; ');
for (var i = arr.length - 1; i >= 0; i--) {
var c = arr[i].split('=');
if (c.length != 2) continue;
if (unescape(c[0]) == name) return unescape(c[1]);
}
return null;
}
function syncCookie(cookieName, propValue) {
var c = getCookie(cookieName);
l += 'g:' + cookieName + ':' + c + '|';
if (c != propValue) {
setCookie(cookieName, propValue);
l += 's:' + cookieName + ':' + propValue + '|';
var check = getCookie(cookieName);
if (check == propValue) forceReload = true;
}
}
var d = new Date();
var utcOffset = d.getTimezoneOffset();
syncCookie('UserUtcOffset', utcOffset);
var connSpeed;
try {
connSpeed = window.external.ClientCaps.connectionType;
} catch (e) {
connSpeed = "undetected";
}
syncCookie('UserConnectionSpeed', connSpeed);
try {
top.log(l);
} catch (e) {}
if (forceReload) location.replace(location.href);
</script>
</head>
<body>
<div style="top:0px; left:0px; width:176px; height:105px;">
<div class="PNGImage" style="width:176px;height:105px;src:/Images/Home/HomeRotatorBGStock.png;"></div>
</div>
<table cellpadding="0" cellspacing="0" class="stocksTbl">
<tbody>
<tr height="8">
<td width="7"></td>
<td width="75"></td>
<td width="5"></td>
<td width="14"></td>
<td width="7"></td>
<td width="65"></td>
<td width="5"></td>
</tr>
<tr>
<td></td>
<td class="stocksCell" style="font-weight:bold; overflow:hidden; text-overflow:ellipsis">Dow</td>
<td></td>
<td>
<div class="PNGImage" style="src:/Images/Home/HomeStocksUpArrow.png; width:14px; height:24px"></div>
</td>
<td></td>
<td class="stocksCell" style="text-align: right;">+54.11</td>
<td></td>
</tr>
<tr>
<td class="stocksRule" colspan="7"></td>
</tr>
<tr>
<td></td>
<td class="stocksCell" style="font-weight:bold; overflow:hidden; text-overflow:ellipsis">Nasdaq</td>
<td></td>
<td>
<div class="PNGImage" style="src:/Images/Home/HomeStocksUpArrow.png; width:14px; height:24px"></div>
</td>
<td></td>
<td class="stocksCell" style="text-align: right;">+6.31</td>
<td></td>
</tr>
<tr>
<td class="stocksRule" colspan="7"></td>
</tr>
<tr>
<td></td>
<td class="stocksCell" style="font-weight:bold; overflow:hidden; text-overflow:ellipsis">S&amp;P</td>
<td></td>
<td>
<div class="PNGImage" style="src:/Images/Home/HomeStocksUpArrow.png; width:14px; height:24px"></div>
</td>
<td></td>
<td class="stocksCell" style="text-align: right;">+3.19</td>
<td></td>
</tr>
<tr>
<td class="stocksRule" colspan="7"></td>
</tr>
<tr>
<td id="ProviderID" class="wthrProvider" colspan="6">Source: MSN Money</td>
</tr>
</tbody>
</table>
<!--<ROTATOR_FEEDBACK></ROTATOR_FEEDBACK>--><!--<ROTATOR_CLICKTHROUGH>/Pages/Money/MyStocks.aspx</ROTATOR_CLICKTHROUGH>-->
<script>
function clickPageRotatePanel() {
location.href = "/Pages/Money/MyStocks.aspx";
}
</script>
</body>
</html>`;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,104 @@
const minisrv_service_file = true;
headers = `200 OK
Content-type: text/html`;
data = `<html>
<head>
<script>
var forceReload = false;
var l = 'd:' + new Date().valueOf() + '|';
function setCookie(name, value) {
var now = new Date();
var expires = new Date(now.getFullYear() + 1, now.getMonth(), now.getDate());
document.cookie = escape(name) + '=' + escape(value) + ';expires=' + expires.toGMTString() + ';path=/';
}
function getCookie(name) {
var str = document.cookie;
var arr = str.split('; ');
for (var i = arr.length - 1; i >= 0; i--) {
var c = arr[i].split('=');
if (c.length != 2) continue;
if (unescape(c[0]) == name) return unescape(c[1]);
}
return null;
}
function syncCookie(cookieName, propValue) {
var c = getCookie(cookieName);
l += 'g:' + cookieName + ':' + c + '|';
if (c != propValue) {
setCookie(cookieName, propValue);
l += 's:' + cookieName + ':' + propValue + '|';
var check = getCookie(cookieName);
if (check == propValue) forceReload = true;
}
}
var d = new Date();
var utcOffset = d.getTimezoneOffset();
syncCookie('UserUtcOffset', utcOffset);
var connSpeed;
try {
connSpeed = window.external.ClientCaps.connectionType;
} catch (e) {
connSpeed = "undetected";
}
syncCookie('UserConnectionSpeed', connSpeed);
try {
top.log(l);
} catch (e) {}
if (forceReload) location.replace(location.href);
</script>
</head>
<body>
<div style="top:0px; left:0px; width:176px; height:105px;" xmlns:msntvuxp="msntvuxp.microsoft.com">
<div class="PNGImage" style="width:176px;height:105px;src:/Images/Home/HomeRotatorBGWeather.png;"></div>
</div>
<div style="position:absolute; top:0px; left:0px; width:178px; height:107px;" xmlns:msntvuxp="msntvuxp.microsoft.com">
<table class="wthrTbl" border="0" cellpadding="1" cellspacing="0">
<tbody>
<tr>
<td height="4" width="4" rowspan="4"><img src="/Images/Shared/s.gif" height="4" width="4"></td>
<td height="4" width="45"><img src="/Images/Shared/s.gif" height="4" width="45"></td>
<td height="4" width="10"><img src="/Images/Shared/s.gif" height="4" width="10"></td>
<td height="4" width="65"><img src="/Images/Shared/s.gif" height="4" width="65"></td>
<td height="4" width="10" rowspan="4"><img src="/Images/Shared/s.gif" height="4" width="10"></td>
</tr>
<tr>
<td id="CityCellID" class="wthrCityCell" colspan="3" valign="top"><span class="wthrCityText">Your City</span></td>
</tr>
<tr>
<td id="TRCID" class="wthrTempCond">
<table>
<tbody>
<tr>
<td id="TemperatureCellID" class="wthrTempCell"><span class="wthrTempTxt">63&#176;/50</span></td>
</tr>
<tr>
<td id="ConditionCellID" class="wthrCondCell"><span class="wthrCondTxt">Mostly</span><br><span class="wthrCondTxt"> Cloudy</span></td>
</tr>
</tbody>
</table>
</td>
<td id="PaddingID" width="10"><img src="/Images/Shared/s.gif" height="1" width="10"></td>
<td id="ConditionIconID" class="wthrCondIcon"><span class="PNGImage" style="src:/Images/Shared/Weather/28.png;width:65px;height:61px;"></span></td>
</tr>
<tr>
<td id="ProviderID" class="wthrProvider" colspan="3">The Weather Channel &#174;</td>
</tr>
</tbody>
</table>
</div>
<!--<ROTATOR_FEEDBACK></ROTATOR_FEEDBACK>--><!--<ROTATOR_CLICKTHROUGH>/Pages/Weather/YourCity.aspx</ROTATOR_CLICKTHROUGH>-->
<script xmlns:msntvuxp="msntvuxp.microsoft.com">
function clickPageRotatePanel() {
location.href = "/Pages/Weather/YourCity.aspx";
}
</script>
</body>
</html>`;

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Some files were not shown because too many files have changed in this diff Show More