Compare commits

..

13 Commits

Author SHA1 Message Date
17e0e6e526 Merge branch 'dev' into feature/FixTv2 2026-05-05 02:15:34 +02:00
Ryder
02a3eef5e7 upload 2026-05-05 01:08:26 +01:00
zefie
11d2ab8c86 some documentation 2026-05-04 20:07:18 -04:00
zefie
778c0a2827 new ssl 2026-05-04 14:01:11 -04:00
zefie
00e385cdbe various fixups, allow FTP max size config 2026-05-04 08:38:22 -04:00
zefie
cf9cc22a1c new depreciation 2026-05-03 15:26:38 -04:00
zefie
0c5dc17ae6 create depreciation scanning tool 2026-05-03 15:23:23 -04:00
zefie
4347543ef7 don't expose version on pc services if hide_minisrv_config is true 2026-05-03 15:15:27 -04:00
zefie
9d51abd9ab documentation is hard 2026-05-03 15:10:44 -04:00
zefie
118443305b fix wrong key in quicksetup 2026-05-03 15:09:35 -04:00
zefie
ab4453487e more quicksetup 2026-05-03 15:07:30 -04:00
zefie
e003d9795b even more accurate QuickSetup.md 2026-05-03 14:59:14 -04:00
zefie
e88dbd98cc more accurate QuickSetup.md 2026-05-03 14:58:17 -04:00
57 changed files with 3275 additions and 307 deletions

View File

@@ -2,7 +2,7 @@
## user_config.json
`user_config.json` (in the project root) is where you put your local configuration overrides. It merges on top of `includes/config.json`**do not edit `includes/config.json` directly**.
`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:
@@ -35,17 +35,19 @@ node tools/configurator.js <dot.path.key> --delete [--overwrite]
## 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 MAME/Viewer on the same machine as minisrv.
`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.bind_ip 192.168.1.x --overwrite
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 break existing accounts.
`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
@@ -55,3 +57,31 @@ To generate a random key:
```
openssl rand -base64 32
```
## Disabling a standard service
You can disable a configured service by setting the `disabled: true` flag for that service. For example, to disable `wtv-admin`:
```
node tools/configurator.js services.wtv-admin.disabled true
```
## Enabling a disabled service
You can disable a configured service by setting the `disabled: false` flag for that service. For example, to enable `pc_services`:
```
node tools/configurator.js services.pc_services.disabled false
```
## Custom service pages
You can place your custom pages in `UserServiceVault/servicename/page.js`. For example, to override `wtv-home:/home`, you would create
`UserServiceVault/wtv-home/home.js`, and the server will automatically prioritize your page. You can mix and match service vaults, accessing
resources in the standard service vault within your custom pages.
## Updating minisrv
You can `git pull`, or extract a new archive over the existing folder. If you followed the directions and kept your changes in `user_config.json` and `UserServiceVault`,
then you can update minisrv without worrying about breakage or losing data. Do pay attention to the console, and if any deprecreations appear, fix them before updating to the version listed in the notice.

View File

@@ -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
@@ -2685,9 +2704,11 @@ if (protocolHandledPorts.size > 0) console.log(` * Started ${protocolHandledPort
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);
// 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, you can run tools/update_user_data_key.js to update existing accounts with the new key.");
console.log(" * After changing the key in the user_config, please restart the server.");
console.log(" * If you had existing users prior to changing the key, you can run tools/update_user_data_key.js to update existing accounts with the new key.");
console.log(" * Making a backup of your user accounts before doing this is highly recommended, in case something goes wrong during the update process.");
}

View File

@@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE-----
MIIEqzCCA5OgAwIBAgIQ3Zq6hcFrpKh4v3/G9sTw3DANBgkqhkiG9w0BAQUFADBw
MQswCQYDVQQGEwJVUzELMAkGA1UECAwCT0gxEzARBgNVBAcMCkJ1dHQgQ3JhY2sx
IDAeBgNVBAoMF1VuZGVyd2VhciBJbnNwZWN0b3IgIzEyMR0wGwYDVQQLDBRUaGlu
ZyBMb29rZXIgRXhwZXJ0czAgFw0wMDAxMDExMjAwMDBaGA8yMDk5MTIzMTIzNTk1
OVowUTEpMCcGA1UEAxMgaGVhZHdhaXRlci50cnVzdGVkLm1zbnR2Lm1zbi5jb20x
FzAVBgNVBAoTDlplZmllIE5ldHdvcmtzMQswCQYDVQQGEwJVUzCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAMtl9J068AFSHykffAlcpspq3D7mE7fFRoyf
+tQCM3Wy7PqJvDAegoI4Zf/QdToTJMS6dkcsEx+dgD01VKJ1B0RdHbg6rFQfymc4
GKyNk6tuqp7YQqElCUc91oFz4pJaJaOYaNBqkAG3MfTg+tSoBXl2YyjPrT0TPhXX
1Cm7BuFZORqNhvTdf33QXzgCQVso9U5X9YBgDaiTcu55etjFKUBEYhSYwTHmennA
FWOjY7ux6HFXBfKAz1QeCE6+corl5+6srCfh7Uz3ZFV9vntEYnyzbJuo6gR5P7GI
IYsygkADQAETHivl6GxeB7SEUfYLnfrZFwZc235tUz7USBdg3gcCAwEAAaOCAVww
ggFYMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMB
MIIBJwYDVR0RBIIBHjCCARqCIGhlYWR3YWl0ZXIudHJ1c3RlZC5tc250di5tc24u
Y29tghcqLnRydXN0ZWQubXNudHYubXNuLmNvbYINbXNudHYubXNuLmNvbYIWbWFp
bC5zZXJ2aWNlcy5saXZlLmNvbYIObG9naW4ubGl2ZS5jb22CEXBvcHRpbWl6ZS5t
c24uY29tghFmYXZvcml0ZXMubXNuLmNvbYIRbWVzc2VuZ2VyLm1zbi5jb22CEWxp
dmVmaWxlc3RvcmUuY29tghZ1c2Vycy5zdG9yYWdlLmxpdmUuY29tgglnLm1zbi5j
b22CF21zbmlhbG9naW4ucGFzc3BvcnQuY29tgg1taW5pc3J2LmxvY2Fsgg8qLm1p
bmlzcnYubG9jYWwwDQYJKoZIhvcNAQEFBQADggEBAAGEINTBTrkbpO0CJPv9w4Nj
IMuOSZETA7uXWyPwoLBIa57yTjNEVvWmjAc2nnrI3D6ijLMiF1eDIEsP4DI/qfMs
J82cS/IOIxXCmReU132NaZ6mSNEZx9QDkz/R8rFq5jKPRYSeguZSsWDxYlaQsbRr
qxQsKkRIOpm5pIOA/UT2gwV0L84a/NHXHNfc+CnPvvy7R9kmUC0XynsqU3lkj4Ah
SNZOgYyWkGWW7AytQWnMxyWm+xQjG4Fwl1Os9en4qwCK0ADyMCQyG3O68Gffu2go
YciXaJquI52fEKDQV4mDxy4B1V8BQ3ywm1iGebLzLgrKK7xPucUU5fqz7v2IUIs=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAy2X0nTrwAVIfKR98CVymymrcPuYTt8VGjJ/61AIzdbLs+om8
MB6Cgjhl/9B1OhMkxLp2RywTH52APTVUonUHRF0duDqsVB/KZzgYrI2Tq26qnthC
oSUJRz3WgXPiklolo5ho0GqQAbcx9OD61KgFeXZjKM+tPRM+FdfUKbsG4Vk5Go2G
9N1/fdBfOAJBWyj1Tlf1gGANqJNy7nl62MUpQERiFJjBMeZ6ecAVY6Nju7HocVcF
8oDPVB4ITr5yiuXn7qysJ+HtTPdkVX2+e0RifLNsm6jqBHk/sYghizKCQANAARMe
K+XobF4HtIRR9gud+tkXBlzbfm1TPtRIF2DeBwIDAQABAoIBAADoPGfDhQfq4IyA
gVjRvhHmDLbTgOACp1aZbUPeNKUmpKVWniCOA+GZZ2V1ZEIOwGaH0sVtF8xXmZeF
5UUKOS7GLL7b6CJB6z2vNt3CJ35av03d3UI2iSzCEYxadd0jcMqJGfwsyLKkdgd6
rDO5m7ikdtT0qpGjEQi46AKkCZseSdIZqtgm1t0NupbXuBKbR13N6iNOr6eVnEpZ
tTrwIvw/OZ3Wf9tVW0DzjB1ejUYXCYAd6mHskMRrNfu/11x2pyXMHOSl24sVZxHe
Dxe+cYjw61/5pMMBq6ZQ61SPvAWZbIOA/PINHIUjisIGxpeqluz0zb+SLLrZnBe5
i9WEHsUCgYEA5lKEc5w02/n9+6wZCK/sPWhNJF3cPw2E0O+kqHPWLx2qPNhKf2Zh
8RycdVCnCcEXVQedYnNpBeAktMNGxAR9IhiVhfwiG0dFYXNUHBIHgVBZqtOeW8sS
uy4/fOw/qCCLWDfnDaozj8RnKsvu6zLm4c73Q4FEr4R6rk4EjfsGA90CgYEA4hMD
9PBdIWAYapP2FRangOmP50YNQVznrmDsfFlm2vh3aFrx83f0IFjYxeUI7OdTjkk3
7m5pEeV/liQwB/D7uXF+gNoUQab61QRhuKd58TbQELJkeBwevUb3rWVZ3LiFNNVB
vcunKTsL4qEADw56qMaczuZ27xXwatgUVtOdbTMCgYAmOA0ojgQreIlPyNgCnAas
jfE3Fqgkgl7GuO1u0oH5IYgNPqrmBxw7gU7pHLALK1Ju1qukGZiU1APjRwAoKMKG
9ONi71rNgf8eU5/iZI+AQtAOS71caA88pkj8tss9X+Efi2840kRqF+IytNJ5juHH
GKvpNLssEOS2pdWVpdDytQKBgF47WNne2qLPwD2kYN1XbQhh0lavL1VAWV2pDsmi
Jio9iOAZkGJQbJSTFAAgwICmx4A2arbalLcd9vlpKhAVVYdtlDI3NFxNMp5ZzjW+
sShnFYDwKsqZxgJVM2W2KifDTdrAzT9ERO/9qa2UOEcOXPg+mRvwmkB735NZb9jl
KehbAoGBANXDltYtJh28hMopP2sZUEx/4A2fnGjVMxBmpSsrk6irz5u/wdhimlDP
+JHZZXrb/OTU2BjCPjCCOj953XUqsCpBvWgGdX8uab0O4Gg8x8ucu/K6/kp0X0jN
EWQYaMAXJlLQM+k7bXrABNIGu8BF3JzC9oYjlKiFpLqNOQpfHdbK
-----END RSA PRIVATE KEY-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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

View File

@@ -1,5 +0,0 @@
-----BEGIN DH PARAMETERS-----
MIGHAoGBAOjeZEDvMxiY+T4AMUIJ6jPFhflzUwO6EPBc0+Fn3C13WGQgsx9N3Rjg
bZsF4Sbqs62+KFTYb5/1PVPSOxyif0CJLRC8VhvCl5CZ2DsS6nJ3sstPxtfhQdn+
X1kbvqAbHlvNtE6w5ketHv3gK6y4d9qdVnwicZW3uV1sJ2dg4RfDAgEC
-----END DH PARAMETERS-----

View File

@@ -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>

View File

@@ -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 + '&';

View File

@@ -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>`;

View File

@@ -0,0 +1,444 @@
const minisrv_service_file = true;
const crypto = require('crypto');
// Sorry Zef :kek
// https://git.computernewb.com/yellows111/msnp-wiki/src/branch/master/docs/services/rst.md
// the RST_ cookie stuff was code that was temp until we had proper token authentication
const NS = {
SOAP: "http://schemas.xmlsoap.org/soap/envelope/",
WSSE: "http://schemas.xmlsoap.org/ws/2003/06/secext",
WSP: "http://schemas.xmlsoap.org/ws/2002/12/policy",
WSU: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
WSA: "http://schemas.xmlsoap.org/ws/2004/03/addressing",
WST: "http://schemas.xmlsoap.org/ws/2004/04/trust",
PSF: "http://schemas.microsoft.com/Passport/SoapServices/SOAPFault",
ENC: "http://www.w3.org/2001/04/xmlenc#",
DS: "http://www.w3.org/2000/09/xmldsig#"
};
function getCookie(cookieString, name) {
if (!cookieString) return null;
const match = cookieString.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : null;
}
function setCookie(name, value, options = {}) {
const cookie = `${name}=${encodeURIComponent(value)}`;
const path = options.path || '/';
const expires = options.expires || '';
return `${cookie}; path=${path}${expires ? `; expires=${expires}` : ''}`;
}
function formatDateTime(dt) {
return dt.toISOString().replace(/\.\d{3}Z$/, 'Z');
}
function getClientIP() {
const forwarded = request_headers['x-forwarded-for'];
if (forwarded) {
const ips = forwarded.split(',');
return ips[0].trim();
}
return request_headers['x-real-ip'] || '127.0.0.1';
}
function generateRandomToken(userId, appliesTo, isLegacy = false) {
const timestamp = Date.now();
const randomPart = crypto.randomBytes(32).toString('hex');
if (isLegacy) {
const tokenData = `${userId}|${appliesTo}|${timestamp}|${randomPart}`;
return crypto.createHash('sha256').update(tokenData).digest('hex');
} else {
const tokenData = {
uid: userId,
app: appliesTo,
ts: timestamp,
rand: randomPart,
ver: '1.0'
};
return Buffer.from(JSON.stringify(tokenData)).toString('base64');
}
}
function extractXmlValue(xml, elementName) {
if (!xml) return null;
const patterns = [
new RegExp(`<${elementName}>([\\s\\S]*?)</${elementName}>`, 'i'),
new RegExp(`<wsse:${elementName}>([\\s\\S]*?)</wsse:${elementName}>`, 'i'),
new RegExp(`<wst:${elementName}>([\\s\\S]*?)</wst:${elementName}>`, 'i'),
new RegExp(`<ps:${elementName}>([\\s\\S]*?)</ps:${elementName}>`, 'i')
];
for (const regex of patterns) {
const match = xml.match(regex);
if (match && match[1]) {
let value = match[1].trim();
value = value.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
return value;
}
}
return null;
}
function extractTokenFromCipherValue(xml) {
if (!xml) return null;
const cipherRegex = /<CipherValue>([\s\S]*?)<\/CipherValue>/gi;
let match;
let token = null;
while ((match = cipherRegex.exec(xml)) !== null) {
let cipherValue = match[1].trim();
if (cipherValue && cipherValue.length > 0) {
token = cipherValue;
console.log("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`;
}
console.log(`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}">
NOBELLIUM 16.0.30846.6
</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);
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);
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 {
console.log("No post_data found. Available keys:", Object.keys(request_headers));
return generateErrorResponse("0x80048820", "No POST data received");
}
if (!requestBody || requestBody.trim() === '') {
console.log("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')) {
console.log("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;
console.log(`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 {
console.log("Token validation failed");
return generateErrorResponse("0x80048821", "Invalid token");
}
} else {
console.log("No token found in CipherValue");
return generateErrorResponse("0x80048820", "Missing credentials/token");
}
}
else if (email && password) {
console.log(`Extracted - Email: ${email}, Password: ${password ? '***' : 'empty'}`);
if (email && email.indexOf('@') < 0) {
const domain = (minisrv_config && minisrv_config.config && minisrv_config.config.domain_name) || 'wtv.zefie.com';
email = `${email}@${domain}`;
}
userEmail = email;
firstName = email.split('@')[0];
userId = crypto.createHash('md5').update(email).digest('hex');
console.log(`Authentication successful for: ${userEmail} (${userId})`);
}
else {
console.log("Missing both credentials and token");
return generateErrorResponse("0x80048820", "Missing credentials/token");
}
if (!userId || !userEmail) {
console.log("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}`);
}
}
let result = rstHandler();
if (result) {
data = result;
}

View File

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

View File

@@ -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>

File diff suppressed because one or more lines are too long

View File

@@ -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,7 +152,7 @@ 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>

View File

@@ -15,7 +15,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) {

View File

@@ -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>

View File

@@ -0,0 +1,706 @@
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);
}
}
if (currentUser != null) {
// Check if We can do IDCRL if not fall back to Legacy XMLlogin
if (TVShell.LoginManager.IDCRLInitialize) {
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");
}
}
}
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;
}

View File

@@ -37,6 +37,8 @@ if (session_data) {
registered = session_data.isRegistered();
if (registered) {
username = session_data.getSessionData("subscriber_username") || '';
Profile_Picture = session_data.getSessionData('ProfilePicture') || '';
}
}
@@ -103,23 +105,23 @@ data = `<html>
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
var banned = ${banned};
var registered = ${registered};
var username = "${username}";
var picture = "${Profile_Picture}";
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();
// 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 CheckForUser(usernameToCheck) {
@@ -146,15 +148,30 @@ data = `<html>
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();
entry = TVShell.ServiceList.Add('connection::login');
entry.URL = 'https://headwaiter.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();
}
}
}
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;
TVShell.ConnectionManager.ServiceState = 'ReSignIn';
if ( signon && panel && panel.Name == "main" )
{
if ( IsMainPanelOnPage( signon.URL ) ) atLogin = true;
@@ -165,7 +182,7 @@ data = `<html>
GotoSignOn();
}
} else {
if (myPanel) myPanel.GotoURL('https://sg1.trusted.msntv.msn.com/register/Establish-your-MSN-TV-Account.html');
if (myPanel) myPanel.GotoURL('https://sg1.trusted.msntv.msn.com/Register/Establish-your-MSN-TV-Account.html');
}
if (myPanel) {
myPanel.ClearTravelLog();
@@ -188,11 +205,7 @@ data = `<html>
"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]);
@@ -236,17 +249,15 @@ data = `<html>
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 url = 'https://sg1.trusted.msntv.msn.com/connection/banned.html';
var myPanel = TVShell.PanelManager.Item('service');
if (myPanel) myPanel.GotoURL(url);
} else if (registered) {
GotoSignOn();
var url = 'https://sg1.trusted.msntv.msn.com/connection/usercheck.html';
var myPanel = tvShell.PanelManager.Item('service');
if (myPanel) myPanel.GotoURL(url);
}
}
@@ -278,7 +289,6 @@ data = `<html>
if (!IsServicePanel()) {
DontContinue();
} else {
CheckBoxID();
DoPoptimization();
DoLogin();
@@ -300,7 +310,7 @@ data = `<html>
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');
if (myPanel) myPanel.GotoURL('https://sg1.trusted.msntv.msn.com/connection/error.html');
}
</script>
</body>

View File

@@ -0,0 +1,193 @@
const minisrv_service_file = true;
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 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('home::home');
entry.URL = 'http://sg1.trusted.msntv.msn.com/Pages/Home/Home.aspx';
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 = 'http://sg1.trusted.msntv.msn.com/BackendProxy';
entry.Safe = true;
entry = tvShell.UserManager.CurrentUser.ServiceList.Add('home::radioplus');
entry.URL = 'http://sg1.trusted.msntv.msn.com/Stations.xml';
entry.Safe = true;
entry = tvShell.UserManager.CurrentUser.ServiceList.Add('mail::listmail');
entry.URL = 'http://sg1.trusted.msntv.msn.com/Pages/Mail/listmail.aspx';
entry.KeyCode = 0xB4; // VK_LAUNCH_MAIL
entry.Safe = true;
entry = tvShell.UserManager.CurrentUser.ServiceList.Add('mail::writemail');
entry.URL = 'http://sg1.trusted.msntv.msn.com/Pages/Mail/writemail.aspx';
entry.Safe = true;
entry = tvShell.UserManager.CurrentUser.ServiceList.Add('chat::home');
entry.URL = 'http://sg1.trusted.msntv.msn.com/Pages/Chat/Chat.html';
entry.Safe = true;
entry = tvShell.UserManager.CurrentUser.ServiceList.Add('messenger::root');
entry.URL = 'http://login.live.com:1863';
entry.Safe = true;
entry = tvShell.UserManager.CurrentUser.ServiceList.Add('messenger::passport');
entry.URL = 'https://login.live.com';
entry = tvShell.UserManager.CurrentUser.ServiceList.Add('search::search');
entry.URL = 'http://sg1.trusted.msntv.msn.com/Pages/Search/search.html';
entry.Safe = true;
entry = tvShell.UserManager.CurrentUser.ServiceList.Add('search::main');
entry.URL = 'http://sg1.trusted.msntv.msn.com/Pages/Search/categories.html';
entry.Safe = true;
entry = tvShell.UserManager.CurrentUser.ServiceList.Add('discuss::home');
entry.URL = 'http://sg1.trusted.msntv.msn.com/';
entry.Safe = true;
entry = tvShell.UserManager.CurrentUser.ServiceList.Add('maps::main');
entry.URL = 'http://sg1.trusted.msntv.msn.com/';
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 = 'http://sg1.trusted.msntv.msn.com/settings/main.aspx';
entry.Safe = true;
entry = tvShell.UserManager.CurrentUser.ServiceList.Add('UAM::UAMbase');
entry.URL = 'http://sg1.trusted.msntv.msn.com/';
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('music::radiohome');
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 = 'http://sg1.trusted.msntv.msn.com/connection/PopupControlWhiteList.ashx';
entry = tvShell.ServiceList.Add('connection::reconnect');
entry.URL = 'http://headwaiter.trusted.msntv.msn.com/connection/GatePage.aspx?phase=Bootstrap&purpose=Authorize';
entry = tvShell.ServiceList.Add('connection::nightly_login');
entry.URL = 'http://headwaiter.trusted.msntv.msn.com/connection/GatePage.aspx?phase=Bootstrap&purpose=Nightly';
entry = tvShell.ServiceList.Add('mail::check');
entry.URL = 'http://sg1.trusted.msntv.msn.com/apps/connection/CheckMail.aspx?phase=CheckMail&purpose=CheckMail';
entry.Safe = true;
if (wanProvider === "MSNIANB") {
entry = tvShell.ServiceList.Add('home::videoplus');
entry.URL = 'msntv:/Video/VideoHome.html';
entry.Safe = true;
} else {
entry = tvShell.ServiceList.Add('home::videoplus');
entry.URL = 'http://sg1.trusted.msntv.msn.com/pages/msnvideo/main';
entry.Safe = true;
}
if (wanProvider === "MSNIANB") {
entry = tvShell.ServiceList.Add('home::musicvideo');
entry.URL = 'msntv:/Music/MusicHome.html';
entry.Safe = true;
} else {
entry = tvShell.ServiceList.Add('home::musicvideo');
entry.URL = 'http://sg1.trusted.msntv.msn.com/pages/msnvideo/main?p=music';
entry.Safe = true;
}
entry = tvShell.ServiceList.Add('connection::resetpassword');
entry.URL = 'http://sg1.trusted.msntv.msn.com/connection/GatePage?phase=Bootstrap&purpose=ResetPassword';
entry = tvShell.ServiceList.Add('connection::pagepatch');
entry.URL = 'http://sg1.trusted.msntv.msn.com/connection/PagePatch.ashx';
entry = tvShell.ServiceList.Add('connection::login');
entry.URL = 'http://headwaiter.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>`;

View File

@@ -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>

View File

@@ -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;
}
@@ -73,6 +74,8 @@ async function processLC2DownloadPage(flashrom_info, headers, numparts = null) {
}
session.lastDownloadTime = now;
if (isNaN(downloadTime) || downloadTime < 1) downloadTime = 1;
headers = `200 OK
Content-type: text/html
@@ -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>

View File

@@ -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");
}
*/

View File

@@ -1163,23 +1163,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,
@@ -1195,23 +1183,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,

View File

@@ -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) => {
@@ -307,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" };
@@ -319,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)) {
@@ -334,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)
@@ -353,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;
@@ -402,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();
@@ -411,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 {
@@ -421,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();
@@ -428,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();
@@ -442,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();
@@ -458,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();
@@ -475,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();
@@ -486,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();

View File

@@ -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;

View File

@@ -1130,6 +1130,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,6 +1142,7 @@ class WTVShared {
delete require.cache[resolvedPath];
}
}
*/
/**
* Returns an absolute path without an trailing path seperator

View File

@@ -351,6 +351,7 @@
"ftp": {
"port": 1650,
"connections": 3,
"max_response_size": 8, // Megabytes
"handler_module": "WTVFTP",
"handler_extra_vars": ["wtvmime"]
},
@@ -521,6 +522,10 @@
],
"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"
]

View 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"
}
]

View File

@@ -9,7 +9,8 @@
"start": "node app.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",

View File

@@ -225,45 +225,33 @@ function buildWebTVPS(videoES, audioES, outputPath, audioIntervalOverride, baHea
return false;
}
// Match known-working WebTV cadence (attract.mpg is ~1 audio per 7 video packs)
const inferredInterval = Math.max(1, Math.round(vChunks.length / aChunks.length));
// 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)
: Math.max(1, Math.round((inferredInterval + 7) / 2));
: 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
// 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;
// Spread audio over the full video timeline to avoid starving early playback
// and dumping remaining audio at EOF.
while (vIdx < vChunks.length || aIdx < aChunks.length) {
if (vIdx >= vChunks.length) {
packs.push(makePack(0xC0, aChunks[aIdx++]));
continue;
}
if (aIdx >= aChunks.length) {
for (let k = 0; k < audioInterval && vIdx < vChunks.length; k++) {
packs.push(makePack(0xE0, vChunks[vIdx++]));
continue;
}
const videoProgress = vIdx / vChunks.length;
const audioProgress = aIdx / aChunks.length;
// Prefer video until audio falls behind target cadence.
if (audioProgress + (1 / Math.max(1, audioInterval * aChunks.length)) < videoProgress) {
if (aIdx < aChunks.length) {
packs.push(makePack(0xC0, aChunks[aIdx++]));
} else {
packs.push(makePack(0xE0, vChunks[vIdx++]));
}
}

View File

@@ -7,12 +7,28 @@ 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",
"*.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",
"*.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 +47,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 +79,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 +120,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');

View File

@@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIErzCCA5egAwIBAgIQ/K/Ib/7QA0rO4hjgM53sGDANBgkqhkiG9w0BAQUFADB0
MRkwFwYDVQQDDBBtaW5pc3J2IHNlcnZpY2VzMREwDwYDVQQIDAhOZXcgWW9yazEL
MAkGA1UEBhMCVVMxHjAcBgkqhkiG9w0BCQEWD3plZmllQHplZmllLm5ldDEXMBUG
A1UECgwOWmVmaWUgTmV0d29ya3MwIBcNMDAwMTAxMTIwMDAwWhgPMjA5OTEyMzEy
MzU5NTlaMFExKTAnBgNVBAMTIGhlYWR3YWl0ZXIudHJ1c3RlZC5tc250di5tc24u
Y29tMRcwFQYDVQQKEw5aZWZpZSBOZXR3b3JrczELMAkGA1UEBhMCVVMwggEiMA0G
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDhh5bdTvB2q03vo/hwikBsfRt3p5Sg
Y7N6+e+UfUK8LxZRLwLwk3TIS7LcU+RE6d8UHO8A68joX61kYggtMAGEvYC2JWGC
XiUb37CZBJrIMTqU+tFn2zTyCvpNBU7Pv6t/PAKPc0XztrQEZ7RmqywDMbWu2B1M
86eil0HN4n4fGMYkVmVjkHJJYNChck+edkL4rDkKnbg4Ar37lYYHofrQDWTAW9eD
QSxXBPQbbqDIDHMdXwHnfCbJZIkgQ7ClK78PN6s9DRgagvnXI8vclyW38YQ/cqlk
DNkGDGcfAOg9BGtwTAUd7Bu/pAjrAR/WRY5jdv3++4/taibz5hKildA7AgMBAAGj
ggFcMIIBWDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEF
BQcDATCCAScGA1UdEQSCAR4wggEagiBoZWFkd2FpdGVyLnRydXN0ZWQubXNudHYu
bXNuLmNvbYIXKi50cnVzdGVkLm1zbnR2Lm1zbi5jb22CDW1zbnR2Lm1zbi5jb22C
Fm1haWwuc2VydmljZXMubGl2ZS5jb22CDmxvZ2luLmxpdmUuY29tghFwb3B0aW1p
emUubXNuLmNvbYIRZmF2b3JpdGVzLm1zbi5jb22CEW1lc3Nlbmdlci5tc24uY29t
ghFsaXZlZmlsZXN0b3JlLmNvbYIWdXNlcnMuc3RvcmFnZS5saXZlLmNvbYIJZy5t
c24uY29tghdtc25pYWxvZ2luLnBhc3Nwb3J0LmNvbYINbWluaXNydi5sb2NhbIIP
Ki5taW5pc3J2LmxvY2FsMA0GCSqGSIb3DQEBBQUAA4IBAQBRY2KlKxhVUCv0h86q
J66TAJocqyPEwnnvuEAxM209DhO84GR4+D9r+/U3aV18MN0tUEFOy/qx918zpwgC
kNghNmtydvW9phMFB//tX56c8UUT0rYESylKCdYDraCh9G3avI8A5hgJQCgfeUGA
l0XJvc/yA3piNAPohLB1zyOBlIONLWJxI4kyKjhOM2mkIkJWmLKXOHGPnnqCUAx+
+NzEZiJst22sngmHikN53zKyUfp2DO9CUY7hbjctAKo0GUC/Q0yQmL95idqnw56j
Gv1deZcTth/1qUqcZRbQDT+546d87rDJLcQDXs/Q34IcmZa0v7jsIL5hJP+/PtOs
fpyG
-----END CERTIFICATE-----

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA4YeW3U7wdqtN76P4cIpAbH0bd6eUoGOzevnvlH1CvC8WUS8C
8JN0yEuy3FPkROnfFBzvAOvI6F+tZGIILTABhL2AtiVhgl4lG9+wmQSayDE6lPrR
Z9s08gr6TQVOz7+rfzwCj3NF87a0BGe0ZqssAzG1rtgdTPOnopdBzeJ+HxjGJFZl
Y5BySWDQoXJPnnZC+Kw5Cp24OAK9+5WGB6H60A1kwFvXg0EsVwT0G26gyAxzHV8B
53wmyWSJIEOwpSu/DzerPQ0YGoL51yPL3Jclt/GEP3KpZAzZBgxnHwDoPQRrcEwF
Hewbv6QI6wEf1kWOY3b9/vuP7Wom8+YSopXQOwIDAQABAoIBAGQK4Q3f2ARUHhjZ
HDpT4ZsebiJIaMIzJ+k72J5+aC2RF63AlGXCi3HUPo3E+wPk2KuT4COA9Fi8JG7c
m7Lr0iifZWqnL1eEyqJQOobOR5jZWZq/nkebMiPSYdDrs1ettvYUWTBoGpYvLDCu
DhTjBn2OPgFG5cx+YxTZUvvw5jCFV7u7CDzC3dD/KvPuGdQPOjBnFM9p9NL7AfRw
qOF4lw5M8ZT9caDuM/J77MuPXLVh5p6LlV9auVeo3s03E7BOw6Iutje0ZcBqKkjr
lDV1QPkXCbbywW1YQDVv1kv9KgAEG1ftbGzq1REEX8SxNWDSzHl1Q04erqsJAI2y
eO4RRVUCgYEA/8vgxZAF38605YPcddSzYzQLLABbQJP+1LFXM8fjpI7I5kb34QNN
YY6tjhuha9fyOdFTZb3Qj5bA661Hh26BCwA9mc/X/49SJqzriQiZt9ZXVJ3ri8Xv
N6N87ELr0uneVeP2zzjJS3E9G2fGqb2ZnRV3sdKpW2m+BNgOEmSdFrUCgYEA4bWL
RhrnN9ZNHWRQTLZSX1ixb9HtFV9AbiPrVPbmzLiqmoV/kppIinr94T3ymlygBTBS
mowlQViQbEfivmG12QNIs3W3nxBc2jw7vz3XYA/TANdQeNCW5mmgdKGN8IukVLHe
tO4Vu5L7lOck+W+LYVgOTgp/c5tFSMPKB4COMS8CgYBbtfRDwQxqHsl61JkRYg5j
DgDHaOVOtQETrvWN6ifzEwJylZVABpgS1z3gioWIjecZ1bQp8TE6mhlXJkxUAUmg
8Rgk8oEF7pPMrAjSm9PJNr8e5nPSLEhFUYdzidbVSuZdMxuFVl3Nf68iCUYQC5ts
14qPpfD0hmgLgo8hBxPPVQKBgEfy0gmv68K92mkjIAHEIK/qGu21MmkcqvIxGwRD
jED4INIO+iKmcbdLC4DweVRBcHUW+U3wnLOe4rLRm3LqvMgHpvYl8TmQQrkCeF02
/l/Ny4o6GJnajC+4vgBPu2pRaTniVUbBmkXnzbCimgO1Te3i3vR7njMg7M4MM2t+
zFTbAoGBAKh9KJs6t6K6bK4I7L7zmRME4L7TCvzXfnqTJHYjXUIuAPDqTaNZAgCd
pV45DfUWIIAis/RswLuR3yT6aH3Wpxx7fTW/DTInvBKfU7Kw2Oyko1jWboS/2E3D
0HWVZzdbU4Hj48XGeldjgPV0D1/vr1JRTYJGXtjcNDkRMk3U3xrT
-----END RSA PRIVATE KEY-----

View 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);
}