From 2de99b4e9c3eaff8058384de76b8fba9d6ed5a04 Mon Sep 17 00:00:00 2001 From: ibratabian17 Date: Sun, 28 Jul 2024 11:52:07 +0800 Subject: [PATCH] Improve User Assigning Profile and Fix Leaderboard Causing recap freezes --- core/core.js | 2 + core/lib/ipResolver.js | 206 ++++++++++++++++++++++++++++++++++++++ core/lib/is.js | 72 +++++++++++++ core/route/account.js | 8 +- core/route/leaderboard.js | 189 ++++++++-------------------------- 5 files changed, 326 insertions(+), 151 deletions(-) create mode 100644 core/lib/ipResolver.js create mode 100644 core/lib/is.js diff --git a/core/core.js b/core/core.js index eaba0c3..f73c506 100644 --- a/core/core.js +++ b/core/core.js @@ -2,12 +2,14 @@ var { main } = require('./var') var { resolvePath } = require('./helper') var { modules } = require('../settings.json') var fs = require("fs"); // require https module +var requestIp = require('./lib/ipResolver.js') function init(app, express) { const bodyParser = require("body-parser"); app.use(express.json()); app.use(bodyParser.raw()); + app.use(requestIp.mw()) app.use((err, req, res, next) => { // shareLog('ERROR', `${err}`) res.status(500).send('Internal Server Error'); diff --git a/core/lib/ipResolver.js b/core/lib/ipResolver.js new file mode 100644 index 0000000..0105d8e --- /dev/null +++ b/core/lib/ipResolver.js @@ -0,0 +1,206 @@ +const is = require('./is'); +//taken from https://github.com/pbojinov/request-ip + +/** + * Parse x-forwarded-for headers. + * + * @param {string} value - The value to be parsed. + * @return {string|null} First known IP address, if any. + */ +function getClientIpFromXForwardedFor(value) { + if (!is.existy(value)) { + return null; + } + + if (is.not.string(value)) { + throw new TypeError(`Expected a string, got "${typeof value}"`); + } + + // x-forwarded-for may return multiple IP addresses in the format: + // "client IP, proxy 1 IP, proxy 2 IP" + // Therefore, the right-most IP address is the IP address of the most recent proxy + // and the left-most IP address is the IP address of the originating client. + // source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For + // Azure Web App's also adds a port for some reason, so we'll only use the first part (the IP) + const forwardedIps = value.split(',').map((e) => { + const ip = e.trim(); + if (ip.includes(':')) { + const splitted = ip.split(':'); + // make sure we only use this if it's ipv4 (ip:port) + if (splitted.length === 2) { + return splitted[0]; + } + } + return ip; + }); + + // Sometimes IP addresses in this header can be 'unknown' (http://stackoverflow.com/a/11285650). + // Therefore taking the right-most IP address that is not unknown + // A Squid configuration directive can also set the value to "unknown" (http://www.squid-cache.org/Doc/config/forwarded_for/) + for (let i = 0; i < forwardedIps.length; i++) { + if (is.ip(forwardedIps[i])) { + return forwardedIps[i]; + } + } + + // If no value in the split list is an ip, return null + return null; +} + +/** + * Determine client IP address. + * + * @param req + * @returns {string} ip - The IP address if known, defaulting to empty string if unknown. + */ +function getClientIp(req) { + // Server is probably behind a proxy. + if (req.headers) { + // Standard headers used by Amazon EC2, Heroku, and others. + if (is.ip(req.headers['x-client-ip'])) { + return req.headers['x-client-ip']; + } + + // Load-balancers (AWS ELB) or proxies. + const xForwardedFor = getClientIpFromXForwardedFor( + req.headers['x-forwarded-for'], + ); + if (is.ip(xForwardedFor)) { + return xForwardedFor; + } + + // Cloudflare. + // @see https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers- + // CF-Connecting-IP - applied to every request to the origin. + if (is.ip(req.headers['cf-connecting-ip'])) { + return req.headers['cf-connecting-ip']; + } + + // DigitalOcean. + // @see https://www.digitalocean.com/community/questions/app-platform-client-ip + // DO-Connecting-IP - applied to app platform servers behind a proxy. + if (is.ip(req.headers['do-connecting-ip'])) { + return req.headers['do-connecting-ip']; + } + + // Fastly and Firebase hosting header (When forwared to cloud function) + if (is.ip(req.headers['fastly-client-ip'])) { + return req.headers['fastly-client-ip']; + } + + // Akamai and Cloudflare: True-Client-IP. + if (is.ip(req.headers['true-client-ip'])) { + return req.headers['true-client-ip']; + } + + // Default nginx proxy/fcgi; alternative to x-forwarded-for, used by some proxies. + if (is.ip(req.headers['x-real-ip'])) { + return req.headers['x-real-ip']; + } + + // (Rackspace LB and Riverbed's Stingray) + // http://www.rackspace.com/knowledge_center/article/controlling-access-to-linux-cloud-sites-based-on-the-client-ip-address + // https://splash.riverbed.com/docs/DOC-1926 + if (is.ip(req.headers['x-cluster-client-ip'])) { + return req.headers['x-cluster-client-ip']; + } + + if (is.ip(req.headers['x-forwarded'])) { + return req.headers['x-forwarded']; + } + + if (is.ip(req.headers['forwarded-for'])) { + return req.headers['forwarded-for']; + } + + if (is.ip(req.headers.forwarded)) { + return req.headers.forwarded; + } + + // Google Cloud App Engine + // https://cloud.google.com/appengine/docs/standard/go/reference/request-response-headers + + if (is.ip(req.headers['x-appengine-user-ip'])) { + return req.headers['x-appengine-user-ip']; + } + } + + // Remote address checks. + // Deprecated + if (is.existy(req.connection)) { + if (is.ip(req.connection.remoteAddress)) { + return req.connection.remoteAddress; + } + if ( + is.existy(req.connection.socket) && + is.ip(req.connection.socket.remoteAddress) + ) { + return req.connection.socket.remoteAddress; + } + } + + if (is.existy(req.socket) && is.ip(req.socket.remoteAddress)) { + return req.socket.remoteAddress; + } + + if (is.existy(req.info) && is.ip(req.info.remoteAddress)) { + return req.info.remoteAddress; + } + + // AWS Api Gateway + Lambda + if ( + is.existy(req.requestContext) && + is.existy(req.requestContext.identity) && + is.ip(req.requestContext.identity.sourceIp) + ) { + return req.requestContext.identity.sourceIp; + } + + // Cloudflare fallback + // https://blog.cloudflare.com/eliminating-the-last-reasons-to-not-enable-ipv6/#introducingpseudoipv4 + if (req.headers) { + if (is.ip(req.headers['Cf-Pseudo-IPv4'])) { + return req.headers['Cf-Pseudo-IPv4']; + } + } + + // Fastify https://www.fastify.io/docs/latest/Reference/Request/ + if (is.existy(req.raw)) { + return getClientIp(req.raw); + } + + return null; +} + +/** + * Expose request IP as a middleware. + * + * @param {object} [options] - Configuration. + * @param {string} [options.attributeName] - Name of attribute to augment request object with. + * @return {*} + */ +function mw(options) { + // Defaults. + const configuration = is.not.existy(options) ? {} : options; + + // Validation. + if (is.not.object(configuration)) { + throw new TypeError('Options must be an object!'); + } + + const attributeName = configuration.attributeName || 'clientIp'; + return (req, res, next) => { + const ip = getClientIp(req); + Object.defineProperty(req, attributeName, { + get: () => ip, + configurable: true, + }); + next(); + }; +} + +module.exports = { + getClientIpFromXForwardedFor, + getClientIp, + mw, +}; \ No newline at end of file diff --git a/core/lib/is.js b/core/lib/is.js new file mode 100644 index 0000000..ec69757 --- /dev/null +++ b/core/lib/is.js @@ -0,0 +1,72 @@ +/** + * Inspired by and credit to is_js [https://github.com/arasatasaygin/is.js] + */ + +const regexes = { + ipv4: /^(?:(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/, + ipv6: /^((?=.*::)(?!.*::.+::)(::)?([\dA-F]{1,4}:(:|\b)|){5}|([\dA-F]{1,4}:){6})((([\dA-F]{1,4}((?!\3)::|:\b|$))|(?!\2\3)){2}|(((2[0-4]|1\d|[1-9])?\d|25[0-5])\.?\b){4})$/i, +}; + +/** + * Helper function which reverses the sense of predicate result + * @param {*} func + * @returns + */ +function not(func) { + return function () { + return !func.apply(null, Array.prototype.slice.call(arguments)); + }; +} + +/** + * Replaces is.existy from is_js. + * @param {*} value - The value to test + * @returns {boolean} True if the value is defined, otherwise false + */ +function existy(value) { + return value != null; +} + +/** + * Replaces is.ip from is_js. + * @param {*} value - The value to test + * @returns {boolean} True if the value is an IP address, otherwise false + */ +function ip(value) { + return ( + (existy(value) && regexes.ipv4.test(value)) || regexes.ipv6.test(value) + ); +} + +/** + * Replaces is.object from is_js. + * @param {*} value - The value to test + * @returns {boolean} True if the value is an object, otherwise false + */ +function object(value) { + return Object(value) === value; +} + +/** + * Replaces is,.string from is_js. + * @param {*} value - The value to test + * @returns True if the value is a string, otherwise false + */ +function string(value) { + return Object.prototype.toString.call(value) === '[object String]'; +} + +const is = { + existy: existy, + ip: ip, + object: object, + string: string, + not: { + existy: not(existy), + ip: not(ip), + object: not(object), + string: not(string), + }, +}; + +module.exports = is; \ No newline at end of file diff --git a/core/route/account.js b/core/route/account.js index a7bdc3d..d9e805d 100644 --- a/core/route/account.js +++ b/core/route/account.js @@ -29,15 +29,16 @@ exports.initroute = (app) => { console.log('[ACC] Is the key correct? are the files corrupted?') console.log('[ACC] Ignore this message if this first run') console.log('[ACC] Resetting All User Data...') + console.log(err) } // Map over profileIds to retrieve corresponding user profiles or create default profiles const responseProfiles = profilesid.map(profileId => { const userProfile = decryptedData[profileId]; // Get user profile based on profileId if (userProfile) { - return { ...userProfile, ip: req.ip, ticket: ticket }; // Add IP to userProfile but not in the response + return { ...userProfile, ip: req.clientIp, ticket: ticket }; // Add IP to userProfile but not in the response } else { - const defaultProfile = { ip: req.ip, ticket: ticket }; // Create a default profile with IP address + const defaultProfile = { ip: req.clientIp, ticket: ticket }; // Create a default profile with IP address decryptedData[profileId] = defaultProfile; // Add default profile to decrypted data return {}; // Return an empty object (don't include defaultProfile in response) } @@ -64,6 +65,7 @@ exports.initroute = (app) => { console.log('[ACC] Is the key correct? are the files corrupted?') console.log('[ACC] Ignore this message if this first run') console.log('[ACC] Resetting All User Data...') + console.log(err) } // Find a matching profile based on name or IP address (only one profile) @@ -75,7 +77,7 @@ exports.initroute = (app) => { } const matchedProfileId = Object.keys(decryptedData).find(profileId => { const userProfile = decryptedData[profileId]; // Get user profile based on profileId - return userProfile.name === content.name || userProfile.ticket === ticket || userProfile.ip === req.ip; // Check for name or IP match + return userProfile.name === content.name || userProfile.ticket === ticket || userProfile.ip === req.clientIp; // Check for name or IP match }); if (matchedProfileId) { diff --git a/core/route/leaderboard.js b/core/route/leaderboard.js index a4423fe..dec52c4 100644 --- a/core/route/leaderboard.js +++ b/core/route/leaderboard.js @@ -9,6 +9,11 @@ const core = { generateCarousel: require('../carousel/carousel').generateCarousel, generateSweatCarousel: require('../carousel/carousel').generateSweatCarousel, generateCoopCarousel: require('../carousel/carousel').generateCoopCarousel, updateMostPlayed: require('../carousel/carousel').updateMostPlayed } const DOTW_PATH = path.join(core.getSavefilePath(), 'leaderboard/dotw/'); +const { getSavefilePath } = require('../helper'); +const { encrypt, decrypt } = require('../lib/encryptor') + +const secretKey = require('../../database/encryption.json').encrpytion.userEncrypt; +decryptedData = {}; function generateToolNickname() { const prefixes = ["Wordkeeper", "Special", "Krakenbite", "DinosaurFan", "Definehub", "Termtracker", "Lexiconet", "Vocabvault", "Lingolink", "Glossarygenius", "Thesaurustech", "Synonymster", "Definitionary", "Jargonjot", "Idiomizer", "Phraseforge", "Meaningmaker", "Languageledger", "Etymologyengine", "Grammarguard", "Syntaxsense", "Semanticsearch", "Orthographix", "Phraseology", "Vernacularvault", "Dictionet", "Slangscroll", "Lingualist", "Grammargrid", "Lingoledge", "Termtoolbox", "Wordware", "Lexigizmo", "Synosearch", "Thesaurustech", "Phrasefinder", "Vocabvortex", "Meaningmatrix", "Languageledger", "Etymologist", "Grammargate", "Syntaxsphere", "Semanticsearch", "Orthographix", "Phraseplay", "Vernacularvault", "Dictionator", "Slangstack", "Lingolink", "Grammarguide", "Lingopedia", "Termtracker", "Wordwizard", "Lexilist", "Synomate", "Thesaurustool", "Definitizer", "Jargonjunction", "Idiomgenius", "Phrasemaker", "Meaningmate", "Duolingo", "Languagelink", "Etymoengine", "Grammarguru", "Syntaxsage", "Semanticsuite", "Orthography", "Phrasefinder", "Vocabverse", "Lexipedia", "Synoscribe", "Thesaurusware", "Definitionary", "Jargonscribe", "Idiomster", "Phrasetech", "Meaningmax", "Flop", "Slayguy", "Languagelex", "Etymoedge", "Grammargenie", "Syntaxsync", "Semanticsearch", "Orthography", "Phraseforge", "Vernacularex", "Dictionmaster", "Slangster", "Lingoware", "Grammargraph", "Lingomate", "Termmate", "Wordwork", "Lexixpert", "Synostar", "Thesaurusmax", "OculusVision", "FlowerPower", "RustySilver", "Underfire", "Shakeawake", "Truthhand", "Kittywake", "Definize", "Jargonize", "Idiomify", "Phrasemaster", "Meaningmark", "Lingualine", "Etymogenius", "Grammarguard", "Syntaxsmart", "Semanticsearch", "Orthography", "Phrasedex", "Vocabmax", "Lexilock", "Synomind", "Thesaurusmart", "Definify", "Jargonmatrix", "Idiomnet", "Phraseplay", "Meaningmate", "Lingolink", "Etymoexpert", "Grammargetter", "Syntaxsage", "Semanticsearch", "Orthography", "Phrasepad", "Vernacularvibe", "Dictiondom", "Slangster", "Lingolytics", "Grammargenie", "Lingotutor", "Termtracker", "Wordwarp", "Lexisync", "Synomind", "Thesaurusmate", "Definizer", "Jargonify", "Idiomster", "Phraselab", "Meaningmark", "Languageleaf", "Etymoedge", "Grammargrid", "Syntaxsync", "Semanticsuite", "Orthographix", "Phraseforge", "Vernacularvibe", "Dictiondom", "Slangster", "Lingolytics", "Grammargenie", "Lingotutor", "Termtracker", "Wordwarp", "Lexisync", "Synomind", "Thesaurusmate", "Definizer", "Jargonify", "Idiomster", "Phraselab", "Meaningmark", "Languageleaf", "Etymoedge", "Grammargrid", "Syntaxsync", "Semanticsuite", "Orthographix"]; @@ -27,38 +32,37 @@ function generateToolNickname() { } -const getProfileData = async (req) => { +function getProfileData(ticket, content, clientIp) { + const dataFilePath = path.join(getSavefilePath(), '/account/profiles/user.json'); try { - const ticket = req.header("Authorization"); - const sku = req.header('X-SkuId'); - const prodwsurl = "https://prod.just-dance.com/"; - - const response = await axios({ - method: req.method, - url: prodwsurl + req.url, - headers: { - "X-SkuId": sku, - "Authorization": ticket, - "Content-Type": "application/json" - }, - data: req.body - }); - - return response.data; - } catch (error) { - if (error.response) { - throw new Error(`HTTP status ${error.response.status}`); - } else if (error.request) { - throw new Error('Network error'); - } else { - throw new Error(error.message); + if (Object.keys(decryptedData).length === 0) { + const encryptedData = fs.readFileSync(dataFilePath, 'utf8'); + decryptedData = JSON.parse(decrypt(encryptedData, secretKey)); } + } catch (err) { + decryptedData = {}; + console.log('[ACC] Unable to read user.json'); + console.log('[ACC] Is the key correct? are the files corrupted?'); + console.log('[ACC] Ignore this message if this first run'); + console.log('[ACC] Resetting All User Data...'); + console.log(err); } -}; + + const matchedProfileId = Object.keys(decryptedData).find(profileId => { + const userProfile = decryptedData[profileId]; + return userProfile.ticket === ticket || userProfile.ip === clientIp; + }); + + if (matchedProfileId) { + return decryptedData[matchedProfileId]; + } else { + return false; + } +} const getGameVersion = (req) => { - const sku = req.header('X-SkuId'); - return sku.substring(0, 6); + const sku = req.header('X-SkuId') || "jd2019-pc-ww"; + return sku.substring(0, 6) || "jd2019"; }; const initroute = (app) => { @@ -102,62 +106,6 @@ const initroute = (app) => { try { leaderboardData.entries.push( - { - "__class": "LeaderboardEntry_Online", - "profileId": "00000000-0000-0000-0000-000000000000", - "score": Math.floor(Math.random() * 1333) + 12000, - "name": generateToolNickname(), - "avatar": Math.floor(Math.random() * 100), - "country": Math.floor(Math.random() * 20), - "platformId": "e3", - "alias": 0, - "aliasGender": 0, - "jdPoints": 0, - "portraitBorder": 0, - "mapName": mapName - }, - { - "__class": "LeaderboardEntry_Online", - "profileId": "00000000-0000-0000-0000-000000000000", - "score": Math.floor(Math.random() * 1333) + 12000, - "name": generateToolNickname(), - "avatar": Math.floor(Math.random() * 100), - "country": Math.floor(Math.random() * 20), - "platformId": "e3", - "alias": 0, - "aliasGender": 0, - "jdPoints": 0, - "portraitBorder": 0, - "mapName": mapName - }, - { - "__class": "LeaderboardEntry_Online", - "profileId": "00000000-0000-0000-0000-000000000000", - "score": Math.floor(Math.random() * 1333) + 12000, - "name": generateToolNickname(), - "avatar": Math.floor(Math.random() * 100), - "country": Math.floor(Math.random() * 20), - "platformId": "e3", - "alias": 0, - "aliasGender": 0, - "jdPoints": 0, - "portraitBorder": 0, - "mapName": mapName - }, - { - "__class": "LeaderboardEntry_Online", - "profileId": "00000000-0000-0000-0000-000000000000", - "score": Math.floor(Math.random() * 1333) + 12000, - "name": generateToolNickname(), - "avatar": Math.floor(Math.random() * 100), - "country": Math.floor(Math.random() * 20), - "platformId": "e3", - "alias": 0, - "aliasGender": 0, - "jdPoints": 0, - "portraitBorder": 0, - "mapName": mapName - }, { "__class": "LeaderboardEntry_Online", "profileId": "00000000-0000-0000-0000-000000000000", @@ -189,62 +137,6 @@ const initroute = (app) => { try { leaderboardData.entries.push( - { - "__class": "LeaderboardEntry_Online", - "profileId": "00000000-0000-0000-0000-000000000000", - "score": Math.floor(Math.random() * 1333) + 12000, - "name": generateToolNickname(), - "avatar": Math.floor(Math.random() * 100), - "country": Math.floor(Math.random() * 20), - "platformId": "e3", - "alias": 0, - "aliasGender": 0, - "jdPoints": 0, - "portraitBorder": 0, - "mapName": mapName - }, - { - "__class": "LeaderboardEntry_Online", - "profileId": "00000000-0000-0000-0000-000000000000", - "score": Math.floor(Math.random() * 1333) + 12000, - "name": generateToolNickname(), - "avatar": Math.floor(Math.random() * 100), - "country": Math.floor(Math.random() * 20), - "platformId": "e3", - "alias": 0, - "aliasGender": 0, - "jdPoints": 0, - "portraitBorder": 0, - "mapName": mapName - }, - { - "__class": "LeaderboardEntry_Online", - "profileId": "00000000-0000-0000-0000-000000000000", - "score": Math.floor(Math.random() * 1333) + 12000, - "name": generateToolNickname(), - "avatar": Math.floor(Math.random() * 100), - "country": Math.floor(Math.random() * 20), - "platformId": "e3", - "alias": 0, - "aliasGender": 0, - "jdPoints": 0, - "portraitBorder": 0, - "mapName": mapName - }, - { - "__class": "LeaderboardEntry_Online", - "profileId": "00000000-0000-0000-0000-000000000000", - "score": Math.floor(Math.random() * 1333) + 12000, - "name": generateToolNickname(), - "avatar": Math.floor(Math.random() * 100), - "country": Math.floor(Math.random() * 20), - "platformId": "e3", - "alias": 0, - "aliasGender": 0, - "jdPoints": 0, - "portraitBorder": 0, - "mapName": mapName - }, { "__class": "LeaderboardEntry_Online", "profileId": "00000000-0000-0000-0000-000000000000", @@ -267,11 +159,12 @@ const initroute = (app) => { }); app.post("/profile/v2/map-ended", async (req, res) => { - const codename = req.body; - + const ticket = req.header("Authorization"); + const clientIp = req.ip; try { - for (let song of codename) { - core.updateMostPlayed(song); + const mapList = req.body; + for (let song of mapList) { + core.updateMostPlayed(song.mapName); const dotwFilePath = path.join(DOTW_PATH, `${song.mapName}.json`); if (fs.existsSync(dotwFilePath)) { @@ -281,9 +174,9 @@ const initroute = (app) => { return res.send('1'); } } else { - const profiljson1 = await getProfileData(req); + const profiljson1 = await getProfileData(ticket, song, clientIp); if (!profiljson1) { - return res.status(500).send('Error fetching profile data'); + return res.send('1') } const jsontodancerweek = { @@ -304,12 +197,12 @@ const initroute = (app) => { fs.writeFileSync(dotwFilePath, JSON.stringify(jsontodancerweek, null, 2)); console.log(`DOTW file for ${song.mapName} created!`); - res.send(profiljson1); + res.send(''); } } } catch (error) { - console.error(error); - res.status(500).send('Internal Server Error'); + console.log(error) + res.status(200).send(''); //keep send } });