Compare commits
23 Commits
94e8ecb60a
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8089bd4439 | ||
|
|
750435fc83 | ||
|
|
fd67132da9 | ||
|
|
43a87347b8 | ||
|
|
e1d2c59ed5 | ||
| 2d64acaab6 | |||
| 17e0e6e526 | |||
|
|
02a3eef5e7 | ||
|
|
11d2ab8c86 | ||
|
|
778c0a2827 | ||
|
|
00e385cdbe | ||
|
|
cf9cc22a1c | ||
|
|
0c5dc17ae6 | ||
|
|
4347543ef7 | ||
|
|
9d51abd9ab | ||
|
|
118443305b | ||
|
|
ab4453487e | ||
|
|
e003d9795b | ||
|
|
e88dbd98cc | ||
|
|
eba447cd06 | ||
|
|
9aec2d3150 | ||
|
|
f2e11f827f | ||
|
|
32b6129ae3 |
87
QuickSetup.md
Normal 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.
|
||||
@@ -228,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
|
||||
const deprecationWarnings = {
|
||||
// Array of deprecated patterns with their details
|
||||
patterns: [
|
||||
{
|
||||
// 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"
|
||||
}
|
||||
],
|
||||
patterns: loadDepreciatedPatterns(),
|
||||
|
||||
// Enable/disable deprecation warnings globally
|
||||
enabled: true,
|
||||
@@ -363,7 +382,7 @@ const runScriptInVM = function (script_data, user_contextObj = {}, privileged =
|
||||
"service_vaults": service_vaults,
|
||||
"service_deps": service_deps,
|
||||
"ssid_sessions": ssid_sessions,
|
||||
"moveArrayKey": wtvshared.moveArrayKey,
|
||||
"moveArrayKey": wtvshared.moveArrayKey, // deprecated - use wtvshared.moveArrayKey() instead
|
||||
"cwd": (filename) ? path.dirname(filename) : __dirname, // current working directory
|
||||
|
||||
// Our prototype overrides
|
||||
@@ -2683,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 (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";
|
||||
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.");
|
||||
}
|
||||
29
zefie_wtvp_minisrv/includes/ServiceDeps/msntv2/minisrv.crt
Normal 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-----
|
||||
27
zefie_wtvp_minisrv/includes/ServiceDeps/msntv2/minisrv.key
Normal 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-----
|
||||
@@ -1,29 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFCzCCA/OgAwIBAgIQdCYWD0et5QHOJ3OYuF0r2TANBgkqhkiG9w0BAQUFADBw
|
||||
MQswCQYDVQQGEwJVUzELMAkGA1UECAwCT0gxEzARBgNVBAcMCkJ1dHQgQ3JhY2sx
|
||||
IDAeBgNVBAoMF1VuZGVyd2VhciBJbnNwZWN0b3IgIzEyMR0wGwYDVQQLDBRUaGlu
|
||||
ZyBMb29rZXIgRXhwZXJ0czAeFw0yNjA1MDEwMjMzMDhaFw00MTA0MjgwMjMzMDha
|
||||
MFExKTAnBgNVBAMTIGhlYWR3YWl0ZXIudHJ1c3RlZC5tc250di5tc24uY29tMRcw
|
||||
FQYDVQQKEw5aZWZpZSBOZXR3b3JrczELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3
|
||||
DQEBAQUAA4IBDwAwggEKAoIBAQCpSe+Vpv9qwb5aDgMNfWRTeXAGvTzhP+olLX+J
|
||||
2WczAXr4FUSLE4LPyD43se26u4GBvGmKD9512/GZKCtMbKPmfBdIqeq/CF2gx8xh
|
||||
e55qF8OuOdxMukOLXsTmvf4slwp3/N6gyze/PMmX+ku/gbotwPL0sv/9Vf1+PVTY
|
||||
6Fje2EU0ra6xJADeL9gazdl6QBxiJ+py+49SiZMS8N4MICOfklykENmjDoM211W6
|
||||
mIRgRZebxijNiZNFeWeXzjxzAAWi701TDs8ksNHSRBG2pajDZ+XgB8D1T+yXWbPz
|
||||
zylePg6HlG8n+asd43wakF8aER26eCT5hyCb4+SkkRClRXLHAgMBAAGjggG+MIIB
|
||||
ujAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATCC
|
||||
AYkGA1UdEQSCAYAwggF8giBoZWFkd2FpdGVyLnRydXN0ZWQubXNudHYubXNuLmNv
|
||||
bYIZc2cxLnRydXN0ZWQubXNudHYubXNuLmNvbYIZc2cyLnRydXN0ZWQubXNudHYu
|
||||
bXNuLmNvbYIZc2czLnRydXN0ZWQubXNudHYubXNuLmNvbYIZc2c0LnRydXN0ZWQu
|
||||
bXNudHYubXNuLmNvbYINbXNudHYubXNuLmNvbYIWbWFpbC5zZXJ2aWNlcy5saXZl
|
||||
LmNvbYIec3luYy1zZzEudHJ1c3RlZC5tc250di5tc24uY29tgg5sb2dpbi5saXZl
|
||||
LmNvbYIRcG9wdGltaXplLm1zbi5jb22CEWZhdm9yaXRlcy5tc24uY29tghFtZXNz
|
||||
ZW5nZXIubXNuLmNvbYIRbGl2ZWZpbGVzdG9yZS5jb22CFnVzZXJzLnN0b3JhZ2Uu
|
||||
bGl2ZS5jb22CCWcubXNuLmNvbYIXbXNuaWFsb2dpbi5wYXNzcG9ydC5jb22CDW1p
|
||||
bmlzcnYubG9jYWwwDQYJKoZIhvcNAQEFBQADggEBAD9O6j8bWtsX9OGf0kT3u1dy
|
||||
n6F+MQWX+vI4C9131Nso7cf7/+FyPcg17ewKw1MJ33ZpzCqhupAnN1lZPikGnl+t
|
||||
VacegsqI2mX1ycD11s1EleobHLc28uEQHDd79Dwn6fA2/EOijyqsILJHB6kzLjH6
|
||||
DV/sapv4JtNMlKDjfHDhtiI2jtpYTfkoZqjs7WsNmaJBcJ/NgTtl3hFSMiN/MLQ0
|
||||
O9wyrvNheINIJ01trpcgLDpmwCG0lYoa8AOdRZccl0KR7IsdVBcV1ANFguepQXI5
|
||||
dc/VJcFWsYs0puGdhPPZHgiZV4pzmfU+rCM/AoNxDdRBrVSal6Um5YyhlmFtobA=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,27 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAqUnvlab/asG+Wg4DDX1kU3lwBr084T/qJS1/idlnMwF6+BVE
|
||||
ixOCz8g+N7HturuBgbxpig/eddvxmSgrTGyj5nwXSKnqvwhdoMfMYXueahfDrjnc
|
||||
TLpDi17E5r3+LJcKd/zeoMs3vzzJl/pLv4G6LcDy9LL//VX9fj1U2OhY3thFNK2u
|
||||
sSQA3i/YGs3ZekAcYifqcvuPUomTEvDeDCAjn5JcpBDZow6DNtdVupiEYEWXm8Yo
|
||||
zYmTRXlnl848cwAFou9NUw7PJLDR0kQRtqWow2fl4AfA9U/sl1mz888pXj4Oh5Rv
|
||||
J/mrHeN8GpBfGhEdungk+Ycgm+PkpJEQpUVyxwIDAQABAoIBAAeA8Rr4Irs/wtqU
|
||||
17BXv20LyGXG+74z3oKu/iTFKfyYnAUfDaouqB+z0z4tG+yo/B3XIK0BC8sY3NpD
|
||||
EKPkkfi19B0qD0xnUON5LA//j0l4lI3dtrHpaSJ9nRCvatuANa2sOPzqIiZDVYLB
|
||||
Y8qCkP+yLqn71HSY81dUl4Zgsznr7w0VGXLzJd3nUKYL/FRVpsBPy7uHsUemB8YO
|
||||
mgGIVg7l7SpVX/tzZFnoQyPeiABv3hHTWoquxQfT9fqGQEm66cmzQP8yefaVvw1l
|
||||
Eiikx/QWX2SsXfqXdMmiQUI/y+1WV2XL955e9BFzlH6HrePaj9CIrP3+HLtw1fMZ
|
||||
It20WqECgYEA6FWcM2auqqVWrbee8oaoUT/XlQH8TZndlbWGSsFcQZptVTyB+ZMf
|
||||
DyDBAAh18ix7XVEtm6+2pioBZtou5Vp/RjWM1OI3f0mvVgVi2zFIIy52WrWxTHiv
|
||||
OSZpAv3NZEoiq/K3InrF3RolVklfQFv9TJBLmfNfTVtuEd47eBiLZckCgYEAuohW
|
||||
n0pPQhnBb1OiDv82GfyUVORVX4zcjvFGnTbqHiyH1l3+KNBtWQ2Wy5TsBifhXk5h
|
||||
TbGydO4RePeCaUBf4QFtABFsGH+et7Ci5ALvZBTLbCouvZRcDINnKsF0Wd1ZP9TW
|
||||
F7ToN6q3vkhbHIpY59Il96Ije7F6CwV7SQR0nA8CgYBcgzpffVOvv4Z0RdmU2OnM
|
||||
8I73VoMQo2QIaO/AdJ43wTYn6qAWsO59J52yVawhcnTtA5YVmDIymCdWvSpPSWnE
|
||||
my4o1qsilEStDBgBD+6Zk7atCAxBVwzuxMyr1EQk2yBTN6KUqC6BjBex9CVpizeh
|
||||
dROlibM5Kl753nPvrlZTgQKBgQC0fi6LbgUpafChv6RlrI/2L1B8oID3tz7IVjFE
|
||||
+RkrX12FkWfYqG3WqO6MSark/fv2HBPNcS/EM4TWr3ESVUcxWwbU9QbK4dp71kCY
|
||||
LzrjdbetD1gw+3jiZtgSKCVku2mb+V+8isHU861eQ3deM4R5tQAmEU8SZpY4SfKU
|
||||
oeoQAwKBgDhaHQinuYCa1w9Mzy6/AagSmS9yhXX1zR4OSgvF2lU0hT18CAR6LgOR
|
||||
gHOc6OscxDxheS38dHSIyRqD7F5hu1mO0KsGSCdmWIEKMkge8/yjrg4E4CPB5ICZ
|
||||
RcKZRL/rSQZIsdeVJM9i5FiDUaJUX+cvLJQAC1XLfRrBxCHsY0E1
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -1,17 +0,0 @@
|
||||
headwaiter.trusted.msntv.msn.com
|
||||
sg1.trusted.msntv.msn.com
|
||||
sg2.trusted.msntv.msn.com
|
||||
sg3.trusted.msntv.msn.com
|
||||
sg4.trusted.msntv.msn.com
|
||||
msntv.msn.com
|
||||
mail.services.live.com
|
||||
sync-sg1.trusted.msntv.msn.com
|
||||
login.live.com
|
||||
poptimize.msn.com
|
||||
favorites.msn.com
|
||||
messenger.msn.com
|
||||
livefilestore.com
|
||||
users.storage.live.com
|
||||
g.msn.com
|
||||
msnialogin.passport.com
|
||||
minisrv.local
|
||||
@@ -1,5 +0,0 @@
|
||||
-----BEGIN DH PARAMETERS-----
|
||||
MIGHAoGBAOjeZEDvMxiY+T4AMUIJ6jPFhflzUwO6EPBc0+Fn3C13WGQgsx9N3Rjg
|
||||
bZsF4Sbqs62+KFTYb5/1PVPSOxyif0CJLRC8VhvCl5CZ2DsS6nJ3sstPxtfhQdn+
|
||||
X1kbvqAbHlvNtE6w5ketHv3gK6y4d9qdVnwicZW3uV1sJ2dg4RfDAgEC
|
||||
-----END DH PARAMETERS-----
|
||||
@@ -1,15 +1,17 @@
|
||||
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
|
||||
Content-Type: text/html`
|
||||
|
||||
data = `<html>
|
||||
<head>
|
||||
<title>zefie minisrv v${minisrv_config.version}</title>
|
||||
<title>${title}</title>
|
||||
</head>
|
||||
<body bgcolor="#000000" text="#449944">
|
||||
<p>
|
||||
Welcome to the zefie minisrv v${minisrv_config.version} PC Services
|
||||
Welcome to ${title}
|
||||
</p>
|
||||
<hr>
|
||||
<a href="/viewergen/">WebTV Viewer Generator</a><br>
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
const minisrv_service_file = true;
|
||||
|
||||
let BoxId = request_headers.query.BoxId;
|
||||
if (Array.isArray(BoxId)) BoxId = BoxId[0];
|
||||
let clientIp = socket.remoteAddress;
|
||||
let banned = false;
|
||||
let sessionId = null;
|
||||
|
||||
// Use the shared MSNTV2 helper injected by WTV-MSNTV2 VM context.
|
||||
if (BoxId) {
|
||||
if (!BoxId || BoxId.length != 20 || !/^\d+$/.test(BoxId))
|
||||
{
|
||||
console.warn("Invalid BoxId format "+BoxId+" from "+clientIp);
|
||||
banned = true;
|
||||
} else {
|
||||
sessionId = encodeSessionID(BoxId);
|
||||
}
|
||||
} else if (request_headers.cookie && request_headers.cookie.SessionID) {
|
||||
BoxID = decodeSessionID(request_headers.cookie.SessionID);
|
||||
sessionId = request_headers.cookie.SessionID;
|
||||
} else {
|
||||
console.warn("No BoxId provided by client "+clientIp);
|
||||
banned = true;
|
||||
}
|
||||
|
||||
if (!sessionId && !banned) {
|
||||
banned = true;
|
||||
}
|
||||
|
||||
if (!session_data && BoxId) {
|
||||
console.log("Missing session_data for BoxId %s", BoxId);
|
||||
}
|
||||
|
||||
let registered = false;
|
||||
let username = '';
|
||||
if (session_data) {
|
||||
registered = session_data.isRegistered();
|
||||
if (registered) {
|
||||
username = session_data.getSessionData("subscriber_username") || '';
|
||||
}
|
||||
}
|
||||
|
||||
// Current UTC time
|
||||
const now = new Date();
|
||||
|
||||
// Time data object
|
||||
const timeData = {
|
||||
hh: now.getUTCHours(),
|
||||
mm: now.getUTCMinutes(),
|
||||
ss: now.getUTCSeconds(),
|
||||
mo: now.getUTCMonth() + 1, // JS months are 0-based
|
||||
dd: now.getUTCDate(),
|
||||
yyyy: now.getUTCFullYear()
|
||||
};
|
||||
|
||||
// Timezone mapping (C# tuple → JS array or object)
|
||||
const timezoneMap = {
|
||||
"UTC": {
|
||||
standardName: "UTC",
|
||||
standardOffset: 0,
|
||||
daylightName: "UTC",
|
||||
daylightOffset: 0
|
||||
}
|
||||
};
|
||||
|
||||
// Destructure like the C# tuple deconstruction
|
||||
const {
|
||||
standardName,
|
||||
standardOffset,
|
||||
daylightName,
|
||||
daylightOffset
|
||||
} = timezoneMap["UTC"];
|
||||
|
||||
// Set session cookie on the client
|
||||
if (sessionId) {
|
||||
setCookie('SessionID', sessionId, { path: '/' });
|
||||
}
|
||||
|
||||
headers = `200 OK
|
||||
Content-type: text/html`
|
||||
|
||||
data = `<html>
|
||||
<head>
|
||||
<title id="title"></title>
|
||||
</head>
|
||||
<body>
|
||||
<iframe id="checkmail" style="display:none"></iframe>
|
||||
<script src="msntv:/Javascript/TVShell.js" language="javascript"></script>
|
||||
<script src="msntv:/Javascript/ServiceList.js" language="javascript"></script>
|
||||
<script src="msntv:/Javascript/GuestUser.js" language="javascript"></script>
|
||||
<script language="javascript">
|
||||
try {
|
||||
var TVShell = new ActiveXObject("MSNTV.TVShell");
|
||||
var sink = new ActiveXObject("MSNTV.MultipleEventSink");
|
||||
|
||||
function getIDCRLCode(value) {
|
||||
return value & 0xFFFF;
|
||||
}
|
||||
|
||||
function isIDCRLErrorCode(value) {
|
||||
return (value >>> 16) != 0;
|
||||
}
|
||||
var email = TVShell.UserManager.EMail;
|
||||
var wanProvider = TVShell.ConnectionManager.WANProvider;
|
||||
|
||||
var banned = ${banned}; // JavaScript boolean value
|
||||
var registered = ${registered}; // JavaScript boolean value
|
||||
var username = "${username}"; // JavaScript string value
|
||||
|
||||
InitializeGuestMode();
|
||||
RemoveGuestUsers();
|
||||
|
||||
if (!banned) {
|
||||
TVShell.AddSecretCode(10000); // sync shit
|
||||
TVShell.AddSecretCode(10001); // sync shit
|
||||
TVShell.AddSecretCode(10002); // sync shit
|
||||
TVShell.AddSecretCode(93288); // Service Select
|
||||
TVShell.AddSecretCode(77437); // Spooky Options
|
||||
TVShell.AddSecretCode(6145539); // Force Crash
|
||||
var entry = TVShell.ServiceList.Add("connection::login");
|
||||
entry.URL = "https://headwaiter.trusted.msntv.msn.com/connection/login.aspx?BoxId=${BoxId}";
|
||||
TVShell.ServiceList.Save();
|
||||
}
|
||||
|
||||
function CheckForUser(usernameToCheck) {
|
||||
var UserManager = TVShell.UserManager;
|
||||
for (var i=0; i<UserManager.Count; i++) {
|
||||
var user = UserManager.Item(i);
|
||||
if (user == usernameToCheck) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function DoLogin() {
|
||||
var currentUser = TVShell.UserManager.CurrentUser;
|
||||
if (currentUser == null) {
|
||||
if (banned === true) {
|
||||
var url = 'https://sg1.trusted.msntv.msn.com/connection/banned.html';
|
||||
var myPanel = TVShell.PanelManager.Item('main');
|
||||
if (myPanel) myPanel.GotoURL(url);
|
||||
} else {
|
||||
if (registered === true) {
|
||||
if (!CheckForUser(username)) {
|
||||
var user = TVShell.UserManager.AddNew(username);
|
||||
if (user) {
|
||||
user.IsPersistent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
SetProgress('Welcome, New User!', 100);
|
||||
var myPanel = TVShell.PanelManager.Item('main')
|
||||
if (registered === true) {
|
||||
var signon = TVShell.BuiltinServiceList.Item("SignOn");
|
||||
var panel = TVShell.PanelManager.FocusedPanel;
|
||||
var atLogin = false;
|
||||
if ( signon && panel && panel.Name == "main" )
|
||||
{
|
||||
if ( IsMainPanelOnPage( signon.URL ) ) atLogin = true;
|
||||
}
|
||||
if (!atLogin) {
|
||||
myPanel.ClearTravelLog();
|
||||
myPanel.NoBackToMe = true;
|
||||
GotoSignOn();
|
||||
}
|
||||
} else {
|
||||
if (myPanel) myPanel.GotoURL('https://sg1.trusted.msntv.msn.com/register/Establish-your-MSN-TV-Account.html');
|
||||
}
|
||||
if (myPanel) {
|
||||
myPanel.ClearTravelLog();
|
||||
myPanel.NoBackToMe = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (banned === true) {
|
||||
var url = 'https://sg1.trusted.msntv.msn.com/connection/banned.html';
|
||||
var myPanel = TVShell.PanelManager.Item('service');
|
||||
if (myPanel) myPanel.GotoURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentUser != null) {
|
||||
var serviceArgs = new Array();
|
||||
var ProductionArgs = new Array("msntv.msn.com", "MBI", 0, 0,
|
||||
"mail.services.live.com", "MBI", 0, 0,
|
||||
"livefilestore.com", "MBI", 0, 0,
|
||||
"messenger.msn.com", "?id=507", 0, 0,
|
||||
"spaces.live.com", "MBI", 0, 0
|
||||
);
|
||||
var PPEArgs = new Array();
|
||||
var INTArgs = new Array();
|
||||
serviceArgs[0] = ProductionArgs;
|
||||
serviceArgs[1] = PPEArgs;
|
||||
serviceArgs[2] = INTArgs;
|
||||
try {
|
||||
TVShell.LoginManager.IDCRLInitialize(0);
|
||||
TVShell.LoginManager.IDCRLLogonAndAuthToServices(serviceArgs[0]);
|
||||
} catch (e) {
|
||||
if (window.console) console.log("IDCRL error: " + e.message);
|
||||
}
|
||||
|
||||
GoToUserCheck();
|
||||
}
|
||||
}
|
||||
|
||||
function DoPoptimization() {
|
||||
if (wanProvider === "MSNIANB") {
|
||||
var connector = GetConnectorByName("LocalPOP");
|
||||
if (connector == null) {
|
||||
connector = TVShell.ConnectionManager.MSNIAManager.Connectors.Add("modem");
|
||||
connector.AreaCode = "";
|
||||
connector.Exchange = "";
|
||||
connector.DialingFlags = 0x00001000;
|
||||
connector.Name = "LocalPOP";
|
||||
connector.LocationName = "LocalPOP";
|
||||
TVShell.ConnectionManager.Save();
|
||||
connector.Poptimize("0", connector.AreaCode, connector.Exchange);
|
||||
}
|
||||
|
||||
if (connector.Phonebook == null || connector.Phonebook.length === 0) {
|
||||
if (connector.AreaCode && connector.Exchange) {
|
||||
connector.Poptimize(connector.AreaCode, connector.Exchange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function GetConnectorByName(name) {
|
||||
var connectors = TVShell.ConnectionManager.MSNIAManager.Connectors;
|
||||
for (var i = 0; i < connectors.length; i++) {
|
||||
if (connectors[i].Name === name) {
|
||||
return connectors[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function CheckBoxID() {
|
||||
SetProgress("${minisrv_config.config.service_name} [${minisrv_config.config.hide_minisrv_version ? "beta" : minisrv_version_string.replace("zefie's wtv minisrv ","")}] Welcome, ${username != '' ? username : 'Guest'}!", 20);
|
||||
}
|
||||
|
||||
function GoToUserCheck() {
|
||||
if (banned === true) {
|
||||
var url = 'https://headwaiter.trusted.msntv.msn.com/connection/banned.html';
|
||||
var myPanel = TVShell.PanelManager.Item('service');
|
||||
if (myPanel) myPanel.GotoURL(url);
|
||||
} else if (registered) {
|
||||
GotoSignOn();
|
||||
}
|
||||
}
|
||||
|
||||
function SetProgress(text, percent) {
|
||||
if (progressPanel) {
|
||||
progressPanel.Document.SetProgressText(text);
|
||||
progressPanel.Document.SetProgressPercent(percent);
|
||||
}
|
||||
}
|
||||
|
||||
var progressPanel = TVShell.PanelManager.Item('progress');
|
||||
|
||||
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 {
|
||||
CheckBoxID();
|
||||
DoPoptimization();
|
||||
DoLogin();
|
||||
|
||||
try {
|
||||
TVShell.DeviceControl.SetTimeZone(${standardOffset}, "${standardName}", 0, "");
|
||||
} catch (e) {
|
||||
if (window.console) console.log("SetTimeZone error: " + e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
TVShell.DeviceControl.SetClock(${timeData.hh}, ${timeData.mm}, ${timeData.ss}, ${timeData.mo}, ${timeData.dd}, ${timeData.yyyy});
|
||||
TVShell.DeviceControl.ClockSet = true;
|
||||
} catch (e) {
|
||||
if (window.console) console.log("SetClock error: " + e.message);
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
if (window.console) console.log("Error in boxcheck: " + e.message);
|
||||
|
||||
var myPanel = TVShell ? TVShell.PanelManager.Item('main') : null;
|
||||
if (myPanel) myPanel.GotoURL('https://headwaiter.trusted.msntv.msn.com/connection/error.html');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
@@ -27,7 +27,7 @@ data = `<HTML>
|
||||
}
|
||||
}
|
||||
function GotoBoxCheck() {
|
||||
var url = 'https://headwaiter.trusted.msntv.msn.com/connection/boxcheck.html';
|
||||
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 + '&';
|
||||
@@ -73,4 +73,4 @@ data = `<HTML>
|
||||
</script>
|
||||
</body>
|
||||
</HTML>
|
||||
`;
|
||||
`;
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
const minisrv_service_file = true;
|
||||
|
||||
// Todo: auth if not guest
|
||||
|
||||
headers = `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");
|
||||
var UserManager = tvShell.UserManager;
|
||||
|
||||
var home = "https://sg1.trusted.msntv.msn.com/Home/Home.aspx";
|
||||
tvShell.ConnectionManager.ServiceState = 'Authorized';
|
||||
UserManager.SetCurrentUserIsAuthorized(true);
|
||||
var currentUser = UserManager.CurrentUser;
|
||||
if (currentUser != null) {
|
||||
currentUser.IsAuthorized = true;
|
||||
}
|
||||
var myPanel = tvShell.PanelManager.Item('main');
|
||||
if (myPanel) {
|
||||
myPanel.ClearTravelLog();
|
||||
myPanel.NoBackToMe = true;
|
||||
myPanel.GotoURL(home);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
@@ -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(/</g, '<').replace(/>/g, '>').replace(/&/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;
|
||||
}
|
||||
@@ -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&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);
|
||||
@@ -4,15 +4,15 @@ const minisrv_service_file = true;
|
||||
const WeatherCity = 'Your City';
|
||||
const WeatherTemp = '72';
|
||||
const WeatherDescription = 'Sunny';
|
||||
const WeatherIcon = '/Pages/Home/Weather/26.gif';
|
||||
const WeatherIcon = '/Home/Weather/26.gif';
|
||||
|
||||
// News headlines
|
||||
const NewsLink1 = 'http://sg1.trusted.msntv.msn.com/Pages/Tricks/he.mp3';
|
||||
const NewsLink2 = 'http://sg1.trusted.msntv.msn.com/Pages/Tricks/pokemon-black-2.mp3';
|
||||
const NewsLink3 = 'http://sg1.trusted.msntv.msn.com/Pages/Tricks/he.mp3';
|
||||
const NewsTitle1 = 'Ryder Smells';
|
||||
const NewsTitle2 = 'Ryder Smells';
|
||||
const NewsTitle3 = 'Ryder Smells';
|
||||
const NewsLink1 = '';
|
||||
const NewsLink2 = '';
|
||||
const NewsLink3 = '';
|
||||
const NewsTitle1 = '...';
|
||||
const NewsTitle2 = '...';
|
||||
const NewsTitle3 = '...';
|
||||
|
||||
headers = `200 OK
|
||||
Content-type: text/html`;
|
||||
@@ -22,7 +22,7 @@ data = `<html xmlns:msntv>
|
||||
|
||||
<head><title>Home</title>
|
||||
<?import namespace="msntv" implementation="HTC/Shared/CustomButton.htc"?>
|
||||
<?import namespace="msntv" implementation="/Pages/Home/Shared/BaseClient/HTCTransforms/en-us/LoopingDIV.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">
|
||||
@@ -187,7 +187,7 @@ data = `<html xmlns:msntv>
|
||||
|
||||
<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="/Pages/Home/ads/webtv3.gif">
|
||||
<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>
|
||||
|
||||
@@ -1,629 +0,0 @@
|
||||
@page "/Pages/Home/Home.aspx"
|
||||
@using MSNTV2MainServer.Components.Layout
|
||||
@using Microsoft.AspNetCore.Http
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using MSNTV2MainServer.Components.APIs
|
||||
@inject IHttpContextAccessor httpContextAccessor
|
||||
|
||||
@layout EmptyLayout
|
||||
|
||||
@if (IsTV2)
|
||||
{
|
||||
|
||||
MarkupString htmlContent = new MarkupString($@"
|
||||
<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=""/Pages/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();
|
||||
|
||||
// when looping div is ready, you can hide the invisible rectangle used for holding Mail's
|
||||
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(URL) {{window.location = URL;}}
|
||||
</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=""/Pages/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"">{@Weather.City}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id=""TRCID"" class=""wthrTempCond"">
|
||||
<table>
|
||||
<tr>
|
||||
<td id=""TemperatureCellID"" class=""wthrTempCell"">
|
||||
<span class=""wthrTempTxt"">{@Weather.Temp}°F</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id=""ConditionCellID"" class=""wthrCondCell"">
|
||||
<span class=""wthrCondTxt"">{Weather?.Description}</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=""{@Weather?.Icon}"" 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></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 URL = 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(URL) {{
|
||||
window.location = URL;
|
||||
}}
|
||||
|
||||
function goToPhotosApp() {{
|
||||
window.location = window.external.SafeGetServiceURL('Photos');
|
||||
}}
|
||||
|
||||
function goToPhotosHome() {{
|
||||
window.location = window.external.SafeGetServiceURL('Photo::Home');
|
||||
}}
|
||||
|
||||
function initIcons() {{
|
||||
for (index = 0; index < nIcons; index++) {{
|
||||
var realIndex = (index + nIcons - 1) % nIcons;
|
||||
var cellHTML = ""<span"" + "" onFocus=\""ImgObjs"" + realIndex + "".src='"" + ImgOverURL[realIndex] + ""'\"""" + "" onBlur=\""ImgObjs"" + realIndex + "".src='"" + ImgURL[realIndex] + ""'\"""";
|
||||
cellHTML += "" onClick=\"""" + URL[realIndex] + ""\"""";
|
||||
cellHTML += "">"" + ""<img"" + "" id='ImgObjs"" + realIndex + ""'"" + "" 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;
|
||||
URL[0] = ""javascript:goToFavorites();"";
|
||||
|
||||
ImgURL[1] = ""/Images/Home/HomeIconMail.jpg"";
|
||||
ImgOverURL[1] = ""/Images/Home/HomeIconMailOver.jpg"";
|
||||
ImgWidth[1] = 70;
|
||||
URL[1] = ""javascript:goToMail();"";
|
||||
|
||||
ImgURL[2] = ""/Images/Home/HomeIconMsgr.jpg"";
|
||||
ImgOverURL[2] = ""/Images/Home/HomeIconMsgrOver.jpg"";
|
||||
ImgWidth[2] = 99;
|
||||
URL[2] = ""javascript:goToMessenger();"";
|
||||
|
||||
ImgURL[3] = ""/Images/Home/HomeIconMaps.jpg"";
|
||||
ImgOverURL[3] = ""/Images/Home/HomeIconMapsOver.jpg"";
|
||||
ImgWidth[3] = 60;
|
||||
URL[3] = ""javascript:goToMaps();"";
|
||||
|
||||
ImgURL[4] = ""/Images/Home/HomeIconPhoto.jpg"";
|
||||
ImgOverURL[4] = ""/Images/Home/HomeIconPhotoOver.jpg"";
|
||||
ImgWidth[4] = 70;
|
||||
URL[4] = ""javascript:goToPhotosApp();"";
|
||||
|
||||
ImgURL[5] = ""/Images/Home/HomeIconVideos.jpg"";
|
||||
ImgOverURL[5] = ""/Images/Home/HomeIconVideosOver.jpg"";
|
||||
ImgWidth[5] = 70;
|
||||
URL[5] = ""javascript:goToVideoHome();"";
|
||||
|
||||
ImgURL[6] = ""/Images/Home/HomeIconMusic.jpg"";
|
||||
ImgOverURL[6] = ""/Images/Home/HomeIconMusicOver.jpg"";
|
||||
ImgWidth[6] = 66;
|
||||
URL[6] = ""javascript:goToMusicHome();"";
|
||||
|
||||
ImgURL[7] = ""/Images/Home/HomeIconDiscuss.jpg"";
|
||||
ImgOverURL[7] = ""/Images/Home/HomeIconDiscussOver.jpg"";
|
||||
ImgWidth[7] = 72;
|
||||
URL[7] = ""javascript:goToCenter('/Pages/Discuss/Home.aspx');"";
|
||||
|
||||
ImgURL[8] = ""/Images/Home/HomeIconMSNBC.jpg"";
|
||||
ImgOverURL[8] = ""/Images/Home/HomeIconMSNBCOver.jpg"";
|
||||
ImgWidth[8] = 68;
|
||||
URL[8] = ""javascript:goToCenter('/Pages/News/TopStories.aspx');"";
|
||||
|
||||
ImgURL[9] = ""/Images/Home/HomeIconTWC.jpg"";
|
||||
ImgOverURL[9] = ""/Images/Home/HomeIconTWCOver.jpg"";
|
||||
ImgWidth[9] = 81;
|
||||
URL[9] = ""javascript:goToCenter('/Pages/Weather/MyCity.aspx');"";
|
||||
|
||||
ImgURL[10] = ""/Images/Home/HomeIconEnt.jpg"";
|
||||
ImgOverURL[10] = ""/Images/Home/HomeIconEntOver.jpg"";
|
||||
ImgWidth[10] = 125;
|
||||
URL[10] = ""javascript:goToCenter('/Pages/Entertainment/Home.aspx');"";
|
||||
|
||||
ImgURL[11] = ""/Images/Home/HomeIconTVListings.jpg"";
|
||||
ImgOverURL[11] = ""/Images/Home/HomeIconTVListingsOver.jpg"";
|
||||
ImgWidth[11] = 96;
|
||||
URL[11] = ""javascript:goToCenter('http://tv.msn.com/tv/guide');"";
|
||||
|
||||
ImgURL[12] = ""/Images/Home/HomeIconSports.jpg"";
|
||||
ImgOverURL[12] = ""/Images/Home/HomeIconSportsOver.jpg"";
|
||||
ImgWidth[12] = 82;
|
||||
URL[12] = ""javascript:goToCenter('/Pages/Sports/Main.aspx');"";
|
||||
|
||||
ImgURL[13] = ""/Images/Home/HomeIconMoney.jpg"";
|
||||
ImgOverURL[13] = ""/Images/Home/HomeIconMoneyOver.jpg"";
|
||||
ImgWidth[13] = 68;
|
||||
URL[13] = ""javascript:goToCenter('/Pages/Money/Home.aspx');"";
|
||||
|
||||
ImgURL[14] = ""/Images/Home/HomeIconShopping.jpg"";
|
||||
ImgOverURL[14] = ""/Images/Home/HomeIconShoppingOver.jpg"";
|
||||
ImgWidth[14] = 62;
|
||||
URL[14] = ""javascript:goToCenter('/Pages/Shopping/Main.aspx');"";
|
||||
|
||||
ImgURL[15] = ""/Images/Home/HomeIconGames.jpg"";
|
||||
ImgOverURL[15] = ""/Images/Home/HomeIconGamesOver.jpg"";
|
||||
ImgWidth[15] = 70;
|
||||
URL[15] = ""javascript:goToCenter('/Pages/Games/Home.aspx');"";
|
||||
|
||||
ImgURL[16] = ""/Images/Home/HomeIconEncarta.jpg"";
|
||||
ImgOverURL[16] = ""/Images/Home/HomeIconEncartaOver.jpg"";
|
||||
ImgWidth[16] = 74;
|
||||
URL[16] = ""javascript:goToCenter('http://g.msn.com/5TVANDURIL/1505')"";
|
||||
|
||||
ImgURL[17] = ""/Images/Home/HomeIconChat.jpg"";
|
||||
ImgOverURL[17] = ""/Images/Home/HomeIconChatOver.jpg"";
|
||||
ImgWidth[17] = 55;
|
||||
URL[17] = ""javascript:goToChat();"";
|
||||
|
||||
ImgURL[18] = ""/Images/Home/HomeIconUsingMSN.jpg"";
|
||||
ImgOverURL[18] = ""/Images/Home/HomeIconUsingMSNOver.jpg"";
|
||||
ImgWidth[18] = 127;
|
||||
URL[18] = ""javascript:goToCenter('/Pages/UsingMSNTV/Main');"";
|
||||
|
||||
ImgURL[19] = ""/Images/Home/HomeIconTTT.jpg"";
|
||||
ImgOverURL[19] = ""/Images/Home/HomeIconTTTOver.jpg"";
|
||||
ImgWidth[19] = 116;
|
||||
URL[19] = ""javascript:goToCenter('/Pages/UsingMSNTV/ThingstoTry');"";
|
||||
|
||||
ImgURL[20] = ""/Images/Home/HomeIconSearch.jpg"";
|
||||
ImgOverURL[20] = ""/Images/Home/HomeIconSearchOver.jpg"";
|
||||
ImgWidth[20] = 84;
|
||||
URL[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>
|
||||
");
|
||||
|
||||
@((MarkupString)htmlContent)
|
||||
}
|
||||
else
|
||||
{
|
||||
<PageTitle>Home</PageTitle>
|
||||
<h1>Hello, world!</h1>
|
||||
<footer>
|
||||
<div style="position:fixed; left: 0px; bottom: 20px; width: 100vw;">
|
||||
<TV2PCHomeNavBar />
|
||||
</div>
|
||||
<div style="position:fixed; left: 0px; bottom: 0; width: 100vw; height: 40px;">
|
||||
<table cellpadding="0" cellspacing="0" width="100%" height="100%">
|
||||
<tr>
|
||||
<td width="560">
|
||||
<img src="/Images/TV2PC/StatusBarBG.jpg" style="width: 100vw; height: 40px; object-fit: fill;"/>
|
||||
<img src="/Images/TV2PC/Logo_MSNTVStatusBar.png" alt="Overlay Logo" class="overlay-logo" style="position: absolute; right: 0.1vw; bottom: 5px; max-width: 150%; z-index: 1001;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</footer>
|
||||
}
|
||||
|
||||
@code {
|
||||
string userAgent { get; set; }
|
||||
bool IsTV2 = false;
|
||||
|
||||
// News
|
||||
string NewsLink1 = "http://sg1.trusted.msntv.msn.com/Pages/Tricks/he.mp3";
|
||||
string NewsLink2 = "http://sg1.trusted.msntv.msn.com/Pages/Tricks/pokemon-black-2.mp3";
|
||||
string NewsLink3 = "http://sg1.trusted.msntv.msn.com/Pages/Tricks/he.mp3";
|
||||
string NewsTitle1 = "Ryder Smells";
|
||||
string NewsTitle2 = "Ryder Smells";
|
||||
string NewsTitle3 = "Ryder Smells";
|
||||
|
||||
// Weather mock
|
||||
WeatherData Weather = new WeatherData
|
||||
{
|
||||
City = "San Jose",
|
||||
Temp = "72",
|
||||
Description = "Sunny",
|
||||
Icon = "/Pages/Home/Weather/26.gif"
|
||||
};
|
||||
|
||||
// Date data
|
||||
DateData date = new DateData();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var request = httpContextAccessor.HttpContext?.Request;
|
||||
IsTV2 = request != null && SecurityHandler.IsMsnTvClient(request) && SecurityHandler.IsTrustedDomain(request);
|
||||
}
|
||||
|
||||
|
||||
class WeatherData
|
||||
{
|
||||
public string City { get; set; }
|
||||
public string Temp { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Icon { get; set; }
|
||||
}
|
||||
|
||||
class DateData
|
||||
{
|
||||
public string date { get; }
|
||||
|
||||
public DateData()
|
||||
{
|
||||
date = DateTime.Now.ToString("dddd, MMMM d");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
@page "/Pages/Home/MoneyModule.aspx"
|
||||
@using MSNTV2MainServer.Components.Layout
|
||||
@using Microsoft.AspNetCore.Http
|
||||
@using Microsoft.AspNetCore.Components;
|
||||
@inject IHttpContextAccessor httpContextAccessor
|
||||
|
||||
<!-- Nulled layout as it is defined manually-->
|
||||
@layout EmptyLayout
|
||||
|
||||
|
||||
@if (IsTV2)
|
||||
{
|
||||
MarkupString htmlContent = new MarkupString($@"
|
||||
<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&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>
|
||||
");
|
||||
|
||||
@((MarkupString)htmlContent)
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
<PageTitle>Home</PageTitle>
|
||||
}
|
||||
|
||||
@code
|
||||
{
|
||||
string userAgent { get; set; }
|
||||
bool IsTV2 = false;
|
||||
|
||||
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
userAgent = httpContextAccessor.HttpContext.Request.Headers["User-Agent"];
|
||||
|
||||
//Check if the client is an MSNTV2 box. If it is, we should return the TV2 page and not the Blazor based Page.
|
||||
if(userAgent.Contains("MSNTV"))
|
||||
{
|
||||
IsTV2 = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 771 B |
|
After Width: | Height: | Size: 685 B |
|
After Width: | Height: | Size: 806 B |
|
After Width: | Height: | Size: 175 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 383 B |
|
After Width: | Height: | Size: 403 B |
@@ -1,166 +0,0 @@
|
||||
@page "/Pages/Home/WeatherModule.aspx"
|
||||
@using MSNTV2MainServer.Components.Layout
|
||||
@using Microsoft.AspNetCore.Http
|
||||
@using Microsoft.AspNetCore.Components;
|
||||
@inject IHttpContextAccessor httpContextAccessor
|
||||
|
||||
<!-- Nulled layout as it is defined manually-->
|
||||
@layout EmptyLayout
|
||||
|
||||
|
||||
@if (IsTV2)
|
||||
{
|
||||
|
||||
MarkupString htmlContent = new MarkupString($@"
|
||||
<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°/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 ®</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>
|
||||
");
|
||||
|
||||
@((MarkupString)htmlContent)
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
<footer>
|
||||
<div style="position:fixed; left: 0px; bottom: 20px; width: 100vw;">
|
||||
|
||||
<TV2PCHomeNavBar />
|
||||
</div>
|
||||
<div style="position:fixed; left: 0px; bottom: 0; width: 100vw; height: 40px;">
|
||||
<table cellpadding="0" cellspacing="0" width="100%" height="100%">
|
||||
<tr>
|
||||
<td width="560">
|
||||
<img src="/Images/TV2PC/StatusBarBG.jpg" style=" width: 100vw; height: 40px; object-fit: fill;"/>
|
||||
<img src="/Images/TV2PC/Logo_MSNTVStatusBar.png" alt="Overlay Logo" class="overlay-logo" style="position: absolute; right: 0.1vw; bottom: 5px; max-width: 150%; z-index: 1001;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</footer>
|
||||
}
|
||||
|
||||
@code
|
||||
{
|
||||
string userAgent { get; set; }
|
||||
bool IsTV2 = false;
|
||||
|
||||
string NewsLink1 = "https://google.com";
|
||||
string NewsLink2 = "https://yahoo.com";
|
||||
string NewsLink3 = "https://bing.com";
|
||||
string NewsTitle1 = "Google reigns superior over the universe";
|
||||
string NewsTitle2 = "Who even uses Yahoo anymore?";
|
||||
string NewsTitle3 = "Oh god, it's Bing! (Now with extra piss)";
|
||||
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
userAgent = httpContextAccessor.HttpContext.Request.Headers["User-Agent"];
|
||||
|
||||
//Check if the client is an MSNTV2 box. If it is, we should return the TV2 page and not the Blazor based Page.
|
||||
@if(userAgent.Contains("MSNTV"))
|
||||
{
|
||||
IsTV2 = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 90 B |
|
After Width: | Height: | Size: 300 B |
|
After Width: | Height: | Size: 300 B |
|
After Width: | Height: | Size: 292 B |
|
After Width: | Height: | Size: 292 B |
|
After Width: | Height: | Size: 91 B |
@@ -0,0 +1,502 @@
|
||||
const minisrv_service_file = true;
|
||||
const wtv_news_service_name = minisrv_config.services[service_name].usenet_service;
|
||||
const wtvnews = new WTVNews(minisrv_config, wtv_news_service_name);
|
||||
const service_config = minisrv_config.services[wtv_news_service_name];
|
||||
if (service_config.local_nntp_port && wtvnewsserver) {
|
||||
const tls_options = {
|
||||
ca: this.wtvshared.getServiceDep('wtv-news/localserver_ca.pem'),
|
||||
key: this.wtvshared.getServiceDep('wtv-news/localserver_key.pem'),
|
||||
cert: this.wtvshared.getServiceDep('wtv-news/localserver_cert.pem'),
|
||||
checkServerIdentity: () => { return null; }
|
||||
}
|
||||
if (wtvnewsserver.username)
|
||||
wtvnews.initializeUsenet("127.0.0.1", service_config.local_nntp_port, tls_options, wtvnewsserver.username, wtvnewsserver.password);
|
||||
else
|
||||
wtvnews.initializeUsenet("127.0.0.1", service_config.local_nntp_port, tls_options);
|
||||
} else {
|
||||
if (service_config.upstream_auth)
|
||||
wtvnews.initializeUsenet(service_config.upstream_address, service_config.upstream_port, service_config.upstream_tls || null, service_config.upstream_auth.username || null, service_config.upstream_auth.password || null);
|
||||
else
|
||||
wtvnews.initializeUsenet(service_config.upstream_address, service_config.upstream_port, service_config.upstream_tls || null);
|
||||
}
|
||||
|
||||
async function throwError(e) {
|
||||
console.log(e);
|
||||
const errpage = wtvshared.doErrorPage(400 + " " + e.toString(), null, e.toString());
|
||||
sendToClient(socket, errpage[0], errpage[1]);
|
||||
}
|
||||
|
||||
function isToday (chkdate) {
|
||||
const today = new Date()
|
||||
return chkdate.getDate() === today.getDate() &&
|
||||
chkdate.getMonth() === today.getMonth() &&
|
||||
chkdate.getFullYear() === today.getFullYear()
|
||||
}
|
||||
|
||||
async function WebTVListGroup(group) {
|
||||
const page_limit_default = 100;
|
||||
wtvnews.connectUsenet().then(() => {
|
||||
wtvnews.selectGroup(group).then((response) => {
|
||||
let limit_per_page = (request_headers.query.limit) ? parseInt(request_headers.query.limit) : page_limit_default;
|
||||
const page = (request_headers.query.chunk) ? parseInt(request_headers.query.chunk) : 0;
|
||||
let page_start = (limit_per_page * page) + 1;
|
||||
let page_end = (page + 1) * limit_per_page;
|
||||
if (page_end > response.group.high) {
|
||||
page_end = response.group.high;
|
||||
limit_per_page = (page_end - (limit_per_page / (page + 1))) + limit_per_page;
|
||||
}
|
||||
wtvnews.listGroup(group, page, limit_per_page).then((response) => {
|
||||
if (response.code === 211) {
|
||||
let NGCount = response.group.number;
|
||||
const NGArticles = response.group.articleNumbers;
|
||||
page_start = (limit_per_page * page) + 1;
|
||||
page_end = (page + 1) * limit_per_page;
|
||||
wtvnews.getHeaderObj(NGArticles).then((messages) => {
|
||||
NGCount = NGArticles.length;
|
||||
messages = wtvnews.sortByResponse(messages);
|
||||
wtvnews.quitUsenet();
|
||||
headers = `200 OK
|
||||
Connection: Keep-Alive
|
||||
Content-Type: text/html
|
||||
wtv-expire: News.aspx?group=${request_headers.query.group}`
|
||||
data = `<HTML>
|
||||
<HEAD>
|
||||
<script language=javascript>
|
||||
if (top.frames.length > 1)
|
||||
top.location="news:${request_headers.query.group}";
|
||||
</script>
|
||||
<TITLE>${request_headers.query.group}</TITLE>
|
||||
</HEAD>
|
||||
<body bgcolor="191919" text="42BD52" link="1bb0f1" vlink="826f7e">
|
||||
<table cellspacing=0 cellpadding=0>
|
||||
<td height=31 valign=top>
|
||||
<font size="+1" color="E7CE4A">
|
||||
<blackface>
|
||||
<shadow>
|
||||
Group: ${request_headers.query.group}
|
||||
</shadow>
|
||||
</blackface>
|
||||
</font>
|
||||
</table>
|
||||
<font size=4>
|
||||
`
|
||||
if (NGCount === 0 || isNaN(NGCount)) {
|
||||
data += `This group has no postings`;
|
||||
} else {
|
||||
data += NGCount + " posting";
|
||||
if (NGCount !== 1)
|
||||
data += "s"
|
||||
}
|
||||
data += `
|
||||
</font>
|
||||
<br>
|
||||
<img src="wtv-home:/ROMCache/Spacer.gif" width=0 height=8>
|
||||
`;
|
||||
if (NGCount > 0) {
|
||||
data += `
|
||||
<td width=180 valign=bottom align=right>
|
||||
<table cellspacing=0 cellpadding=0>
|
||||
<td rowspan=4 height=26 width=30>
|
||||
${(page > 0) ? `<a href="News.aspx?group=${group}&chunk=${page - 1}${(limit_per_page !== page_limit_default) ? `&limit=${limit_per_page}` : ''}"><img src="ListPrevious.gif"></a>` : `<img src="ListPrevious_D.gif">`}
|
||||
<td rowspan=4 height=26 width=11>
|
||||
<img src="ListLeftEdge.gif">
|
||||
<td height=2 valign=top align=left bgcolor="2b2b2b">
|
||||
<td rowspan=4 height=26 width=11>
|
||||
<img src="ListRightEdge.gif">
|
||||
<td rowspan=4 height=26 width=30>
|
||||
${(page_end < NGCount) ? `<a href="News.aspx?group=${group}&chunk=${page + 1}${(limit_per_page !== page_limit_default) ? `&limit=${limit_per_page}` : ''}"><img src="ListNext.gif"></a>` : `<img src="ListNext_D.gif">`}
|
||||
<td rowspan=4 width=5>
|
||||
<tr>
|
||||
<td height=2 valign=top align=left>
|
||||
<img src="wtv-home:/ROMCache/Spacer.gif" width=1 height=1>
|
||||
<tr>
|
||||
<td height=20 valign=middle align=center>
|
||||
${page_start}-${page_end}
|
||||
<tr>
|
||||
<td height=2 valign=top align=left bgcolor="000000">
|
||||
<img src="wtv-home:/ROMCache/Spacer.gif" width=1 height=1>
|
||||
<tr>
|
||||
<td colspan=5 height=3>
|
||||
</table> `;
|
||||
}
|
||||
data += `</table>
|
||||
<TABLE width=446 cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td rowspan=4>
|
||||
<tr>
|
||||
<img src="wtv-home:/ROMCache/Spacer.gif" width=10 height=1>
|
||||
<td height=2 width=436 bgcolor="2B2B2B">
|
||||
<img src="wtv-home:/ROMCache/Spacer.gif" width=436 height=1>
|
||||
<tr>
|
||||
<td height=1>
|
||||
<tr>
|
||||
<td height=2 bgcolor="0D0D0D">
|
||||
<img src="wtv-home:/ROMCache/Spacer.gif" width=1 height=1>
|
||||
<tr>
|
||||
<td height=6>
|
||||
</TABLE>`
|
||||
if (NGCount > 0) {
|
||||
Object.keys(messages).forEach(function (k) {
|
||||
const message = messages[k].article;
|
||||
const has_relation = (messages[k].relation !== null) ? true : false;
|
||||
const date_obj = new Date(Date.parse(message.headers.DATE));
|
||||
const date = (isToday(date_obj)) ? strftime("%I:%M %p", date_obj) : strftime("%b %d '%y", date_obj)
|
||||
data += `
|
||||
<table cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td abswidth=10>
|
||||
<td abswidth=426 height=42 valign=bottom>
|
||||
<table cellspacing=0 cellpadding=0 nocolor selected>
|
||||
<tr>
|
||||
${(has_relation) ? `<td abswidth=20 rowspan=2 valign=top><font size="+2">•` : ''}
|
||||
<td abswidth=426 maxlines=1>
|
||||
<a href="News.aspx?group=${request_headers.query.group}&article=${message.articleNumber}" id="${message.messageId}">
|
||||
<font color=1bb0f1>${(message.headers.SUBJECT) ? wtvshared.htmlEntitize(message.headers.SUBJECT) : "(No Subject)"}
|
||||
</a>
|
||||
<tr>
|
||||
<td maxlines=1>
|
||||
<font size="-1" color=544f53><b>
|
||||
${(message.headers.FROM.indexOf(' ') > 0) ? message.headers.FROM.split(' ')[0] : message.headers.FROM}, ${date}
|
||||
</table>
|
||||
<td abswidth=10>
|
||||
</table>`;
|
||||
});
|
||||
}
|
||||
data += `
|
||||
<img src="wtv-home:/ROMCache/Spacer.gif" width=1 height=6><br>
|
||||
<TABLE width=446 cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td rowspan=4 width=10 height=1>
|
||||
<img src="wtv-home:/ROMCache/Spacer.gif" width=436 height=1>
|
||||
<tr>
|
||||
<td height=1>
|
||||
<tr>
|
||||
<td height=2 bgcolor="0D0D0D">
|
||||
<img src="wtv-home:/ROMCache/Spacer.gif" width=1 height=1>
|
||||
<tr>
|
||||
<td height=6>
|
||||
</TABLE>
|
||||
<table cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td width=10 height=1 valign=top align=left>
|
||||
<img src="wtv-home:/ROMCache/Spacer.gif" width=10 height=1>
|
||||
<td height=33 width=256 valign=bottom>
|
||||
</BODY>
|
||||
</HTML>`;
|
||||
|
||||
sendToClient(socket, headers, data);
|
||||
}).catch((e) => {
|
||||
// getHeaderObj err
|
||||
throwError(e)
|
||||
});;
|
||||
}
|
||||
}).catch((e) => {
|
||||
// listGroup error
|
||||
throwError("No such group");
|
||||
});
|
||||
}).catch((e) => {
|
||||
// selectGroup error
|
||||
throwError("No such group")
|
||||
});
|
||||
}).catch((e) => {
|
||||
// connect error
|
||||
throwError(e)
|
||||
});
|
||||
}
|
||||
|
||||
async function WebTVShowMessage(group, article) {
|
||||
const theArticle = parseInt(article);
|
||||
wtvnews.connectUsenet().then(() => {
|
||||
wtvnews.selectGroup(group).then((response) => {
|
||||
wtvnews.getArticle(theArticle).then((response) => {
|
||||
wtvnews.quitUsenet();
|
||||
headers = `200 OK
|
||||
Content-type: text/html`;
|
||||
let signature = null;
|
||||
let message_colors = session_data.mailstore.defaultColors;
|
||||
const display_signature = true; // todo make a toggle
|
||||
const message = wtvnews.parseAttachments(response);
|
||||
const message_body = message.text;
|
||||
let attachments = null;
|
||||
let signature_index = null;
|
||||
wtvnews.debug(message);
|
||||
if (message.attachments) attachments = message.attachments;
|
||||
if (attachments) {
|
||||
if (Object.keys(attachments).length > 0) {
|
||||
Object.keys(attachments).forEach((k) => {
|
||||
if (attachments[k].filename === "wtv_signature.html" && attachments[k].content_type.match(/text\/html/)) {
|
||||
signature = attachments[k].data;
|
||||
signature_index = k;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (signature_index) attachments.splice(signature_index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (message_body.indexOf("<body")) {
|
||||
const default_colors = session_data.mailstore.defaultColors;
|
||||
message_colors = session_data.mailstore.getSignatureColors(message_body);
|
||||
if ((message_colors === default_colors) && signature) message_colors = null;
|
||||
}
|
||||
if (!message_colors && signature) message_colors = session_data.mailstore.getSignatureColors(signature);
|
||||
|
||||
if (signature) message_colors = session_data.mailstore.getSignatureColors(signature);
|
||||
|
||||
data = `<head>
|
||||
<sendpanel
|
||||
action="/Post.aspx?message_forward_id=1&mailbox_name=inbox"
|
||||
message="Forward this post to someone else."
|
||||
label="Forward">
|
||||
<title>
|
||||
${(response.article.headers.SUBJECT) ? wtvshared.htmlEntitize(response.article.headers.SUBJECT) : '(No subject)'}
|
||||
</title>
|
||||
</head>
|
||||
<print blackandwhite>
|
||||
<body bgcolor=${message_colors.bgcolor}
|
||||
text=${message_colors.text}
|
||||
link=${message_colors.link}
|
||||
vlink=${message_colors.vlink}
|
||||
vspace=0
|
||||
hspace=0>
|
||||
<table cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td abswidth=10 rowspan=99>
|
||||
<td height=16>
|
||||
<td rowspan=99>
|
||||
<td>
|
||||
<td abswidth=20 rowspan=99>
|
||||
<tr>
|
||||
<td colspan=3 height=39 valign=top>
|
||||
<font color="E7CE4A" size=+1><blackface><shadow>
|
||||
Post
|
||||
<tr>
|
||||
<td valign=top>
|
||||
Group:
|
||||
<td>
|
||||
${wtvshared.htmlEntitize(response.article.headers.NEWSGROUPS)}
|
||||
<tr>
|
||||
<td valign=top>
|
||||
Date: <td>
|
||||
${strftime("%a, %b %e, %Y, %I:%M%P", new Date(Date.parse(response.article.headers.DATE)))}
|
||||
<tr>
|
||||
<td valign=top>
|
||||
From:
|
||||
<td>`;
|
||||
if (message.from_name !== message.from_addr) {
|
||||
data += `<a href="client:showalert?sound=none&message=Would%20you%20like%20to%20add%20%3Cblackface%3E${wtvshared.htmlEntitize(message.from_name)}%3C%2Fblackface%3E%20to%20your%20address%20list%3F&buttonlabel2=No&buttonaction2=client:donothing&buttonlabel1=Yes&buttonaction1=wtv-mail:/addressbook%3Faction%3Deditfromheader%26noresponse%3Dtrue%26nickname%3D${wtvshared.escape(wtvshared.escape(message.from_name))}%26address%3D${wtvshared.escape(wtvshared.escape(message.from_addr))}%26new_address%3Dtrue">${wtvshared.htmlEntitize(message.from_addr)} </a>`;
|
||||
} else {
|
||||
data += `${wtvshared.htmlEntitize(response.article.headers.FROM)}`;
|
||||
}
|
||||
|
||||
data += `<tr>
|
||||
<td nowrap valign=top>
|
||||
<td>
|
||||
<tr>
|
||||
<td valign=top>Subject:
|
||||
<td>${(response.article.headers.SUBJECT) ? wtvshared.htmlEntitize(response.article.headers.SUBJECT) : '(No subject)'}
|
||||
</table>
|
||||
<br><br>
|
||||
<table cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td abswidth=10 rowspan=99>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
`;
|
||||
let allow_html = false;
|
||||
let body_data = '';
|
||||
let attachment_data = '';
|
||||
let signature_data = '';
|
||||
|
||||
if (message_body) {
|
||||
if (message_body.indexOf("<html>") >= 0) {
|
||||
allow_html = true;
|
||||
}
|
||||
body_data += (allow_html) ? wtvshared.sanitizeSignature(message_body) : wtvshared.htmlEntitize(message_body, true)
|
||||
body_data += `<br><br>`;
|
||||
}
|
||||
|
||||
if (signature) signature_data += wtvshared.sanitizeSignature(signature);
|
||||
|
||||
|
||||
if (attachments) {
|
||||
if (attachments[0]) {
|
||||
if (attachments[0].filename === "message.html") {
|
||||
body_data += wtvshared.sanitizeSignature(attachments[0].data);
|
||||
delete attachments[0];
|
||||
}
|
||||
}
|
||||
const supported_images = /image\/(jpe?g|png|gif|x-wtv-bitmap)/;
|
||||
const supported_audio = /audio\/(mp[eg|2|3]|midi?|wav|x-wav|mod|x-mod)/;
|
||||
attachments.forEach((v, k) => {
|
||||
if (v.content_type) {
|
||||
if (v.content_type.match(supported_images))
|
||||
attachment_data += `<img border=2 src="attachment.aspx?group=${group}&article=${article}&attachment_id=${k}&wtv-title=Video%20Snapshot"><br><br>`;
|
||||
else if (v.content_type.match(supported_audio))
|
||||
attachment_data += `<table href="attachment.aspx?group=${group}&article=${article}&attachment_id=${k}" width=386 cellspacing=0 cellpadding=0>
|
||||
<td align=left valign=middle><img src="FileSound.gif" align=absmiddle><font color="#189CD6"> ${(v.filename) ? (v.filename) : "Audio file"} (${v.content_type.split('/')[1]} attachment)</font>
|
||||
<td align=right valign=middle>
|
||||
</table><br><br>`;
|
||||
else if (v.content_type.match("text/html"))
|
||||
attachment_data += wtvshared.sanitizeSignature(v.data);
|
||||
else
|
||||
attachment_data += `<table width=386><td><td align=left valign=middle><font color="#565656"><i>A file ${(v.filename) ? `(${v.filename}) ` : ''}that WebTV cannot use, with type ${v.content_type} is attached to this message.</i></font>`
|
||||
}
|
||||
});
|
||||
}
|
||||
/*
|
||||
if (message.url) {
|
||||
data += `Included Page: <a href="${(message.url)}">${wtvshared.htmlEntitize(message.url_title).replace(/'/gi, "'")}`;
|
||||
}
|
||||
*/
|
||||
data += body_data;
|
||||
data += signature_data + "<p>";
|
||||
data += attachment_data;
|
||||
data += "</table></body></html>";
|
||||
sendToClient(socket, headers, data);
|
||||
|
||||
}).catch((e) => {
|
||||
// no such article
|
||||
const post_unavailable_file = this.wtvshared.getServiceDep('wtv-news/post-unavailable.html');
|
||||
console.log(e);
|
||||
if (fs.existsSync(post_unavailable_file)) {
|
||||
headers = "200 OK\nContent-type: text/html";
|
||||
data = fs.readFileSync(post_unavailable_file).toString('ascii').replace("${group}", group).replace("${minisrv_config.config.service_logo}", minisrv_config.config.service_logo).replace("${message_colors.bgcolor}",session_data.mailstore.defaultColors.bgcolor);
|
||||
sendToClient(socket, headers, data);
|
||||
} else {
|
||||
throwError(e);
|
||||
}
|
||||
});
|
||||
}).catch((e) => {
|
||||
// no such group
|
||||
throwError(e);
|
||||
});
|
||||
}).catch((e) => {
|
||||
//no connection
|
||||
throwError(e);
|
||||
});
|
||||
}
|
||||
|
||||
function WebTVSearchGroups(search) {
|
||||
wtvnews.connectUsenet().then(() => {
|
||||
wtvnews.listGroups(search).then((response) => {
|
||||
wtvnews.quitUsenet();
|
||||
headers = `200 OK
|
||||
Content-type: text/html
|
||||
wtv-expire-all: News.aspx?search=`;
|
||||
|
||||
data = `<HTML>
|
||||
<HEAD>
|
||||
<DISPLAY fontsize=medium>
|
||||
<TITLE>${(response.length === 0) ? "No " : ""}Discussion groups found</TITLE>
|
||||
</HEAD>
|
||||
<body
|
||||
bgcolor="191919" text="42BD52" link="189CD6"
|
||||
vlink="189CD6"
|
||||
hspace=0
|
||||
vspace=0>
|
||||
<table cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td abswidth=10>
|
||||
<td colspan=3>
|
||||
<table cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td valign=center absheight=80>
|
||||
<font size="+2" color="E7CE4A"><blackface><shadow>
|
||||
${(response.length === 0) ? "No " : ""}Discussion groups found
|
||||
</table>
|
||||
<td abswidth=20>
|
||||
<tr>
|
||||
<td>
|
||||
<td WIDTH=198 HEIGHT=200 VALIGN=top ALIGN=left>`;
|
||||
|
||||
if (response.length === 0) {
|
||||
data += `There are no discussion groups that match your request. Do you want to look for something else?`;
|
||||
} else {
|
||||
response.forEach((group) => {
|
||||
data += `<hr width=436>
|
||||
<IMG src="wtv-home:/ROMCache/Spacer.gif" width=1 height=6><br>
|
||||
<table cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td width=10>
|
||||
<td width=426> <table selected cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td abswidth=401 height=19 valign=top>
|
||||
<a href="News.aspx?group=${group.name}"><shadow><b>${group.name}</b></shadow></a>
|
||||
<td width=10>
|
||||
|
||||
`
|
||||
if (group.description) {
|
||||
data += `<tr><td colspan=3 width=10 height=6><tr><td width=10><td colspan=99><i><font color=828282>${group.description}</font></i>`
|
||||
}
|
||||
data += "</table>";
|
||||
});
|
||||
}
|
||||
|
||||
data += `
|
||||
</table>
|
||||
<TABLE width=446 cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td rowspan=3 width=10 height=1>
|
||||
<img src="wtv-home:/ROMCache/Spacer.gif" width=10 height=1>
|
||||
<td height=2 width=436 bgcolor="2B2B2B">
|
||||
<img src="wtv-home:/ROMCache/Spacer.gif" width=436 height=1>
|
||||
<tr>
|
||||
<td height=1>
|
||||
<tr>
|
||||
<td height=2 bgcolor="0D0D0D">
|
||||
<img src="wtv-home:/ROMCache/Spacer.gif" width=1 height=1>
|
||||
</TABLE>
|
||||
<table cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td rowspan=2 abswidth=10>
|
||||
<td absheight=10>
|
||||
<tr>
|
||||
<td abswidth=416 valign=top align=left>
|
||||
Do you want to look for something else?<br>
|
||||
<img src="/ROMCache/Spacer.gif" width=1 height=4>
|
||||
<form action="News.aspx">
|
||||
<input name="search" bgcolor=#202020 cursor=#cc9933 text="E7CE4A" font=proportional value="${request_headers.query.search}" SIZE=28 MAXLENGTH=100>
|
||||
|
||||
<font color=E7CE4A><shadow>
|
||||
<input type=submit borderimage="file://ROM/Borders/ButtonBorder2.bif" value="Look for" usestyle>
|
||||
</shadow></font>
|
||||
</form>
|
||||
</table>
|
||||
</BODY>
|
||||
</HTML>`;
|
||||
sendToClient(socket, headers, data);
|
||||
}).catch((e) => {
|
||||
// listGroups error
|
||||
throwError(e);
|
||||
});
|
||||
|
||||
}).catch((e) => {
|
||||
// no connection
|
||||
throwError(e);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (!wtvnews.client) {
|
||||
const errpage = wtvshared.doErrorPage();
|
||||
headers = errpage[0];
|
||||
data = errpage[1];
|
||||
} else {
|
||||
request_is_async = true;
|
||||
if (request_headers.query.search) {
|
||||
WebTVSearchGroups(request_headers.query.search)
|
||||
} else if (request_headers.query.group) {
|
||||
if (request_headers.query.article) {
|
||||
WebTVShowMessage(request_headers.query.group, request_headers.query.article);
|
||||
} else {
|
||||
WebTVListGroup(request_headers.query.group);
|
||||
}
|
||||
} else {
|
||||
// redirect to lobby if no understandable queries passed
|
||||
headers = "300 OK\nLocation: wtv-news:/lobby";
|
||||
sendToClient(socket, headers, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
const minisrv_service_file = true;
|
||||
|
||||
// max of 6, any more will be ignored
|
||||
|
||||
headers = `200 OK
|
||||
Connection: Keep-Alive
|
||||
Content-Type: text/html`
|
||||
|
||||
data = `<HTML>
|
||||
<HEAD>
|
||||
<DISPLAY fontsize=medium>
|
||||
<TITLE>Featured discussion groups</TITLE>
|
||||
</HEAD>
|
||||
|
||||
<body
|
||||
bgcolor="191919" text="42BD52" link="189CD6"
|
||||
vlink="189CD6"
|
||||
hspace=0
|
||||
vspace=0>
|
||||
<table cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td abswidth=10>
|
||||
<td colspan=3>
|
||||
<table cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td valign=center absheight=80>
|
||||
<font size="+2" color="E7CE4A"><blackface><shadow>
|
||||
Featured discussions
|
||||
</table>
|
||||
<td abswidth=20>
|
||||
<tr>
|
||||
<td>
|
||||
<td WIDTH=198 HEIGHT=200 VALIGN=top ALIGN=left>`;
|
||||
|
||||
const featuredGroups = minisrv_config.services[minisrv_config.services[service_name].usenet_service].featuredGroups;
|
||||
const limit = 6;
|
||||
while (featuredGroups.length > limit) featuredGroups.pop(); // remove anything passing our limit
|
||||
|
||||
function printGroup(group) {
|
||||
return `<a href="News.aspx?group=${group.group}"><b>${group.name}</b></a><br>${group.description}<BR>`;
|
||||
}
|
||||
|
||||
// evens
|
||||
Object.keys(featuredGroups).forEach((k) => { if (k % 2 === 0) { data += printGroup(featuredGroups[k]); } });
|
||||
|
||||
if (featuredGroups.length > 1) data += `<td WIDTH=20><td WIDTH=198 HEIGHT=220 VALIGN=top ALIGN=left>`;
|
||||
|
||||
// odds
|
||||
Object.keys(featuredGroups).forEach((k) => { if (k % 2 !== 0) data += printGroup(featuredGroups[k]); });
|
||||
|
||||
|
||||
data += `
|
||||
</table>
|
||||
<hr>
|
||||
<table cellspacing=0 cellpadding=0>
|
||||
<tr>
|
||||
<td rowspan=2 abswidth=10>
|
||||
<td absheight=10>
|
||||
<tr>
|
||||
<td abswidth=416 valign=top align=left>
|
||||
Type a discussion topic<br>
|
||||
<img src="/ROMCache/Spacer.gif" width=1 height=4>
|
||||
<form action="News.aspx" method="GET">
|
||||
<input name="search" bgcolor=#202020 cursor=#cc9933 text="E7CE4A" font=proportional value="" SIZE=28 MAXLENGTH=100>
|
||||
|
||||
<font color=E7CE4A><shadow>
|
||||
<input type=submit borderimage="file://ROM/Borders/ButtonBorder2.bif" value="Look for" usestyle>
|
||||
</shadow></font>
|
||||
</form>
|
||||
</table>
|
||||
</BODY>
|
||||
</HTML>`;
|
||||
@@ -53,6 +53,8 @@ data = `<HTML xmlns:msntv>
|
||||
</p>
|
||||
<p style="display: inline; left: 76px; position: relative;">Example:</p>
|
||||
<p style="display: inline; left: 110px; position: relative;">ABCEAZ82KDKA</p>
|
||||
<p> </p>
|
||||
<p>One day, certain promotion codes will unlock special features or content on minisrv. But for now, this is just a placeholder page.</p>
|
||||
</DIV>
|
||||
<div id="footer">
|
||||
<msntv:CustomButton id="continue" label="Continue" href="javascript:SubmitForm()" />
|
||||
|
||||
@@ -86,7 +86,7 @@ data = `<HTML xmlns:msntv>
|
||||
<tr style="margin: 0; padding: 0; top: 2px; position: relative;">
|
||||
<td style="margin: 0; padding: 0; vertical-align: middle; top: 2px; position: relative;"><img src="msntv:/Shared/Images/BulletCustom.gif" height="14" width="7" alt="Bullet"></td>
|
||||
<td style="margin: 0; padding: 0; width: 4px;"></td>
|
||||
<td style="margin: 0; padding: 0; font:bold 18; line-height: 20px;"><a class="shrLnk2" href="https://headwaiter.trusted.msntv.msn.com/connection/boxcheck.html" style="display: inline-block; line-height: 20px;">I want to start over</a></td>
|
||||
<td style="margin: 0; padding: 0; font:bold 18; line-height: 20px;"><a class="shrLnk2" href="msntv:/Registration/pages/Welcome.html" style="display: inline-block; line-height: 20px;">I want to start over</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -75,29 +75,42 @@ data = `<html xmlns:msntv>
|
||||
</STYLE>
|
||||
|
||||
<script>
|
||||
var tvShell = new ActiveXObject("MSNTV.TVShell");
|
||||
tvShell.UserManager.SetCurrentUserIsAuthorized(false);
|
||||
var TVShell = new ActiveXObject("MSNTV.TVShell");
|
||||
TVShell.UserManager.SetCurrentUserIsAuthorized(false);
|
||||
|
||||
function AddUser() {
|
||||
var user = tvShell.UserManager.AddNew("${username}");
|
||||
var user = TVShell.UserManager.AddNew("${username}");
|
||||
|
||||
if (user) {
|
||||
user.IsPersistent = true;
|
||||
user.setAttribute("GuestUser", false);
|
||||
var dt = new Date();
|
||||
|
||||
TVShell.UserManager.LastLoginTime = dt.getTime() / 1000 + dt.getTimezoneOffset() * 60;
|
||||
|
||||
TVShell.UserManager.OfflineAppMaxAccessDays = 20;
|
||||
TVShell.UserManager.OfflineAppMaxAccessTimes = 20;
|
||||
TVShell.UserManager.CurrentUser = user;
|
||||
user.LargeIcon = "msntv:/SignInPics/big/${picture}.png";
|
||||
user.SmallIcon = "msntv:/SignInPics/small/${picture}.gif";
|
||||
TVShell.UserManager.Save();
|
||||
} else {
|
||||
user = tvShell.UserManager.Item("${username}");
|
||||
user = TVShell.UserManager.Item("${username}");
|
||||
if (user && !user.IsPersistent) {
|
||||
user.IsPersistent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (user) {
|
||||
user.LargeIcon = "msntv:/tvshell/images/${picture}.png";
|
||||
user.SmallIcon = "msntv:/tvshell/images/${picture}.gif";
|
||||
}
|
||||
function GobacktoSignon() {
|
||||
entry = TVShell.ServiceList.Add('connection::login');
|
||||
entry.URL = 'https://sg1.trusted.msntv.msn.com/connection/GatePage.aspx?phase=Bootstrap&purpose=Authorize';
|
||||
entry.Description = '${minisrv_config.config.service_name}/sg1 [${minisrv_config.config.hide_minisrv_version ? "beta" : minisrv_version_string.replace("zefie's wtv minisrv ","")}]';
|
||||
TVShell.ServiceList.Save();
|
||||
TVShell.ConnectionManager.ServiceState = 'ReSignIn';
|
||||
}
|
||||
|
||||
AddUser();
|
||||
tvShell.UserManager.Save();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
@@ -116,7 +129,7 @@ data = `<html xmlns:msntv>
|
||||
<table class="ApolloIcons" tabindex="-1">
|
||||
<tr height="70">
|
||||
<td tabindex="-1">
|
||||
<span style='display:inline-block; width:142px; height:158px; behavior:url(#default#alphaImageLoader); src:url(msntv:/SignInPics/Big/${picture}.png);'></span>
|
||||
<span style='display:inline-block; width:142px; height:158px; behavior:url(#default#alphaImageLoader); src:url(msntv:/SignInPics/big/${picture}.png);'></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -139,9 +152,9 @@ data = `<html xmlns:msntv>
|
||||
</p>
|
||||
|
||||
<div id="footer">
|
||||
<msntv:CustomButton id="continue" label="Continue" href="/Home/Home.aspx" />
|
||||
<msntv:CustomButton id="continue" label="Continue" onclick="GobacktoSignon()" />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ headers = `Status: 200 OK
|
||||
Content-type: text/html`;
|
||||
|
||||
data = `<HTML xmlns:msntv>
|
||||
<?import namespace="msntv" implementation="HTC/Shared/CustomButton.htc">
|
||||
<?import namespace="msntv" implementation="https://sg1.trusted.msntv.msn.com/Include/HTC/Shared/CustomButton.htc">
|
||||
<HEAD>
|
||||
<title id="title">Learning to use the keyboard</title>
|
||||
<meta name="vs_targetSchema" content="http://schemas.microsoft.com/intellisense/ie5">
|
||||
@@ -32,26 +32,24 @@ data = `<HTML xmlns:msntv>
|
||||
<BODY width="520" height="388">
|
||||
<DIV id="title">Logging into your account</DIV>
|
||||
<DIV id="main">
|
||||
<p>When using MSNTV2, you can access the same minisrv account as your WebTV.</p>
|
||||
<p>When using MSNTV2, you can use an existing minisrv account.</p>
|
||||
<br>
|
||||
<p>Type your existing primary username in the box below:</p>
|
||||
<div class="input-container">
|
||||
<textarea name="username" id="username" rows="1" cols="15"></textarea>
|
||||
<p style="display: inline;">@${minisrv_config.config.service_name}</p>
|
||||
<p style="display: inline;">@${minisrv_config.config.domain_name}</p>
|
||||
</div>
|
||||
<br>
|
||||
<div class="input-container">
|
||||
<textarea name="password" id="password" rows="1" cols="15"></textarea>
|
||||
</div>
|
||||
<br>
|
||||
If you don't have an existing minisrv account, select the <EM>Back</EM> Button.
|
||||
If you do not have a password set on your primary user account, you will need to set one up on your WebTV before you can log in here. After entering your username and password, select the <EM>Continue</EM> Button.
|
||||
If you do not have a password set on your primary user account, you will need to set one up on your WebTV before you can connect it to this MSNTV2.
|
||||
After entering your username and password, select the <EM>Continue</EM> Button.
|
||||
</DIV>
|
||||
<div id="footer">
|
||||
<msntv:CustomButton id="continue" label="Continue" href="/Register/ConnectionType.aspx"/>
|
||||
</div>
|
||||
<div id="Back">
|
||||
<msntv:CustomButton id="continue2" label="Back" href="/Register/ConnectionType.aspx"/>
|
||||
<msntv:CustomButton id="continue2" label="Back" href="/Register/Establish-your-MSN-TV-Account.html"/>
|
||||
<msntv:CustomButton id="continue" label="Continue" disabled="true" href="/Register/Validate-account.aspx"/>
|
||||
</div>
|
||||
</BODY>
|
||||
</HTML>`;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const minisrv_service_file = true;
|
||||
|
||||
|
||||
// TODO: if the user hits back, and removes the password from the user/pass page, any previously entered password
|
||||
// will still be in the cookie and they will skip the password page.
|
||||
// We should probably clear the password cookie when they hit back from the password page, but how?
|
||||
// We can't assume password = '' means they hit back, because some pages will not send it.
|
||||
let email = request_headers.query.email || '';
|
||||
if (Array.isArray(email)) email = email[0];
|
||||
if (!email && request_headers.cookie) {
|
||||
@@ -15,7 +18,7 @@ if (!password && request_headers.cookie) {
|
||||
if (pm) password = decodeURIComponent(pm[1]);
|
||||
}
|
||||
|
||||
if (email && email.indexOf('@') < 0) email += "@"+minisrv_config.config.service_name;
|
||||
if (email && email.indexOf('@') < 0) email += "@"+minisrv_config.config.domain_name;
|
||||
let userAvail = false;
|
||||
|
||||
if (email) {
|
||||
@@ -31,14 +34,16 @@ data = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
if (!password && !headers) {
|
||||
headers = `Status: 302 Found
|
||||
Location: https://sg1.trusted.msntv.msn.com/Register/Password-Required.aspx`;
|
||||
data = "";
|
||||
deleteCookie('register_password', { path: '/' });
|
||||
} else {
|
||||
setCookie('register_password', password, { path: '/' });
|
||||
}
|
||||
|
||||
if (userAvail && password) {
|
||||
if (password) setCookie('register_password', password, { path: '/' });
|
||||
|
||||
headers = `Status: 200 OK
|
||||
Content-type: text/html`;
|
||||
@@ -75,7 +80,7 @@ data = `<HTML xmlns:msntv>
|
||||
<tr style="margin: 0; padding: 0; top: 2px; position: relative;">
|
||||
<td style="margin: 0; padding: 0; vertical-align: middle; top: 2px; position: relative;"><img src="msntv:/Shared/Images/BulletCustom.gif" height="14" width="7" alt="Bullet"></td>
|
||||
<td style="margin: 0; padding: 0; width: 4px;"></td>
|
||||
<td style="margin: 0; padding: 0; font-size: 24px; font:bold 18; line-height: 20px;"><a class="shrLnk2" href="/Register/Enter-Promotion-Code.aspx" style="display: inline-block; line-height: 20px;">Yes, I have a Promotion Code.</a></td>
|
||||
<td style="margin: 0; padding: 0; font-size: 24px; font:bold 18; line-height: 20px;"><a class="shrLnk2" href="/Register/Enter-Promotion-Code.aspx" style="display: inline-block; line-height: 20px;">Yes, I have a Promotion Code.</a> (TODO)</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -83,4 +88,4 @@ data = `<HTML xmlns:msntv>
|
||||
</BODY>
|
||||
</HTML>`;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ data = `<HTML xmlns:msntv>
|
||||
<p>Type your minisrv username:</p>
|
||||
<div class="input-container">
|
||||
<td><input type="text" id="email" class="inputText" name="email" maxlength="32" size="25"> </td>
|
||||
<p style="display: inline; bottom: 4px; position: relative;">@${minisrv_config.config.service_name}</p>
|
||||
<p style="display: inline; bottom: 4px; position: relative;">@${minisrv_config.config.domain_name}</p>
|
||||
</div>
|
||||
<br>
|
||||
<p>Next, enter a password:</p>
|
||||
|
||||
@@ -6,7 +6,7 @@ Content-type: text/html`;
|
||||
data = `<HTML xmlns:msntv>
|
||||
<?import namespace="msntv" implementation="/Include/HTC/Shared/CustomButton.htc">
|
||||
<HEAD>
|
||||
<title id="title">Login to Passport</title>
|
||||
<title id="title">Username Not Available</title>
|
||||
<meta name="vs_targetSchema" content="http://schemas.microsoft.com/intellisense/ie5">
|
||||
<link rel="stylesheet" type="text/css" href="msntv:/Registration/css/Registration.css">
|
||||
<STYLE>
|
||||
|
||||
@@ -0,0 +1,719 @@
|
||||
const minisrv_service_file = true;
|
||||
|
||||
// Get the phase parameter from the query string
|
||||
let phase = request_headers.query.phase;
|
||||
if (Array.isArray(phase)) phase = phase[0];
|
||||
|
||||
let BoxId = request_headers.query.BoxId;
|
||||
if (Array.isArray(BoxId)) BoxId = BoxId[0];
|
||||
let clientIp = socket.remoteAddress;
|
||||
let banned = false;
|
||||
let sessionId = null;
|
||||
ServiceDomain = minisrv_config.config.domain_name;
|
||||
|
||||
// Use the shared MSNTV2 helper injected by WTV-MSNTV2 VM context.
|
||||
if (BoxId) {
|
||||
if (!BoxId || BoxId.length != 20 || !/^\d+$/.test(BoxId))
|
||||
{
|
||||
console.warn("Invalid BoxId format "+BoxId+" from "+clientIp);
|
||||
banned = true;
|
||||
} else {
|
||||
sessionId = encodeSessionID(BoxId);
|
||||
}
|
||||
} else if (request_headers.cookie && request_headers.cookie.SessionID) {
|
||||
BoxID = decodeSessionID(request_headers.cookie.SessionID);
|
||||
sessionId = request_headers.cookie.SessionID;
|
||||
} else {
|
||||
console.warn("No BoxId provided by client "+clientIp);
|
||||
banned = true;
|
||||
}
|
||||
|
||||
if (!sessionId && !banned) {
|
||||
banned = true;
|
||||
}
|
||||
|
||||
if (!session_data && BoxId) {
|
||||
console.log("Missing session_data for BoxId %s", BoxId);
|
||||
}
|
||||
|
||||
let registered = false;
|
||||
let username = '';
|
||||
let Profile_Picture = '';
|
||||
if (session_data) {
|
||||
registered = session_data.isRegistered();
|
||||
if (registered) {
|
||||
username = session_data.getSessionData("subscriber_username") || '';
|
||||
Profile_Picture = session_data.getSessionData('ProfilePicture') || '';
|
||||
}
|
||||
}
|
||||
|
||||
// Current UTC time
|
||||
const now = new Date();
|
||||
|
||||
const timeData = {
|
||||
hh: now.getUTCHours(),
|
||||
mm: now.getUTCMinutes(),
|
||||
ss: now.getUTCSeconds(),
|
||||
mo: now.getUTCMonth() + 1,
|
||||
dd: now.getUTCDate(),
|
||||
yyyy: now.getUTCFullYear()
|
||||
};
|
||||
|
||||
const timezoneMap = {
|
||||
"UTC": {
|
||||
standardName: "UTC",
|
||||
standardOffset: 0,
|
||||
daylightName: "UTC",
|
||||
daylightOffset: 0
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
standardName,
|
||||
standardOffset,
|
||||
daylightName,
|
||||
daylightOffset
|
||||
} = timezoneMap["UTC"];
|
||||
|
||||
// Set session cookie on the client
|
||||
if (sessionId) {
|
||||
setCookie('SessionID', sessionId, { path: '/' });
|
||||
}
|
||||
|
||||
// Handle different phases
|
||||
switch (phase) {
|
||||
case "Bootstrap":
|
||||
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>`;
|
||||
break;
|
||||
|
||||
case "BoxCheck":
|
||||
|
||||
headers = `200 OK
|
||||
Content-type: text/html`;
|
||||
|
||||
data = `<html>
|
||||
<head>
|
||||
<title id="title"></title>
|
||||
</head>
|
||||
<body>
|
||||
<iframe id="checkmail" style="display:none"></iframe>
|
||||
<script src="msntv:/Javascript/TVShell.js" language="javascript"></script>
|
||||
<script src="msntv:/Javascript/ServiceList.js" language="javascript"></script>
|
||||
<script src="msntv:/Javascript/GuestUser.js" language="javascript"></script>
|
||||
<script language="javascript">
|
||||
try {
|
||||
var TVShell = new ActiveXObject("MSNTV.TVShell");
|
||||
var Sink = new ActiveXObject("MSNTV.MultipleEventSink");
|
||||
var email = TVShell.UserManager.EMail;
|
||||
var wanProvider = TVShell.ConnectionManager.WANProvider;
|
||||
|
||||
var banned = ${banned};
|
||||
var registered = ${registered};
|
||||
var username = "${username}";
|
||||
var picture = "${Profile_Picture}";
|
||||
var ServiceDomain = "${ServiceDomain}";
|
||||
var MSNTVToken = "";
|
||||
var serviceArgs = new Array();
|
||||
var ProductionArgs = new Array("msntv.msn.com", "MBI", 0, 0,
|
||||
"mail.services.live.com", "MBI", 0, 0,
|
||||
"livefilestore.com", "MBI", 0, 0,
|
||||
"messenger.msn.com", "?id=507", 0, 0,
|
||||
"spaces.live.com", "MBI", 0, 0
|
||||
);
|
||||
serviceArgs[0] = ProductionArgs;
|
||||
|
||||
if (!banned) {
|
||||
// DEBUG ONLY! USE WITH CAUTION!
|
||||
TVShell.AddSecretCode(10000); // Power-on for nightly update
|
||||
TVShell.AddSecretCode(10001); // Power-on for nightly email check at anchor time
|
||||
TVShell.AddSecretCode(10002); // Power-on for nightly email check at non-anchor time
|
||||
TVShell.AddSecretCode(77437); // spooky dialing options
|
||||
TVShell.AddSecretCode(93288); // Service Selection Page
|
||||
TVShell.AddSecretCode(6145539); // crash the system
|
||||
TVShell.AddSecretCode(3932397); // update loop test
|
||||
}
|
||||
|
||||
function isIDCRLErrorCode( theCode )
|
||||
{
|
||||
// when high bit is set, it is an error
|
||||
if ( theCode & 0x80000000 )
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getIDCRLCode( theCode )
|
||||
{
|
||||
return (theCode & 0xFF);
|
||||
}
|
||||
|
||||
/*
|
||||
function CheckForUser(usernameToCheck) {
|
||||
var UserManager = TVShell.UserManager;
|
||||
for (var i=0; i<UserManager.Count; i++) {
|
||||
var user = UserManager.Item(i);
|
||||
if (user == usernameToCheck) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}*/
|
||||
|
||||
function DoLogin() {
|
||||
var currentUser = TVShell.UserManager.CurrentUser;
|
||||
if (currentUser == null) {
|
||||
if (banned === true) {
|
||||
var url = 'https://sg1.trusted.msntv.msn.com/connection/banned.html';
|
||||
var myPanel = TVShell.PanelManager.Item('main');
|
||||
if (myPanel) myPanel.GotoURL(url);
|
||||
} else {
|
||||
if (registered === true) {
|
||||
var user = TVShell.UserManager.AddNew(username + '@' + ServiceDomain);
|
||||
if (user) {
|
||||
var useroptions = UserManager.Item(username + '@' + ServiceDomain);
|
||||
useroptions.IsPersistent = true;
|
||||
//user.setAttribute("GuestUser", false);
|
||||
useroptions.LargeIcon = "msntv:/SignInPics/big/"+ picture + ".png";
|
||||
useroptions.SmallIcon = "msntv:/SignInPics/small/"+ picture + ".gif";
|
||||
TVShell.UserManager.Save();
|
||||
}
|
||||
}
|
||||
var myPanel = TVShell.PanelManager.Item('main')
|
||||
if (registered === true) {
|
||||
entry = TVShell.ServiceList.Add('connection::login');
|
||||
entry.URL = 'https://sg1.trusted.msntv.msn.com/connection/GatePage.aspx?phase=Bootstrap&purpose=Authorize';
|
||||
entry.Description = '${minisrv_config.config.service_name}/sg1 [${minisrv_config.config.hide_minisrv_version ? "beta" : minisrv_version_string.replace("zefie's wtv minisrv ","")}]';
|
||||
TVShell.ServiceList.Save();
|
||||
var signon = TVShell.BuiltinServiceList.Item("SignOn");
|
||||
var panel = TVShell.PanelManager.FocusedPanel;
|
||||
var atLogin = false;
|
||||
TVShell.ConnectionManager.ServiceState = 'ReSignIn';
|
||||
if ( signon && panel && panel.Name == "main" )
|
||||
{
|
||||
if ( IsMainPanelOnPage( signon.URL ) ) atLogin = true;
|
||||
}
|
||||
if (!atLogin) {
|
||||
myPanel.ClearTravelLog();
|
||||
myPanel.NoBackToMe = true;
|
||||
GotoSignOn();
|
||||
}
|
||||
} else {
|
||||
if (myPanel) myPanel.GotoURL('https://sg1.trusted.msntv.msn.com/Register/Establish-your-MSN-TV-Account.html');
|
||||
}
|
||||
if (myPanel) {
|
||||
myPanel.ClearTravelLog();
|
||||
myPanel.NoBackToMe = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (banned === true) {
|
||||
var url = 'https://sg1.trusted.msntv.msn.com/connection/banned.html';
|
||||
var myPanel = TVShell.PanelManager.Item('service');
|
||||
if (myPanel) myPanel.GotoURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
var hasIDCRL = false;
|
||||
|
||||
try {
|
||||
hasIDCRL = (typeof TVShell.LoginManager.IDCRLInitialize === "unknown" ||
|
||||
typeof TVShell.LoginManager.IDCRLInitialize === "function");
|
||||
} catch (e) {
|
||||
hasIDCRL = false;
|
||||
}
|
||||
|
||||
if (currentUser != null) {
|
||||
// Check if We can do IDCRL if not fall back to Legacy XMLlogin
|
||||
try {
|
||||
if (hasIDCRL) {
|
||||
DoIDCRLLogin();
|
||||
} else {
|
||||
// Non IDCRL Auth Code (Pre 5.x)
|
||||
Sink.AttachEvent(TVShell.LoginManager, 'OnLoginResult', OnLoginResult);
|
||||
TVShell.LoginManager.PassportSiteIDs = '507';
|
||||
TVShell.LoginManager.LoginURL = "https://login.live.com/ppsecure/clientpost.srf";
|
||||
TVShell.LoginManager.LogoutURL = "https://login.live.com/ppsecure/logoutxml.srf";
|
||||
TVShell.LoginManager.ResetPasswordURL = "https://login.live.com/ppsecure/MSRV_ResetPW_ClientPost.srf";
|
||||
TVShell.LoginManager.ChangePasswordURL = "https://login.live.com/ppsecure/MSRV_ChangePW_ClientPost.srf";
|
||||
TVShell.LoginManager.RequestProfileURL = "https://login.live.com/ppsecure/ClientProfileRequest.srf";
|
||||
TVShell.LoginManager.UpdateProfileURL = "https://login.live.com/ClientEditProf.srf";
|
||||
TVShell.LoginManager.Authenticate(email, "", "https://login.live.com/ppsecure/clientpost.srf");
|
||||
}
|
||||
} catch (e) {
|
||||
TVShell.EventLog.Important("Login error: " + e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function OnLoginResult(hr,t,p)
|
||||
{
|
||||
MSNTVToken = t;
|
||||
GoToUserCheck();
|
||||
}
|
||||
|
||||
function DoIDCRLLogin()
|
||||
{
|
||||
try {
|
||||
TVShell.LoginManager.IDCRLInitialize(0);
|
||||
Sink.AttachEvent(TVShell.LoginManager, "IDCRLOnAuthStateChanged",IDCRLOnAuthStateChanged);
|
||||
TVShell.LoginManager.IDCRLLogonAndAuthToServices(serviceArgs[0]);
|
||||
} catch (e) {
|
||||
TVShell.EventLog.Important("IDCRL error: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function IDCRLOnAuthStateChanged(result, authState, requestStatus, user, serviceTarget, servicePolicy, token, webFlowUrl)
|
||||
{
|
||||
// Find the matching policy in ProductionArgs for this serviceTarget
|
||||
var expectedPolicy = "";
|
||||
for(var i = 0; i < ProductionArgs.length; i++) {
|
||||
if(ProductionArgs[i] == serviceTarget && i+1 < ProductionArgs.length) {
|
||||
expectedPolicy = ProductionArgs[i+1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Now check with the correctly matched policy
|
||||
if(TVShell.UserManager.CurrentUser.EMail != user ||
|
||||
ProductionArgs[0] != serviceTarget ||
|
||||
expectedPolicy != servicePolicy ||
|
||||
(isIDCRLErrorCode(authState) || getIDCRLCode(authState) != 0x03) ||
|
||||
(isIDCRLErrorCode(requestStatus) || getIDCRLCode(requestStatus) != 0x00)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (token != ""){
|
||||
var tIndex = token.indexOf("t=");
|
||||
var pIndex = token.indexOf("&p=");
|
||||
// make sure there is only the "t" token exists, this is for RPS compact ticket
|
||||
// it is possible that compact ticket contains empty p parameter.
|
||||
MSNTVToken = token;
|
||||
GoToUserCheck();
|
||||
}
|
||||
else
|
||||
TVShell.EventLog.Important("No token");
|
||||
}
|
||||
|
||||
function DoPoptimization() {
|
||||
if (wanProvider === "MSNIANB") {
|
||||
var connector = GetConnectorByName("LocalPOP");
|
||||
if (connector == null) {
|
||||
connector = TVShell.ConnectionManager.MSNIAManager.Connectors.Add("modem");
|
||||
connector.AreaCode = "";
|
||||
connector.Exchange = "";
|
||||
connector.DialingFlags = 0x00001000;
|
||||
connector.Name = "LocalPOP";
|
||||
connector.LocationName = "LocalPOP";
|
||||
TVShell.ConnectionManager.Save();
|
||||
connector.Poptimize("0", connector.AreaCode, connector.Exchange);
|
||||
}
|
||||
|
||||
if (connector.Phonebook == null || connector.Phonebook.length === 0) {
|
||||
if (connector.AreaCode && connector.Exchange) {
|
||||
connector.Poptimize(connector.AreaCode, connector.Exchange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function GetConnectorByName(name) {
|
||||
var connectors = TVShell.ConnectionManager.MSNIAManager.Connectors;
|
||||
for (var i = 0; i < connectors.length; i++) {
|
||||
if (connectors[i].Name === name) {
|
||||
return connectors[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function GoToUserCheck() {
|
||||
if (banned === true) {
|
||||
var url = 'https://sg1.trusted.msntv.msn.com/connection/banned.html';
|
||||
var myPanel = TVShell.PanelManager.Item('service');
|
||||
if (myPanel) myPanel.GotoURL(url);
|
||||
} else if (registered) {
|
||||
var url = 'https://sg1.trusted.msntv.msn.com/connection/GatePage.aspx?phase=UserCheck&purpose=Authorize&t=' + MSNTVToken ;
|
||||
var myPanel = TVShell.PanelManager.Item('service');
|
||||
if (myPanel) myPanel.GotoURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
function SetProgress(text, percent) {
|
||||
if (progressPanel) {
|
||||
progressPanel.Document.SetProgressText(text);
|
||||
progressPanel.Document.SetProgressPercent(percent);
|
||||
}
|
||||
}
|
||||
|
||||
var progressPanel = TVShell.PanelManager.Item('progress');
|
||||
|
||||
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 {
|
||||
DoPoptimization();
|
||||
DoLogin();
|
||||
|
||||
try {
|
||||
TVShell.DeviceControl.SetTimeZone(${standardOffset}, "${standardName}", 0, "");
|
||||
} catch (e) {
|
||||
TVShell.EventLog.Important("SetTimeZone error: " + e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
TVShell.DeviceControl.SetClock(${timeData.hh}, ${timeData.mm}, ${timeData.ss}, ${timeData.mo}, ${timeData.dd}, ${timeData.yyyy});
|
||||
} catch (e) {
|
||||
TVShell.EventLog.Important("SetClock error: " + e.message);
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
TVShell.EventLog.Important("Error in boxcheck: " + e.message);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
break;
|
||||
|
||||
case "UserCheck":
|
||||
headers = `Content-type: text/html`;
|
||||
|
||||
// Check if the msntv.msn.com token is correct TODO
|
||||
|
||||
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");
|
||||
var wanProvider = TVShell.ConnectionManager.WANProvider;
|
||||
function SetServiceList() {
|
||||
var entry;
|
||||
|
||||
// BuiltinServiceList - for main MSN TV services
|
||||
|
||||
TVShell.UserManager.CurrentUser.ServiceList.Clear(); //Always clear the list first to avoid dupes.
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('help::help');
|
||||
entry.URL = 'http://sg1.msntv.msn.com/health/Help.aspx';
|
||||
entry.KeyCode = 0xAC; // VK_BROWSER_HOME
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('home::home');
|
||||
entry.URL = 'http://sg1.trusted.msntv.msn.com/Home/Home.aspx?WANProvider=' + wanProvider;
|
||||
entry.KeyCode = 0xAC; // VK_BROWSER_HOME
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('home::bgmusic');
|
||||
entry.URL = '';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('home::backendproxy');
|
||||
entry.URL = 'https://sg1.trusted.msntv.msn.com/BackendProxy';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('home::radioplus');
|
||||
entry.URL = 'https://sg1.trusted.msntv.msn.com/Stations.xml';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.ServiceList.Add('music::radiohome');
|
||||
entry.URL = 'http://msntv.msn.com/pages/radio/home.aspx';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('Livefilestore::AuthServer');
|
||||
entry.URL = 'livefilestore.com';
|
||||
entry.Description = 'MBI'
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('Skydrive::AuthServer');
|
||||
entry.URL = 'favorites.live.com';
|
||||
entry.Description = 'MBI'
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('Skydrive::Browse');
|
||||
// entry.URL = 'users.storage.live.com';
|
||||
entry.URL = 'favorites.msn.com';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('Skydrive::AppId');
|
||||
entry.Description = '1'
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('Skydrive::ApiServer');
|
||||
entry.URL = 'api.live.net';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('onlinestorage::root');
|
||||
entry.URL = 'https://livefilestore/onlinestorage/';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('Favorites::RoamingServer');
|
||||
entry.URL = 'https://livefilestore.com/onlinestorage/';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('Favorites::Migration');
|
||||
entry.URL = 'https://livefilestore.com/onlinestorage/';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('Favorites::SyncServer');
|
||||
entry.URL = 'https://livefilestore.com/onlinestorage/';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('onlinestorage::authServer');
|
||||
entry.URL = 'http://77.68.90.130/';
|
||||
entry.Description = 'MBI'
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('mail::listmail');
|
||||
entry.URL = 'http://mail-sgN.msntv.msn.com/apps/mail/listmail.aspx';
|
||||
entry.KeyCode = 0xB4; // VK_LAUNCH_MAIL
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('mail::writemail');
|
||||
entry.URL = 'http://mail-sg1.trusted.msntv.msn.com/apps/mail/writemail.aspx';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('chat::home');
|
||||
entry.URL = 'https://sg1.trusted.msntv.msn.com/Pages/Chat/Chat.aspx';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('chat::ServiceTarget');
|
||||
entry.URL = 'chat.msn.com';
|
||||
entry.Description = '?id=2260'
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('messenger::root');
|
||||
entry.URL = 'http://ms.msgrsvcs.ctsrv.gay:1863';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('messenger::passport');
|
||||
entry.URL = 'https://login.live.com/messenger';
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('messenger::ServiceTarget');
|
||||
entry.URL = 'messenger.msn.com';
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('search::search');
|
||||
entry.URL = 'https://sg1.trusted.msntv.msn.com/Pages/Search/search.html';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('search::main');
|
||||
entry.URL = 'https://sg1.msntv.msn.com/search/Search.aspx';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('discuss::home');
|
||||
entry.URL = 'http://sg1.msntv.msn.com/apps/discuss/DiscussLobby.aspx';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('maps::main');
|
||||
entry.URL = 'https://sg1.msntv.msn.com/apps/maps/GetMap.aspx';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('Settings::HomeNetwork');
|
||||
entry.URL = 'msntv:/Settings/Network/HomeNetworking.html';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('settings::mainindex');
|
||||
entry.URL = 'https://sg1.trusted.msntv.msn.com/apps/settings/MainIndex.aspx';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('UAM::UAMbase');
|
||||
entry.URL = 'https://sg1.trusted.msntv.msn.com/apps/uam/pages/settings.aspx';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('Photo::Home');
|
||||
entry.URL = 'msntv:/Photo/PhotoHome.html';
|
||||
entry.Safe = true;
|
||||
|
||||
entry = TVShell.UserManager.CurrentUser.ServiceList.Add('Photos');
|
||||
entry.URL = 'msntv:/Photo/PhotoHome.html';
|
||||
entry.Safe = true;
|
||||
|
||||
// Add services to ServiceList
|
||||
TVShell.ServiceList.Clear();
|
||||
|
||||
entry = TVShell.ServiceList.Add('home::cinemanow');
|
||||
entry.URL = 'http://g.msn.com/5TVANDURIL/4000';
|
||||
|
||||
entry = TVShell.ServiceList.Add('msn::radioplus');
|
||||
entry.URL = 'http://radio.msn.com/asx/generate';
|
||||
|
||||
entry = TVShell.ServiceList.Add('msn::musicnews');
|
||||
entry.URL = 'http://www.msnbc.msn.com/id/3032433/';
|
||||
|
||||
entry = TVShell.ServiceList.Add('connection::popupcontrol');
|
||||
entry.URL = 'https://sg1.trusted.msntv.msn.com/connection/PopupControlWhiteList.ashx';
|
||||
|
||||
entry = TVShell.ServiceList.Add('connection::reconnect');
|
||||
entry.URL = 'https://sg1.trusted.msntv.msn.com/connection/GatePage.aspx?phase=Bootstrap&purpose=ReAuthorize';
|
||||
|
||||
entry = TVShell.ServiceList.Add('connection::nightly_login');
|
||||
entry.URL = 'https://sg1.trusted.msntv.msn.com/connection/GatePage.aspx?phase=Bootstrap&purpose=Nightly';
|
||||
|
||||
entry = TVShell.ServiceList.Add('mail::check');
|
||||
entry.URL = 'https://sg1.trusted.msntv.msn.com/apps/connection/CheckMail.aspx?phase=CheckMail&purpose=CheckMail';
|
||||
entry.Safe = true;
|
||||
|
||||
if (wanProvider === "BYOA") {
|
||||
entry = TVShell.ServiceList.Add('home::videoplus');
|
||||
entry.URL = 'http://msntv.msn.com/pages/msnvideo/main.aspx';
|
||||
entry.Safe = true;
|
||||
}
|
||||
|
||||
if (wanProvider === "BYOA") {
|
||||
entry = TVShell.ServiceList.Add('home::musicvideo');
|
||||
entry.URL = 'http://msntv.msn.com/pages/msnvideo/main.aspx?p=music';
|
||||
entry.Safe = true;
|
||||
}
|
||||
|
||||
entry = TVShell.ServiceList.Add('connection::resetpassword');
|
||||
entry.URL = 'https://sg1.trusted.msntv.msn.com/connection/GatePage.aspx?phase=Bootstrap&purpose=ResetPassword';
|
||||
|
||||
entry = TVShell.ServiceList.Add('connection::pagepatch');
|
||||
entry.URL = 'https://sg1.trusted.msntv.msn.com/connection/PagePatch.ashx';
|
||||
|
||||
entry = TVShell.ServiceList.Add('connection::login');
|
||||
entry.URL = 'https://sg1.trusted.msntv.msn.com/connection/GatePage.aspx?phase=Bootstrap&purpose=Authorize';
|
||||
entry.Description = '${minisrv_config.config.service_name}/sg1 [${minisrv_config.config.hide_minisrv_version ? "beta" : minisrv_version_string.replace("zefie's wtv minisrv ","")}]';
|
||||
|
||||
entry = TVShell.ServiceList.Add('ctags::main');
|
||||
entry.URL = 'http://c.msn.com/c.gif?di=1455&pi=68206&tp=http%3a%2f%2fmsntv.msn.com%2fclient%2f';
|
||||
TVShell.ServiceList.Save();
|
||||
}
|
||||
|
||||
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) {
|
||||
TVShell.PanelManager.Item('main').GotoURL(TVShell.UserManager.CurrentUser.ServiceList.Item('home::home').URL);
|
||||
TVShell.PanelManager.Item('main').ClearTravelLog();
|
||||
TVShell.PanelManager.Item('main').NoBackToMe = true;
|
||||
} else {
|
||||
TVShell.ConnectionManager.ServiceState = 'ReSignIn';
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsServicePanel()) {
|
||||
DontContinue();
|
||||
} else {
|
||||
SetServiceList();
|
||||
|
||||
TVShell.UserManager.SetCurrentUserIsAuthorized(true);
|
||||
TVShell.ConnectionManager.ServiceState = 'Authorized';
|
||||
var dt = new Date();
|
||||
TVShell.UserManager.LastLoginTime = dt.getTime() / 1000 + dt.getTimezoneOffset() * 60;
|
||||
TVShell.UserManager.OfflineAppMaxAccessDays = 20;
|
||||
TVShell.UserManager.OfflineAppMaxAccessTimes = 20;
|
||||
TVShell.UserManager.Save();
|
||||
TVShell.PanelManager.Item('main').GotoURL(TVShell.UserManager.CurrentUser.ServiceList.Item('home::home').URL);
|
||||
TVShell.PanelManager.Item('main').ClearTravelLog();
|
||||
TVShell.PanelManager.Item('main').NoBackToMe = true;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
break;
|
||||
|
||||
default:
|
||||
headers = `200 OK
|
||||
Content-type: text/html`;
|
||||
|
||||
data = `<HTML>
|
||||
<HEAD>
|
||||
<title id="title"></title>
|
||||
</HEAD>
|
||||
</HTML>`;
|
||||
break;
|
||||
}
|
||||
@@ -197,10 +197,12 @@ if (minisrv_config.services["wtv-author"].max_pages) {
|
||||
</table>
|
||||
<p>A maximum of <b>${minisrv_config.services["wtv-author"].max_pages}</b> pages can be created, regardless of publish status.
|
||||
<br><br>
|
||||
Your published pages are available at<br>
|
||||
`
|
||||
if (numofpages > 0) {
|
||||
`Your published pages are available at<br>
|
||||
<a href="http://${site}/${session_data.getSessionData("subscriber_username")}/">http://${site}/${session_data.getSessionData("subscriber_username")}/</a>
|
||||
</table>`
|
||||
|
||||
}
|
||||
}
|
||||
data += `
|
||||
<SCRIPT language=JavaScript>
|
||||
|
||||
@@ -38,6 +38,7 @@ async function processLC2DownloadPage(flashrom_info, headers, numparts = null) {
|
||||
if (!flashrom_info.part_count) flashrom_info.part_count = parseInt(flashrom_info.message.slice(flashrom_info.message.length - 4).replace(/\D/g, ''));
|
||||
if (parseInt(flashrom_info.part_number) >= 0 && flashrom_info.rompath && flashrom_info.next_rompath) {
|
||||
if (!flashrom_info.message && flashrom_info.is_bootrom) {
|
||||
flashrom_info.part_count = 16;
|
||||
flashrom_info.message = "BootRom Part " + (flashrom_info.part_number + 1) + " of " + flashrom_info.part_count;
|
||||
}
|
||||
|
||||
@@ -72,6 +73,8 @@ async function processLC2DownloadPage(flashrom_info, headers, numparts = null) {
|
||||
downloadTime = elapsedMinutes * remainingParts;
|
||||
}
|
||||
session.lastDownloadTime = now;
|
||||
|
||||
if (isNaN(downloadTime) || downloadTime < 1) downloadTime = 1;
|
||||
|
||||
|
||||
headers = `200 OK
|
||||
@@ -127,7 +130,7 @@ Updating now
|
||||
<font size=+1>
|
||||
Your ${session_data.getBoxName()} is being<br>updated automatically.
|
||||
<p> <font size=+1>
|
||||
This will take about ${downloadTime} minutes and<br>then you can use your ${session_data.getBoxName()} again.
|
||||
This will take about ${downloadTime} minute${downloadTime !== 1 ? "s" : ""} and<br>then you can use your ${session_data.getBoxName()} again.
|
||||
`;
|
||||
if (flashrom_info.is_bootrom && flashrom_info.part_number === (flashrom_info.part_count - 1)) {
|
||||
data += `<p>
|
||||
|
||||
@@ -57,7 +57,7 @@ Welcome to Mail
|
||||
In Mail, you can exchange typed messages—called
|
||||
<i>m-mail</i>—with anyone who is on ${minisrv_config.config.service_name}, as well as anyone using other compatible MiniSrvs around the world. This is your m-mail address:
|
||||
<blockquote>
|
||||
<b>${session_data.getSessionData("subscriber_username")}@${minisrv_config.config.service_name}</b>
|
||||
<b>${session_data.getSessionData("subscriber_username")}@${minisrv_config.config.domain_name}</b>
|
||||
</blockquote>
|
||||
Choose <b>Begin</b> to start using Mail. <!-- Or to learn more,
|
||||
choose this link:
|
||||
|
||||
@@ -176,7 +176,7 @@ E-mail addresses for ${session_data.getSessionData("subscriber_username")}
|
||||
<tr absheight=26>
|
||||
<td rowspan=1000 abswidth=8>
|
||||
<td colspan=3>
|
||||
Your address is ${session_data.getSessionData("subscriber_username")}@${minisrv_config.config.service_name}
|
||||
Your address is ${session_data.getSessionData("subscriber_username")}@${minisrv_config.config.domain_name}
|
||||
<tr absheight=8>
|
||||
<td colspan=3>
|
||||
<img src="wtv-home:/ROMCache/Spacer.gif" width=1 height=8>
|
||||
@@ -702,7 +702,7 @@ ${(!newaddress) ? `<input type=hidden name="id" value="${request_headers.query.i
|
||||
let addrExists = false;
|
||||
// dumbass protection for making addresses look proper in the list
|
||||
let address = request_headers.query.address.split("@")[0];
|
||||
address += `@${minisrv_config.config.service_name}`;
|
||||
address += `@${minisrv_config.config.domain_name}`;
|
||||
// sanity checks to make sure the user doesn't have duplicate names/addresses
|
||||
address_book.forEach(user => {
|
||||
if (user.name.includes(request_headers.query.nickname)) {
|
||||
@@ -745,7 +745,7 @@ Location: wtv-mail:/addressbook`;
|
||||
}
|
||||
// dumbass protection for making addresses look proper in the list
|
||||
address = address.split("@")[0];
|
||||
address += `@${minisrv_config.config.service_name}`;
|
||||
address += `@${minisrv_config.config.domain_name}`;
|
||||
nameExists = false;
|
||||
addrExists = false;
|
||||
if (address_book.length > 1) {
|
||||
|
||||
@@ -262,7 +262,7 @@ label="View saved e-mail messages">
|
||||
<font sizerange=medium> ${message_list_string}
|
||||
<table cellspacing=0 cellpadding=0 border=0>
|
||||
<TR><TD maxlines="1">
|
||||
${username}@${minisrv_config.config.service_name}
|
||||
${username}@${minisrv_config.config.domain_name}
|
||||
</TD></TR>
|
||||
</TABLE>
|
||||
</font><br>
|
||||
@@ -321,7 +321,7 @@ ${message_font_close}
|
||||
data += `
|
||||
<font sizerange=medium> No ${(mailbox_name === "Inbox") ? `new e-mail messages for<table cellspacing=0 cellpadding=0 border=0>
|
||||
<TR><TD maxlines="1">
|
||||
${username}@${minisrv_config.config.service_name}
|
||||
${username}@${minisrv_config.config.domain_name}
|
||||
</TD></TR>
|
||||
</TABLE>` : 'e-mail messages in mailbox ' + mailbox_name}
|
||||
</font><br>
|
||||
|
||||
@@ -127,7 +127,7 @@ Content-Type: audio/wav`;
|
||||
|
||||
const username = session_data.getSessionData("subscriber_username");
|
||||
const userdisplayname = wtvshared.htmlEntitize(session_data.getSessionData("subscriber_name"));
|
||||
const address = username + "@" + minisrv_config.config.service_name //minisrv_config.config.domain_name
|
||||
const address = username + "@" + minisrv_config.config.domain_name //minisrv_config.config.domain_name
|
||||
const notImplementedAlert = new clientShowAlert({
|
||||
'image': minisrv_config.config.service_logo,
|
||||
'message': "This feature is not available.",
|
||||
|
||||
@@ -37,7 +37,7 @@ if (minisrv_config.config.hide_incomplete_features) {
|
||||
}
|
||||
|
||||
/* We need to fix most webtv viewers for this, since they spoof a build that doesn't support messenger?
|
||||
if (!session_data.hasCap("client-can-use-messenger")) {
|
||||
if (!session_data.capabilities.get("client-can-use-messenger")) {
|
||||
removeSettingByUrl("wtv-setup:/messenger");
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -27,6 +27,7 @@ class WTVMSNTV2 {
|
||||
this.tlsContext = this.loadTlsContext();
|
||||
this.forgeTlsCredentials = this.loadForgeTlsCredentials();
|
||||
this.server = net.createServer((socket) => this.handleConnection(socket));
|
||||
this.tokens = {};
|
||||
this.mimeTypes = {
|
||||
html: 'text/html',
|
||||
htm: 'text/html',
|
||||
@@ -129,13 +130,20 @@ class WTVMSNTV2 {
|
||||
socket.rawDataListener = (chunk) => this.handleData(socket, chunk);
|
||||
socket.on('data', socket.rawDataListener);
|
||||
socket.on('error', (err) => {
|
||||
if (this.service_config.show_verbose_errors) console.error('[WTV-MSNTV2] socket error:', err.message);
|
||||
if (this.service_config.debug) {
|
||||
if (err.message == 'read ECONNRESET') {
|
||||
console.warn('[WTV-MSNTV2] Client disconnected');
|
||||
} else {
|
||||
console.error('[WTV-MSNTV2] socket error:', err.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleData(socket, chunk) {
|
||||
socket.buffer = Buffer.concat([socket.buffer, chunk]);
|
||||
if (socket.buffer.length > parseFloat(this.minisrv_config.services[service_name].max_response_size || 128) * 1024 * 1024) {
|
||||
const maxRequestBytes = this.maxProxyResponseBytes;
|
||||
if (socket.buffer.length > maxRequestBytes) {
|
||||
this.writeError(socket, 413, 'Request Entity Too Large');
|
||||
socket.destroy();
|
||||
return;
|
||||
@@ -169,6 +177,11 @@ class WTVMSNTV2 {
|
||||
});
|
||||
|
||||
const contentLength = parseInt(headers['content-length'] || '0', 10) || 0;
|
||||
if (contentLength > 0 && contentLength > maxRequestBytes) {
|
||||
this.writeError(socket, 413, 'Request Entity Too Large');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
const requestLength = headerEnd + 4 + contentLength;
|
||||
if (socket.buffer.length < requestLength) return;
|
||||
|
||||
@@ -196,14 +209,14 @@ class WTVMSNTV2 {
|
||||
|
||||
const userAgent = request_headers['User-Agent'] || request_headers['user-agent'] || '';
|
||||
if (!this.isAllowedUserAgent(userAgent)) {
|
||||
if (this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3) {
|
||||
if (this.service_config.debug || this.minisrv_config.config.verbosity >= 3) {
|
||||
console.warn('[WTV-MSNTV2] unsupported User-Agent rejected:', userAgent || '<none>');
|
||||
}
|
||||
this.writeError(socket, 403, 'Forbidden', request_headers);
|
||||
return;
|
||||
}
|
||||
|
||||
const verbose = this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3;
|
||||
const verbose = this.service_config.debug || this.minisrv_config.config.verbosity >= 3;
|
||||
if (verbose) {
|
||||
console.log('[WTV-MSNTV2] incoming request:', requestLine);
|
||||
if (body.length) {
|
||||
@@ -244,7 +257,7 @@ class WTVMSNTV2 {
|
||||
handleConnect(socket, requestUrl) {
|
||||
const [host, portString] = requestUrl.split(':');
|
||||
const port = parseInt(portString, 10) || 443;
|
||||
const verbose = this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3;
|
||||
const verbose = this.service_config.debug || this.minisrv_config.config.verbosity >= 3;
|
||||
const connectIntercept = this.getConnectIntercept(host);
|
||||
if (verbose) console.log('[WTV-MSNTV2] CONNECT request:', requestUrl, 'intercept:', !!connectIntercept);
|
||||
|
||||
@@ -270,13 +283,13 @@ class WTVMSNTV2 {
|
||||
});
|
||||
|
||||
remote.on('error', (err) => {
|
||||
if (this.service_config.show_verbose_errors) console.error('[WTV-MSNTV2] CONNECT error:', err.message);
|
||||
if (this.service_config.debug) console.error('[WTV-MSNTV2] CONNECT error:', err.message);
|
||||
this.writeError(socket, 502, 'Bad Gateway');
|
||||
});
|
||||
}
|
||||
|
||||
setupTlsSocket(tlsSocket) {
|
||||
const verbose = this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3;
|
||||
const verbose = this.service_config.debug || this.minisrv_config.config.verbosity >= 3;
|
||||
const sslDebug = this.sslv2Debug;
|
||||
tlsSocket.on('secureConnect', () => {
|
||||
if (sslDebug) console.log('[WTV-MSNTV2] TLS handshake complete for intercepted CONNECT', tlsSocket.connectIntercept.match);
|
||||
@@ -296,7 +309,7 @@ class WTVMSNTV2 {
|
||||
}
|
||||
|
||||
setupForgeTls(socket, connectIntercept) {
|
||||
const verbose = this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3;
|
||||
const verbose = this.service_config.debug || this.minisrv_config.config.verbosity >= 3;
|
||||
const sslDebug = this.sslv2Debug;
|
||||
const creds = this.forgeTlsCredentials;
|
||||
if (!creds) {
|
||||
@@ -376,6 +389,11 @@ class WTVMSNTV2 {
|
||||
}
|
||||
});
|
||||
const contentLength = parseInt(headers['content-length'] || '0', 10) || 0;
|
||||
const maxRequestBytes = this.maxProxyResponseBytes;
|
||||
if (contentLength > 0 && contentLength > maxRequestBytes) {
|
||||
this.writeError(socket, 413, 'Request Entity Too Large');
|
||||
return;
|
||||
}
|
||||
const requestLength = headerEnd + 4 + contentLength;
|
||||
if (connection.buffer.length < requestLength) break;
|
||||
const body = connection.buffer.slice(headerEnd + 4, requestLength);
|
||||
@@ -1115,6 +1133,12 @@ class WTVMSNTV2 {
|
||||
}
|
||||
});
|
||||
const contentLength = parseInt(headers['content-length'] || '0', 10) || 0;
|
||||
const maxRequestBytes = this.maxProxyResponseBytes;
|
||||
if (contentLength > 0 && contentLength > maxRequestBytes) {
|
||||
this.writeError(socket, 413, 'Request Entity Too Large');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
const requestLength = headerEnd + 4 + contentLength;
|
||||
if (socket.sslv2AppBuffer.length < requestLength) return;
|
||||
const body = socket.sslv2AppBuffer.slice(headerEnd + 4, requestLength);
|
||||
@@ -1146,23 +1170,11 @@ class WTVMSNTV2 {
|
||||
|
||||
loadTlsContext() {
|
||||
try {
|
||||
const certCandidates = [
|
||||
['msntv2/msn_domains.crt', 'msntv2/msn_domains.key']
|
||||
];
|
||||
let certFile = null;
|
||||
let keyFile = null;
|
||||
for (const [certPath, keyPath] of certCandidates) {
|
||||
const candidateCert = this.wtvshared.getServiceDep(certPath, true);
|
||||
const candidateKey = this.wtvshared.getServiceDep(keyPath, true);
|
||||
if (candidateCert && candidateKey) {
|
||||
certFile = candidateCert;
|
||||
keyFile = candidateKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!certFile || !keyFile) return null;
|
||||
const certPem = fs.readFileSync(certFile);
|
||||
const keyPem = fs.readFileSync(keyFile);
|
||||
const candidateCert = (this.service_config.ssl) ? this.wtvshared.parseConfigVars(this.service_config.ssl.cert) : null;
|
||||
const candidateKey = (this.service_config.ssl) ? this.wtvshared.parseConfigVars(this.service_config.ssl.key) : null;
|
||||
if (!candidateCert || !candidateKey) return null;
|
||||
const certPem = fs.readFileSync(candidateCert);
|
||||
const keyPem = fs.readFileSync(candidateKey);
|
||||
return tls.createSecureContext({
|
||||
cert: certPem,
|
||||
key: keyPem,
|
||||
@@ -1178,23 +1190,12 @@ class WTVMSNTV2 {
|
||||
|
||||
loadForgeTlsCredentials() {
|
||||
try {
|
||||
const certCandidates = [
|
||||
['msntv2/msn_domains.crt', 'msntv2/msn_domains.key']
|
||||
];
|
||||
let certFile = null;
|
||||
let keyFile = null;
|
||||
for (const [certPath, keyPath] of certCandidates) {
|
||||
const candidateCert = this.wtvshared.getServiceDep(certPath, true);
|
||||
const candidateKey = this.wtvshared.getServiceDep(keyPath, true);
|
||||
if (candidateCert && candidateKey) {
|
||||
certFile = candidateCert;
|
||||
keyFile = candidateKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!certFile || !keyFile) return null;
|
||||
const certPem = fs.readFileSync(certFile, 'utf8');
|
||||
const keyPem = fs.readFileSync(keyFile, 'utf8');
|
||||
const candidateCert = (this.service_config.ssl) ? this.wtvshared.parseConfigVars(this.service_config.ssl.cert) : null;
|
||||
const candidateKey = (this.service_config.ssl) ? this.wtvshared.parseConfigVars(this.service_config.ssl.key) : null;
|
||||
|
||||
if (!candidateCert || !candidateKey) return null;
|
||||
const certPem = fs.readFileSync(candidateCert, 'utf8');
|
||||
const keyPem = fs.readFileSync(candidateKey, 'utf8');
|
||||
return {
|
||||
certPem,
|
||||
keyPem,
|
||||
@@ -1217,7 +1218,7 @@ class WTVMSNTV2 {
|
||||
const requestLine = headerLines.shift();
|
||||
const requestParts = requestLine.split(' ');
|
||||
if (requestParts.length < 3) {
|
||||
const verbose = this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3;
|
||||
const verbose = this.service_config.debug || this.minisrv_config.config.verbosity >= 3;
|
||||
if (verbose) {
|
||||
console.warn('[WTV-MSNTV2] TLS invalid request line:', requestLine);
|
||||
console.warn('[WTV-MSNTV2] TLS raw header block:', headerBlock);
|
||||
@@ -1242,6 +1243,12 @@ class WTVMSNTV2 {
|
||||
});
|
||||
|
||||
const contentLength = parseInt(headers['content-length'] || '0', 10) || 0;
|
||||
const maxRequestBytes = this.maxProxyResponseBytes;
|
||||
if (contentLength > 0 && contentLength > maxRequestBytes) {
|
||||
this.writeError(tlsSocket, 413, 'Request Entity Too Large');
|
||||
tlsSocket.destroy();
|
||||
return;
|
||||
}
|
||||
const requestLength = headerEnd + 4 + contentLength;
|
||||
if (tlsSocket.buffer.length < requestLength) return;
|
||||
|
||||
@@ -1257,7 +1264,7 @@ class WTVMSNTV2 {
|
||||
};
|
||||
Object.assign(request_headers, headers);
|
||||
|
||||
const verbose = this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3;
|
||||
const verbose = this.service_config.debug || this.minisrv_config.config.verbosity >= 3;
|
||||
if (verbose) {
|
||||
console.log('[WTV-MSNTV2] decrypted request:', requestLine);
|
||||
console.log('[WTV-MSNTV2] decrypted headers:\n' + rawHeaders.join('\r\n'));
|
||||
@@ -1417,14 +1424,14 @@ class WTVMSNTV2 {
|
||||
const candidate = this.wtvshared.makeSafePath(base, '');
|
||||
// Exact match first
|
||||
if (candidate && fs.existsSync(candidate) && fs.lstatSync(candidate).isFile()) {
|
||||
if (this.service_config.show_verbose_errors) console.log('[WTV-MSNTV2] local intercept:', candidate);
|
||||
if (this.service_config.debug) console.log('[WTV-MSNTV2] local intercept:', candidate);
|
||||
return candidate;
|
||||
}
|
||||
// Dynamic suffixes: e.g. kickstart.aspx -> kickstart.aspx.js
|
||||
for (const suffix of dynSuffixes) {
|
||||
const dynCandidate = this.wtvshared.makeSafePath(base + suffix, '');
|
||||
if (dynCandidate && fs.existsSync(dynCandidate) && fs.lstatSync(dynCandidate).isFile()) {
|
||||
if (this.service_config.show_verbose_errors) console.log('[WTV-MSNTV2] local intercept (dynamic):', dynCandidate);
|
||||
if (this.service_config.debug) console.log('[WTV-MSNTV2] local intercept (dynamic):', dynCandidate);
|
||||
return dynCandidate;
|
||||
}
|
||||
}
|
||||
@@ -1522,6 +1529,16 @@ class WTVMSNTV2 {
|
||||
|
||||
const self = this;
|
||||
const responseCookies = [];
|
||||
// try to make the debug name
|
||||
let debug_name = (filepath) ? filepath.split(path.sep) : null;
|
||||
if (debug_name) {
|
||||
if (this.wtvshared.isConfiguredService(debug_name[debug_name.length - 2]))
|
||||
// service:/filename
|
||||
debug_name = debug_name[debug_name.length - 2] + ":/" + debug_name[debug_name.length - 1];
|
||||
else
|
||||
// filename
|
||||
debug_name = debug_name[debug_name.length - 1];
|
||||
}
|
||||
const contextObj = {
|
||||
socket,
|
||||
request_headers,
|
||||
@@ -1545,6 +1562,7 @@ class WTVMSNTV2 {
|
||||
cwd: path.dirname(filepath),
|
||||
// Cookie helpers available to scripts
|
||||
response_cookies: responseCookies,
|
||||
debug: require('debug')((debug_name) ? debug_name : 'service_script'),
|
||||
setCookie(name, value, opts) {
|
||||
opts = opts || {};
|
||||
let s = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
||||
@@ -1593,6 +1611,9 @@ class WTVMSNTV2 {
|
||||
if (socket.ssid && this.ssid_sessions && vmResult.session_data !== undefined) {
|
||||
this.ssid_sessions[socket.ssid] = vmResult.session_data;
|
||||
}
|
||||
if (vmResult.socket !== socket) {
|
||||
socket = vmResult.socket;
|
||||
}
|
||||
if (!vmResult.request_is_async) {
|
||||
this._sendScriptResult(socket, request_headers, vmResult.headers, vmResult.data, responseCookies);
|
||||
}
|
||||
@@ -1730,14 +1751,14 @@ class WTVMSNTV2 {
|
||||
target = new url.URL(requestUrl);
|
||||
}
|
||||
} catch (err) {
|
||||
if (this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3) {
|
||||
if (this.service_config.debug || this.minisrv_config.config.verbosity >= 3) {
|
||||
console.error('[WTV-MSNTV2] invalid URL:', requestUrl, err.message);
|
||||
}
|
||||
this.writeError(socket, 400, 'Bad Request', request_headers);
|
||||
return;
|
||||
}
|
||||
|
||||
const verbose = this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3;
|
||||
const verbose = this.service_config.debug || this.minisrv_config.config.verbosity >= 3;
|
||||
const isHttps = target.protocol === 'https:';
|
||||
const agent = isHttps ? https : http;
|
||||
const requestPath = target.pathname + (target.search || '');
|
||||
@@ -1770,7 +1791,7 @@ class WTVMSNTV2 {
|
||||
|
||||
const maxResponseBytes = this.maxProxyResponseBytes;
|
||||
const proxyReq = agent.request(options, (res) => {
|
||||
const verbose = this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3;
|
||||
const verbose = this.service_config.debug || this.minisrv_config.config.verbosity >= 3;
|
||||
if (verbose) {
|
||||
console.log('[WTV-MSNTV2] upstream response:', res.statusCode, res.statusMessage);
|
||||
console.log('[WTV-MSNTV2] upstream response headers:', JSON.stringify(res.headers));
|
||||
|
||||
@@ -56,18 +56,32 @@ class WTVClientSessionData {
|
||||
this.loginWhitelist.push("wtv-head-waiter:/confirm-transfer");
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a new WTVMail instance to the session's mailstore property, using the current minisrv_config and session data.
|
||||
*/
|
||||
assignMailStore() {
|
||||
this.mailstore = new WTVMail(this.minisrv_config, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a new WTVFavorites instance to the session's favstore property, using the current minisrv_config and session data.
|
||||
*/
|
||||
assignFavoriteStore() {
|
||||
this.mailstore = this.favstore = new WTVFavorites(this.minisrv_config, this)
|
||||
this.favstore = new WTVFavorites(this.minisrv_config, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new WTVSec session. Used for RC4 SECURE ON requests.
|
||||
* @returns {WTVSec} A new WTVSec session instance
|
||||
*/
|
||||
createWTVSecSession() {
|
||||
return new WTVSec(this.minisrv_config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the total number of unread messages for the primary account.
|
||||
* @returns {number} Number of unread messages
|
||||
*/
|
||||
getAccountTotalUnreadMessages() {
|
||||
if (!this.isRegistered()) return false; // unregistered
|
||||
if (this.user_id > 0) return false; // not primary user or pre-login
|
||||
@@ -88,6 +102,9 @@ class WTVClientSessionData {
|
||||
return total_unread_messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all user session data from memory, including session store and data store, and resets mail and favorite stores.
|
||||
*/
|
||||
clearUserSessionMemory() {
|
||||
this.setUserLoggedIn(false);
|
||||
this.data_store = [];
|
||||
@@ -96,6 +113,13 @@ class WTVClientSessionData {
|
||||
this.assignMailStore()
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches the current user ID and optionally updates related data stores.
|
||||
* @param {number} user_id The user ID to switch to
|
||||
* @param {boolean} update_mail Whether to update the mail store
|
||||
* @param {boolean} update_ticket Whether to update the ticket data
|
||||
* @param {boolean} update_favorite Whether to update the favorite store
|
||||
*/
|
||||
switchUserID(user_id, update_mail = true, update_ticket = true, update_favorite = true) {
|
||||
this.user_id = parseInt(user_id);
|
||||
if (user_id !== null) {
|
||||
@@ -143,6 +167,11 @@ class WTVClientSessionData {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given mail address is in the address book.
|
||||
* @param {string} addr The mail address to check against the address book
|
||||
* @returns {boolean} True if the address is in the address book, false otherwise
|
||||
*/
|
||||
isAddressInAddressBook(addr) {
|
||||
const addresses = this.getSessionData("address_book");
|
||||
if (addresses) {
|
||||
@@ -156,6 +185,11 @@ class WTVClientSessionData {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first available user slot for a new user.
|
||||
* Can only be used by the primary account (user_id 0).
|
||||
* @returns {number|boolean} The first available user slot index, or false if no slots are available
|
||||
*/
|
||||
findFreeUserSlot() {
|
||||
if (this.user_id !== 0) return false; // subscriber only command
|
||||
const master_directory = this.getUserStoreDirectory(true);
|
||||
@@ -170,16 +204,30 @@ class WTVClientSessionData {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display name of the current user.
|
||||
* @returns {string} The subscriber's display name if user_id is 0, otherwise the current user's display name.
|
||||
*/
|
||||
getDisplayName() {
|
||||
return (this.user_id === 0) ? this.getSessionData("subscriber_name") : this.getSessionData("display_name");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of users for this SSID.
|
||||
* Can only be used by the primary account (user_id 0).
|
||||
* @returns {number} The number of users this SSID has
|
||||
*/
|
||||
getNumberOfUserAccounts() {
|
||||
if (!this.isRegistered()) return false;
|
||||
if (this.user_id !== 0) return false; // subscriber only command
|
||||
return Object.keys(this.listPrimaryAccountUsers()).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all primary account users for this SSID.
|
||||
*
|
||||
* @returns {Array} An array containing the account data of all users for this SSID
|
||||
*/
|
||||
listPrimaryAccountUsers() {
|
||||
if (this.user_id !== 0) return false; // subscriber only command
|
||||
|
||||
@@ -206,6 +254,10 @@ class WTVClientSessionData {
|
||||
return account_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively creates directories for the given path.
|
||||
* @param {string} thedir The directory path to create
|
||||
*/
|
||||
mkdirRecursive(thedir) {
|
||||
thedir.split(this.path.sep).reduce(
|
||||
(directories, directory) => {
|
||||
@@ -219,13 +271,63 @@ class WTVClientSessionData {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the account store directory
|
||||
* @returns {string} Absolute path to the account store directory
|
||||
*/
|
||||
getAccountStoreDirectory() {
|
||||
return this.wtvshared.getAbsolutePath(this.minisrv_config.config.SessionStore + this.path.sep + "accounts");
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an account's SSID and User ID from just the username
|
||||
* @param {string} username The username to search for
|
||||
* @returns {Array} [found {boolean}, ssid {string|null}, user_id {number|null}]
|
||||
*/
|
||||
findAccountByUsername(username) {
|
||||
const accounts_dir = this.getAccountStoreDirectory();
|
||||
if (this.fs.existsSync(accounts_dir)) {
|
||||
const account_dirs = this.fs.readdirSync(accounts_dir);
|
||||
for (let i = 0; i < account_dirs.length; i++) {
|
||||
const account_dir = accounts_dir + this.path.sep + account_dirs[i];
|
||||
if (this.fs.lstatSync(account_dir).isDirectory()) {
|
||||
const user_dirs = this.fs.readdirSync(account_dir);
|
||||
for (let j = 0; j < user_dirs.length; j++) {
|
||||
const user_file = account_dir + this.path.sep + user_dirs[j] + this.path.sep + `user${j}.json`;
|
||||
if (this.fs.existsSync(user_file)) {
|
||||
const user_data = JSON.parse(this.fs.readFileSync(user_file));
|
||||
if (user_data.subscriber_username.toLowerCase() === username.toLowerCase()) {
|
||||
return [true, account_dirs[i], j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return [false, null, null];
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the SSID for this session, and load a new user's session data, but only if the session
|
||||
* was initialized with a null SSID. This is primarily used for MSNTV2/Passport services
|
||||
* where the SSID is not known at the time of session initialization.
|
||||
* @param {string} ssid The new SSID to set
|
||||
* @param {number} userID The user ID to switch to after setting the SSID, defaults to 0 (primary account)
|
||||
* @param {boolean} forceSwitch If true, allows switching SSID even if one is already set (use with caution, can cause data corruption if used improperly)
|
||||
* @return {boolean} True if the SSID was set and session data loaded successfully, false otherwise
|
||||
*/
|
||||
setSSID(ssid, userID = 0, forceSwitch = false) {
|
||||
if (this.ssid !== null && !forceSwitch) return false; // SSID already set, cannot switch
|
||||
this.ssid = ssid;
|
||||
this.clearUserSessionMemory();
|
||||
this.switchUserID(userID);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the user's file store, or false if unregistered
|
||||
* @param subscriber {boolean} Returns the parent subscriber directory instead of the user's directory
|
||||
* @param {boolean} subscriber Returns the parent subscriber directory instead of the user's directory
|
||||
* @param {number|null} user_id The user ID to get the store for, or null for the current user
|
||||
* @returns {string|boolean} Absolute path to the user's file store, or false if unregistered
|
||||
*/
|
||||
getUserStoreDirectory(subscriber = false, user_id = null) {
|
||||
@@ -236,6 +338,11 @@ class WTVClientSessionData {
|
||||
return userstore + this.path.sep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a user from the account store
|
||||
* @param {number} user_id The ID of the user to remove
|
||||
* @returns {boolean} True if the user was successfully removed, false otherwise
|
||||
*/
|
||||
removeUser(user_id) {
|
||||
if (!this.isRegistered()) return false; // not registered
|
||||
if (parseInt(this.user_id) !== 0) return false; // not primary account
|
||||
@@ -252,6 +359,13 @@ class WTVClientSessionData {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* A part of the account transfer process, creates a pending transfer file in both the source and target account
|
||||
* store directories with the relevant SSID and transfer type (source or target) for each account.
|
||||
* This allows the transfer process to be completed or cancelled later, and ensures that only accounts
|
||||
* with a pending transfer can complete the transfer process.
|
||||
* @param {string} ssid
|
||||
*/
|
||||
setPendingTransfer(ssid) {
|
||||
const pending_file = this.getUserStoreDirectory(true) + this.path.sep + "pending_transfer.json";
|
||||
let ssidobj = { "ssid": ssid, "type": "source" };
|
||||
@@ -264,6 +378,10 @@ class WTVClientSessionData {
|
||||
this.fs.writeFileSync(dest_pending_file, JSON.stringify(ssidobj));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a pending account transfer, if it exists.
|
||||
* @returns {string|null} The SSID of the cancelled transfer if a pending transfer was found and cancelled, or null if no pending transfer was found
|
||||
*/
|
||||
cancelPendingTransfer() {
|
||||
const pending_file = this.getUserStoreDirectory(true) + this.path.sep + "pending_transfer.json";
|
||||
if (this.fs.existsSync(pending_file)) {
|
||||
@@ -279,6 +397,10 @@ class WTVClientSessionData {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize the transfer, completely moving all user data from the source account to the target account, and removing the pending transfer files.
|
||||
* @returns {boolean} Success of the transfer
|
||||
*/
|
||||
finalizePendingTransfer() {
|
||||
const pending_file = this.getUserStoreDirectory(true) + this.path.sep + "pending_transfer.json";
|
||||
const file = this.fs.readFileSync(pending_file)
|
||||
@@ -298,13 +420,18 @@ class WTVClientSessionData {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is a pending transfer for this account, and optionally check if the pending transfer type matches the specified dtype.
|
||||
* @param {string} dtype {source|target} If specified, only returns the SSID if the pending transfer type matches the specified dtype. If null, returns the pending transfer object with ssid and type.
|
||||
* @returns {string|object|boolean} The SSID of the pending transfer if dtype matches, the pending transfer object if dtype is null, or false if no pending transfer is found
|
||||
*/
|
||||
hasPendingTransfer(dtype = null) {
|
||||
const pending_file = this.getUserStoreDirectory(true) + this.path.sep + "pending_transfer.json";
|
||||
if (this.fs.existsSync(pending_file)) {
|
||||
const ssidobj = JSON.parse(this.fs.readFileSync(pending_file));
|
||||
console.log(ssidobj)
|
||||
if (dtype) {
|
||||
(ssidobj.type === dtype) ? ssidobj.ssid : false;
|
||||
return (ssidobj.type === dtype) ? ssidobj.ssid : false;
|
||||
}
|
||||
else {
|
||||
return ssidobj;
|
||||
@@ -347,6 +474,10 @@ class WTVClientSessionData {
|
||||
return result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has a scrapbook directory.
|
||||
* @returns {boolean} True if the scrapbook directory exists, false otherwise.
|
||||
*/
|
||||
scrapbookExists() {
|
||||
if (this.scrapbook_dir === null) {
|
||||
const userstore_dir = this.getUserStoreDirectory();
|
||||
@@ -356,6 +487,10 @@ class WTVClientSessionData {
|
||||
return this.fs.existsSync(this.scrapbook_dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a scrapbook directory for the user if it does not already exist.
|
||||
* @returns {boolean} Success of the creation
|
||||
*/
|
||||
createScrapbook() {
|
||||
if (!this.scrapbookExists()) {
|
||||
try {
|
||||
@@ -366,6 +501,10 @@ class WTVClientSessionData {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper that returns the scrapbook directory, and creates it if it doesn't exist.
|
||||
* @returns {string} The path to the scrapbook directory
|
||||
*/
|
||||
scrapbookDir() {
|
||||
if (!this.scrapbookExists()) {
|
||||
this.createScrapbook();
|
||||
@@ -373,6 +512,10 @@ class WTVClientSessionData {
|
||||
return this.scrapbook_dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* List the files in the user's scrapbook directory, sorted in ascending order, and excluding any .meta files.
|
||||
* @returns {Array} A filelist of the user's scrapbook files
|
||||
*/
|
||||
listScrapbook() {
|
||||
if (!this.scrapbookExists()) {
|
||||
this.createScrapbook();
|
||||
@@ -387,6 +530,10 @@ class WTVClientSessionData {
|
||||
return filteredFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the next available ID slot for a new scrapbook entry.
|
||||
* @returns {number} An available ID slot
|
||||
*/
|
||||
getFreeScrapbookID() {
|
||||
if (!this.scrapbookExists()) {
|
||||
this.createScrapbook();
|
||||
@@ -403,6 +550,10 @@ class WTVClientSessionData {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total size of the user's scrapbook directory.
|
||||
* @returns {number} The total size in bytes
|
||||
*/
|
||||
getScrapbookUsage() {
|
||||
if (!this.scrapbookExists()) {
|
||||
this.createScrapbook();
|
||||
@@ -420,6 +571,10 @@ class WTVClientSessionData {
|
||||
return total_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the percentage of the scrapbook storage space that is in use.
|
||||
* @returns {number} Percentage of the scrapbook storage space that is in use, out of the total allotted.
|
||||
*/
|
||||
getScrapbookUsagePercent() {
|
||||
if (!this.scrapbookExists()) {
|
||||
this.createScrapbook();
|
||||
@@ -431,6 +586,11 @@ class WTVClientSessionData {
|
||||
return Math.round(usage_percent, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a scrapbook image by its ID.
|
||||
* @param {number} id
|
||||
* @returns {Buffer|null} The image data as a Buffer, or null if the image does not exist
|
||||
*/
|
||||
getScrapbookImage(id) {
|
||||
if (!this.scrapbookExists()) {
|
||||
this.createScrapbook();
|
||||
@@ -692,14 +852,13 @@ class WTVClientSessionData {
|
||||
return CryptoJS.AES.decrypt(crypt, this.cryptoKey).toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
|
||||
|
||||
oldDecodePassword(passwd) {
|
||||
return CryptoJS.SHA512(passwd).toString(CryptoJS.enc.Base64);
|
||||
}
|
||||
|
||||
encodePassword(passwd) {
|
||||
//return CryptoJS.SHA512(passwd).toString(CryptoJS.enc.Base64);
|
||||
return this.encryptPassword(passwd);
|
||||
// SHA512 the user's password, then encrypt the hash with AES using the server's user_data_key.
|
||||
return this.encryptPassword(CryptoJS.SHA512(passwd).toString(CryptoJS.enc.Hex));
|
||||
}
|
||||
|
||||
setUserPassword(passwd) {
|
||||
@@ -729,8 +888,12 @@ class WTVClientSessionData {
|
||||
|
||||
validateUserPassword(passwd) {
|
||||
if (!this.getUserPasswordEnabled()) return true; // no password is set so always validate
|
||||
if (passwd === this.decryptPassword(this.getSessionData("subscriber_password"))) return true; // check against current encryption
|
||||
else if (this.oldDecodePassword(passwd) === this.getSessionData("subscriber_password")) {
|
||||
if (CryptoJS.SHA512(passwd).toString(CryptoJS.enc.Hex) === this.decryptPassword(this.getSessionData("subscriber_password"))) return true; // check against current encryption
|
||||
else if (passwd === this.decryptPassword(this.getSessionData("subscriber_password"))) {
|
||||
// check against the short-lived new encryption, if it matches then update to new encryption
|
||||
this.setUserPassword(passwd);
|
||||
return true;
|
||||
} else if (this.oldDecodePassword(passwd) === this.getSessionData("subscriber_password")) {
|
||||
// if password matches old hash, update to new encryption
|
||||
this.setUserPassword(passwd);
|
||||
return true;
|
||||
|
||||
@@ -70,7 +70,7 @@ class WTVFTP {
|
||||
stream.on('data', (chunk) => {
|
||||
chunks.push(chunk);
|
||||
totalsize += chunk.length;
|
||||
if (totalsize > 1024 * 1024 * 4) {
|
||||
if (totalsize > 1024 * 1024 * this.minisrv_config.services[this.service_name].max_response_size) {
|
||||
this.sendToClient(socket, { 'Status': '413 The item chosen contains too much information to be used.', 'Content-Type': 'text/plain' }, 'Item too large');
|
||||
ftpClient.end();
|
||||
return;
|
||||
|
||||
@@ -111,11 +111,24 @@ class WTVHTTP {
|
||||
}
|
||||
const req = this.proxy_agent.request(options, (res) => {
|
||||
let total_data = 0;
|
||||
let abortedBySize = false;
|
||||
const maxBytes = 1024 * 1024 * parseFloat(this.minisrv_config.services[request_type].max_response_size || 16);
|
||||
const responseContentLength = parseInt(res.headers['content-length'] || res.headers['Content-Length'] || '0', 10) || 0;
|
||||
if (responseContentLength > 0 && responseContentLength > maxBytes) {
|
||||
console.warn(` * Proxy response contains Content-Length ${responseContentLength} bytes > limit ${maxBytes} bytes, destroying...`);
|
||||
abortedBySize = true;
|
||||
res.destroy();
|
||||
const errpage = this.wtvshared.doErrorPage(400, "The item chosen is too large to be used.");
|
||||
this.sendToClient(socket, errpage[0], errpage[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
res.on('data', d => {
|
||||
if (abortedBySize) return;
|
||||
data.push(d);
|
||||
total_data += d.length;
|
||||
if (total_data > 1024 * 1024 * parseFloat(this.minisrv_config.services[request_type].max_response_size || 16)) {
|
||||
if (total_data > maxBytes) {
|
||||
abortedBySize = true;
|
||||
console.warn(` * Response data exceeded ${this.minisrv_config.services[request_type].max_response_size || 16}MB limit, destroying...`);
|
||||
res.destroy();
|
||||
const errpage = this.wtvshared.doErrorPage(400, "The item chosen is too large to be used.");
|
||||
@@ -133,6 +146,7 @@ class WTVHTTP {
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
if (abortedBySize) return;
|
||||
// For when http proxies behave correctly
|
||||
if (!this.minisrv_config.services[request_type].external_proxy_is_http1 || data.length > 0) {
|
||||
this.handleProxy(socket, request_type, request_headers, res, data);
|
||||
|
||||
@@ -21,6 +21,7 @@ class WTVShared {
|
||||
process = require('process');
|
||||
shenanigans = null;
|
||||
appdir = this.path.resolve(__dirname + this.path.sep + ".." + this.path.sep + "..");
|
||||
tokens = {};
|
||||
|
||||
minisrv_config = [];
|
||||
|
||||
@@ -629,6 +630,97 @@ class WTVShared {
|
||||
return this.fixPathSlashes(check_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the session data (BoxID, UserID, creation time, expiry) associated with a given token, also deletes the token if expired.
|
||||
* @param {string} token
|
||||
* @return {object|null} { boxID, userId, created, expires } for the token, or null if token is invalid/expired
|
||||
*/
|
||||
getTokenData(token) {
|
||||
const session = this.tokens[token];
|
||||
if (session && session.expires > Date.now()) {
|
||||
return { boxID: session.boxID, userId: session.userId, created: session.timestamp, expires: session.expires };
|
||||
}
|
||||
this.deleteToken(token);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a token is valid (exists and not expired)
|
||||
* @param {string} token The token to check
|
||||
* @returns {boolean} true if valid, false if not
|
||||
*/
|
||||
isTokenValid(token) {
|
||||
const session = this.tokens[token];
|
||||
if (session && session.expires > Date.now()) {
|
||||
return true;
|
||||
}
|
||||
this.deleteToken(token);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a token from the token store.
|
||||
* @param {string} token The token to delete
|
||||
*/
|
||||
deleteToken(token) {
|
||||
delete this.tokens[token];
|
||||
this.saveTokens();
|
||||
}
|
||||
|
||||
mkdirRecursive(dirPath) {
|
||||
if (!this.path.isAbsolute(dirPath)) {
|
||||
dirPath = this.getAbsolutePath(this.parentDirectory + this.path.sep + dirPath);
|
||||
}
|
||||
const parts = dirPath.split(this.path.sep);
|
||||
let currentPath = '';
|
||||
for (const part of parts) {
|
||||
if (part) {
|
||||
if (currentPath === '') {
|
||||
currentPath = part;
|
||||
} else {
|
||||
currentPath += this.path.sep + part;
|
||||
}
|
||||
if (!this.fs.existsSync(currentPath)) {
|
||||
try {
|
||||
this.fs.mkdirSync(currentPath);
|
||||
} catch (e) {
|
||||
if (e.code !== 'EEXIST') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveTokens() {
|
||||
const session_store = this.minisrv_config.config.SessionStore + this.path.sep + "msntv2";
|
||||
if (!this.fs.existsSync(session_store)) {
|
||||
this.mkdirRecursive(session_store);
|
||||
}
|
||||
const tokenFile = this.getAbsolutePath(this.path.join(session_store, `tokens.json`));
|
||||
this.fs.writeFile(tokenFile, JSON.stringify(this.tokens), (err) => {
|
||||
if (err) {
|
||||
console.error('[WTV-MSNTV2] Error writing token file:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a token with its associated BoxID and UserID.
|
||||
* @param {string} token
|
||||
* @param {string} boxID
|
||||
* @param {number} userId
|
||||
* @param {string|null} expiresTime - Optional expiration time for the token, otherwise uses server config defaults
|
||||
*/
|
||||
storeToken(token, boxID, userId, expiresTime = null) {
|
||||
delete this.tokens[token]; // ensure any existing token with the same value is removed before storing new data
|
||||
this.tokens[token] = { boxID, userId, timestamp: Date.now(), expires: expiresTime ? new Date(expiresTime).getTime() : Date.now() + (this.minisrv_config.services[this.service_name]?.token_expiry || 3600) * 1000 }; // 1 hour expiry
|
||||
this.saveTokens();
|
||||
this.debug(" * MSNTV2 stored token for BoxID %s (UserID: %s), token expires in %d seconds", boxID, userId, (this.tokens[token].expires - Date.now()) / 1000);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Detects if the client is in MiniBrowser mode
|
||||
* @param {object} ssid_session
|
||||
@@ -1130,6 +1222,7 @@ class WTVShared {
|
||||
// DON'T USE THIS
|
||||
// Saved for reference until I come up with a better way
|
||||
// If used, this will exceed the stack limit over time
|
||||
/*
|
||||
unloadModule(moduleName) {
|
||||
// Prevent usage
|
||||
return;
|
||||
@@ -1141,7 +1234,8 @@ class WTVShared {
|
||||
delete require.cache[resolvedPath];
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns an absolute path without an trailing path seperator
|
||||
* @param {string} path
|
||||
|
||||
@@ -48,8 +48,7 @@
|
||||
"show_diskmap": false, // Useful for debugging custom Diskmaps
|
||||
"unauthorized_url": "wtv-1800:/unauthorized?", // Where to send unauthorized users
|
||||
"enable_port_isolation": true, // Only respond to services on their correct ports
|
||||
"domain_name": "wtv.zefie.com", // For usenet and future stuff, no need to change just yet,
|
||||
"max_post_length": 20, // in megabytes
|
||||
"domain_name": "minisrv.local", // For personalizing mail and MSNTV2
|
||||
"require_valid_ssid": false, // require a valid SSID (with valid CRC)
|
||||
"user_accounts": { // user account settings
|
||||
"max_users_per_account": 6, // Max total users (including primary) per account
|
||||
@@ -352,6 +351,7 @@
|
||||
"ftp": {
|
||||
"port": 1650,
|
||||
"connections": 3,
|
||||
"max_response_size": 8, // Megabytes
|
||||
"handler_module": "WTVFTP",
|
||||
"handler_extra_vars": ["wtvmime"]
|
||||
},
|
||||
@@ -522,9 +522,15 @@
|
||||
],
|
||||
"handler_extra_vars": ["runScriptInVM", "handlePHP", "handleCGI", "ssid_sessions", "WTVClientSessionData", "socket_sessions"],
|
||||
"show_verbose_errors": false,
|
||||
"ssl": {
|
||||
"cert": "%ServiceDeps%/msntv2/minisrv.crt",
|
||||
"key": "%ServiceDeps%/msntv2/minisrv.key"
|
||||
},
|
||||
"modules": [
|
||||
"WTVRegister"
|
||||
]
|
||||
"WTVRegister",
|
||||
"WTVNews"
|
||||
],
|
||||
"usenet_service": "wtv-news"
|
||||
}
|
||||
},
|
||||
"favorites": {
|
||||
|
||||
26
zefie_wtvp_minisrv/includes/depreciated.json
Normal file
@@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"id": "session_data.hasCap",
|
||||
"pattern": "session\\_data\\.hasCap\\s*\\(",
|
||||
"flags": "g",
|
||||
"message": "session_data.hasCap() is deprecated and will be removed",
|
||||
"removeVersion": "0.9.80",
|
||||
"replacement": "Use session_data.capabilities.get() instead"
|
||||
},
|
||||
{
|
||||
"id": "getServiceString",
|
||||
"pattern": "(?<!wtvshared\\.)getServiceString\\s*\\(",
|
||||
"flags": "g",
|
||||
"message": "getServiceString() is deprecated and will be removed",
|
||||
"removeVersion": "0.9.80",
|
||||
"replacement": "Use wtvshared.getServiceString() instead"
|
||||
},
|
||||
{
|
||||
"id": "moveArrayKey",
|
||||
"pattern": "(?<!wtvshared\\.)moveArrayKey\\s*\\(",
|
||||
"flags": "g",
|
||||
"message": "moveArrayKey() is deprecated and will be removed",
|
||||
"removeVersion": "0.9.80",
|
||||
"replacement": "Use wtvshared.moveArrayKey() instead"
|
||||
}
|
||||
]
|
||||
4
zefie_wtvp_minisrv/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "zefie_wtvp_minisrv",
|
||||
"version": "0.9.73",
|
||||
"version": "0.9.74",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "zefie_wtvp_minisrv",
|
||||
"version": "0.9.73",
|
||||
"version": "0.9.74",
|
||||
"license": "GPL3",
|
||||
"dependencies": {
|
||||
"@serialport/parser-readline": "^13.0.0",
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "zefie_wtvp_minisrv",
|
||||
"version": "0.9.73",
|
||||
"version": "0.9.74",
|
||||
"description": "WebTV Service (WTVP) Emulation Server",
|
||||
"main": "app.js",
|
||||
"homepage": "https://github.com/zefie/zefie_wtvp_minisrv",
|
||||
"license": "GPL3",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"irc": "node irconly.js",
|
||||
"test": "node test.js",
|
||||
"debug": "cross-env DEBUG=* node app.js",
|
||||
"modem-proxy": "node modem_proxy.js"
|
||||
"modem-proxy": "node modem_proxy.js",
|
||||
"scan-service-deprecations": "node tools/scan_service_vault_deprecations.js"
|
||||
},
|
||||
"author": {
|
||||
"name": "zefie",
|
||||
|
||||
215
zefie_wtvp_minisrv/tools/configurator.js
Normal file
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const readline = require('readline');
|
||||
|
||||
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||
const USER_CONFIG_PATH = path.join(ROOT_DIR, 'user_config.json');
|
||||
|
||||
function printUsage() {
|
||||
console.log('Usage:');
|
||||
console.log(' node tools/configurator.js <dot.path.key> <value> [--overwrite]');
|
||||
console.log(' node tools/configurator.js <dot.path.key> --delete [--overwrite]');
|
||||
console.log('Examples:');
|
||||
console.log(' node tools/configurator.js config.keys.user_data_key mynewkey');
|
||||
console.log(' node tools/configurator.js config.passwords.enabled true --overwrite');
|
||||
console.log(' node tools/configurator.js config.fake.newkey --delete --overwrite');
|
||||
}
|
||||
|
||||
function parseJsonWithComments(json) {
|
||||
if (typeof json !== 'string') json = json ? json.toString() : '';
|
||||
let result = '';
|
||||
let i = 0;
|
||||
let isString = false;
|
||||
let isEscape = false;
|
||||
let isBlockComment = false;
|
||||
let isLineComment = false;
|
||||
|
||||
while (i < json.length) {
|
||||
const char = json[i];
|
||||
const nextChar = json[i + 1];
|
||||
|
||||
if (!isString && !isEscape && char === '/' && nextChar === '*') {
|
||||
isBlockComment = true;
|
||||
i += 1;
|
||||
} else if (isBlockComment && char === '*' && nextChar === '/') {
|
||||
isBlockComment = false;
|
||||
i += 1;
|
||||
} else if (!isString && !isEscape && char === '/' && nextChar === '/') {
|
||||
isLineComment = true;
|
||||
i += 1;
|
||||
} else if (isLineComment && (char === '\n' || char === '\r')) {
|
||||
isLineComment = false;
|
||||
} else if (!isBlockComment && !isLineComment) {
|
||||
if (char === '"' && !isEscape) {
|
||||
isString = !isString;
|
||||
}
|
||||
isEscape = char === '\\' && !isEscape;
|
||||
result += char;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return JSON.parse(result);
|
||||
}
|
||||
|
||||
function parseInputValue(raw) {
|
||||
const trimmed = raw.trim();
|
||||
const isLiteral = /^(?:-?\d+(?:\.\d+)?|true|false|null)$/i.test(trimmed);
|
||||
const startsLikeJson = trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('"');
|
||||
|
||||
if (!isLiteral && !startsLikeJson) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(trimmed);
|
||||
} catch (_error) {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
function readUserConfig(filePath) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
fs.writeFileSync(filePath, '{}\n', 'utf8');
|
||||
return {};
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
if (!content.trim()) return {};
|
||||
|
||||
try {
|
||||
return parseJsonWithComments(content);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse user_config.json: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function askYesNo(question) {
|
||||
return new Promise((resolve) => {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
rl.question(question, (answer) => {
|
||||
rl.close();
|
||||
const normalized = String(answer || '').trim().toLowerCase();
|
||||
resolve(normalized === 'y' || normalized === 'yes');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sortKeys(obj) {
|
||||
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj;
|
||||
return Object.keys(obj).sort().reduce((sorted, key) => {
|
||||
sorted[key] = sortKeys(obj[key]);
|
||||
return sorted;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function getPathInfo(rootObj, parts) {
|
||||
let current = rootObj;
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i += 1) {
|
||||
const part = parts[i];
|
||||
|
||||
if (!current[part] || typeof current[part] !== 'object' || Array.isArray(current[part])) {
|
||||
current[part] = {};
|
||||
}
|
||||
|
||||
current = current[part];
|
||||
}
|
||||
|
||||
const leafKey = parts[parts.length - 1];
|
||||
const exists = Object.prototype.hasOwnProperty.call(current, leafKey);
|
||||
const oldValue = exists ? current[leafKey] : undefined;
|
||||
|
||||
return {
|
||||
parent: current,
|
||||
leafKey,
|
||||
exists,
|
||||
oldValue
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const overwrite = args.includes('--overwrite');
|
||||
const deleteMode = args.includes('--delete');
|
||||
const positional = args.filter((arg) => arg !== '--overwrite' && arg !== '--delete');
|
||||
|
||||
if ((deleteMode && positional.length < 1) || (!deleteMode && positional.length < 2)) {
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const dotPath = positional[0];
|
||||
const valueRaw = deleteMode ? null : positional.slice(1).join(' ');
|
||||
const pathParts = dotPath.split('.').filter(Boolean);
|
||||
|
||||
if (pathParts.length === 0) {
|
||||
throw new Error('Invalid path. Provide a dot-separated path like config.keys.user_data_key');
|
||||
}
|
||||
|
||||
if (deleteMode && pathParts.length === 1) {
|
||||
const rootKey = pathParts[0];
|
||||
if (rootKey === 'config' || rootKey === 'services') {
|
||||
throw new Error('Deletion of root keys "config" and "services" is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
const config = readUserConfig(USER_CONFIG_PATH);
|
||||
|
||||
if (deleteMode) {
|
||||
const pathInfo = getPathInfo(config, pathParts);
|
||||
if (!pathInfo.exists) {
|
||||
console.log('Key does not exist. No changes made.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!overwrite) {
|
||||
const question = `Delete ${dotPath}? [y/N] `;
|
||||
const approved = await askYesNo(question);
|
||||
if (!approved) {
|
||||
console.log('No changes made.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
delete pathInfo.parent[pathInfo.leafKey];
|
||||
fs.writeFileSync(USER_CONFIG_PATH, `${JSON.stringify(sortKeys(config), null, 2)}\n`, 'utf8');
|
||||
console.log(`Deleted ${dotPath}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const pathInfo = getPathInfo(config, pathParts);
|
||||
const newValue = parseInputValue(valueRaw);
|
||||
|
||||
if (pathInfo.exists && !overwrite) {
|
||||
const question = `Key ${dotPath} already exists. Overwrite? [y/N] `;
|
||||
const approved = await askYesNo(question);
|
||||
|
||||
if (!approved) {
|
||||
console.log('No changes made.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
pathInfo.parent[pathInfo.leafKey] = newValue;
|
||||
|
||||
fs.writeFileSync(USER_CONFIG_PATH, `${JSON.stringify(sortKeys(config), null, 2)}\n`, 'utf8');
|
||||
|
||||
if (pathInfo.exists) {
|
||||
console.log(`Updated ${dotPath}.`);
|
||||
} else {
|
||||
console.log(`Created ${dotPath}.`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.message || error);
|
||||
process.exit(1);
|
||||
});
|
||||
430
zefie_wtvp_minisrv/tools/encode_webtv_mpeg.js
Normal file
@@ -0,0 +1,430 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* WebTV MPEG-1 PS Encoder
|
||||
*
|
||||
* This tool is incomplete, and may not generate correct WebTV MPEG yet
|
||||
*
|
||||
* Two-pass pipeline:
|
||||
* 1. ffmpeg encodes input to MPEG-1 PS (codec settings only)
|
||||
* 2. ES extracted via structure-aware pack walk (never naive payload scan)
|
||||
* 3. Video ES patched: fr_code=10, constrained_parameters_flag=1
|
||||
* 4. Output rebuilt as strict 2048-byte packs matching attract.mpg structure:
|
||||
* - One PES per pack: BA(12) + PES_hdr(6) + ff_0f(2) + data(2028) = 2048
|
||||
* - All PES optional headers: ff 0f (no timestamps)
|
||||
* - 3 audio pre-fill packs, then 1 audio per ~7 video packs
|
||||
* - No BB system header packet
|
||||
*
|
||||
* Usage: node encode_webtv_mpeg.js <input_video> <output.mpg> [duration_seconds]
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const PACK_SIZE = 2048;
|
||||
// MPEG-1 pack header variants used by known working files.
|
||||
const BA_HDR_MPEG1 = Buffer.from('000001ba2100010001802711', 'hex');
|
||||
const BA_HDR_ATTRACT = Buffer.from('000001ba0000025447474747', 'hex');
|
||||
const MP2_BITRATE_MPEG1_L2 = [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0];
|
||||
const MP2_SR_MPEG1 = [44100, 48000, 32000, 0];
|
||||
// Usable data per pack: 2048 - BA(12) - PES_fixed_hdr(6) - ff_0f(2) = 2028
|
||||
const DATA_PER_PACK = PACK_SIZE - BA_HDR_MPEG1.length - 6 - 2; // 2028
|
||||
|
||||
|
||||
function runCmd(args, description) {
|
||||
console.log(`[*] ${description}...`);
|
||||
try {
|
||||
execFileSync(args[0], args.slice(1), { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
console.log('[+] OK');
|
||||
return true;
|
||||
} catch (e) {
|
||||
const stderr = e.stderr ? e.stderr.toString().slice(0, 500) : e.message;
|
||||
console.error(`[!] Failed: ${stderr}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extract video (E0) and audio (C0) elementary streams.
|
||||
* Uses structure-aware pack walk — never scans payload bytes for start codes.
|
||||
*/
|
||||
function extractES(mpgPath) {
|
||||
console.log('[*] Extracting elementary streams (structure-aware)...');
|
||||
|
||||
const d = fs.readFileSync(mpgPath);
|
||||
const videoChunks = [];
|
||||
const audioChunks = [];
|
||||
|
||||
let i = 0;
|
||||
while (i < d.length - 4) {
|
||||
if (d[i] !== 0x00 || d[i+1] !== 0x00 || d[i+2] !== 0x01) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
const sid = d[i+3];
|
||||
|
||||
if (sid === 0xBA) {
|
||||
const mpeg2 = (d[i+4] & 0xC0) === 0x40;
|
||||
const hlen = mpeg2 ? 14 + (d[i+13] & 0x07) : 12;
|
||||
i += hlen;
|
||||
|
||||
} else if (sid === 0xE0 || sid === 0xC0) {
|
||||
const pktLen = (d[i+4] << 8) | d[i+5];
|
||||
const end = i + 6 + pktLen;
|
||||
let j = i + 6;
|
||||
|
||||
// Skip stuffing bytes (0xFF)
|
||||
while (j < end && d[j] === 0xFF) j++;
|
||||
|
||||
if (j < end) {
|
||||
let h = d[j];
|
||||
// Skip STD buffer (0x4x marker, 2 bytes)
|
||||
if ((h & 0xC0) === 0x40) {
|
||||
j += 2;
|
||||
h = j < end ? d[j] : 0;
|
||||
}
|
||||
// Skip PTS-only (5 bytes) or PTS+DTS (10 bytes) or no-ts (1 byte)
|
||||
if ((h & 0xF0) === 0x20) {
|
||||
j += 5;
|
||||
} else if ((h & 0xF0) === 0x30) {
|
||||
j += 10;
|
||||
} else if (h === 0x0F) {
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (j <= end) {
|
||||
const payload = d.slice(j, end);
|
||||
if (sid === 0xE0) videoChunks.push(payload);
|
||||
else audioChunks.push(payload);
|
||||
}
|
||||
i = end;
|
||||
|
||||
} else if (sid === 0xB9) {
|
||||
break; // PS end code
|
||||
|
||||
} else if (sid >= 0xBB && sid <= 0xBF) {
|
||||
const pktLen = (d[i+4] << 8) | d[i+5];
|
||||
i += 6 + pktLen;
|
||||
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
const videoES = Buffer.concat(videoChunks);
|
||||
const audioES = Buffer.concat(audioChunks);
|
||||
console.log(`[+] Extracted: video=${videoES.length} bytes, audio=${audioES.length} bytes`);
|
||||
return { videoES, audioES };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Patch fr_code=10 and constrained_parameters_flag=1 in video ES buffer in-place.
|
||||
*/
|
||||
function patchSequenceHeaders(videoES) {
|
||||
let i = 0;
|
||||
let n = 0;
|
||||
while (i < videoES.length - 11) {
|
||||
// Find 00 00 01 B3
|
||||
if (videoES[i] === 0x00 && videoES[i+1] === 0x00 &&
|
||||
videoES[i+2] === 0x01 && videoES[i+3] === 0xB3) {
|
||||
videoES[i+7] = (videoES[i+7] & 0xF0) | 0x0A; // fr_code = 10
|
||||
videoES[i+11] |= 0x04; // constrained_parameters_flag
|
||||
n++;
|
||||
i += 4;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
console.log(`[+] Patched ${n} sequence header(s): fr_code=10, constrained=1`);
|
||||
return n;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Normalize MP2 frame headers for maximum WebTV compatibility.
|
||||
* Clears the "original" bit (header byte3 bit2), matching attract.mpg (fffd50c0).
|
||||
*/
|
||||
function normalizeMP2Headers(audioES) {
|
||||
let i = 0;
|
||||
let patched = 0;
|
||||
|
||||
while (i < audioES.length - 4) {
|
||||
if (audioES[i] === 0xFF && (audioES[i + 1] & 0xE0) === 0xE0) {
|
||||
const b1 = audioES[i + 1];
|
||||
const b2 = audioES[i + 2];
|
||||
|
||||
const version = (b1 >> 3) & 0x03; // 3 => MPEG-1
|
||||
const layer = (b1 >> 1) & 0x03; // 2 => Layer II
|
||||
const brIdx = (b2 >> 4) & 0x0F;
|
||||
const srIdx = (b2 >> 2) & 0x03;
|
||||
const pad = (b2 >> 1) & 0x01;
|
||||
|
||||
if (version === 3 && layer === 2 && MP2_BITRATE_MPEG1_L2[brIdx] && MP2_SR_MPEG1[srIdx]) {
|
||||
const bitrate = MP2_BITRATE_MPEG1_L2[brIdx] * 1000;
|
||||
const sampleRate = MP2_SR_MPEG1[srIdx];
|
||||
const frameLen = Math.floor((144 * bitrate) / sampleRate) + pad;
|
||||
|
||||
// Clear "original" bit (bit2 in 4th header byte): c4 -> c0
|
||||
audioES[i + 3] &= 0xFB;
|
||||
patched++;
|
||||
|
||||
i += frameLen;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
console.log(`[+] Normalized MP2 headers: patched ${patched} frame(s)`);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build MPEG-1 PS with strict 2048-byte packs matching attract.mpg structure:
|
||||
* - One PES per pack
|
||||
* - All PES optional headers: ff 0f (no timestamps)
|
||||
* - 3 audio pre-fill packs, then 1 audio per ~7 video packs
|
||||
* - No BB system header
|
||||
*/
|
||||
function buildWebTVPS(videoES, audioES, outputPath, audioIntervalOverride, baHeaderMode) {
|
||||
console.log('[*] Building WebTV MPEG-1 PS...');
|
||||
|
||||
const P = DATA_PER_PACK; // 2028
|
||||
|
||||
const baHdr = baHeaderMode === 'attract' ? BA_HDR_ATTRACT : BA_HDR_MPEG1;
|
||||
|
||||
function makePack(streamId, data) {
|
||||
// Pad or trim data to exactly P bytes
|
||||
const payload = Buffer.alloc(P);
|
||||
data.copy(payload, 0, 0, Math.min(data.length, P));
|
||||
|
||||
const pktLen = P + 2; // ff 0f(2) + data(P) = 2030
|
||||
const pesHdr = Buffer.alloc(8);
|
||||
pesHdr[0] = 0x00; pesHdr[1] = 0x00; pesHdr[2] = 0x01; pesHdr[3] = streamId;
|
||||
pesHdr[4] = (pktLen >> 8) & 0xFF;
|
||||
pesHdr[5] = pktLen & 0xFF;
|
||||
pesHdr[6] = 0xFF; // ff 0f
|
||||
pesHdr[7] = 0x0F;
|
||||
|
||||
return Buffer.concat([baHdr, pesHdr, payload]); // 12 + 6 + 2 + 2028 = 2048
|
||||
}
|
||||
|
||||
// Split ES into P-byte chunks
|
||||
const vChunks = [];
|
||||
for (let i = 0; i < videoES.length; i += P) vChunks.push(videoES.slice(i, i + P));
|
||||
const aChunks = [];
|
||||
for (let i = 0; i < audioES.length; i += P) aChunks.push(audioES.slice(i, i + P));
|
||||
|
||||
if (!vChunks.length || !aChunks.length) {
|
||||
console.error('[!] Empty video or audio stream');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use natural A/V ratio — matches actual bitrate split in the encoded file.
|
||||
// attract.mpg uses 7 because its video bitrate is ~7x its audio bitrate.
|
||||
// Our encoded video is lower bitrate so the natural ratio is ~3.
|
||||
const naturalInterval = Math.max(1, Math.round(vChunks.length / aChunks.length));
|
||||
const audioInterval = Number.isFinite(audioIntervalOverride) && audioIntervalOverride > 0
|
||||
? Math.floor(audioIntervalOverride)
|
||||
: naturalInterval;
|
||||
console.log(`[*] ${vChunks.length} video chunks, ${aChunks.length} audio chunks, ` +
|
||||
`1 audio per ~${audioInterval} video`);
|
||||
|
||||
const packs = [];
|
||||
let aIdx = 0;
|
||||
|
||||
// Pre-fill: 3 audio packs to prime the WebTV audio buffer (matches attract.mpg)
|
||||
const preFill = Math.min(3, aChunks.length);
|
||||
for (let k = 0; k < preFill; k++, aIdx++) {
|
||||
packs.push(makePack(0xC0, aChunks[aIdx]));
|
||||
}
|
||||
|
||||
// Simple fixed-interval interleave: emit audioInterval video packs, then 1 audio pack
|
||||
let vIdx = 0;
|
||||
while (vIdx < vChunks.length || aIdx < aChunks.length) {
|
||||
for (let k = 0; k < audioInterval && vIdx < vChunks.length; k++) {
|
||||
packs.push(makePack(0xE0, vChunks[vIdx++]));
|
||||
}
|
||||
if (aIdx < aChunks.length) {
|
||||
packs.push(makePack(0xC0, aChunks[aIdx++]));
|
||||
}
|
||||
}
|
||||
|
||||
const endCode = Buffer.from([0x00, 0x00, 0x01, 0xB9]);
|
||||
const output = Buffer.concat([...packs, endCode]);
|
||||
|
||||
fs.writeFileSync(outputPath, output);
|
||||
console.log(`[+] Wrote ${packs.length} packs (${output.length} bytes)`);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function verifyFile(mpgPath) {
|
||||
console.log('[*] Verifying file structure...');
|
||||
try {
|
||||
const result = execFileSync('ffprobe', [
|
||||
'-v', 'error',
|
||||
'-show_entries', 'stream=codec_name,codec_type',
|
||||
'-of', 'default=noprint_wrappers=1',
|
||||
mpgPath
|
||||
], { encoding: 'utf8' });
|
||||
if (result.includes('mpeg1video') && result.includes('mp2')) {
|
||||
console.log('[+] Valid: mpeg1video + mp2');
|
||||
return true;
|
||||
}
|
||||
console.error('[!] Missing video or audio stream');
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.error(`[!] ffprobe failed: ${e.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function checkPacks(mpgPath) {
|
||||
console.log('[*] Checking pack structure...');
|
||||
const d = fs.readFileSync(mpgPath);
|
||||
const baPos = [];
|
||||
for (let i = 0; i < d.length - 3; i++) {
|
||||
if (d[i] === 0x00 && d[i+1] === 0x00 && d[i+2] === 0x01 && d[i+3] === 0xBA) {
|
||||
baPos.push(i);
|
||||
i += 3;
|
||||
}
|
||||
}
|
||||
if (baPos.length < 2) {
|
||||
console.error('[!] Less than 2 BA packs found');
|
||||
return false;
|
||||
}
|
||||
const strides = new Set();
|
||||
for (let i = 0; i < baPos.length - 1; i++) {
|
||||
strides.add(baPos[i+1] - baPos[i]);
|
||||
}
|
||||
if (strides.size === 1 && strides.has(2048)) {
|
||||
console.log(`[+] Perfect: all ${baPos.length} packs are 2048 bytes`);
|
||||
return true;
|
||||
}
|
||||
console.log(`[!] Pack strides vary: ${[...strides].sort((a, b) => a - b).join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encode video to WebTV-compatible MPEG-1 PS.
|
||||
*
|
||||
* @param {string} inputFile Any video file ffmpeg can read
|
||||
* @param {string} outputFile Output .mpg path
|
||||
* @param {number|null} duration Optional clip length in seconds
|
||||
*/
|
||||
function encodeWebTV(inputFile, outputFile, duration, audioIntervalOverride, baHeaderMode, audioEncoder, audioESOverridePath) {
|
||||
const tmpFile = outputFile.replace(/(\.[^.]+)$/, '_raw$1');
|
||||
|
||||
// Step 1: Encode with ffmpeg (MPEG-1 video + MP2 audio, raw mux)
|
||||
const cmd = ['ffmpeg', '-y', '-i', inputFile];
|
||||
if (duration != null) cmd.push('-t', String(duration));
|
||||
cmd.push(
|
||||
'-vf', 'fps=15,scale=272:208,setsar=1',
|
||||
'-c:v', 'mpeg1video',
|
||||
'-b:v', '500k',
|
||||
'-maxrate', '700k',
|
||||
'-bufsize', '1024k',
|
||||
'-g', '15',
|
||||
'-bf', '2',
|
||||
'-c:a', audioEncoder,
|
||||
'-ar', '44100',
|
||||
'-ac', '1',
|
||||
'-b:a', '80k',
|
||||
'-strict', 'unofficial',
|
||||
'-f', 'mpeg',
|
||||
'-muxrate', '2000k',
|
||||
tmpFile
|
||||
);
|
||||
if (!runCmd(cmd, 'Encoding with ffmpeg (MPEG-1 PS)')) return false;
|
||||
|
||||
// Step 2: Extract ES using structure-aware pack walk
|
||||
const { videoES, audioES } = extractES(tmpFile);
|
||||
if (!videoES.length || !audioES.length) {
|
||||
console.error('[!] Failed to extract elementary streams');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 3: Patch video sequence headers (fr_code=10, constrained=1)
|
||||
const videoMut = Buffer.from(videoES);
|
||||
patchSequenceHeaders(videoMut);
|
||||
|
||||
let audioSource = audioES;
|
||||
if (audioESOverridePath) {
|
||||
if (!fs.existsSync(audioESOverridePath)) {
|
||||
console.error(`[!] Audio ES override not found: ${audioESOverridePath}`);
|
||||
return false;
|
||||
}
|
||||
audioSource = fs.readFileSync(audioESOverridePath);
|
||||
console.log(`[+] Using external audio ES override: ${audioESOverridePath} (${audioSource.length} bytes)`);
|
||||
}
|
||||
|
||||
const audioMut = Buffer.from(audioSource);
|
||||
normalizeMP2Headers(audioMut);
|
||||
|
||||
// Step 4: Rebuild as proper WebTV PS
|
||||
if (!buildWebTVPS(videoMut, audioMut, outputFile, audioIntervalOverride, baHeaderMode)) return false;
|
||||
|
||||
// Step 5: Verify
|
||||
if (!verifyFile(outputFile)) return false;
|
||||
checkPacks(outputFile);
|
||||
|
||||
// Cleanup temp file
|
||||
try { fs.unlinkSync(tmpFile); } catch (_) {}
|
||||
|
||||
console.log(`\n[+] Successfully encoded: ${outputFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// --- CLI entry point ---
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length < 2) {
|
||||
const script = path.basename(process.argv[1]);
|
||||
console.error(`Usage: node ${script} <input_video> <output.mpg> [duration_seconds]`);
|
||||
console.error(`Example: node ${script} myvideo.mp4 webtv.mpg 15`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let audioIntervalOverride = null;
|
||||
let baHeaderMode = 'mpeg1';
|
||||
let audioEncoder = 'mp2fixed';
|
||||
let audioESOverridePath = null;
|
||||
const nonFlagArgs = [];
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--audio-interval' && i + 1 < args.length) {
|
||||
audioIntervalOverride = parseInt(args[i + 1], 10);
|
||||
i++;
|
||||
} else if (args[i] === '--ba-header' && i + 1 < args.length) {
|
||||
baHeaderMode = String(args[i + 1]).toLowerCase() === 'attract' ? 'attract' : 'mpeg1';
|
||||
i++;
|
||||
} else if (args[i] === '--audio-encoder' && i + 1 < args.length) {
|
||||
const v = String(args[i + 1]).toLowerCase();
|
||||
audioEncoder = (v === 'mp2fixed') ? 'mp2fixed' : 'mp2';
|
||||
i++;
|
||||
} else if (args[i] === '--audio-es' && i + 1 < args.length) {
|
||||
audioESOverridePath = args[i + 1];
|
||||
i++;
|
||||
} else {
|
||||
nonFlagArgs.push(args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const [inputFile, outputFile, durationArg] = nonFlagArgs;
|
||||
const duration = durationArg != null ? parseFloat(durationArg) : null;
|
||||
|
||||
if (!fs.existsSync(inputFile)) {
|
||||
console.error(`[!] Input file not found: ${inputFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!encodeWebTV(inputFile, outputFile, duration, audioIntervalOverride, baHeaderMode, audioEncoder, audioESOverridePath)) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -7,12 +7,30 @@ const forge = require('node-forge');
|
||||
const workspaceRoot = __dirname;
|
||||
const httpsDir = path.join(workspaceRoot, '..', 'includes', 'ServiceDeps', 'https');
|
||||
const msnDir = path.join(workspaceRoot, '..', 'includes', 'ServiceDeps', 'msntv2');
|
||||
const domainsFile = path.join(msnDir, 'msn_domains.txt');
|
||||
|
||||
const defaultCaCertPath = path.join(msnDir, 'msntv2.crt');
|
||||
const defaultCaKeyPath = path.join(msnDir, 'msntv2.key');
|
||||
const defaultOutCertPath = path.join(msnDir, 'msn_domains.crt');
|
||||
const defaultOutKeyPath = path.join(msnDir, 'msn_domains.key');
|
||||
const domains = [
|
||||
"headwaiter.trusted.msntv.msn.com",
|
||||
"sg1.trusted.msntv.msn.com",
|
||||
"sg2.trusted.msntv.msn.com",
|
||||
"sg3.trusted.msntv.msn.com",
|
||||
"sg4.trusted.msntv.msn.com",
|
||||
"msntv.msn.com",
|
||||
"mail.services.live.com",
|
||||
"login.live.com",
|
||||
"poptimize.msn.com",
|
||||
"favorites.msn.com",
|
||||
"messenger.msn.com",
|
||||
"livefilestore.com",
|
||||
"users.storage.live.com",
|
||||
"g.msn.com",
|
||||
"msnialogin.passport.com",
|
||||
"minisrv.local"
|
||||
]
|
||||
|
||||
const defaultCaCertPath = path.join(msnDir, 'emac.crt');
|
||||
const defaultCaKeyPath = path.join(msnDir, 'emac.key');
|
||||
const defaultOutCertPath = path.join(msnDir, 'minisrv.crt');
|
||||
const defaultOutKeyPath = path.join(msnDir, 'minisrv.key');
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = {};
|
||||
@@ -31,42 +49,6 @@ function parseArgs(argv) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractDomainsFromRedirectMap(text) {
|
||||
const found = [];
|
||||
const seen = new Set();
|
||||
const re = /"([A-Za-z0-9.-]+\.)"\s*:\s*self\.redirect_ip/g;
|
||||
let match;
|
||||
while ((match = re.exec(text))) {
|
||||
const clean = match[1].replace(/\.$/, '').toLowerCase();
|
||||
if (!seen.has(clean)) {
|
||||
seen.add(clean);
|
||||
found.push(clean);
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function loadDomains(args) {
|
||||
if (args['from-map-file']) {
|
||||
const mapText = fs.readFileSync(path.resolve(workspaceRoot, args['from-map-file']), 'utf8');
|
||||
const domains = extractDomainsFromRedirectMap(mapText);
|
||||
if (!domains.length) {
|
||||
throw new Error('No domains were extracted from --from-map-file.');
|
||||
}
|
||||
return domains;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(domainsFile)) {
|
||||
throw new Error('Domain file not found: ' + domainsFile);
|
||||
}
|
||||
|
||||
const domains = fs.readFileSync(domainsFile, 'utf8')
|
||||
.split(/\r?\n/)
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter((s) => s && !s.startsWith('#'));
|
||||
|
||||
return Array.from(new Set(domains));
|
||||
}
|
||||
|
||||
function loadPemOrThrow(filePath, label) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
@@ -99,9 +81,8 @@ function generateCert({ domains, caCertPem, caKeyPem, outCertPath, outKeyPath, y
|
||||
cert.publicKey = keys.publicKey;
|
||||
cert.serialNumber = forge.util.bytesToHex(forge.random.getBytesSync(16));
|
||||
|
||||
const now = new Date();
|
||||
cert.validity.notBefore = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
cert.validity.notAfter = new Date(now.getTime() + years * 365 * 24 * 60 * 60 * 1000);
|
||||
cert.validity.notBefore = new Date('2000-01-01T12:00:00Z');
|
||||
cert.validity.notAfter = new Date('2099-12-31T23:59:59Z');
|
||||
|
||||
const cn = domains[0] || 'headwaiter.trusted.msntv.msn.com';
|
||||
cert.setSubject([
|
||||
@@ -141,7 +122,6 @@ function main() {
|
||||
const years = Number(args.years || 15);
|
||||
const sig = String(args.sig || 'sha1');
|
||||
|
||||
const domains = loadDomains(args);
|
||||
const caCertPem = loadPemOrThrow(caCertPath, 'CA cert');
|
||||
const caKeyPem = loadPemOrThrow(caKeyPath, 'CA key');
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIE8TCCA9mgAwIBAgIQoRyc0ioSEdkYGPQxwYXUDjANBgkqhkiG9w0BAQUFADB0
|
||||
MRkwFwYDVQQDDBBtaW5pc3J2IHNlcnZpY2VzMREwDwYDVQQIDAhOZXcgWW9yazEL
|
||||
MAkGA1UEBhMCVVMxHjAcBgkqhkiG9w0BCQEWD3plZmllQHplZmllLm5ldDEXMBUG
|
||||
A1UECgwOWmVmaWUgTmV0d29ya3MwIBcNMDAwMTAxMTIwMDAwWhgPMjA5OTEyMzEy
|
||||
MzU5NTlaMFExKTAnBgNVBAMTIGhlYWR3YWl0ZXIudHJ1c3RlZC5tc250di5tc24u
|
||||
Y29tMRcwFQYDVQQKEw5aZWZpZSBOZXR3b3JrczELMAkGA1UEBhMCVVMwggEiMA0G
|
||||
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCbEGP/jBpuLj8aDzeIzcN+HWaorttU
|
||||
WKqF5IY5s/nRm4WHdYUaKgDf9ksBJm2+igB0XSI1d06hvrI1VkMejvJzZpq+RNJe
|
||||
TqMy7cp5zPcLLymUkb8a1ziY6ZjezHMLCRyfU2zyMR3yqHCAaYP6JtEZqc3Ht4o6
|
||||
NfVCF4A8uynHZEGW47Iz4e6gLnzXutN7/ngDPw2hi/2XKN/E5djBg1yXHeQ+Y14n
|
||||
Ab0sHO4DgsUOYruTZu/TyO1A2ewpsGC40cRsbPAkeHtViOqVBUegDTfiqxbGpxaQ
|
||||
Qfdq/b8NYQmaRO/I/kH1IfYMiI+RxeBhUiBYlSEwOBNcJrc4etIzdYOVAgMBAAGj
|
||||
ggGeMIIBmjAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEF
|
||||
BQcDATCCAWkGA1UdEQSCAWAwggFcgiBoZWFkd2FpdGVyLnRydXN0ZWQubXNudHYu
|
||||
bXNuLmNvbYIZc2cxLnRydXN0ZWQubXNudHYubXNuLmNvbYIZc2cyLnRydXN0ZWQu
|
||||
bXNudHYubXNuLmNvbYIZc2czLnRydXN0ZWQubXNudHYubXNuLmNvbYIZc2c0LnRy
|
||||
dXN0ZWQubXNudHYubXNuLmNvbYINbXNudHYubXNuLmNvbYIWbWFpbC5zZXJ2aWNl
|
||||
cy5saXZlLmNvbYIObG9naW4ubGl2ZS5jb22CEXBvcHRpbWl6ZS5tc24uY29tghFm
|
||||
YXZvcml0ZXMubXNuLmNvbYIRbWVzc2VuZ2VyLm1zbi5jb22CEWxpdmVmaWxlc3Rv
|
||||
cmUuY29tghZ1c2Vycy5zdG9yYWdlLmxpdmUuY29tgglnLm1zbi5jb22CF21zbmlh
|
||||
bG9naW4ucGFzc3BvcnQuY29tgg1taW5pc3J2LmxvY2FsMA0GCSqGSIb3DQEBBQUA
|
||||
A4IBAQBCeWJbS8JJNlC6OIgUQue4bkDk5bNDUFuktAWXqmoM7Kt4MLDB1O60ez/T
|
||||
9cSGqU9QicSn1eL0O7ETpEPO4tSDU0KB9GnulndWbyI/OgaVOC6ON7v4Uv0z7+nw
|
||||
ninT66R+wajuSf9HyC8HA2FSMcIdiLBDtzUsSIxxG/EneEuxkBv31kQkHbWubIY4
|
||||
9SJeIF5evjQZOC3KRVlfa9nwM3qon7RUfLWF1CZlRmaFfZQf2r0cH5cVt13kyxKI
|
||||
gc8rwbGGOGsQFlw6GzGTUNQSv35xOCRNDvPtkp+dJ4GFMCbrg2OVk8n0Ecwu9jma
|
||||
TSZ7B6w5w/1YzXyFlcZolxrxrupi
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAmxBj/4wabi4/Gg83iM3Dfh1mqK7bVFiqheSGObP50ZuFh3WF
|
||||
GioA3/ZLASZtvooAdF0iNXdOob6yNVZDHo7yc2aavkTSXk6jMu3Kecz3Cy8plJG/
|
||||
Gtc4mOmY3sxzCwkcn1Ns8jEd8qhwgGmD+ibRGanNx7eKOjX1QheAPLspx2RBluOy
|
||||
M+HuoC5817rTe/54Az8NoYv9lyjfxOXYwYNclx3kPmNeJwG9LBzuA4LFDmK7k2bv
|
||||
08jtQNnsKbBguNHEbGzwJHh7VYjqlQVHoA034qsWxqcWkEH3av2/DWEJmkTvyP5B
|
||||
9SH2DIiPkcXgYVIgWJUhMDgTXCa3OHrSM3WDlQIDAQABAoIBABsMUiEgp3iaLQwm
|
||||
5N1CZSwhxUc9zwjouYAHA4nbtMNFEVb2XywX8RSgAefWs/R1XoxttXqlj8wDd1L5
|
||||
89XIAg8yseHoH9ju9yU6WOQDpoEnLiXaAX+VVKxJrQ6KE7Q0V++Lzhh2DF8IVi10
|
||||
Gl8fd8B1/+zrPVuXj2tCra4KSMiclRBEvj0DjMlCHzR+JXDU8AVDg8KVi7ZQeKkx
|
||||
kYEFzEwtzuOEOZQte7I1mqZ6Uns5tnpkcJISS9p8OzOK8+FvQY+w/Ya1g5bS8wXk
|
||||
e56UtjZ33myUzwaVUEIBqSXEsAAFvnvvvv95LQPd0CRppOJWxGPcFTrW9Lsc38Ex
|
||||
5k/0TOkCgYEAzaF/j6de4dq7r5WMk+ZYOWJpoT56SEJiEPhOCOZEA0eiBhKpwsE7
|
||||
U/qOYaJdS8nR5XPz9H5fNn0cs+1f4Em6vUSHZHfSJ20ovwxH4Dgoa4MTJg7dRe4H
|
||||
Bw9PQLSy8EIYYjjmbHbTNvdx947eW3SCqEE+hKPhuI0vHTYA19gLIckCgYEAwQv/
|
||||
E8qNkXhJg5vCUrWXWyi1BOF5pHMLE2YY4S5744w5fy4rQ7Squrx22nE8pvSmNOs+
|
||||
2ZMk9J2RKry6ZHG6tx8mlstHJFdkVUeYo4Dx6K5C0YH5+0j6VLEdnRPt5gCDhOAp
|
||||
l/vmMK3vaPwm3sN9k1dvfS6Nu6HKf3dF+U7PmW0CgYEAx11xA6JOR8N+fLoN1cda
|
||||
eiiEKSUeAbj6w2E6pz45asSkviaSGJSoJL+kE90Lf1NpAYHuYHm1bmrWzUuzzQ0u
|
||||
1BoA+tOBGWCoRLJvbWCFL9ehVSDMP+SLQ7kAKcgIRRCP+4sXPMnn9j7qbA5EX38S
|
||||
vnajWo5ZZkjcCQI2xw5bnKECgYBkxQBXsmoHv4SQYKQlTIk/mpYzgAdhYnQroiXm
|
||||
lb8x3qa8zrri0tI0O+DG0klY1WhVQ19Bb9/gmMeISY/6kqtmn6ojGOWAAOZs5by1
|
||||
zL96OEzE1FZLZ6LUxsewkRCj+SyuFd1gaquUSZcpdEZODjnkycV20PBHO4Na2kX6
|
||||
h7syWQKBgQC8SNTXuawSYMH8KZk8WmxgejSz1t67CkZduhErKzci8iuR+WKtB24J
|
||||
EvAWfMeWUTW5vGokHJrillrrWdw1J7QivcxXXZ48GNQKxbXFUY4KBB67v8wIu0lq
|
||||
O+tlfmj14IJeXc4u1S/I5lSKnG+niIBYzqXeyxNWwd4H1zncP+dssg==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
354
zefie_wtvp_minisrv/tools/scan_service_vault_deprecations.js
Normal file
@@ -0,0 +1,354 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||
const DEFAULT_USER_CONFIG_PATH = path.join(ROOT_DIR, 'user_config.json');
|
||||
const DEFAULT_BASE_CONFIG_PATH = path.join(ROOT_DIR, 'includes', 'config.json');
|
||||
const DEPRECIATED_CONFIG_PATH = path.join(ROOT_DIR, 'includes', 'depreciated.json');
|
||||
|
||||
|
||||
function printUsage() {
|
||||
console.log('Usage: node tools/scan_service_vault_deprecations.js [options]');
|
||||
console.log('');
|
||||
console.log('Options:');
|
||||
console.log(' --vault <path> Add a vault root to scan (can be repeated).');
|
||||
console.log(' --config <path> Config file to read ServiceVaults from (default: user_config.json).');
|
||||
console.log(' --base-config <path> Base config fallback (default: includes/config.json).');
|
||||
console.log(' --ext <csv> File extensions to scan (default: .js). Example: --ext .js,.txt');
|
||||
console.log(' --json Emit machine-readable JSON output.');
|
||||
console.log(' --fail-on-found Exit with code 2 if any deprecations are found.');
|
||||
console.log(' -h, --help Show this help text.');
|
||||
}
|
||||
|
||||
function parseJsonWithComments(json) {
|
||||
if (typeof json !== 'string') json = json ? json.toString() : '';
|
||||
|
||||
let result = '';
|
||||
let i = 0;
|
||||
let isString = false;
|
||||
let isEscape = false;
|
||||
let isBlockComment = false;
|
||||
let isLineComment = false;
|
||||
|
||||
while (i < json.length) {
|
||||
const ch = json[i];
|
||||
const next = json[i + 1];
|
||||
|
||||
if (!isString && !isEscape && ch === '/' && next === '*') {
|
||||
isBlockComment = true;
|
||||
i += 1;
|
||||
} else if (isBlockComment && ch === '*' && next === '/') {
|
||||
isBlockComment = false;
|
||||
i += 1;
|
||||
} else if (!isString && !isEscape && ch === '/' && next === '/') {
|
||||
isLineComment = true;
|
||||
i += 1;
|
||||
} else if (isLineComment && (ch === '\n' || ch === '\r')) {
|
||||
isLineComment = false;
|
||||
} else if (!isBlockComment && !isLineComment) {
|
||||
if (ch === '"' && !isEscape) {
|
||||
isString = !isString;
|
||||
}
|
||||
|
||||
isEscape = ch === '\\' && !isEscape;
|
||||
result += ch;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return JSON.parse(result);
|
||||
}
|
||||
|
||||
function readConfigIfExists(filePath) {
|
||||
if (!filePath || !fs.existsSync(filePath)) return null;
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
if (!raw.trim()) return {};
|
||||
return parseJsonWithComments(raw);
|
||||
}
|
||||
|
||||
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 || null,
|
||||
replacement: entry.replacement || null
|
||||
}));
|
||||
|
||||
return mapped.length > 0 ? mapped : {};
|
||||
} catch (error) {
|
||||
console.warn(`Warning: failed to load ${DEPRECIATED_CONFIG_PATH}: ${error.message}`);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = argv.slice(2);
|
||||
const options = {
|
||||
vaults: [],
|
||||
configPath: DEFAULT_USER_CONFIG_PATH,
|
||||
baseConfigPath: DEFAULT_BASE_CONFIG_PATH,
|
||||
extensions: new Set(['.js']),
|
||||
json: false,
|
||||
failOnFound: false
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
|
||||
if (arg === '-h' || arg === '--help') {
|
||||
printUsage();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (arg === '--vault') {
|
||||
i += 1;
|
||||
const value = args[i];
|
||||
if (!value) throw new Error('Missing value for --vault');
|
||||
options.vaults.push(value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--config') {
|
||||
i += 1;
|
||||
const value = args[i];
|
||||
if (!value) throw new Error('Missing value for --config');
|
||||
options.configPath = path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--base-config') {
|
||||
i += 1;
|
||||
const value = args[i];
|
||||
if (!value) throw new Error('Missing value for --base-config');
|
||||
options.baseConfigPath = path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--ext') {
|
||||
i += 1;
|
||||
const value = args[i];
|
||||
if (!value) throw new Error('Missing value for --ext');
|
||||
|
||||
options.extensions = new Set(
|
||||
value
|
||||
.split(',')
|
||||
.map((ext) => ext.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
.map((ext) => (ext.startsWith('.') ? ext : `.${ext}`))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--json') {
|
||||
options.json = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--fail-on-found') {
|
||||
options.failOnFound = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function getConfiguredVaults(configPath, baseConfigPath) {
|
||||
const userConfig = readConfigIfExists(configPath) || {};
|
||||
const baseConfig = readConfigIfExists(baseConfigPath) || {};
|
||||
|
||||
const userVaults = userConfig.config && Array.isArray(userConfig.config.ServiceVaults)
|
||||
? userConfig.config.ServiceVaults
|
||||
: null;
|
||||
|
||||
const baseVaults = baseConfig.config && Array.isArray(baseConfig.config.ServiceVaults)
|
||||
? baseConfig.config.ServiceVaults
|
||||
: null;
|
||||
|
||||
const selected = userVaults && userVaults.length > 0 ? userVaults : (baseVaults || []);
|
||||
return selected.map((vault) => path.resolve(ROOT_DIR, String(vault)));
|
||||
}
|
||||
|
||||
function walkFiles(rootDir, extensions, fileList = []) {
|
||||
if (!fs.existsSync(rootDir)) return fileList;
|
||||
|
||||
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(rootDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
walkFiles(fullPath, extensions, fileList);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.isFile()) continue;
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (extensions.has(ext)) {
|
||||
fileList.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return fileList;
|
||||
}
|
||||
|
||||
function toLineColumn(text, index) {
|
||||
let line = 1;
|
||||
let column = 1;
|
||||
for (let i = 0; i < index; i += 1) {
|
||||
if (text[i] === '\n') {
|
||||
line += 1;
|
||||
column = 1;
|
||||
} else {
|
||||
column += 1;
|
||||
}
|
||||
}
|
||||
return { line, column };
|
||||
}
|
||||
|
||||
function scanFile(filePath, patterns) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const findings = [];
|
||||
|
||||
for (const rule of patterns) {
|
||||
rule.pattern.lastIndex = 0;
|
||||
let match;
|
||||
while ((match = rule.pattern.exec(content)) !== null) {
|
||||
const loc = toLineColumn(content, match.index);
|
||||
findings.push({
|
||||
ruleId: rule.id,
|
||||
match: match[0],
|
||||
message: rule.message,
|
||||
removeVersion: rule.removeVersion,
|
||||
replacement: rule.replacement,
|
||||
line: loc.line,
|
||||
column: loc.column
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return findings;
|
||||
}
|
||||
|
||||
function formatRelative(targetPath) {
|
||||
return path.relative(ROOT_DIR, targetPath) || targetPath;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const options = parseArgs(process.argv);
|
||||
const deprecationPatterns = loadDepreciatedPatterns();
|
||||
if (deprecationPatterns.length === 0) {
|
||||
console.warn('No deprecation patterns found. Exiting without scanning.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const configuredVaults = getConfiguredVaults(options.configPath, options.baseConfigPath);
|
||||
const explicitVaults = options.vaults.map((vault) => (
|
||||
path.isAbsolute(vault) ? path.resolve(vault) : path.resolve(process.cwd(), vault)
|
||||
));
|
||||
|
||||
const vaultsToScan = [...new Set([...configuredVaults, ...explicitVaults])];
|
||||
if (vaultsToScan.length === 0) {
|
||||
throw new Error('No ServiceVault paths found. Define config.ServiceVaults or pass --vault.');
|
||||
}
|
||||
|
||||
const missingVaults = [];
|
||||
const filesToScan = [];
|
||||
|
||||
for (const vaultPath of vaultsToScan) {
|
||||
if (!fs.existsSync(vaultPath)) {
|
||||
missingVaults.push(vaultPath);
|
||||
continue;
|
||||
}
|
||||
walkFiles(vaultPath, options.extensions, filesToScan);
|
||||
}
|
||||
|
||||
const results = [];
|
||||
let totalFindings = 0;
|
||||
|
||||
for (const filePath of filesToScan) {
|
||||
const findings = scanFile(filePath, deprecationPatterns);
|
||||
if (findings.length > 0) {
|
||||
totalFindings += findings.length;
|
||||
results.push({ file: filePath, findings });
|
||||
}
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
const payload = {
|
||||
rootDir: ROOT_DIR,
|
||||
scannedVaults: vaultsToScan,
|
||||
missingVaults,
|
||||
scannedFiles: filesToScan.length,
|
||||
matchedFiles: results.length,
|
||||
totalFindings,
|
||||
results
|
||||
};
|
||||
console.log(JSON.stringify(payload, null, 2));
|
||||
} else {
|
||||
console.log('ServiceVault deprecation scan');
|
||||
console.log('- Deprecation count: ' + deprecationPatterns.length);
|
||||
console.log(`- Vault roots: ${vaultsToScan.length}`);
|
||||
console.log(`- Missing vault roots: ${missingVaults.length}`);
|
||||
console.log(`- Files scanned: ${filesToScan.length}`);
|
||||
console.log(`- Files with deprecations: ${results.length}`);
|
||||
console.log(`- Total deprecations: ${totalFindings}`);
|
||||
|
||||
if (missingVaults.length > 0) {
|
||||
console.log('');
|
||||
console.log('Missing vault roots:');
|
||||
for (const missing of missingVaults) {
|
||||
console.log(` - ${formatRelative(missing)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (results.length > 0) {
|
||||
console.log('');
|
||||
for (const result of results) {
|
||||
console.log(formatRelative(result.file));
|
||||
for (const finding of result.findings) {
|
||||
console.log(` ${finding.line}:${finding.column} ${finding.ruleId}`);
|
||||
console.log(` ${finding.message}`);
|
||||
if (finding.removeVersion) {
|
||||
console.log(` Remove version: ${finding.removeVersion}`);
|
||||
}
|
||||
console.log(` Match: ${finding.match}`);
|
||||
if (finding.replacement) {
|
||||
console.log(` Fix: ${finding.replacement}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalFindings > 0 && options.failOnFound) {
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message || error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
257
zefie_wtvp_minisrv/tools/update_user_data_key.js
Normal file
@@ -0,0 +1,257 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const CryptoJS = require('crypto-js');
|
||||
|
||||
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||
const DEFAULT_CONFIG_PATH = path.join(ROOT_DIR, 'includes', 'config.json');
|
||||
const USER_CONFIG_PATH = path.join(ROOT_DIR, 'user_config.json');
|
||||
|
||||
function parseJsonWithComments(json) {
|
||||
if (typeof json !== 'string') json = json ? json.toString() : '';
|
||||
let result = '';
|
||||
let i = 0;
|
||||
let isString = false;
|
||||
let isEscape = false;
|
||||
let isBlockComment = false;
|
||||
let isLineComment = false;
|
||||
|
||||
while (i < json.length) {
|
||||
const char = json[i];
|
||||
const nextChar = json[i + 1];
|
||||
|
||||
if (!isString && !isEscape && char === '/' && nextChar === '*') {
|
||||
isBlockComment = true;
|
||||
i += 1;
|
||||
} else if (isBlockComment && char === '*' && nextChar === '/') {
|
||||
isBlockComment = false;
|
||||
i += 1;
|
||||
} else if (!isString && !isEscape && char === '/' && nextChar === '/') {
|
||||
isLineComment = true;
|
||||
i += 1;
|
||||
} else if (isLineComment && (char === '\n' || char === '\r')) {
|
||||
isLineComment = false;
|
||||
} else if (!isBlockComment && !isLineComment) {
|
||||
if (char === '"' && !isEscape) {
|
||||
isString = !isString;
|
||||
}
|
||||
isEscape = char === '\\' && !isEscape;
|
||||
result += char;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return JSON.parse(result);
|
||||
}
|
||||
|
||||
function readJsonWithComments(filePath, required) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
if (required) throw new Error(`Required file not found: ${filePath}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
return parseJsonWithComments(raw);
|
||||
}
|
||||
|
||||
function integrateConfig(main, user) {
|
||||
const out = Array.isArray(main) ? main.slice() : { ...main };
|
||||
for (const key of Object.keys(user || {})) {
|
||||
const userVal = user[key];
|
||||
if (userVal && typeof userVal === 'object' && !Array.isArray(userVal)) {
|
||||
out[key] = integrateConfig(out[key] || {}, userVal);
|
||||
} else {
|
||||
out[key] = userVal;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function isAbsoluteLike(p) {
|
||||
return /^(?:[a-zA-Z]:)?[\\/]/.test(p);
|
||||
}
|
||||
|
||||
function resolveFromRoot(p) {
|
||||
if (isAbsoluteLike(p)) return path.normalize(p);
|
||||
return path.resolve(ROOT_DIR, p);
|
||||
}
|
||||
|
||||
function listUserJsonFiles(accountsRoot) {
|
||||
const files = [];
|
||||
|
||||
function walk(dir) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(fullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^user\d+\.json$/i.test(entry.name)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync(accountsRoot)) walk(accountsRoot);
|
||||
return files;
|
||||
}
|
||||
|
||||
function decryptWithKey(value, key) {
|
||||
return CryptoJS.AES.decrypt(value, key).toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
|
||||
function encryptWithKey(value, key) {
|
||||
return CryptoJS.AES.encrypt(value, key).toString();
|
||||
}
|
||||
|
||||
function isPrintableAndSane(str) {
|
||||
if (typeof str !== 'string') return false;
|
||||
if (str.length === 0 || str.length > 512) return false;
|
||||
if (str.includes('\uFFFD')) return false;
|
||||
if (/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/.test(str)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function isSaneDecryptedValue(fieldName, decrypted) {
|
||||
if (!isPrintableAndSane(decrypted)) return false;
|
||||
|
||||
if (fieldName === 'subscriber_password') {
|
||||
const isSha512Hex = /^[a-f0-9]{128}$/i.test(decrypted);
|
||||
if (isSha512Hex) return true;
|
||||
return decrypted.length <= 128;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasNestedKey(obj, pathParts) {
|
||||
let cur = obj;
|
||||
for (const part of pathParts) {
|
||||
if (!cur || typeof cur !== 'object' || !Object.prototype.hasOwnProperty.call(cur, part)) return false;
|
||||
cur = cur[part];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
let oldKey = null;
|
||||
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
if (args[i] === '--oldkey' && args[i + 1]) {
|
||||
oldKey = args[i + 1];
|
||||
i += 1;
|
||||
} else if (args[i].startsWith('--oldkey=')) {
|
||||
oldKey = args[i].slice('--oldkey='.length);
|
||||
}
|
||||
}
|
||||
|
||||
return { oldKey };
|
||||
}
|
||||
|
||||
function main() {
|
||||
const { oldKey } = parseArgs();
|
||||
|
||||
const defaultConfig = readJsonWithComments(DEFAULT_CONFIG_PATH, true);
|
||||
const userConfig = readJsonWithComments(USER_CONFIG_PATH, false);
|
||||
|
||||
const defaultKey = oldKey ||
|
||||
(defaultConfig && defaultConfig.config && defaultConfig.config.keys
|
||||
? defaultConfig.config.keys.user_data_key
|
||||
: null);
|
||||
|
||||
const userHasKey = hasNestedKey(userConfig, ['config', 'keys', 'user_data_key']);
|
||||
const userKey = userHasKey ? userConfig.config.keys.user_data_key : null;
|
||||
|
||||
if (oldKey) {
|
||||
console.log(`Using provided --oldkey for decryption.`);
|
||||
}
|
||||
|
||||
if (!defaultKey || typeof defaultKey !== 'string') {
|
||||
throw new Error('Default config key config.keys.user_data_key is missing or invalid.');
|
||||
}
|
||||
|
||||
if (!userHasKey) {
|
||||
console.log('No config.keys.user_data_key found in user_config.json. Nothing to migrate.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof userKey !== 'string' || userKey.length === 0) {
|
||||
throw new Error('user_config.json config.keys.user_data_key is invalid.');
|
||||
}
|
||||
|
||||
if (userKey === defaultKey) {
|
||||
console.log('user_config.json key matches the default key. Nothing to migrate.');
|
||||
return;
|
||||
}
|
||||
|
||||
const mergedConfig = integrateConfig(defaultConfig, userConfig);
|
||||
const sessionStore = mergedConfig && mergedConfig.config ? mergedConfig.config.SessionStore : null;
|
||||
if (!sessionStore || typeof sessionStore !== 'string') {
|
||||
throw new Error('config.SessionStore is missing or invalid.');
|
||||
}
|
||||
|
||||
const accountsRoot = path.join(resolveFromRoot(sessionStore), 'accounts');
|
||||
const userFiles = listUserJsonFiles(accountsRoot);
|
||||
|
||||
if (userFiles.length === 0) {
|
||||
console.log(`No user account files found in ${accountsRoot}. Nothing to migrate.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const writableUpdates = [];
|
||||
let touchedPasswordFields = 0;
|
||||
|
||||
for (const userFile of userFiles) {
|
||||
let accountData;
|
||||
try {
|
||||
accountData = JSON.parse(fs.readFileSync(userFile, 'utf8'));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse account file ${userFile}: ${error.message}`);
|
||||
}
|
||||
|
||||
let fileChanged = false;
|
||||
|
||||
for (const fieldName of ['subscriber_password', 'subscriber_smtp_password']) {
|
||||
const encryptedValue = accountData[fieldName];
|
||||
if (encryptedValue === null || typeof encryptedValue === 'undefined') continue;
|
||||
if (typeof encryptedValue !== 'string') {
|
||||
throw new Error(`Suspicious ${fieldName} value in ${userFile}: expected string/null.`);
|
||||
}
|
||||
if (encryptedValue.length === 0) continue;
|
||||
|
||||
const decryptedValue = decryptWithKey(encryptedValue, defaultKey);
|
||||
if (!isSaneDecryptedValue(fieldName, decryptedValue)) {
|
||||
throw new Error(
|
||||
`Aborting: decrypted ${fieldName} in ${userFile} appears invalid/binary. No files were updated.\n` +
|
||||
`If you previously used a different key, re-run with: --oldkey "<your previous key>"`
|
||||
);
|
||||
}
|
||||
|
||||
accountData[fieldName] = encryptWithKey(decryptedValue, userKey);
|
||||
fileChanged = true;
|
||||
touchedPasswordFields += 1;
|
||||
}
|
||||
|
||||
if (fileChanged) {
|
||||
writableUpdates.push({ filePath: userFile, accountData });
|
||||
}
|
||||
}
|
||||
|
||||
for (const update of writableUpdates) {
|
||||
fs.writeFileSync(update.filePath, JSON.stringify(update.accountData), 'utf8');
|
||||
}
|
||||
|
||||
console.log(`Migrated ${touchedPasswordFields} encrypted password field(s) across ${writableUpdates.length} account file(s).`);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(error.message || error);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -15,6 +15,9 @@
|
||||
"C:/Users/zefie/webtv/ServiceVault2",
|
||||
"/home/zefie/webtv/ServiceVault"
|
||||
],
|
||||
"keys": {
|
||||
"user_data_key": "SOMETHING_RANDOM_AND_SECRET" // this can be any string, but should be changed to a random value before registering any users.
|
||||
},
|
||||
"php_enabled": true, // enables PHP CGI support
|
||||
"php_binpath": "/usr/bin/php-cgi", // path to PHP CGI binary
|
||||
"cgi_enabled": true, // enables CGI Support
|
||||
|
||||