mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-03-21 17:24:28 -05:00
Added Cemu files download
This commit is contained in:
parent
3da33fa3d2
commit
f8f651d641
|
|
@ -16,5 +16,6 @@
|
|||
"tester_roles": [
|
||||
"role id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"aes_key": "hex key here"
|
||||
}
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
|||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.9",
|
||||
"colors": "^1.4.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"discord-oauth2": "github:ryanblenis/discord-oauth2",
|
||||
|
|
@ -306,6 +307,14 @@
|
|||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/adm-zip": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
|
||||
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
|
|
@ -2668,6 +2677,11 @@
|
|||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"adm-zip": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
|
||||
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg=="
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
},
|
||||
"homepage": "https://github.com/PretendoNetwork/website#readme",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.9",
|
||||
"colors": "^1.4.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"discord-oauth2": "github:ryanblenis/discord-oauth2",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,17 @@
|
|||
border-radius: 100%;
|
||||
background: var(--btn-secondary);
|
||||
}
|
||||
.account-sidebar .user #download-cemu-files {
|
||||
width: 100%;
|
||||
padding: 4px 16px;
|
||||
cursor: pointer;
|
||||
background: #383f6b;
|
||||
font-size: 0.9rem;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
.account-sidebar .user .cemu-warning {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
/* Settings */
|
||||
.settings-wrapper {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
document.getElementById('remove-discord-connection').addEventListener('click', () => {
|
||||
document.getElementById('remove-discord-connection')?.addEventListener('click', () => {
|
||||
// TODO: Refresh access token if expired (move this to the backend maybe?)
|
||||
|
||||
const tokenType = document.cookie.split('; ').find(row => row.startsWith('token_type=')).split('=')[1];
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
const { Router } = require('express');
|
||||
const crypto = require('crypto');
|
||||
const DiscordOauth2 = require('discord-oauth2');
|
||||
const AdmZip = require('adm-zip');
|
||||
const util = require('../util');
|
||||
const config = require('../../config.json');
|
||||
const router = new Router();
|
||||
const aesKey = Buffer.from(config.aes_key, 'hex');
|
||||
|
||||
// Create OAuth client
|
||||
const discordOAuth = new DiscordOauth2({
|
||||
|
|
@ -15,7 +17,7 @@ const discordOAuth = new DiscordOauth2({
|
|||
|
||||
router.get('/', async (request, response) => {
|
||||
// Verify the user is logged in
|
||||
if (!request.cookies.access_token || !request.cookies.refresh_token) {
|
||||
if (!request.cookies.access_token || !request.cookies.refresh_token || !request.cookies.ph) {
|
||||
return response.redirect('/account/login');
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +193,7 @@ router.get('/login', async (request, response) => {
|
|||
router.post('/login', async (request, response) => {
|
||||
const { username, password } = request.body;
|
||||
|
||||
const apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
|
||||
let apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
|
||||
username,
|
||||
password,
|
||||
grant_type: 'password'
|
||||
|
|
@ -208,6 +210,22 @@ router.post('/login', async (request, response) => {
|
|||
response.cookie('access_token', tokens.access_token, { domain : '.pretendo.network' });
|
||||
response.cookie('token_type', tokens.token_type, { domain : '.pretendo.network' });
|
||||
|
||||
apiResponse = await util.apiGetRequest('/v1/user', {
|
||||
'Authorization': `${tokens.token_type} ${tokens.access_token}`
|
||||
});
|
||||
|
||||
const account = apiResponse.body;
|
||||
|
||||
const hashedPassword = util.nintendoPasswordHash(password, account.pid);
|
||||
const hashedPasswordBuffer = Buffer.from(hashedPassword, 'hex');
|
||||
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', aesKey, Buffer.alloc(16));
|
||||
|
||||
let encryptedBody = cipher.update(hashedPasswordBuffer);
|
||||
encryptedBody = Buffer.concat([encryptedBody, cipher.final()]);
|
||||
|
||||
response.cookie('ph', encryptedBody.toString('hex'), { domain: '.pretendo.network' });
|
||||
|
||||
response.redirect('/account');
|
||||
});
|
||||
|
||||
|
|
@ -304,9 +322,9 @@ router.get('/connect/discord', async (request, response) => {
|
|||
|
||||
const tokens = apiResponse.body;
|
||||
|
||||
response.cookie('refresh_token', tokens.refresh_token, { domain : '.pretendo.network' });
|
||||
response.cookie('access_token', tokens.access_token, { domain : '.pretendo.network' });
|
||||
response.cookie('token_type', tokens.token_type, { domain : '.pretendo.network' });
|
||||
response.cookie('refresh_token', tokens.refresh_token, { domain: '.pretendo.network' });
|
||||
response.cookie('access_token', tokens.access_token, { domain: '.pretendo.network' });
|
||||
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
|
||||
|
||||
apiResponse = await util.apiPostGetRequest('/v1/connections/add/discord', {
|
||||
'Authorization': `${tokens.token_type} ${tokens.access_token}`
|
||||
|
|
@ -330,4 +348,76 @@ router.get('/connect/discord', async (request, response) => {
|
|||
response.cookie('linked', 'Discord', { domain: '.pretendo.network' }).redirect('/account');
|
||||
});
|
||||
|
||||
router.get('/online-files', async (request, response) => {
|
||||
|
||||
// Verify the user is logged in
|
||||
if (!request.cookies.access_token || !request.cookies.refresh_token|| !request.cookies.ph) {
|
||||
return response.redirect('/account/login');
|
||||
}
|
||||
|
||||
// Attempt to get user data
|
||||
let apiResponse = await util.apiGetRequest('/v1/user', {
|
||||
'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
|
||||
});
|
||||
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
// Assume expired, refresh and retry request
|
||||
apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
|
||||
refresh_token: request.cookies.refresh_token,
|
||||
grant_type: 'refresh_token'
|
||||
});
|
||||
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
// TODO: Error message
|
||||
return response.status(apiResponse.statusCode).json({
|
||||
error: 'Bad'
|
||||
});
|
||||
}
|
||||
|
||||
const tokens = apiResponse.body;
|
||||
|
||||
response.cookie('refresh_token', tokens.refresh_token, { domain: '.pretendo.network' });
|
||||
response.cookie('access_token', tokens.access_token, { domain: '.pretendo.network' });
|
||||
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
|
||||
|
||||
apiResponse = await util.apiGetRequest('/v1/user', {
|
||||
'Authorization': `${tokens.token_type} ${tokens.access_token}`
|
||||
});
|
||||
}
|
||||
|
||||
// If still failed, something went horribly wrong
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
// TODO: Error message
|
||||
return response.status(apiResponse.statusCode).json({
|
||||
error: 'Bad'
|
||||
});
|
||||
}
|
||||
|
||||
const account = apiResponse.body;
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, Buffer.alloc(16));
|
||||
|
||||
let decryptedPasswordHash = decipher.update(Buffer.from(request.cookies.ph, 'hex'));
|
||||
decryptedPasswordHash = Buffer.concat([decryptedPasswordHash, decipher.final()]);
|
||||
|
||||
let accountDat = 'AccountInstance_00000000\n';
|
||||
accountDat += `AccountPasswordCache=${decryptedPasswordHash.toString('hex')}\n`;
|
||||
accountDat += 'IsPasswordCacheEnabled=1\n';
|
||||
accountDat += `AccountId=${account.username}\n`;
|
||||
accountDat += 'PersistentId=80000001';
|
||||
|
||||
const onlineFiles = new AdmZip();
|
||||
|
||||
onlineFiles.addFile('mlc01/usr/save/system/act/80000001/account.dat', Buffer.from(accountDat)); // Minimal account.dat
|
||||
onlineFiles.addFile('otp.bin', Buffer.alloc(0x400)); // nulled OTP
|
||||
onlineFiles.addFile('seeprom.bin', Buffer.alloc(0x200)); // nulled SEEPROM
|
||||
|
||||
response.writeHead(200, {
|
||||
'Content-Disposition': 'attachment; filename="Online Files.zip"',
|
||||
'Content-Type': 'application/zip',
|
||||
});
|
||||
|
||||
response.end(onlineFiles.toBuffer());
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
19
src/util.js
19
src/util.js
|
|
@ -1,5 +1,6 @@
|
|||
const fs = require('fs-extra');
|
||||
const got = require('got');
|
||||
const crypto = require('crypto');
|
||||
const logger = require('./logger');
|
||||
|
||||
function fullUrl(request) {
|
||||
|
|
@ -52,10 +53,26 @@ function apiDeleteGetRequest(path, headers, json) {
|
|||
});
|
||||
}
|
||||
|
||||
function nintendoPasswordHash(password, pid) {
|
||||
const pidBuffer = Buffer.alloc(4);
|
||||
pidBuffer.writeUInt32LE(pid);
|
||||
|
||||
const unpacked = Buffer.concat([
|
||||
pidBuffer,
|
||||
Buffer.from('\x02\x65\x43\x46'),
|
||||
Buffer.from(password)
|
||||
]);
|
||||
|
||||
const hashed = crypto.createHash('sha256').update(unpacked).digest().toString('hex');
|
||||
|
||||
return hashed;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fullUrl,
|
||||
getLocale,
|
||||
apiGetRequest,
|
||||
apiPostGetRequest,
|
||||
apiDeleteGetRequest
|
||||
apiDeleteGetRequest,
|
||||
nintendoPasswordHash
|
||||
};
|
||||
|
|
@ -9,7 +9,9 @@
|
|||
<div class="user">
|
||||
<img src="{{account.mii.image_url}}" class="mii" />
|
||||
<p class="miiname">{{account.mii.name}}</p>
|
||||
<p class="username">PNID: {{account.username}}</p>
|
||||
<p class="username" value="{{account.username}}">PNID: {{account.username}}</p>
|
||||
<a class="button secondary" id="download-cemu-files" href="/account/online-files" download>Download account files</a>
|
||||
<p class="cemu-warning">(will not work with Nintendo Network)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user