├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package-lock.json └── package.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2020: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'airbnb-base', 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 11, 12 | }, 13 | rules: { 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Binaries 107 | get-exchange-code-win.exe 108 | get-exchange-code-linux 109 | get-exchange-code-macos 110 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Nils S. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # exchange-code-generator 2 | A tool to create exchange codes for epicgames online services 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const axios = require('axios').default; 3 | const { createInterface, emitKeypressEvents } = require('readline'); 4 | const { 5 | readdir, mkdir, writeFile, readFile, 6 | } = require('fs').promises; 7 | const { join } = require('path'); 8 | const puppeteer = require('puppeteer-extra'); 9 | const stealthPlugin = require('puppeteer-extra-plugin-stealth'); 10 | const { exec } = require('child_process'); 11 | 12 | puppeteer.use(stealthPlugin()); 13 | 14 | delete axios.defaults.headers.post['Content-Type']; 15 | 16 | const makeBool = (text) => text.toLowerCase() === 'y' || text.toLowerCase() === 'yes' || !text; 17 | 18 | const consoleQuestion = (question, isYN = false) => new Promise((res) => { 19 | const itf = createInterface(process.stdin, process.stdout); 20 | itf.question(isYN ? `${question}(yes/no) ` : question, (answer) => { 21 | res(isYN ? makeBool(answer) : answer); 22 | itf.close(); 23 | }); 24 | }); 25 | 26 | const makeForm = (keyValuePairs) => { 27 | const data = new URLSearchParams(); 28 | Object.keys(keyValuePairs).forEach((key) => data.append(key, keyValuePairs[key])); 29 | return data.toString(); 30 | }; 31 | 32 | const copyToClipboard = (text) => { 33 | switch (process.platform) { 34 | case 'darwin': exec(`echo '${text}' | pbcopy`); break; 35 | case 'linux': exec(`echo ${text} | xclip -sel c`); break; 36 | case 'win32': exec(`echo | set /p ecgvar="${text}" | clip`); break; 37 | default: console.log('your OS is not supported'); 38 | } 39 | }; 40 | 41 | const useDeviceAuth = async (deviceAuth) => { 42 | const { data: { access_token: accessToken } } = await axios({ 43 | method: 'POST', 44 | url: 'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token', 45 | headers: { 46 | 'Content-Type': 'application/x-www-form-urlencoded', 47 | Authorization: 'basic MzQ0NmNkNzI2OTRjNGE0NDg1ZDgxYjc3YWRiYjIxNDE6OTIwOWQ0YTVlMjVhNDU3ZmI5YjA3NDg5ZDMxM2I0MWE=', 48 | }, 49 | data: makeForm({ 50 | grant_type: 'device_auth', 51 | account_id: deviceAuth.accountId, 52 | device_id: deviceAuth.deviceId, 53 | secret: deviceAuth.secret, 54 | }), 55 | }); 56 | return (await axios({ 57 | url: 'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/exchange', 58 | headers: { 59 | Authorization: `bearer ${accessToken}`, 60 | }, 61 | })).data; 62 | }; 63 | 64 | const generateDeviceAuth = async (exchangeCode) => { 65 | const { data: { access_token: accessToken, account_id: accountId, displayName } } = await axios({ 66 | method: 'POST', 67 | url: 'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token', 68 | headers: { 69 | 'Content-Type': 'application/x-www-form-urlencoded', 70 | Authorization: 'basic MzQ0NmNkNzI2OTRjNGE0NDg1ZDgxYjc3YWRiYjIxNDE6OTIwOWQ0YTVlMjVhNDU3ZmI5YjA3NDg5ZDMxM2I0MWE=', 71 | }, 72 | data: makeForm({ 73 | grant_type: 'exchange_code', 74 | exchange_code: exchangeCode, 75 | token_type: 'eg1', 76 | }), 77 | }); 78 | 79 | const { data: { deviceId, secret } } = await axios({ 80 | method: 'POST', 81 | url: `https://account-public-service-prod.ol.epicgames.com/account/api/public/account/${accountId}/deviceAuth`, 82 | headers: { 83 | Authorization: `bearer ${accessToken}`, 84 | }, 85 | }); 86 | return { 87 | accountId, deviceId, secret, displayName, 88 | }; 89 | }; 90 | 91 | const getExchangeCode = async (savingFolder) => { 92 | console.log('Checking for Chrome installation'); 93 | let chromeIsAvailable = true; 94 | try { 95 | if (process.platform !== 'win32') throw new Error(); 96 | const chromePath = await readdir(`${process.env['ProgramFiles(x86)']}\\Google\\Chrome\\Application`); 97 | if (!chromePath.find((f) => f === 'chrome.exe')) throw new Error(); 98 | } catch (e) { 99 | chromeIsAvailable = false; 100 | } 101 | 102 | let executablePath; 103 | if (chromeIsAvailable) { 104 | executablePath = `${process.env['ProgramFiles(x86)']}\\Google\\Chrome\\Application\\chrome.exe`; 105 | console.log('Chrome is already installed'); 106 | } else { 107 | const browserFetcher = puppeteer.createBrowserFetcher({ 108 | path: join(savingFolder, 'ecg'), 109 | }); 110 | console.log(await browserFetcher.canDownload('666595') ? 'Downloading Chrome. This may take a while!' : 'Chrome is already installed'); 111 | const downloadInfo = await browserFetcher.download('666595'); 112 | executablePath = downloadInfo.executablePath; 113 | } 114 | console.log('Starting chrome...'); 115 | const browser = await puppeteer.launch({ 116 | executablePath, 117 | headless: false, 118 | devtools: false, 119 | defaultViewport: { 120 | width: 500, height: 800, 121 | }, 122 | args: ['--window-size=500,800', '--lang=en-US'], 123 | }); 124 | 125 | console.log('Chrome started! Please log in'); 126 | 127 | const page = await browser.pages().then((p) => p[0]); 128 | await page.goto('https://epicgames.com/id'); 129 | await (await page.waitForSelector('#login-with-epic')).click(); 130 | await page.waitForRequest((req) => req.url() === 'https://www.epicgames.com/account/personal' && req.method() === 'GET', { 131 | timeout: 120000000, 132 | }); 133 | 134 | const oldXSRF = (await page.cookies()).find((c) => c.name === 'XSRF-TOKEN').value; 135 | let newXSRF; 136 | 137 | page.on('request', (req) => { 138 | if (['https://www.epicgames.com/id/api/authenticate', 'https://www.epicgames.com/id/api/csrf'].includes(req.url())) { 139 | req.continue({ 140 | method: 'GET', 141 | headers: { 142 | ...req.headers, 143 | 'X-XSRF-TOKEN': oldXSRF, 144 | }, 145 | }); 146 | } else if (req.url() === 'https://www.epicgames.com/id/api/exchange/generate') { 147 | req.continue({ 148 | method: 'POST', 149 | headers: { 150 | ...req.headers, 151 | 'X-XSRF-TOKEN': newXSRF, 152 | }, 153 | }); 154 | } else { 155 | req.continue(); 156 | } 157 | }); 158 | 159 | await page.setRequestInterception(true); 160 | 161 | await page.goto('https://www.epicgames.com/id/api/authenticate'); 162 | 163 | try { 164 | await page.goto('https://www.epicgames.com/id/api/csrf'); 165 | } catch (e) { /* ignore */ } 166 | 167 | newXSRF = (await page.cookies()).find((c) => c.name === 'XSRF-TOKEN').value; 168 | 169 | const pageJSON = await (await page.goto('https://www.epicgames.com/id/api/exchange/generate')).json(); 170 | await browser.close(); 171 | 172 | return pageJSON.code; 173 | }; 174 | 175 | (async () => { 176 | const savingFolder = process.env.APPDATA || (process.platform === 'darwin' ? `${process.env.HOME}/Library/Preferences` : `${process.env.HOME}/.local/share`); 177 | const userFolders = await readdir(savingFolder); 178 | if (!userFolders.find((f) => f === 'ecg')) await mkdir(join(savingFolder, 'ecg')); 179 | 180 | let deviceAuth; 181 | try { 182 | deviceAuth = JSON.parse(await readFile(join(savingFolder, 'ecg', 'deviceauth'))); 183 | } catch (e) { /* ignore */ } 184 | if (!deviceAuth || !await consoleQuestion(`Found a saved profile${deviceAuth.displayName ? ` (${deviceAuth.displayName})` : ''}! Do you want to use it? `, true)) { 185 | const exchangeCode = await getExchangeCode(savingFolder); 186 | 187 | console.log('Saving login credentials'); 188 | deviceAuth = await generateDeviceAuth(exchangeCode); 189 | await writeFile(join(savingFolder, 'ecg', 'deviceauth'), JSON.stringify(deviceAuth)); 190 | } 191 | 192 | delete deviceAuth.displayName; 193 | 194 | console.log('Generating exchange code'); 195 | const { code: exchangeCode } = await useDeviceAuth(deviceAuth); 196 | console.log(`\nYour exchange code is: ${exchangeCode}\nYour device auth is: ${JSON.stringify(deviceAuth)}\n`); 197 | console.log('Press E to copy the exchange code to your clipboard\nPress A to copy the device auth to your clipboard\n'); 198 | const itf = createInterface(process.stdin, process.stdout); 199 | emitKeypressEvents(process.stdin, itf); 200 | process.stdin.setRawMode(true); 201 | process.stdin.on('keypress', (str, key) => { 202 | process.stdout.cursorTo(0); 203 | process.stdout.write('\r\x1b[K'); 204 | if (key.name.toLowerCase() === 'e') { 205 | copyToClipboard(exchangeCode); 206 | process.stdout.write('The exchange code was copied to your clipboard'); 207 | } else if (key.name.toLowerCase() === 'a') { 208 | copyToClipboard(JSON.stringify(deviceAuth)); 209 | process.stdout.write('The device auth was copied to your clipboard'); 210 | } 211 | }); 212 | 213 | process.stdin.resume(); 214 | })(); 215 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-exchange-code", 3 | "version": "1.0.0", 4 | "description": "A tool to create exchange codes for epicgames online services", 5 | "main": "index.js", 6 | "scripts": { 7 | "app": "node index.js", 8 | "pack": "pkg ." 9 | }, 10 | "author": "ThisNils", 11 | "license": "MIT", 12 | "dependencies": { 13 | "axios": "^0.21.1", 14 | "open": "^7.4.0", 15 | "puppeteer-core": "^19.2.2", 16 | "puppeteer-extra": "^3.2.3", 17 | "puppeteer-extra-plugin-stealth": "^2.9.0" 18 | }, 19 | "pkg": { 20 | "scripts": "index.js", 21 | "targets": [ 22 | "node10-win", 23 | "node10-linux", 24 | "node10-macos" 25 | ] 26 | }, 27 | "bin": "index.js", 28 | "devDependencies": { 29 | "eslint": "^7.19.0", 30 | "eslint-config-airbnb-base": "^14.2.1", 31 | "eslint-plugin-import": "^2.22.1", 32 | "pkg": "^4.4.8" 33 | } 34 | } 35 | --------------------------------------------------------------------------------