├── .gitignore ├── LICENSE.md ├── README.md ├── authenticators ├── altening.js ├── altserver.js ├── base.js ├── easymc.js ├── mcleak.js ├── microsoft.js └── mojang.js ├── create-image.js ├── index.css ├── index.html ├── index.js ├── main.js ├── mc-multicast.js ├── mc-proxy.js ├── package.json └── screenshots ├── mc-proxy-altening.PNG ├── minecraft-multiplayer-lan-world.PNG ├── minehut-with-altening-using-proxy.PNG └── saving-alts.png /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | dist -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 SqueezedSlime 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mc-proxy proxies client connections under a different minecraft account. This account can be your alt account or an alt account from three major alt sites. 2 | Its essentially a Man in the middle proxy. It works because the minecraft client does not verify the server (with for example a certificate). Only the server will verify a minecraft user using the mojang authentication server as authority. 3 | The application is not meant to be implemented at a minecraft server. 4 | 5 | This application has two major use cases: 6 | 1. Sharing your alt account with your friend, without giving him the passwords, using this program. 7 | 2. Using alts from the 3 major alt websites without downloading (potential malicious) third party launchers. 8 | 9 | And the best thing is that you don't have to modify anything of minecraft. You can even run the proxy server on an other machine that does not even have minecraft (or even java). 10 | 11 | So this means that you can now play with those accounts without a new launcher or mods. The entire project is open source and licensed under MIT. 12 | 13 | # How to use 14 | It is pretty simple to use. 15 | 16 | 1. You select your authentication server. It can be cracked, mojang, microsoft, an alt server etc. 17 | 18 | 2. You type your credentials a (access) token or your username or password. For microsoft you will get a web login. 19 | 20 | 3. You select how this proxy server needs to listen on your device. Listen on the loopback (127.0.0.1) so that only you can access it, listen it as a open to lan world and multicast the server to your home network or make it a public server (which is in online mode and encrypted) everyone that is on the whitelist can join (maybe port forward required). You must know that only 1 person can join the proxy server at a time, of course because only 1 user with the same login can be connected to the desintation. 21 | 22 | 4. You select the destination server, where this proxy server connects to if it has accepted a connection. 23 | 24 | The proxy server will decrypt any packet receiving from a minecraft client and re-encrypt it before sending it to the destination server. 25 | And will decrypt any packet from the destination server and encrypt it again for the minecraft client. 26 | 27 | On the LAN and Host-only type the proxy server is Cracked meaning it will not encrypt the packets going to the minecraft client. 28 | 29 | For alt servers you don't have to go to the site of the alt server. You can generate tokens inside the program. 30 | 31 | # Screenshot 32 | 33 | ![Altening on this proxy](screenshots/mc-proxy-altening.PNG?raw=true "MC altening on mc proxy") 34 | 35 | Check the screenshots directory for more screenshots 36 | 37 | # Install 38 | 39 | There are three ways to install it (see https://github.com/SqueezedSlime/mc-proxy/releases/tag/1.3.0), the project does not require any dependency. 40 | 41 | 1. Windows 42 | Go to the latest release, download the setup file and double click to run it. It will install the required files on your OS and make a shortcut on your desktop. 43 | 44 | Linux 45 | Go to the latest release, downlod the AppImage, make it executable and double click to run. 46 | 47 | Mac 48 | Goto step 2. I actually haven't verified if this project works on Mac because I haven't one, however it worked instantly without changes on Windows so big chance it will work on Mac too. 49 | 50 | 51 | The only files (in the executable) are Electron and the github projects contents (nothing more because there is no dependency), you can verify the files if you like. See the create-image.js 52 | 2. Download pre-built zips in the releases tab. I made those zips exactly the same as how you will do it on step 3. 53 | 3. 54 | If you do not want to download anything from this page, you can also download Electron and extract the source code into it. 55 | Download the latest stable release. 56 | https://github.com/electron/electron/releases 57 | 58 | Download the zip file for your OS, unpack it and navigate to /resources (linux/windows) or /Electron.app/Contents/Resources/ (mac). Create the folder app if it doesn't exist and make sure it is empty. 59 | Delete the default_app.asar in resources if it exists. Now download the ZIP file of the project https://github.com/SqueezedSlime/mc-proxy/archive/refs/heads/main.zip 60 | And extract the entire contents of that ZIP in resources/app. Make sure that there isn't a sub folder in resources/app (such as resources/app/mc-proxy) with all the contents, the projects roots needs to be exactly on resources/app 61 | 62 | Click the electron executable in the root folder to start the app. 63 | 64 | # Features 65 | 66 | 1. Easy to use 67 | 68 | 2. Mods or modified launchers are not required. 69 | 70 | 3. Let your friend play with your (alt) account, without giving the credentials. 71 | 72 | 4. Tons of alts from 3 major alt servers: MCLeaks, EasyMC and The altening 73 | 74 | 5. Switch very quickly between alt accounts 75 | 76 | 6. Don't let your account be exposed to alt servers or the minecraft server 77 | 78 | By using the Proxy as server IP instead of the real server IP it is harder to accidentally expose your MC account to the server, because you always connect to the proxy server. If the proxy server is offline, you simply can't connect until you change the IP of the server. If you also use a VPN with a kill switch, you have less chance that your IP is exposed. 79 | 80 | 7. Save your alts 81 | 82 | ![Saved alts](screenshots/saving-alts.png.PNG?raw=true "Saved alts on the proxy") 83 | 84 | # How safe is it to use alts from MCLeaks, the altening or EasyMC 85 | 86 | Before you are going to use those alts, you need to know one thing for sure: the alts are never permanent. If their alts are malicious, it is not their alts, it is the (unofficial) launchers which even can be made by other users. 87 | Most of those launcher install self-signed certificates of the mojang authentication sites and redirects all connections from your browser to their sites. This means that also your non-alt logins are redirected to their sites. 88 | Even if you have switched to mojang authentication in some authenticators. 89 | 90 | On the contrary, this project does never redirect any authentication request. If you start minecraft, cracked or not, you just join the proxy server instead of the alt server. Its completely fine to join the proxy server with your own account. 91 | The proxy server does the authentication with the alt server (in a secure way), then its sends an update packet to your client to change the UUID and name for the player for the server and finally it just forwards any data from the server to you and vice versa (without modifications). 92 | 93 | # Saving alts 94 | 95 | It is very easy to save alts. Especially for altening alts because they do not support renewing tokens and if you want to save the alts you need to pay. 96 | The alts are saved as long as mc-proxy remains open. It does this by refreshing the tokens every 5 minutes. You can have as many as saved alts you desire. If you also store the tokens somewhere else, you might refresh the tokens later if you closed the proxy program (not possible for altening alts). 97 | 98 | # Creating tokens inside mc-proxy 99 | 100 | You can generate your own tokens inside the mc-proxy program without using their sites. The proxy program does not open their sites and instead uses ajax/api requests to the alt websites. You might be prompted for recaptcha if you try to generate alts or if you try to renew tokens. 101 | 102 | # About the proxy 103 | 104 | Play using ALTS on your own risk, you do not own those accounts and they can be removed anytime. To get a legit ALT, buy one from minecraft.net (on all other sites, your alt is never 100% permanent). Anyway playing with these alts can be still fun. 105 | 106 | This program does not need administrator/root permission. The windows setup program may ask for admin permissions to install it in the programs directory. If you don't want to give that permission, you can also download the zip file instead. 107 | 108 | -------------------------------------------------------------------------------- /authenticators/altening.js: -------------------------------------------------------------------------------- 1 | const { Waitlistable, makeHTTPSRequest, parseCookies } = require('./base'); 2 | const { makeAltServerRequest, AltServerAuthenticator } = require('./altserver'); 3 | 4 | 5 | var alteningStatus = { 6 | authentication: "unknown", 7 | sessions: "unknown", 8 | website: "unknown", 9 | checker: "unknown" 10 | }; 11 | 12 | var hasStatus = false; 13 | 14 | var getEndpoints = null; 15 | 16 | var alteningOptions = { 17 | getEndpoints: () => { 18 | if(!getEndpoints) getAlteningStatus(true); 19 | return getEndpoints 20 | }, 21 | certs: [ 22 | "40:B7:E3:2E:38:99:73:44:7A:1B:DB:53:6C:0C:82:C2:97:09:70:48:B2:35:8A:DD:80:B5:F5:DC:37:87:75:7D", 23 | "C4:96:7E:29:5A:B7:8E:C6:32:69:9D:D2:46:6C:7C:8C:9C:B6:02:D1:14:52:4F:41:8B:B7:15:73:EB:B6:E3:76", 24 | "17:59:AE:78:E0:08:30:9E:32:6C:43:56:7A:AE:B0:FA:45:CD:F8:EE:55:01:AB:B1:DA:5D:76:49:BB:DD:54:EE" 25 | ], 26 | hostnames: ["api.thealtening.com"] 27 | } 28 | 29 | function getAlteningStatus(refresh = false) { 30 | if(!refresh && hasStatus) return Promise.resolve(Object.assign({}, alteningStatus)); 31 | 32 | var pr; 33 | getEndpoints = new Promise(callback => { 34 | pr = makeAltServerRequest(alteningOptions, 'api.thealtening.com', '/status').then(json => { 35 | for(var name in alteningStatus) { 36 | alteningStatus[name] = (json.status[name] && (json.status[name] === 'OK' ? 'ok' : 'down')) || 'unknown'; 37 | } 38 | hasStatus = true; 39 | callback({ 40 | authentication: String(json.endpoints.server_authentication), 41 | session: String(json.endpoints.server_session) 42 | }); 43 | return (Object.assign({}, alteningStatus)); 44 | }).catch(ex => { 45 | callback(ex); 46 | throw ex; 47 | }); 48 | }); 49 | 50 | return pr; 51 | } 52 | 53 | 54 | class AlteningAuthenticator extends AltServerAuthenticator { 55 | /** 56 | * Create a new AlteningAuthenticator, use login() to redeem the alt token 57 | * @param {string} altToken An altening alt token, you can get one by https://thealtening.com 58 | */ 59 | constructor(altToken) { 60 | super(alteningOptions, altToken); 61 | } 62 | } 63 | 64 | /** 65 | * Create a AlteningAuthenticator that is already authenticated 66 | * @param {string} altToken An altening alt token, you can get one by https://thealtening.com 67 | * @returns {Promsie} A promise that resolves to a pre-authenticated AlteningAuthenticator (login() is already called) or rejects with an error. 68 | */ 69 | function redeemAlteningToken(altToken) { 70 | try { 71 | var session = new AlteningAuthenticator(altToken); 72 | return session.login().then(() => session); 73 | } catch(ex) { 74 | return Promise.reject(ex); 75 | } 76 | } 77 | 78 | class AlteningGenerateSession extends Waitlistable { 79 | constructor() { 80 | super(); 81 | } 82 | 83 | /** 84 | * Authenticate with captcha code. The requests are rate limited. 85 | * 86 | * NOTE: 87 | * You must call generateToken() after 6 seconds of the authenticate() request 88 | * Otherwise you will get the rate_limited error 89 | * @param {strign} captcha re captcha code for the site 90 | * @returns 91 | */ 92 | authenticate(captcha) { 93 | return this.addWaitlist(() => makeHTTPSRequest({ 94 | host: 'api.thealtening.com', 95 | path: '/free/initialise', 96 | method: 'post', 97 | headers: { 98 | 'Accept': '*/*', 99 | 'Origin': 'https://thealtening.com', 100 | 'Referer': 'https://thealtening.com/' 101 | }, 102 | supplyHeaders: true, 103 | rejectUnauthorized: false, 104 | body: { captcha } 105 | }).then(res => { 106 | this.token = parseCookies(res.headers['set-cookie']); 107 | if(!this.token) throw new Error("No cookies provided"); 108 | })).then(() => alteningGenerator.validate()) 109 | } 110 | 111 | validate() { 112 | if(!this.token) return Promise.resolve(false); 113 | return this.addWaitlist(() => makeHTTPSRequest({ 114 | host: 'api.thealtening.com', 115 | path: '/free/validate', 116 | method: 'get', 117 | text: true, 118 | rejectUnauthorized: false, 119 | headers: { 120 | 'Accept': '*/*', 121 | 'Cookie': this.token, 122 | 'Origin': 'https://thealtening.com', 123 | 'Referer': 'https://thealtening.com/' 124 | } 125 | }).then(() => true)); 126 | } 127 | 128 | generateToken() { 129 | if(!this.token) return Promise.reject(new Error("This generator is not authenticated")); 130 | return this.addWaitlist(() => makeHTTPSRequest({ 131 | host: 'api.thealtening.com', 132 | path: '/free/generate', 133 | method: 'get', 134 | rejectUnauthorized: false, 135 | headers: { 136 | 'Accept': '*/*', 137 | 'Cookie': this.token, 138 | 'Origin': 'https://thealtening.com', 139 | 'Referer': 'https://thealtening.com/' 140 | } 141 | }).then(res => { 142 | if(!res.token || typeof res.token !== 'string') throw new Error("No token from generator"); 143 | return res.token; 144 | })); 145 | } 146 | } 147 | 148 | function createAlteningTokenGenerator(captcha) { 149 | var generator = new AlteningGenerateSession(); 150 | return generator.authenticate(captcha).then(() => generator); 151 | } 152 | 153 | module.exports = { 154 | getAlteningStatus, 155 | AlteningAuthenticator, 156 | redeemAlteningToken, 157 | alteningStatus, 158 | alteningOptions, 159 | AlteningGenerateSession, 160 | createAlteningTokenGenerator 161 | } -------------------------------------------------------------------------------- /authenticators/altserver.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | const tls = require('tls'); 3 | const { generateClientToken } = require('./mojang'); 4 | const { MCAuthenticator } = require('./base'); 5 | 6 | 7 | //The altening uses hacks with HTTPS requests 8 | //So it deserves a makeRequest function on its own... 9 | function makeAltServerRequest({ getEndpoints, certs, hostnames }, host, endpoint, body) { 10 | //needEndpoints is true if it uses the mojang endpoints (masked on the altening servers) with self signed certs. 11 | //false if it just uses an normal https endpoint (such as https://api.thealtening.com) 12 | //It can get the ips by https://api.thealtening.com/status 13 | var needEndpoints = ['sessionserver.mojang.com', 'authserver.mojang.com'].includes(host); 14 | //a promise that resolves with the endpoints (if needed), otherwise empty object 15 | var onEndpoints = Promise.resolve(needEndpoints ? getEndpoints() : {}); 16 | 17 | //headers for the post reqest 18 | var postHeaders = {}; 19 | var data = null; 20 | if(body) { 21 | data = Buffer.from(JSON.stringify(body), 'utf-8'); 22 | postHeaders = { 23 | 'Content-Type': 'application/json', 24 | 'Content-Length': data.length 25 | } 26 | } 27 | 28 | 29 | return onEndpoints.then(endpoints => new Promise((resolve, reject) => { 30 | if(endpoints instanceof Error) { 31 | reject(endpoints); 32 | return; 33 | } 34 | 35 | //if needendpoints=true: 36 | //We must make sure that the request EXACTLY match as the one that will be generated by the vanilla minecraft launcher 37 | //The only variable that changes, is the destination IP for IP packets, the rest remain the same (HOST, SNI, headers, path and payload) 38 | //Notice: there is not only a HTTP HOST header, but SSL also has a SNI (server name) field before the encryption. 39 | // So the server knows which certificate it must use. 40 | // 41 | // 42 | //The altening authenticator just redirects any IP packet originally destinated to authserver.mojang.com/sessionserver.mojang.com to the altening servers 43 | //To ensure that you don't get SSL verfication errors, the altening authenticator installs their certs on your windows system. 44 | 45 | var req = https.request({ 46 | method: "POST", 47 | //a mapping to get the altening replacement IP for the authentication servers of Mojang. 48 | host: ({'sessionserver.mojang.com': endpoints.session, 'authserver.mojang.com': endpoints.authentication})[host] || host, 49 | port: 443, 50 | path: endpoint, 51 | servername: host, //servername == SNI for ssl 52 | rejectUnauthorized: false, 53 | headers: { 54 | 'Host': host, 55 | ...postHeaders 56 | } 57 | }, res => { 58 | var data = []; 59 | var len = 0; 60 | res.on('error', ex => reject(ex)); 61 | res.on('data', d => { 62 | len += d.length; 63 | //we limit the amount of data 64 | if(len > 65535) { 65 | reject(new Error("Too much data")); 66 | try{res.destroy(new Error("Too much data"));}catch(_){} 67 | return; 68 | } 69 | data.push(d); 70 | }); 71 | res.on('end', () => { 72 | try { 73 | if(res.statusCode === 204 || data.length < 1) { 74 | if(len > 0) reject(new RangeError("Data on no content")); 75 | else resolve(null); 76 | return; 77 | } 78 | if(res.statusCode !== 200) { 79 | var message = "Unknown error"; 80 | try { 81 | var json = JSON.parse(Buffer.concat(data).toString('utf-8')); 82 | if(json && typeof json == 'object') message = json.errormessage || json.errorMessage || json.message || json.description || json.error || message; 83 | } catch(_) {} 84 | var err = new Error("Request failed: " + res.statusCode + " " + message); 85 | err.response = res; 86 | reject(err); 87 | return; 88 | } 89 | resolve(JSON.parse(Buffer.concat(data).toString('utf-8'))); 90 | } catch(ex) { 91 | reject(ex); 92 | } 93 | }); 94 | }).on('socket', socket => socket.prependListener('secure', () => { 95 | //we check the cert by ourself. 96 | try { 97 | //see https://github.com/nodejs/node/blob/master/lib/_tls_wrap.js 98 | //if isSessionReused() == true, checkServerIdentity is not called. So we do not call it either. 99 | if(socket.isSessionReused()) return; 100 | //check cert by ourself 101 | var cert = socket.getPeerCertificate(true); 102 | if(!cert || !cert.fingerprint256) throw new Error("No cert"); 103 | if(!needEndpoints) { //useless if needEndpoints == true. Should always return an error then. 104 | try { 105 | if(tls.checkServerIdentity(host, cert) == null) return; 106 | } catch(ex){ 107 | socket.destroy(ex); 108 | return; 109 | } 110 | } 111 | if(!['sessionserver.mojang.com', 'authserver.mojang.com', ...hostnames].includes(host)) throw new Error("Invalid hostname") 112 | while(cert.issuerCertificate && cert.issuerCertificate !== cert) cert = cert.issuerCertificate; 113 | if(!certs.includes(String(cert.fingerprint256).toUpperCase())) throw new Error("Invalid cert fingerprint"); 114 | } catch(ex) { 115 | socket.destroy(ex); 116 | return; 117 | } 118 | })).on('error', ex => reject(ex)); 119 | if(data) req.write(data); 120 | req.end(); 121 | })); 122 | } 123 | 124 | class AltServerAuthenticator extends MCAuthenticator { 125 | /** 126 | * Create a new AltServerAuthenticator, for (most) websites where you can redeem alt tokens to get accounts, use login() to redeem the alt token 127 | * @param {{ getEndpoints: async () => {[host: string]: string}, certs: string[], hostnames: []}} options Config that describes 128 | * @param {string} altToken An alt token 129 | */ 130 | constructor(options, altToken) { 131 | super(); 132 | this.serverOptions = options; 133 | this.altToken = String(altToken); 134 | } 135 | 136 | //You may want to look at this: https://wiki.vg/Authentication 137 | //The endpoints are the same 138 | 139 | /** 140 | * Redeem the ALT token, that is used to construct this class 141 | * @returns {Promise} Promise that resolves with this class or rejects with an error. 142 | */ 143 | login() { 144 | return this.addWaitlist(() => generateClientToken() 145 | .then(clientToken => this.clientToken = clientToken) 146 | .then(() => makeAltServerRequest(this.serverOptions, 'authserver.mojang.com', '/authenticate', { 147 | agent: { 148 | name: "minecraft", 149 | version: 1 150 | }, 151 | username: this.altToken, 152 | password: 'anything', //password does not matter, we also use the same value that their /info endpoint (not used here) returns in the password field. 153 | clientToken: this.clientToken, 154 | requestUser: true 155 | })) 156 | .then(result => { 157 | if(!result || typeof result !== 'object' || !result.accessToken || typeof result.accessToken !== 'string' || !result.selectedProfile || typeof result.selectedProfile !== 'object' || !result.selectedProfile.name || typeof result.selectedProfile.name !== 'string' || !result.selectedProfile.id || typeof result.selectedProfile.id !== 'string') 158 | throw new TypeError("Invalid JSON data"); 159 | Object.assign(this, { 160 | name: result.selectedProfile.name, 161 | uuid: result.selectedProfile.id, 162 | accessToken: result.accessToken, 163 | created: new Date(), 164 | refreshed: new Date() 165 | }); 166 | }) 167 | ); 168 | } 169 | 170 | /** 171 | * Refresh the access token (and expands lifetime of BOTH the alt token and alt session) 172 | * @returns {Promise} promise that resolves with null or rejects with an Error 173 | */ 174 | refresh() { 175 | var accessToken, clientToken, uuid, name; 176 | return this.addWaitlist(() => Promise.resolve() 177 | .then(() => { 178 | if(!this.accessToken) throw new Error("You must first redeem the code"); 179 | accessToken = this.accessToken; 180 | clientToken = this.clientToken; 181 | uuid = this.uuid; 182 | name = this.name; 183 | }) 184 | .then(() => makeAltServerRequest(this.serverOptions, 'authserver.mojang.com', '/refresh', { 185 | accessToken, 186 | clientToken, 187 | selectedProfile: { 188 | id: uuid, 189 | name 190 | }, 191 | requestUser: true 192 | })).then(result => { 193 | if(!result || typeof result !== 'object' || !result.accessToken || typeof result.accessToken !== 'string') 194 | throw new TypeError("Invalid JSON data"); 195 | return Object.assign(this, { 196 | accessToken: result.accessToken, 197 | created: new Date(), 198 | refreshed: new Date() 199 | }); 200 | }) 201 | ); 202 | } 203 | 204 | /** 205 | * Validate the current session. If not valid you can try to redeem the alt token again 206 | * @returns {Promise} resolves with true or false (never errors) 207 | */ 208 | validate() { 209 | return this.addWaitlist(() => Promise.resolve() 210 | .then(() => { 211 | if(!this.accessToken) throw new Error("You must first redeem the code"); 212 | }) 213 | .then(() => makeAltServerRequest(this.serverOptions, 'authserver.mojang.com', '/validate', { 214 | accessToken: this.accessToken, 215 | clientToken: this.clientToken 216 | })).then(res => { 217 | if(res !== null) throw new Error("Expected 204 if valid"); 218 | return true; 219 | }, () => false) 220 | ); 221 | } 222 | 223 | /** 224 | * Invalidates the current (fake altening) session token, you can still try to redeem it again with redeem() 225 | * So this is maybe useless, because you are not invalidating the alt token (which is, so far I found, not possible). 226 | * @returns {Promise} 227 | */ 228 | invalidate() { 229 | this.keepAlive(false); 230 | return this.addWaitlist(() => Promise.resolve() 231 | .then(() => { 232 | if(!this.accessToken) throw new Error("You must first redeem the code"); 233 | }) 234 | .then(() => makeAltServerRequest(this.serverOptions, 'authserver.mojang.com', '/invalidate', { 235 | accessToken: this.accessToken, 236 | clientToken: this.clientToken 237 | })) 238 | .then(res => { 239 | Object.assign(this, { 240 | accessToken: null, 241 | clientToken: null 242 | }); 243 | if(res !== null) throw new Error("Expected 204 if valid"); 244 | return true; 245 | }) 246 | ); 247 | } 248 | 249 | signServerHash(serverHash) { 250 | return this.addWaitlist(() => Promise.resolve() 251 | .then(() => { 252 | if(!this.accessToken) throw new Error("You must first redeem the code"); 253 | }).then(() => makeAltServerRequest(this.serverOptions, 'sessionserver.mojang.com', '/session/minecraft/join', { 254 | accessToken: this.accessToken, 255 | selectedProfile: this.uuid, 256 | serverId: serverHash 257 | })) 258 | .then(res => { 259 | if(res != null) throw new Error("Join server expects 204"); 260 | return true; 261 | }) 262 | ); 263 | 264 | } 265 | 266 | 267 | } 268 | 269 | module.exports = { makeAltServerRequest, AltServerAuthenticator }; 270 | -------------------------------------------------------------------------------- /authenticators/base.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | const { createProxyServer, resolveMCSrvRecord } = require('../mc-proxy'); 3 | 4 | function makeHTTPSRequest({ host, port, path, method, headers, body, text, supplyHeaders, rejectUnauthorized }) { 5 | var stack = new Error("Caused by"); 6 | return (new Promise((resolve, reject) => { 7 | if (!headers) headers = {}; 8 | if (body && typeof body === 'object') { 9 | body = JSON.stringify(body); 10 | headers['Content-Type'] = 'application/json'; 11 | } 12 | if (!headers["Accept"]) headers["Accept"] = "application/json"; 13 | if(!headers['User-Agent']) headers['User-Agent'] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36"; 14 | if (!port) port = 443; 15 | if (!method) method = 'GET'; 16 | if (body) body = Buffer.from(String(body), 'utf-8'); 17 | if (body) headers['Content-Length'] = String(body.length); 18 | if(typeof rejectUnauthorized !== 'boolean') rejectUnauthorized = true; 19 | var req = https.request({ host, port, path, method, headers, rejectUnauthorized, timeout: 4000 }, res => { 20 | var data = []; 21 | var len = 0; 22 | res.on('error', ex => reject(ex)); 23 | res.on('data', d => { 24 | len += d.length; 25 | if (len > 524288) { 26 | res.destroy(new RangeError("Too much data")); 27 | return; 28 | } 29 | data.push(d); 30 | }); 31 | res.on('end', () => { 32 | try { 33 | if (res.statusCode === 302 && res.headers.location) { 34 | resolve(supplyHeaders ? { 35 | headers: res.headers, 36 | redirect: res.headers.location 37 | } : {redirect: res.headers.location}); 38 | return; 39 | } 40 | if (res.statusCode === 204) { 41 | if (len > 0) reject(new RangeError("Data on no content")); 42 | else resolve(null); 43 | return; 44 | } 45 | if (res.statusCode !== 200) { 46 | var message = "Unknown error"; 47 | if(!text) { 48 | try { 49 | var json = JSON.parse(Buffer.concat(data).toString('utf-8')); 50 | if (json && typeof json == 'object') message = json.errormessage || json.errorMessage || json.message || json.description || json.error || message; 51 | } catch (_) { } 52 | } 53 | var err = new Error("Request failed: " + res.statusCode + " " + message); 54 | err.response = res; 55 | reject(err); 56 | return; 57 | } 58 | var str = Buffer.concat(data).toString('utf-8'); 59 | var dat = text ? str : JSON.parse(str); 60 | if(supplyHeaders) { 61 | if(typeof dat == 'string') dat = {data: dat}; 62 | dat.headers = res.headers; 63 | } 64 | resolve(dat); 65 | } catch (ex) { 66 | reject(ex); 67 | } 68 | }); 69 | }).on('error', err => reject(err)).on('close', () => reject(new Error("Closed"))); 70 | if (body) req.write(body); 71 | req.end(); 72 | })).catch(ex => { 73 | if (ex.stack && stack.stack) ex.stack += stack.stack; 74 | throw ex; 75 | }) 76 | } 77 | 78 | 79 | class Waitlistable { 80 | constructor() { 81 | this.waitlist = Promise.resolve(true); 82 | } 83 | 84 | /** 85 | * Call this function after all other operations completed 86 | * And a next operation will only occur if the operation of this function completes. 87 | * 88 | * Function must return a promise. A operation is also completed, if promise rejects. 89 | * Function is only called if other operations completed. 90 | * 91 | * Returns a new promise, that resolves/rejects with the promise of func. 92 | * @param {() => Promise} func the function that will return a new operation, if all other operations completed 93 | * @returns {Promise} A promise that will resolves/rejects with the return value of the new operation. 94 | */ 95 | addWaitlist(func) { 96 | var oldwaitlist = this.waitlist; 97 | var callback; 98 | this.waitlist = new Promise(resolve => callback = resolve); 99 | var promise = oldwaitlist.then(typeof func == 'function' ? func : () => func); 100 | promise.finally(() => callback()); 101 | return promise; 102 | } 103 | } 104 | 105 | class MCAuthenticator extends Waitlistable { 106 | /** 107 | * Constructs a new MCAuthenticator. 108 | * 109 | * In the constructor of a implementing class it must add a argument where the user can supply credentials. 110 | * The constructor only stores the credentials and does not validate them or make any https requests. 111 | * That will happen in the login() method. 112 | */ 113 | constructor() { 114 | super(); 115 | if (this.constructor === MCAuthenticator) throw new Error("This is an abstract class"); 116 | this.keepAliveInterval = null; 117 | /** 118 | * The name of the user. A class MUST set this value after the login() method succeeds. 119 | * @return {string} 120 | */ 121 | this.name = null; 122 | /** 123 | * The uuid of the user. A class MUST set this value after the login() method succeeds. 124 | * 125 | * The uuid is wihout dashes 126 | * @returns {string} 127 | */ 128 | this.uuid = null; 129 | } 130 | 131 | /** 132 | * Get a session token from a (third-party) authentication server 133 | * @returns {Promise} 134 | */ 135 | login() { 136 | return Promise.reject(new TypeError("This class does not implement the required login method")); 137 | } 138 | 139 | /** 140 | * Refreshes the current session token. 141 | * 142 | * If not implemented, this function will just call login() again to get a new session token. 143 | * @returns {Promise} 144 | */ 145 | refresh() { 146 | return this.login(); 147 | } 148 | 149 | /** 150 | * Validate this session if it is logged in. Returns false if not valid. 151 | * 152 | * May return true even if session is not valid, in such case, try to login again(). 153 | * Depends on implementation 154 | * @returns {Promise} 155 | */ 156 | validate() { 157 | return Promise.resolve(true); 158 | } 159 | 160 | /** 161 | * Invalidates this session. May silently fail if this function is not supported. 162 | * 163 | * You can always get a new session with login() (if credentials are still valid) 164 | * 165 | * Invalidate MUST stop the keepAlive (this.keepAlive(false)) 166 | * @returns 167 | */ 168 | invalidate() { 169 | this.keepAlive(false); 170 | return Promise.resolve(null); 171 | } 172 | 173 | /** 174 | * Install an interval that will refresh the access token every 5 minutes 175 | * @param {boolean} run to enable/disable it. 176 | * @param {Error => void} onError if an error occured, the keep alive will terminate 177 | * @returns 178 | */ 179 | keepAlive(run = true, onError) { 180 | if (run) { 181 | if (this.keepAliveInterval) return; 182 | this.keepAliveInterval = setInterval(() => { 183 | if (!this.accessToken) return; 184 | this.refresh().catch(ex => { 185 | if (onError) onError(ex); 186 | this.keepAlive(false); 187 | }); 188 | }, 300000) 189 | } else { 190 | if (!this.keepAliveInterval) return; 191 | clearInterval(this.keepAliveInterval); 192 | this.keepAliveInterval = null; 193 | } 194 | } 195 | 196 | /** 197 | * Sign server hash with the account using the mojang session server. 198 | * 199 | * Try createProxyServer() for a much simpler API 200 | * 201 | * What is the server hash? 202 | * You get the server hash after you received the encryption response packet from the server. 203 | * Server hash is then hexdigest(sha1(serverName + 128 bits (16 bytes) shared AES secret + 1024 bits RSA public key of server in DER format)) 204 | * hexdigest is non standard. It is actually pretty simple: the sha1 hash as a 20-byte signed (two complement) integer encoded as a hex number (base 16, not base 10) and minus if negative. 205 | * library functions that convert binary to hex are usually unsigned. 206 | * the serverName value is usually empty. 207 | * See https://wiki.vg/Protocol_Encryption for more details about the encryption. 208 | * 209 | * About privacy: 210 | * A third-party authentication server can't know the server you are playing on because he can't decrypt the SHA1 hash. 211 | * However, mojang can still see it, because the server must validate the server hash with the hasJoined endpoint (and mojang then knows the IP of the server). 212 | * 213 | * It is recommended to use keepAlive to make sure that your session never invalidates if you are using this function. 214 | * @param {String} serverHash the server hash (calculated from the SHA1) as a MC hexdigest string 215 | * @returns {Promise} a promise that resolves to null if success and rejects with an Error if failed. 216 | */ 217 | signServerHash(serverHash) { 218 | return Promise.reject(new Error("This class does not implement the required sign server hash method")) 219 | } 220 | 221 | /** 222 | * Create a proxy server, you can connect to this proxy server using the minecraft client without any modification, man in the middle attack etc. 223 | * The proxy server does then the authentication with (third-party) authentication server to join the original server. 224 | * You can only use this function if you already exchanged the alt token for a session. 225 | * 226 | * The original player name will be censored. It can still be verified, but then it will only be used to verify it and will never be sent to the original server. 227 | * 228 | * If the server is hosted on loopback, you can set the server in offline mode because it is impossible to eavesdrop a loopback. 229 | * However if you also host it to the outside world, enable online mode (setting whitelist to [] or a list of allowed players) because offline mode does NOT have encryption 230 | * meaning the connection between you and the proxy server can be eavesdropped and you lose the privacy. 231 | * 232 | * It is not possible to use offline mode with encryption. 233 | * 234 | * The proxy server will then decrypt any packet comming from the client and encrypt it again with the shared secret between proxy and the original server. (and vice versa) 235 | * 236 | * @param {string} host The host to connect to (can have a SRV record) 237 | * @param {number} port the port to connect to 238 | * @param {undefined | null | string[]} whitelist empty array if premium account is necessary, null if the proxy server is in offline mode, non empty if you only allow specific players (with premium accounts). Undefined if default (localhost offline, rest online) 239 | * @returns 240 | */ 241 | createProxyServer(host, port, whitelist = undefined, sharedSecret, displayHost = null) { 242 | if (whitelist === undefined) { 243 | if (host == 'localhost' || host.startsWith('127.')) { 244 | whitelist = null; 245 | } else { 246 | whitelist = []; 247 | } 248 | } 249 | var isOffline = !whitelist; 250 | if (!whitelist) whitelist = []; 251 | 252 | var hostPromise = resolveMCSrvRecord(host); 253 | 254 | return createSessionProxyServer(async request => { 255 | var host = (await hostPromise).name; 256 | var req = ({ 257 | host: host, 258 | port: ((await hostPromise).port) || port, 259 | displayHost: displayHost || host, 260 | cracked: isOffline, 261 | getSecret: typeof sharedSecret === 'function' ? sharedSecret : async () => sharedSecret, 262 | getSession: async () => this, 263 | verifyLogin: async () => !whitelist.length || whitelist.includes(request.username), 264 | getDisconnectMessage: async reason => reason == 'user-denied' ? { text: 'You are not whitelisted on this server.' } : null, 265 | }); 266 | return req; 267 | }); 268 | } 269 | 270 | } 271 | 272 | class CrackedMCAuthenticator extends MCAuthenticator { 273 | constructor(name) { 274 | super(); 275 | this.accessToken = 'cracked'; 276 | this.name = name; 277 | this.uuid = '00000000000000000000000000000000'; 278 | } 279 | 280 | login() { 281 | return Promise.resolve(true); 282 | } 283 | 284 | keepAlive() { 285 | return; 286 | } 287 | 288 | signServerHash(hash) { 289 | return Promise.resolve(true); 290 | } 291 | } 292 | 293 | /** 294 | * Returns a new session that represents a cracked minecraft account (username only) 295 | * 296 | * You can only use this session to join cracked server (and Open to LAN). 297 | * If you try to join a online server, it will give you 'Cannot verify user' error. 298 | * @param {string} name the name 299 | * @returns {CrackedMCAuthenticator} A new authenticator that lets you only login cracked servers, with the name 300 | */ 301 | function createCrackedSession(name) { 302 | return new CrackedMCAuthenticator(name); 303 | } 304 | 305 | /** 306 | * Create a new proxy server, that will authenticate using a session. 307 | * 308 | * Options is a function that accepts a proxy request and returns a proxy response (with some changes): 309 | * 1. getSession() to get the MCAuthenticator session (async) 310 | * 2. getSecret() optional to get a pre defined shared secret (for debugging) 311 | * @param {((request: {state: 'login' | 'status', host: string, port: number, version: number, username: string}) => (Promise<{ host: string, port: number, version?: number, getSession: () => (MCAuthenticator | Promise (Buffer | Promise) }> | { host: string, port: number, version?: number, getSession: () => (MCAuthenticator | Promise (Buffer | Promise) })) | { host: string, port: number, version?: number, getSession: () => (MCAuthenticator | Promise (Buffer | Promise) }} options 312 | * @returns 313 | */ 314 | function createSessionProxyServer(options) { 315 | if (typeof options !== 'function') (data => options = () => data)(options); 316 | return createProxyServer(request => Promise.resolve(options(request)).then(res => { 317 | res.getUser = async () => { 318 | var session = await res.getSession(); 319 | var sharedSecret = (await res.getSecret()) || undefined; 320 | return { 321 | username: session.name, 322 | uuid: session.uuid, 323 | joinServer: session.signServerHash.bind(session), 324 | sharedSecret 325 | } 326 | }; 327 | return res; 328 | })); 329 | } 330 | 331 | function parseCookies(cookies, saved = '') { 332 | if(!cookies) cookies = []; 333 | if(!(cookies instanceof Array)) cookies = [cookies]; 334 | var changed = []; 335 | return cookies.map(x => /^\s*([a-zA-Z0-9_]+)\s*=\s*([^;]+)\s*/.exec(x)).filter(x => x && x[1] && x[2]).map(x => { 336 | changed.push(x[1].trim()); 337 | return x[1].trim() + '=' + x[2].trim(); 338 | }).concat(saved ? String(saved).split(';').map(x => x.split('=')).filter(x => x && x[0] && x[1]).map(x => [x[0], ...x.slice(1)]).filter(x => !changed.includes(x[0].trim())).map(x => x[0].trim() + '=' + x[1].trim()) : []).join('; '); 339 | } 340 | 341 | module.exports = { 342 | Waitlistable, 343 | MCAuthenticator, 344 | createCrackedSession, 345 | createSessionProxyServer, 346 | makeHTTPSRequest, 347 | parseCookies 348 | }; 349 | -------------------------------------------------------------------------------- /authenticators/easymc.js: -------------------------------------------------------------------------------- 1 | const { Waitlistable, makeHTTPSRequest, parseCookies } = require('./base'); 2 | const { AltServerAuthenticator } = require('./altserver') 3 | 4 | var easyMCOptions = { 5 | getEndpoints: () => { 6 | return {authentication: "51.68.172.243", session: "51.68.172.243"} 7 | }, 8 | certs: [ 9 | "5E:91:EE:69:0C:24:5C:6C:75:0E:15:51:75:26:98:1B:42:36:C9:EC:69:B0:1A:DD:FE:11:4D:88:8D:57:1D:83", 10 | "90:B4:BD:5E:12:F4:41:D0:97:A2:A4:DE:C3:67:41:E8:C0:5C:3D:EA:BE:FC:DB:DE:F5:99:40:62:07:66:6F:5B" 11 | //api cert not needed because it is valid 12 | ], 13 | hostnames: ["api.easymc.io"] //not used 14 | } 15 | 16 | class EasyMCAuthenticator extends AltServerAuthenticator { 17 | /** 18 | * Create a new EasyMCAuthenticator, use login() to redeem the alt token 19 | * @param {string} altToken An altening alt token, you can get one by https://easymc.io 20 | */ 21 | constructor(altToken) { 22 | super(easyMCOptions, altToken); 23 | } 24 | } 25 | 26 | /** 27 | * Create a EasyMCAuthenticator that is already authenticated 28 | * @param {string} altToken An altening alt token, you can get one by https://easymc.io 29 | * @returns {Promsie} A promise that resolves to a pre-authenticated EasyMCAuthenticator (login() is already called) or rejects with an error. 30 | */ 31 | function redeemEasyMCToken(altToken) { 32 | try { 33 | var session = new EasyMCAuthenticator(altToken); 34 | return session.login().then(() => session); 35 | } catch(ex) { 36 | return Promise.reject(ex); 37 | } 38 | } 39 | 40 | //EasyMC identifies users based on IP not Cookies. So there is no EasyMC generate session. 41 | 42 | function generateEasyMCToken(recaptcha = '') { 43 | return makeHTTPSRequest({ 44 | host: 'api.easymc.io', 45 | path: '/v1/token?new=true' + (recaptcha ? '&captcha=' + encodeURIComponent(recaptcha) : ''), 46 | method: 'get', 47 | headers: { 48 | 'Accept': '*/*', 49 | 'Origin': 'https://easymc.io', 50 | 'Referer': 'https://easymc.io/' 51 | } 52 | }).then(res => { 53 | if(!res.token) throw new Error("No new token supplied"); 54 | return res.token; 55 | }); 56 | } 57 | 58 | function renewEasyMCToken(alttoken, recaptcha) { 59 | return makeHTTPSRequest({ 60 | host: 'api.easymc.io', 61 | path: '/v1/token/renew', 62 | method: 'post', 63 | headers: { 64 | 'Accept': '*/*' 65 | }, 66 | body: { 67 | params: { 68 | token: alttoken, 69 | captcha: recaptcha 70 | } 71 | } 72 | }).then(res => { 73 | if(!res.token) throw new Error("No new token supplied"); 74 | return res.token; 75 | }) 76 | } 77 | 78 | module.exports = { 79 | EasyMCAuthenticator, 80 | redeemEasyMCToken, 81 | easyMCOptions, 82 | generateEasyMCToken, 83 | renewEasyMCToken 84 | } -------------------------------------------------------------------------------- /authenticators/mcleak.js: -------------------------------------------------------------------------------- 1 | //Mcleaks (unlike thealtenticator) provides their api: see: https://mcleaks.net/apidoc 2 | 3 | const { Waitlistable, MCAuthenticator, makeHTTPSRequest, parseCookies } = require('./base'); 4 | 5 | function makeMCLeakRequest(endpoint, body) { 6 | return makeHTTPSRequest({ host: 'auth.mcleaks.net', method: body == null ? 'GET' : 'POST', path: '/v1/' + endpoint, body }); 7 | } 8 | 9 | 10 | class MCLeakAuthenticator extends MCAuthenticator { 11 | constructor(altToken) { 12 | super(); 13 | this.altToken = String(altToken); 14 | } 15 | 16 | /** 17 | * Redeem the alt token, to get an access token, the name and uuid. 18 | * 19 | * You cannot invalidate the access token, and to refresh it you call login() again. 20 | * Actually the refresh() function just calls login() 21 | * @returns {Promise} 22 | */ 23 | login() { 24 | return this.addWaitlist(() => makeMCLeakRequest('redeem', { token: this.altToken }).then(res => { 25 | if(!res.success) throw new Error("Request failed: " + (res.errorMessage || 'Unknown error')); 26 | this.accessToken = res.result.session; 27 | //Why does this API not provide a UUID?? 28 | return makeHTTPSRequest({ host: 'api.mojang.com', path: '/users/profiles/minecraft/' + encodeURIComponent(String(res.result.mcname)), method: 'get'}) 29 | })).then(res => { 30 | this.name = res.name; 31 | this.uuid = res.id; 32 | this.created = new Date(); 33 | this.refresed = new Date(); 34 | return this; 35 | }); 36 | } 37 | 38 | validate() { 39 | return this.signServerHash('-25c65c11a194b4f2cdaa40106a9fe76f5027f8f7'); 40 | } 41 | 42 | refresh() { 43 | return this.validate(); 44 | } 45 | 46 | signServerHash(serverId) { 47 | return this.addWaitlist(() => Promise.resolve() 48 | .then(() => { 49 | if(!this.accessToken) throw new Error("No access token, use login() first"); 50 | }) 51 | .then(() => makeMCLeakRequest('joinserver', { 52 | session: this.accessToken, 53 | mcname: this.name, 54 | serverhash: serverId, 55 | server: 'censored.example.com:25565' //we will not provide the server address for privacy. It is also not important for https://sessionserver.mojang.com/session/minecraft/join 56 | }).then(res => { 57 | if(!res.success) throw new Error("Request failed: " + (res.errorMessage || 'Unknown error')); 58 | return true; 59 | })) 60 | ) 61 | } 62 | } 63 | 64 | function redeemMCLeakToken(altToken) { 65 | try { 66 | var session = new MCLeakAuthenticator(altToken); 67 | return session.login().then(() => session); 68 | } catch(ex) { 69 | return Promise.reject(ex); 70 | } 71 | } 72 | 73 | class MCLeakGenerateSession extends Waitlistable { 74 | constructor() { 75 | super(); 76 | } 77 | 78 | generateToken(recaptchaCode = '') { 79 | var req = { 80 | host: 'mcleaks.net', 81 | path: '/get', 82 | method: 'post', 83 | text: true, 84 | supplyHeaders: true, 85 | body: 'posttype=true', 86 | headers: { 87 | "Content-Type": "application/x-www-form-urlencoded", 88 | "Accept": "*/*", 89 | "Origin": "https://mcleaks.net", 90 | "Referer": "https://mcleaks.net/" 91 | } 92 | }; 93 | if(this.token) req.headers.Cookie = this.token; 94 | if(recaptchaCode) { 95 | req.body = "posttype=true&g-recaptcha-response=" + encodeURIComponent(recaptchaCode); 96 | } 97 | 98 | return this.addWaitlist(() => makeHTTPSRequest(req).then(res => { 99 | var token = parseCookies(res.headers['set-cookie'], this.token); 100 | if(!token) throw new Error("No cookies provided from MCLeaks"); 101 | this.token = token; 102 | if(res && res.redirect) { 103 | return makeHTTPSRequest({ 104 | host: 'mcleaks.net', 105 | path: '/get', 106 | method: 'get', 107 | text: true, 108 | supplyHeaders: true, 109 | headers: { 110 | "Accept": "*/*", 111 | "Cookie": token, 112 | "Referer": "https://mcleaks.net/" 113 | } 114 | }) 115 | } else return res; 116 | }).then(res => { 117 | if(!res) throw new Error("No response received"); 118 | var token = parseCookies(res.headers['set-cookie'], this.token); 119 | if(!token) throw new Error("No cookies provided from MCLeaks"); 120 | this.token = token; 121 | res = res.data; 122 | var found = / makeHTTPSRequest({ 136 | host: 'mcleaks.net', 137 | path: '/getajax?refresh', 138 | method: 'get', //json 139 | text: true, 140 | supplyHeaders: true, 141 | headers 142 | }).then(res => { 143 | if(!res) throw new Error("No response received"); 144 | var token = parseCookies(res.headers['set-cookie'], this.token); 145 | if(!token) throw new Error("No cookies provided from MCLeaks"); 146 | this.token = token; 147 | return true; 148 | })); 149 | } 150 | 151 | renewToken(alttoken, recaptchaCode) { 152 | var headers = { 153 | "Content-Type": "application/x-www-form-urlencoded", 154 | "Origin": "https://mcleaks.net", 155 | "Referer": "https://mcleaks.net/renew" 156 | }; 157 | if(this.token) headers.Cookie = this.token; 158 | return this.addWaitlist(() => makeHTTPSRequest({ 159 | host: 'mcleaks.net', 160 | path: "/renew?_=" + new Date().getTime(), 161 | method: 'post', 162 | text: true, 163 | supplyHeaders: true, 164 | headers, 165 | body: 'alttoken=' + encodeURIComponent(alttoken) + '&captcha=' + encodeURIComponent(recaptchaCode) 166 | }).then(res => { 167 | if(!res) throw new Error("No response received"); 168 | var token = parseCookies(res.headers['set-cookie'], this.token); 169 | if(!token) throw new Error("No cookies provided from MCLeaks"); 170 | this.token = token; 171 | return makeHTTPSRequest({ 172 | host: 'mcleaks.net', 173 | path: '/get', 174 | method: 'get', 175 | text: true, 176 | supplyHeaders: true, 177 | headers: { 178 | "Accept": "*/*", 179 | "Cookie": token, 180 | "Referer": "https://mcleaks.net/renew" 181 | } 182 | }); 183 | }).then(res => { 184 | if(!res) throw new Error("No response received"); 185 | var token = parseCookies(res.headers['set-cookie'], this.token); 186 | if(!token) throw new Error("No cookies provided from MCLeaks"); 187 | this.token = token; 188 | res = res.data; 189 | var found = / code; 10 | } 11 | this.get_authorization_code = get_authorization_code; 12 | if(!redirect_uri) redirect_uri = "https://login.live.com/oauth20_desktop.srf"; 13 | this.redirect_uri = String(redirect_uri) 14 | } 15 | 16 | login() { 17 | if(!this.get_authorization_code) return Promise.reject(new Error("Can only login once")); 18 | 19 | return Promise.resolve(this).then(this.get_authorization_code).then(code => this.addWaitlist(() => 20 | makeHTTPSRequest({ 21 | host: 'login.live.com', 22 | port: 443, 23 | path: '/oauth20_token.srf', 24 | method: 'POST', 25 | headers: { 26 | 'Content-Type': 'application/x-www-form-urlencoded' 27 | }, 28 | body: 'client_id=00000000402b5328&code=' + encodeURIComponent(String(code)) + '&grant_type=authorization_code&redirect_uri=' + encodeURIComponent(this.redirect_uri) 29 | }).finally(() => this.get_authorization_code = null).then(res => makeHTTPSRequest({ 30 | host: 'user.auth.xboxlive.com', 31 | port: 443, 32 | path: '/user/authenticate', 33 | method: 'POST', 34 | body: { 35 | Properties: { 36 | AuthMethod: 'RPS', 37 | SiteName: 'user.auth.xboxlive.com', 38 | RpsTicket: String(res.access_token), 39 | }, 40 | RelyingParty: 'http://auth.xboxlive.com', 41 | TokenType: 'JWT' 42 | } 43 | })).then(res => makeHTTPSRequest({ 44 | host: 'xsts.auth.xboxlive.com', 45 | port: 443, 46 | path: '/xsts/authorize', 47 | method: 'POST', 48 | body: { 49 | Properties: { 50 | SandboxId: 'RETAIL', 51 | UserTokens: [String(res.Token)], 52 | }, 53 | RelyingParty: 'rp://api.minecraftservices.com/', 54 | TokenType: 'JWT' 55 | } 56 | })).then(res => makeHTTPSRequest({ 57 | host: 'api.minecraftservices.com', 58 | port: 443, 59 | path: '/authentication/login_with_xbox', 60 | method: 'POST', 61 | body: { 62 | identityToken: "XBL3.0 x=" + String(res.DisplayClaims.xui[0].uhs) + ";" + String(res.Token) 63 | } 64 | })).then(res => { 65 | this.accessToken = String(res.access_token); 66 | return makeHTTPSRequest({ 67 | host: 'api.minecraftservices.com', 68 | port: 443, 69 | path: '/minecraft/profile', 70 | method: 'GET', 71 | headers: { 72 | 'Authorization': 'Bearer ' + this.accessToken 73 | } 74 | }).catch(ex => { 75 | if(ex instanceof Error && ex.response && ex.response.statusCode == 404) { 76 | var err = new Error("This microsoft account does not have a valid minecraft license"); 77 | err.cause = ex; 78 | err.response = ex.response; 79 | throw err; 80 | } else throw ex; //rethrow 81 | }); 82 | }).then(res => { 83 | this.name = String(res.name); 84 | this.uuid = String(res.id); 85 | }) 86 | )); 87 | } 88 | } 89 | 90 | function loginMicrosoftAccount(authorizationCode, redirect_uri) { 91 | try { 92 | var session = new MicrosoftAccountAuthenicator(authorizationCode, redirect_uri); 93 | return session.login().then(() => session); 94 | } catch(ex) { 95 | return Promise.reject(ex); 96 | } 97 | } 98 | 99 | module.exports = { MicrosoftAccountAuthenicator, loginMicrosoftAccount } -------------------------------------------------------------------------------- /authenticators/mojang.js: -------------------------------------------------------------------------------- 1 | const { uuidWithoutDashes } = require('../mc-proxy'); 2 | const { MCAuthenticator, makeHTTPSRequest } = require('./base'); 3 | const crypto = require('crypto'); 4 | 5 | function makeMojangAuthRequest(endpoint, body) { 6 | return makeHTTPSRequest({ host: 'authserver.mojang.com', method: body == null ? 'GET' : 'POST', path: endpoint, body }); 7 | } 8 | 9 | function makeMojangSessionRequest(endpoint, body) { 10 | return makeHTTPSRequest({ host: 'sessionserver.mojang.com', method: body == null ? 'GET' : 'POST', path: endpoint, body }); 11 | } 12 | 13 | function generateClientToken() { 14 | return new Promise((resolve, reject) => { 15 | crypto.randomFill(Buffer.alloc(16), (err, buff) => { 16 | if(err) reject(err); 17 | else { 18 | this.clientToken = buff.toString('hex'); 19 | resolve(this.clientToken); 20 | } 21 | }); 22 | }); 23 | } 24 | 25 | //You may want to look at this: https://wiki.vg/Authentication, for info about the endpoints. 26 | 27 | class MinecraftAuthenticator extends MCAuthenticator { 28 | constructor() { 29 | super(); 30 | if(this.constructor === MinecraftAuthenticator) throw new TypeError("This is an abstract class that must be implemented (with login())") 31 | /** 32 | * The mojang JWT access token. Must be set by the login() method (that is the only method that must be implemented). 33 | * @type {string} 34 | */ 35 | this.accessToken = null; 36 | /** 37 | * The mojang JWT client token. Must be set by the login() method 38 | * use the generateClientToken() (returns a Promise!) to get a random secure client token. 39 | */ 40 | this.clientToken = null; 41 | } 42 | 43 | refresh() { 44 | var accessToken, clientToken, uuid, name; 45 | return this.addWaitlist(Promise.resolve() 46 | .then(() => { 47 | if(!this.accessToken) throw new Error("You must first redeem the code"); 48 | accessToken = this.accessToken; 49 | clientToken = this.clientToken || undefined; 50 | uuid = this.uuid; 51 | name = this.name; 52 | }) 53 | .then(() => makeMojangAuthRequest('/refresh', { 54 | accessToken, 55 | clientToken: clientToken || undefined, 56 | selectedProfile: { 57 | id: uuid, 58 | name 59 | }, 60 | requestUser: true 61 | })).then(result => { 62 | if(!result || typeof result !== 'object' || !result.accessToken || typeof result.accessToken !== 'string') 63 | throw new TypeError("Invalid JSON data"); 64 | return Object.assign(this, { 65 | accessToken: result.accessToken, 66 | created: new Date(), 67 | refreshed: new Date() 68 | }); 69 | }) 70 | ); 71 | } 72 | 73 | /** 74 | * Validate the current session, if not you may need to (relogin) again. 75 | * @returns {Promise} resolves with true or false (never errors) 76 | */ 77 | validate() { 78 | return this.addWaitlist(() => Promise.resolve() 79 | .then(() => { 80 | if(!this.accessToken) throw new Error("You must first redeem the code"); 81 | }) 82 | .then(() => makeMojangAuthRequest('/validate', { 83 | accessToken: this.accessToken, 84 | clientToken: this.clientToken || undefined 85 | })).then(res => { 86 | if(res !== null) throw new Error("Expected 204 if valid"); 87 | return true; 88 | }, () => false) 89 | .finally(() => callback()) 90 | ); 91 | } 92 | 93 | /** 94 | * Invalidates the current session token 95 | * @returns {Promise} 96 | */ 97 | invalidate() { 98 | this.keepAlive(false); 99 | return this.addWaitlist(() => Promise.resolve() 100 | .then(() => { 101 | if(!this.accessToken) throw new Error("You must first redeem the code"); 102 | }) 103 | .then(() => makeMojangAuthRequest('/invalidate', { 104 | accessToken: this.accessToken, 105 | clientToken: this.clientToken || undefined 106 | })) 107 | .then(res => { 108 | Object.assign(this, { 109 | accessToken: null, 110 | clientToken: null 111 | }); 112 | if(res !== null) throw new Error("Expected 204 if valid"); 113 | return true; 114 | }) 115 | ); 116 | } 117 | 118 | signServerHash(serverHash) { 119 | return this.addWaitlist(() => Promise.resolve() 120 | .then(() => { 121 | if(!this.accessToken) throw new Error("You must first redeem the code"); 122 | }).then(() => makeMojangSessionRequest('/session/minecraft/join', { 123 | accessToken: this.accessToken, 124 | selectedProfile: this.uuid, 125 | serverId: serverHash 126 | })) 127 | .then(res => { 128 | if(res != null) throw new Error("Join server expects 204"); 129 | return true; 130 | }) 131 | ); 132 | 133 | } 134 | } 135 | 136 | class MojangAccountAuthenicator extends MinecraftAuthenticator { 137 | constructor(credentials, password) { 138 | super(); 139 | if(typeof credentials == 'string') { 140 | if(!password) throw new TypeError("No password supplied"); 141 | this.credentials = async () => ({ username: credentials, password: String(password) }); 142 | } else if(typeof credentials == 'function') { 143 | this.credentials = async () => Promise.resolve(credentials()).then(value => ({ username: String(value.username), password: String(value.password) })); 144 | } else if(typeof credentials == 'object') { 145 | if(!credentials) throw new TypeError("Credentials cannot be null"); 146 | if(!credentials.username || !credentials.password) throw new TypeError("No username/password"); 147 | this.credentials = async () => ({ username: String(credentials.username), password: String(credentials.password )}); 148 | } else throw new TypeError("Unknown type for credentials"); 149 | } 150 | 151 | login() { 152 | return Promise.resolve(this).then(this.credentials).then(credentials => this.addWaitlist(() => generateClientToken() 153 | .then(clientToken => this.clientToken = clientToken) 154 | .then(() => makeMojangAuthRequest('/authenticate', { 155 | agent: { 156 | name: "minecraft", 157 | version: 1 158 | }, 159 | username: credentials.username, 160 | password: credentials.password, 161 | clientToken: this.clientToken, 162 | requestUser: true 163 | })) 164 | .then(result => { 165 | this.accessToken = result.accessToken; 166 | if(!result.selectedProfile) throw new Error("Mojang account does not have a valid minecraft license"); 167 | if(!result || typeof result !== 'object' || !result.accessToken || typeof result.accessToken !== 'string' || typeof result.selectedProfile !== 'object' || !result.selectedProfile.name || typeof result.selectedProfile.name !== 'string' || !result.selectedProfile.id || typeof result.selectedProfile.id !== 'string') 168 | throw new TypeError("Invalid JSON data"); 169 | 170 | Object.assign(this, { 171 | name: result.selectedProfile.name, 172 | uuid: result.selectedProfile.id, 173 | created: new Date(), 174 | refreshed: new Date() 175 | }); 176 | }) 177 | )); 178 | } 179 | } 180 | 181 | class MCAccessTokenAuthenticator extends MinecraftAuthenticator { 182 | constructor(token, name, clientToken) { 183 | super(); 184 | this.accessToken = String(token); 185 | this.clientToken = clientToken || null; 186 | if(name.length > 16) { 187 | this.uuid = uuidWithoutDashes(name); 188 | } else { 189 | this.name = name; 190 | } 191 | } 192 | 193 | login() { 194 | return this.addWaitlist(() => 195 | makeMojangAuthRequest('/validate', { 196 | clientToken: this.clientToken || undefined, 197 | accessToken: this.accessToken 198 | }) 199 | .then(res => { 200 | if(res !== null) throw new Error("Expected 204 if valid"); 201 | if(this.uuid) { 202 | return makeHTTPSRequest({ host: 'api.mojang.com', path: '/user/profiles/' + this.uuid + '/names', method: 'get' }).then(res => ({ uuid: this.uuid, name: [...res].find(x => !x.changedToAt).name })); 203 | } else { 204 | return makeHTTPSRequest({ host: 'api.mojang.com', path: '/users/profiles/minecraft/' + encodeURIComponent(String(this.name)), method: 'get'}).then(res => ({ uuid: res.id, name: this.name })); 205 | } 206 | }) 207 | .then(res => Object.assign(this, res)) 208 | ); 209 | } 210 | 211 | /* Do nothing on invalidate()/refresh() because reauth must be possible */ 212 | invalidate() { 213 | return Promise.resolve(true); 214 | } 215 | 216 | refresh() { 217 | return this.addWaitlist(() => { 218 | Promise.resolve() 219 | .then(() => { 220 | if(!this.accessToken || !this.name || !this.uuid) throw new Error("Use the long() method to get the necessary information about the user."); 221 | }) 222 | makeMojangAuthRequest('/validate', { 223 | clientToken: this.clientToken || undefined, 224 | accessToken: this.accessToken 225 | }) 226 | .then(res => { 227 | if(res !== null) throw new Error("Expected 204 if valid"); 228 | }) 229 | }); 230 | } 231 | } 232 | 233 | function loginMojangAccount(credentials, password) { 234 | try { 235 | var session = new MojangAccountAuthenicator(credentials, password); 236 | return session.login().then(() => session); 237 | } catch(ex) { 238 | return Promise.reject(ex); 239 | } 240 | } 241 | 242 | function sessionFromAccesToken(accessToken, name, clientToken) { 243 | try { 244 | var session = new MCAccessTokenAuthenticator(accessToken, name, clientToken); 245 | return session.login().then(() => session); 246 | } catch(ex) { 247 | return Promise.reject(ex); 248 | } 249 | } 250 | 251 | module.exports = { 252 | MinecraftAuthenticator, 253 | MojangAccountAuthenicator, 254 | MCAccessTokenAuthenticator, 255 | generateClientToken, 256 | 257 | loginMojangAccount, 258 | sessionFromAccesToken, 259 | 260 | makeMojangAuthRequest, 261 | makeMojangSessionRequest 262 | }; -------------------------------------------------------------------------------- /create-image.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | //Create a setup / AppImage 3 | 4 | const builder = require("electron-builder") 5 | const Platform = builder.Platform 6 | 7 | // Promise is returned 8 | builder.build({ 9 | targets: Platform.WINDOWS.createTarget(), //select OS HERE 10 | config: { 11 | appId: "com.github.SqueezedSlime.MinecraftAltProxy", 12 | productName: "MinecraftAltProxy", 13 | copyright: "Copyright © year SqueezedSlime" 14 | } 15 | }) 16 | .then(res => { 17 | console.log(res); 18 | }) 19 | .catch((error) => { 20 | console.error(error); 21 | }) 22 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 16px; 3 | max-width: 800px; 4 | margin: 0 auto; 5 | } 6 | 7 | input[type=text], input[type=password], input[type=number], select { 8 | width: 100%; 9 | color: rgb(85, 85, 85); 10 | box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px inset; 11 | transition: border 0.2s ease-in-out 0s, box-shadow 0.2s ease-in-out 0s; 12 | border-radius: 5px; 13 | border-width: 1px; 14 | border-style: solid; 15 | border-color: rgb(204, 204, 204); 16 | border-image: initial; 17 | padding: 12px 20px; 18 | margin: 8px 0; 19 | outline: none; 20 | } 21 | 22 | input[type=text], input[type=password], input[type=number] { 23 | background-color: rgb(255, 255, 255); 24 | } 25 | 26 | select { 27 | background-color: #dddddd; 28 | } 29 | 30 | input[type=text]:focus, input[type=password]:focus, input[type=number]:focus { 31 | box-shadow: rgba(1, 110, 167, 0.6) 0px 0px 4px; 32 | border-color: rgba(1, 110, 167, 0.6); 33 | } 34 | 35 | input[type=submit], button { 36 | width: 100%; 37 | background-color: #4CAF50; 38 | color: white; 39 | padding: 14px 20px; 40 | margin: 8px 0; 41 | border: none; 42 | border-radius: 4px; 43 | cursor: pointer; 44 | } 45 | 46 | input[type=submit]:hover, button:hover { 47 | background-color: #45a049; 48 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 19 |
20 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 |
33 |
34 | 38 | 39 | 45 | 53 | 54 | 55 | 56 | 67 | 68 | 69 |
70 |

71 |             
72 |         
73 | 74 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron'); 2 | const { generateMCSharedSecret, uuidWithDashes, getServerStatus, getServerPublicKey, resolveMCSrvRecord, parsePingMotdObject, chatObjectToString } = require('./mc-proxy') 3 | const { createCrackedSession } = require('./authenticators/base'); 4 | const { loginMojangAccount, sessionFromAccesToken } = require('./authenticators/mojang'); 5 | const { loginMicrosoftAccount } = require('./authenticators/microsoft'); 6 | const { redeemAlteningToken, AlteningGenerateSession } = require('./authenticators/altening'); 7 | const { redeemEasyMCToken, generateEasyMCToken, renewEasyMCToken } = require('./authenticators/easymc') 8 | const { redeemMCLeakToken, MCLeakGenerateSession } = require('./authenticators/mcleak'); 9 | const { bindMulticastClient } = require('./mc-multicast') 10 | const dns = require('dns'); 11 | 12 | 13 | var getEl = document.getElementById.bind(document); 14 | var alteningGenerator = new AlteningGenerateSession(); 15 | var mcleakGenerator = new MCLeakGenerateSession(); 16 | var savedSessions = {}; 17 | 18 | var elements = { 19 | authentication_type: getEl('authentication_type'), 20 | credentials_block: getEl('mc_credentials'), 21 | name_label: getEl('mc_credentials_name_label'), 22 | name: getEl('mc_credentials_name'), 23 | token_block: getEl('mc_token_block'), 24 | token: getEl('mc_token'), 25 | password_block: getEl('mc_credentials_password_block'), 26 | password: getEl('mc_credentials_password'), 27 | server_type: getEl('server_type'), 28 | bind_block: getEl('mc_bind_block'), 29 | bind_address: getEl('mc_bind_address'), 30 | whitelist_block: getEl('mc_whitelist_block'), 31 | whitelist: getEl('mc_whitelist'), 32 | host: getEl('mc_host'), 33 | button: getEl('mc_play_button'), 34 | server_status: getEl('mc_server_status'), 35 | generate_token: getEl('generate_token_button'), 36 | renew_token: getEl('renew_token_button'), 37 | save_token: getEl('save_token_button'), 38 | saved_alts_block: getEl('mc_saved_alts_block'), 39 | saved_alts: getEl('mc_saved_alts'), 40 | server_hash_block: getEl('mc_serverhash_block'), 41 | server_ipv4: getEl('mc_serverip'), 42 | server_hash: getEl('mc_serverhash'), 43 | publickey: getEl('mc_publickey'), 44 | servername: getEl('mc_servername') 45 | } 46 | 47 | var windowPromise = Promise.resolve(); 48 | function retrieveUserCaptchaCode(host, sitekey) { 49 | var oldPromise = windowPromise; 50 | var promise = oldPromise.catch(() => null).then(() => new Promise(resolve => { 51 | ipcRenderer.once('captcha-result', (e, code) => { 52 | resolve(code || ''); 53 | }) 54 | ipcRenderer.send('prompt-captcha', { host, sitekey }); 55 | })); 56 | windowPromise = promise; 57 | return promise; 58 | } 59 | 60 | function renderSavedAccounts() { 61 | elements.saved_alts.innerHTML = ''; 62 | for(var altName in savedSessions) { 63 | let el = document.createElement('option'); 64 | el.value = altName; 65 | el.textContent = altName; 66 | elements.saved_alts.appendChild(el); 67 | } 68 | } 69 | 70 | function setAuthServer(value) { 71 | elements.password.value = ''; 72 | elements.token.value = ''; 73 | elements.saved_alts_block.style.display = value == 'saved' ? 'block' : 'none'; 74 | elements.server_hash_block.style.display = value == 'serverhash' ? 'block' : 'none'; 75 | switch(value) { 76 | case 'saved': 77 | renderSavedAccounts(); 78 | 79 | //fallthrough (set all elements to display none) 80 | case 'microsoft': 81 | elements.credentials_block.style.display = 'none'; 82 | elements.password_block.style.display = 'none'; 83 | elements.token_block.style.display = 'none'; 84 | elements.generate_token.style.display = 'none'; 85 | elements.renew_token.style.display = 'none'; 86 | elements.save_token.style.display = 'none'; 87 | return; 88 | case 'serverhash': 89 | 90 | //fallthrough 91 | case 'cracked': 92 | elements.password_block.style.display = 'none'; 93 | elements.token_block.style.display = 'none'; 94 | elements.name.maxLength = 16; 95 | elements.name_label.innerText = value == 'serverhash' ? 'Username of the MC account' : 'Cracked username'; 96 | elements.name.placeholder = 'Username for server'; 97 | elements.credentials_block.style.display = 'block'; 98 | elements.generate_token.style.display = 'none'; 99 | elements.renew_token.style.display = 'none'; 100 | elements.save_token.style.display = 'none'; 101 | return; 102 | case 'altening': 103 | elements.name.maxLength = 512; 104 | elements.password_block.style.display = 'none'; 105 | elements.token_block.style.display = 'none'; 106 | elements.name_label.innerText = 'Alt token'; 107 | elements.name.placeholder = 'Alt token from thealtening.com' 108 | elements.credentials_block.style.display = 'block'; 109 | elements.generate_token.style.display = 'block'; 110 | elements.renew_token.style.display = 'none'; 111 | elements.save_token.style.display = 'block'; 112 | return; 113 | case 'easymc': 114 | elements.name.maxLength = 512; 115 | elements.password_block.style.display = 'none'; 116 | elements.token_block.style.display = 'none'; 117 | elements.name_label.innerText = 'Alt token'; 118 | elements.name.placeholder = 'Alt token from easymc.io' 119 | elements.credentials_block.style.display = 'block'; 120 | elements.generate_token.style.display = 'block'; 121 | elements.renew_token.style.display = 'block'; 122 | elements.save_token.style.display = 'block'; 123 | return; 124 | case 'mcleaks': 125 | elements.name.maxLength = 512; 126 | elements.password_block.style.display = 'none'; 127 | elements.token_block.style.display = 'none'; 128 | elements.name_label.innerText = 'Alt token'; 129 | elements.name.placeholder = 'Alt token from mcleaks.net' 130 | elements.credentials_block.style.display = 'block'; 131 | elements.generate_token.style.display = 'block'; 132 | elements.renew_token.style.display = 'block'; 133 | elements.save_token.style.display = 'block'; 134 | return; 135 | case 'token': 136 | elements.name.maxLength = 512; 137 | elements.password_block.style.display = 'none'; 138 | elements.token_block.style.display = 'block'; 139 | elements.name_label.innerText = 'Username/UUID'; 140 | elements.name.placeholder = 'In-game username or UUID (not e-mail)' 141 | elements.credentials_block.style.display = 'block'; 142 | elements.generate_token.style.display = 'none'; 143 | elements.renew_token.style.display = 'none'; 144 | elements.save_token.style.display = 'none'; 145 | return; 146 | default: 147 | elements.name.maxLength = 512; 148 | elements.password_block.style.display = 'block'; 149 | elements.token_block.style.display = 'none'; 150 | elements.name_label.innerText = 'Username/e-mail'; 151 | elements.name.placeholder = 'Your username/e-mail' 152 | elements.credentials_block.style.display = 'block'; 153 | elements.generate_token.style.display = 'none'; 154 | elements.renew_token.style.display = 'none'; 155 | elements.save_token.style.display = 'none'; 156 | return; 157 | } 158 | } 159 | 160 | function setServerType(value) { 161 | elements.bind_block.style.display = ((value == 'public' || value == 'cracked') ? 'block' : 'none'); 162 | elements.whitelist_block.style.display = value == 'public' ? 'block' : 'none'; 163 | } 164 | 165 | var authOpen = false; 166 | var proxyServer; 167 | var session; 168 | var host, displayHost, selectedHost, port, motd; 169 | var redeemedSession = {type: 'none', token: '', session: null}; 170 | var sharedSecret = generateMCSharedSecret(); 171 | 172 | function startServer(session) { 173 | this.session = session; 174 | authOpen = false; 175 | if(proxyServer) { 176 | proxyServer.close(); 177 | proxyServer = null; 178 | } 179 | 180 | var serverType = elements.server_type.value; 181 | var server, bind_address, bind_port; 182 | 183 | if(serverType == 'public' || serverType == 'cracked') { 184 | bind_address = String(elements.bind_address.value) || '0.0.0.0'; 185 | portIndex = bind_address.indexOf(':'); 186 | bind_port = 25565; 187 | if(portIndex >= 0) { 188 | bind_port = Number(bind_address.substr(portIndex + 1)); 189 | bind_address = bind_address.substr(0, portIndex); 190 | } 191 | if(!bind_address) bind_address = '0.0.0.0'; 192 | if(!bind_port) bind_port = 25565; 193 | } 194 | 195 | function addSecret() { 196 | sharedSecret.then(secret => elements.server_status.innerText += '\nAES Shared Secret: ' + secret.toString('base64') + "\nCTRL+SHIFT+I for console log\nYou can use a packet sniffer \n(such as wireshark and https://github.com/aresrpg/minecraft-dissector)\n with the key to decrypt packets."); 197 | } 198 | 199 | switch(serverType) { 200 | case 'host': 201 | server = session.createProxyServer(host, port, null, sharedSecret, selectedHost); 202 | server.listen(25565, '127.0.0.1', () => { 203 | //invalidate also stops the keepalive 204 | elements.server_status.innerText = 'Type: "host-only"\nIP: "localhost"\nPort: 25565\nOnline-mode: no\nDestination: ' + JSON.stringify(displayHost) + "\n" + motd + "\nUsername: " + JSON.stringify(session.name) + "\nUUID: " + JSON.stringify(uuidWithDashes(session.uuid)); 205 | addSecret(); 206 | server.once('close', () => session.noInvalidate || session.invalidate().catch(ex => console.error(ex))); 207 | console.log('Localhost server on ' + server.address().port); 208 | session.keepAlive(true); 209 | elements.button.innerText = 'Stop'; 210 | }); 211 | server.type = 'host'; 212 | 213 | break; 214 | case 'lan': 215 | server = session.createProxyServer(host, port, null, sharedSecret, selectedHost); 216 | server.listen(0, '0.0.0.0', () => { 217 | var port = server.address().port; 218 | elements.server_status.innerText = 'Type: "lan"\nIP: Check LAN worlds on multilayer\nPort: ' + Number(port) + "\nOnline-mode: no\nDestination: " + JSON.stringify(displayHost) + "\n" + motd + "\nUsername: " + JSON.stringify(session.name) + "\nUUID: " + JSON.stringify(uuidWithDashes(session.uuid)); 219 | addSecret(); 220 | console.log('LAN server on ' + port); 221 | var multicast = bindMulticastClient(port, 'MC proxy - ' + displayHost, '0.0.0.0'); 222 | server.once('close', () => { session.noInvalidate || session.invalidate().catch(ex => console.error(ex)); multicast.close(); }); 223 | server.multicast = multicast; 224 | session.keepAlive(true); 225 | elements.button.innerText = 'Stop'; 226 | }); 227 | server.type = 'lan'; 228 | 229 | break; 230 | case 'public': 231 | var whitelist = String(elements.whitelist.value).trim().split(',').map(x => x.trim()).filter(x => x); 232 | server = session.createProxyServer(host, port, whitelist, sharedSecret, selectedHost); 233 | server.listen(bind_port, bind_address, () => { 234 | elements.server_status.innerText = 'Type: "public"\nIP: ' + JSON.stringify(bind_address) + "\nPort: " + Number(port) + "\nOnline-mode: yes\nWhitelist: " + whitelist.map(x => JSON.stringify(x)).join(', ') + "\nDestination: " + JSON.stringify(displayHost) + "\n" + motd + "\nUsername: " + JSON.stringify(session.name) + "\nUUID: " + JSON.stringify(uuidWithDashes(session.uuid)); 235 | addSecret(); 236 | server.once('close', () => session.noInvalidate || session.invalidate().catch(ex => console.error(ex))); 237 | session.keepAlive(true); 238 | elements.button.innerText = 'Stop'; 239 | }); 240 | server.type = 'public'; 241 | break; 242 | case 'cracked': 243 | server = session.createProxyServer(host, port, null, sharedSecret, selectedHost); 244 | server.listen(bind_port, bind_address, () => { 245 | elements.server_status.innerText = 'Type: "public"\nIP: ' + JSON.stringify(bind_address) + "\nPort: " + Number(port) + "\nOnline-mode: no\nDestination: " + JSON.stringify(displayHost) + "\n" + motd + "\nUsername: " + JSON.stringify(session.name) + "\nUUID: " + JSON.stringify(uuidWithDashes(session.uuid)); 246 | addSecret(); 247 | server.once('close', () => session.noInvalidate || session.invalidate().catch(ex => console.error(ex))); 248 | session.keepAlive(true); 249 | elements.button.innerText = 'Stop'; 250 | }); 251 | server.type = 'cracked'; 252 | break; 253 | default: 254 | alert('Unknown server type'); 255 | return; 256 | } 257 | server.on('error', ex => { console.error(ex); alert(ex.message) }); 258 | server.on('client-error', ex => console.error(ex)); 259 | proxyServer = server; 260 | return server; 261 | } 262 | 263 | ipcRenderer.on('auth-success', (e, code) => { 264 | loginMicrosoftAccount(code, "https://login.live.com/oauth20_desktop.srf") 265 | .then(session => startServer(session)) 266 | .catch(ex => { console.error(ex); alert(ex.message) }) 267 | .then(() => authOpen = false); 268 | }); 269 | 270 | ipcRenderer.on('auth-failed', (e, message) => { 271 | authOpen = false; 272 | console.error(message); 273 | if(message) alert(message); 274 | }); 275 | 276 | function loadAccountSession() { 277 | var promise; 278 | switch(elements.authentication_type.value) { 279 | case 'mojang': 280 | promise = loginMojangAccount(elements.name.value, elements.password.value) 281 | break; 282 | case 'microsoft': 283 | promise = Promise.reject("Microsoft authentication requires a window"); 284 | break; 285 | case 'cracked': 286 | promise = Promise.resolve(createCrackedSession(elements.name.value)); 287 | break; 288 | case 'serverhash': 289 | promise = Promise.reject("Serverhash does not have a session"); 290 | break; 291 | case 'altening': 292 | if(redeemedSession.type == 'altening' && redeemedSession.token == elements.name.value) { 293 | promise = redeemedSession.session.refresh().then(() => redeemedSession.session, ex => redeemAlteningToken(elements.name.value)); 294 | } else { 295 | if(redeemedSession.session && !redeemedSession.session.saved) redeemedSession.session.keepAlive(false); 296 | let alttoken = elements.name.value; 297 | promise = redeemAlteningToken(alttoken); 298 | promise.then(session => {session.noInvalidate = true; redeemedSession = {type: 'altening', token: alttoken, session};}); 299 | } 300 | break; 301 | case 'easymc': 302 | if(redeemedSession.type == 'easymc' && redeemedSession.token == elements.name.value) { 303 | promise = redeemedSession.session.refresh().then(() => redeemedSession.session, ex => redeemEasyMCToken(elements.name.value)); 304 | } else { 305 | if(redeemedSession.session && !redeemedSession.session.saved) redeemedSession.session.keepAlive(false); 306 | let alttoken = elements.name.value; 307 | promise = redeemEasyMCToken(alttoken); 308 | promise.then(session => {session.noInvalidate = true; redeemedSession = {type: 'easymc', token: alttoken, session};}); 309 | } 310 | break; 311 | case 'mcleaks': 312 | if(redeemedSession.type == 'mcleaks' && redeemedSession.token == elements.name.value) { 313 | promise = redeemedSession.session.refresh().then(() => redeemedSession.session, ex => redeemMCLeakToken(elements.name.value)); 314 | } else { 315 | if(redeemedSession.session && !redeemedSession.session.saved) redeemedSession.session.keepAlive(false); 316 | let alttoken = elements.name.value; 317 | promise = redeemMCLeakToken(alttoken); 318 | promise.then(session => {session.noInvalidate = true; redeemedSession = {type: 'mcleaks', token: alttoken, session};}); 319 | } 320 | break; 321 | case 'token': 322 | promise = sessionFromAccesToken(elements.token.value, elements.name.value); 323 | break; 324 | case 'saved': 325 | let sess = savedSessions[elements.saved_alts.value]; 326 | promise = sess ? sess.validate().then(() => sess) : Promise.reject(new Error("No account selected")); 327 | break; 328 | default: 329 | promise = Promise.reject(new Error('Unknown authentication type')); 330 | break; 331 | } 332 | return promise; 333 | } 334 | 335 | function onButtonClick() { 336 | if(authOpen) return; 337 | if(proxyServer) { 338 | proxyServer.close(); 339 | proxyServer = null; 340 | if(!session.noInvalidate) session.keepAlive(false); 341 | session = null; 342 | elements.server_status.innerText = ''; 343 | elements.button.innerText = 'Start'; 344 | elements.publickey.value = ''; 345 | elements.servername.value = ''; 346 | elements.server_hash.value = ''; 347 | elements.server_ipv4.value = ''; 348 | } else { 349 | host = elements.host.value; 350 | displayHost = host; 351 | var portIndex = host.indexOf(':'); 352 | port = 25565; 353 | if(portIndex >= 0) { 354 | port = Number(host.substr(portIndex + 1)); 355 | host = host.substr(0, portIndex); 356 | } 357 | if(!host || !port) { 358 | alert("No host/port is given"); 359 | return; 360 | } 361 | 362 | try { 363 | authOpen = true; 364 | resolveMCSrvRecord(host).then(h => { 365 | host = h.name || host; 366 | port = h.port || port; 367 | selectedHost = host; 368 | return getServerStatus({ host, port }); 369 | }).catch(ex => { 370 | console.error(ex); 371 | return {data: {description: {text: 'Cannot ping server'}, version: {name: "Cannot ping server", protocol: 65535}, players: {max: 0, online: 0}}, ping: 0} 372 | }).then(res => { 373 | var txt = parsePingMotdObject(res.data.description || {}); 374 | motd = "Version: " + res.data.version.name + "\nMOTD: " + txt[0] + "\nMOTD: " + txt[1] + "\nPlayers: " + res.data.players.online + "/" + res.data.players.max + "\nPing: " + String(res.ping) + "ms"; 375 | if(elements.authentication_type.value == 'microsoft') { 376 | ipcRenderer.send('microsoft-auth'); 377 | return; 378 | } else if(elements.authentication_type.value == 'serverhash') { 379 | if(res.data.version.protocol == 65535) throw new Error("Cannot ping server, to get serverhash"); 380 | var username = elements.name.value; 381 | if(username == '') throw new Error("Cannot join a online server with an empty name"); 382 | 383 | return (new Promise((resolve, reject) => { 384 | dns.lookup(host, {family: 4}, (err, address, family) => { 385 | try { 386 | if(err) throw err; 387 | if(family !== 4) throw new Error("Invalid IP family " + family); 388 | host = address; 389 | elements.server_ipv4.value = host; 390 | resolve(); 391 | } catch(ex) { 392 | reject(ex); 393 | } 394 | }) 395 | })).then(() => getServerPublicKey({ 396 | protocolVersion: res.data.version.protocol, 397 | host, 398 | port, 399 | displayHost: selectedHost, 400 | username 401 | })).then(res => { 402 | switch(res.status) { 403 | case 'online': 404 | return sharedSecret.then(secret => { 405 | elements.publickey.value = res.publicKey.toString('base64'); 406 | elements.servername.value = res.serverName; 407 | elements.server_hash.value = res.createHash(secret); 408 | return startServer(createCrackedSession(username)); 409 | }); 410 | case 'cracked': 411 | throw new Error("This is a cracked server, use cracked instead"); 412 | case 'disconnect': 413 | throw new Error("Got disconnected: " + chatObjectToString(res.message)); 414 | default: 415 | throw new Error("Unknown status: " + res.status); 416 | } 417 | }).finally(() => authOpen = false); 418 | } 419 | return loadAccountSession().then(session => startServer(session)).catch(ex => { console.error(ex); alert(ex.message); elements.button.innerText = 'Start'; }).finally(() => { 420 | authOpen = false; 421 | }); 422 | }).catch(ex => { 423 | console.error(ex); 424 | alert(ex.message); 425 | authOpen = false; 426 | elements.button.innerText = 'Start'; 427 | }) 428 | elements.button.innerText = 'Starting...'; 429 | } catch(ex) { 430 | authOpen = false; 431 | console.error(ex); 432 | alert(ex.message); 433 | elements.button.innerText = 'Start'; 434 | return; 435 | } 436 | } 437 | } 438 | 439 | function retrieveAlteningCaptchaCode() { 440 | return retrieveUserCaptchaCode('thealtening.com', '6LcvulQUAAAAALiRGtcfohNRfk-UQGolutRdQBFL') 441 | } 442 | 443 | function retrieveMCLeakCaptchaCode() { 444 | return retrieveUserCaptchaCode('mcleaks.net', '6Lc01gkTAAAAAIKbJuNejSIoQR-2ihS3N0-sOBiI'); 445 | } 446 | 447 | function retrieveEasyMCCaptchaCode() { 448 | return retrieveUserCaptchaCode('easymc.io', '6Lffq-YUAAAAAI8_bb1q1bln6-CD-gtqPj2FryfQ'); 449 | } 450 | 451 | function generateToken() { 452 | if(elements.generate_token.disabled) return; 453 | elements.name.value = ''; 454 | elements.generate_token.disabled = true; 455 | switch(elements.authentication_type.value) { 456 | case 'altening': 457 | alteningGenerator.validate().then(valid => { 458 | if(valid) { 459 | return alteningGenerator.generateToken(); 460 | } else { 461 | return retrieveAlteningCaptchaCode() 462 | .then(code => code ? alteningGenerator.authenticate(code) : false) 463 | .then(res => 464 | res !== false ? 465 | new Promise(resolve => setTimeout(resolve, 6000)).then(() => alteningGenerator.generateToken()) : 466 | false 467 | ) 468 | } 469 | }) 470 | .then(code => { if(code) elements.name.value = code; }) 471 | .catch(ex => { console.error(ex); alert(ex.message); }) 472 | .finally(() => elements.generate_token.disabled = false); 473 | break; 474 | case 'mcleaks': 475 | mcleakGenerator.refresh().then(() => mcleakGenerator.generateToken()).catch(ex => { 476 | console.info(ex); 477 | return retrieveMCLeakCaptchaCode() 478 | .then(code => code && mcleakGenerator.generateToken(code)) 479 | }) 480 | .then(code => { if(code) elements.name.value = code; }) 481 | .catch(ex => { console.error(ex); alert(ex.message); }) 482 | .finally(() => elements.generate_token.disabled = false); 483 | break; 484 | case 'easymc': 485 | generateEasyMCToken().catch(ex => { 486 | console.info(ex); 487 | return retrieveEasyMCCaptchaCode() 488 | .then(code => code && generateEasyMCToken(code)) 489 | }) 490 | .then(code => { if(code) elements.name.value = code; }) 491 | .catch(ex => { console.error(ex); alert(ex.message); }) 492 | .finally(() => elements.generate_token.disabled = false); 493 | break; 494 | default: 495 | elements.generate_token.disabled = false; 496 | break; 497 | } 498 | } 499 | 500 | function renewToken() { 501 | if(elements.renew_token.disabled) return; 502 | var token = elements.name.value; 503 | if(!token) { 504 | alert("You need to give a expired/used token to renew it"); 505 | return; 506 | } 507 | switch(elements.authentication_type.value) { 508 | case 'mcleaks': 509 | retrieveMCLeakCaptchaCode() 510 | .then(code => code && mcleakGenerator.renewToken(token, code)) 511 | .then(code => { if(code) elements.name.value = code; }) 512 | .catch(ex => { console.error(ex); alert(ex.message); }) 513 | .finally(() => elements.renew_token.disabled = false); 514 | break; 515 | case 'easymc': 516 | retrieveEasyMCCaptchaCode() 517 | .then(code => code && renewEasyMCToken(token, code)) 518 | .then(code => { if(code) elements.name.value = code; }) 519 | .catch(ex => { console.error(ex); alert(ex.message); }) 520 | .finally(() => elements.renew_token.disabled = false); 521 | break; 522 | default: 523 | elements.renew_token.disabled = false; 524 | break; 525 | } 526 | } 527 | 528 | function saveToken() { 529 | if(elements.save_token.disabled) return; 530 | var type = elements.authentication_type.value; 531 | if(!['mcleaks', 'altening', 'easymc'].includes(type)) return; 532 | var token = elements.name.value; 533 | if(!token) { 534 | alert("You need to give a expired/used token to save it"); 535 | return; 536 | } 537 | elements.save_token.disabled = true; 538 | loadAccountSession().then(session => { 539 | session.saved = true; 540 | session.keepAlive(true); 541 | var name = type + " - " + session.name; 542 | savedSessions[name] = session; 543 | elements.authentication_type.value = 'saved'; 544 | setAuthServer('saved'); 545 | elements.saved_alts.value = name; 546 | }).catch(ex => { 547 | console.error(ex); 548 | alert(ex.message); 549 | }).finally(() => elements.save_token.disabled = false); 550 | } 551 | 552 | function deleteToken() { 553 | if(elements.authentication_type.value != 'saved') return; 554 | var sess = savedSessions[elements.saved_alts.value]; 555 | if(sess) { 556 | sess.saved = false; 557 | if(sess != session) sess.keepAlive(false); 558 | } 559 | delete savedSessions[elements.saved_alts.value]; 560 | renderSavedAccounts(); 561 | } -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const { app, BrowserWindow } = electron; 3 | const https = require('https'); 4 | const path = require('path'); 5 | var authWindow; 6 | 7 | 8 | 9 | 10 | /* 11 | Function that retrieves captcha codes for abritrary sites 12 | 13 | We use a nice hack for this one ;-) 14 | We overload the HTTPS protocol handler, that will instead load the index.html of the site, load our super simple page (on their domain). 15 | This page only has a captcha button, to verify your are human (needed to generate tokens). 16 | Captcha thinks that he is on the original site but actually it is on a custom page. 17 | 18 | To prevent (and for privacy) that the https loader will be used in the rest of the app, we set the captcha window (and protocol overloader) in an another Electron partition. 19 | */ 20 | function retrieveUserCaptchaCode(host, sitekey) { 21 | return new Promise((resolve, reject) => { 22 | var modalwindow; 23 | 24 | var partition = "captcha-" + host; 25 | var session = electron.session.fromPartition(partition); 26 | 27 | session.protocol.uninterceptProtocol('https'); 28 | var r = session.protocol.interceptBufferProtocol('https', (request, callback) => { 29 | function sendError() { 30 | try { 31 | var resp = Buffer.from('Error...'); 32 | callback({ 33 | statusCode: 500, 34 | data: resp, 35 | mimeType: 'text/plain', 36 | headers: { 37 | "Content-Type": "text/plain;utf-8", 38 | "Content-Length": resp.length 39 | } 40 | }); 41 | } catch(ex) {} 42 | } 43 | try { 44 | var data = /^https:\/\/([a-zA-Z\.]+)\/recaptcha/.exec(request.url); 45 | if(data && data[1] == host) { 46 | var response = Buffer.from(` 47 | 48 | 49 | 50 | ${host} Recaptcha 51 | 52 | 53 | 64 | 65 |
66 | 67 | 68 | `); 69 | callback({ 70 | statusCode: 200, 71 | data: response, 72 | mimeType: 'text/html', 73 | headers: { 74 | "Content-Type": "text/html;utf-8", 75 | "Content-Length": response.length 76 | } 77 | }); 78 | } else { 79 | var parsed = /^https:\/\/([a-zA-Z.]+)\/(.*)$/.exec(request.url); 80 | if(!parsed || !parsed[1] || !parsed[2]) throw new Error("Invalid url"); 81 | if(parsed[1] == host) throw new Error("Invalid host url"); 82 | var httpRequest = https.request({ 83 | host: parsed[1], 84 | port: 443, 85 | path: '/' + parsed[2], 86 | method: request.method, 87 | headers: request.headers 88 | }, res => { 89 | try { 90 | if(!modalwindow) return; 91 | var buffers = []; 92 | var len = 0; 93 | res.on('error', () => { 94 | sendError(); 95 | if(!modalwindow) return; 96 | modalwindow = null; 97 | w.destroy(); 98 | session.protocol.uninterceptProtocol('https'); 99 | reject(ex); 100 | }); 101 | res.on('data', d => { 102 | if(!modalwindow) { 103 | sendError(); 104 | res.destroy(); 105 | return; 106 | } 107 | len += d.length; 108 | if(len > 10485760) { 109 | console.error(new Error("Too much data to receive")); 110 | sendError(); 111 | return; 112 | } 113 | buffers.push(d); 114 | }); 115 | res.on('end', () => { 116 | try { 117 | if(!modalwindow) return; 118 | callback({ 119 | headers: res.headers, 120 | mimeType: res.headers["Content-Type"] || undefined, 121 | data: Buffer.concat(buffers) 122 | }); 123 | } catch(ex) { 124 | console.error(ex); 125 | sendError(); 126 | } 127 | }); 128 | } catch(ex) { 129 | console.error(ex); 130 | sendError(); 131 | } 132 | }); 133 | httpRequest.on('error', ex => { 134 | sendError(); 135 | if(!modalwindow) return; 136 | modalwindow = null; 137 | w.destroy(); 138 | session.protocol.uninterceptProtocol('https'); 139 | reject(ex); 140 | }) 141 | if(request.uploadData) { 142 | for(var upload of request.uploadData) { 143 | if(!upload.bytes) throw new Error("Not allowed to receive other than bytes"); 144 | httpRequest.write(upload.bytes); 145 | } 146 | } 147 | httpRequest.end(); 148 | } 149 | } catch(ex) { 150 | console.error(ex); 151 | sendError(); 152 | } 153 | }); 154 | if(!r) throw new Error("Failed to set HTTPS protocol overloader for captcha"); 155 | 156 | modalwindow = new BrowserWindow({ 157 | autoHideMenuBar: true, 158 | webPreferences: { 159 | sandbox: true, 160 | nodeIntegration: false, 161 | contextIsolation: true, 162 | disableHtmlFullscreenWindowResize: true, 163 | spellcheck: false, 164 | webSecurity: true, 165 | allowRunningInsecureContent: false, 166 | partition: "captcha-" + host 167 | } 168 | }); 169 | var list = (ev, url) => { 170 | if(!modalwindow) return; 171 | var w = modalwindow; 172 | if(url.startsWith('https://recaptcha/?code=')) { 173 | modalwindow = null; 174 | w.destroy(); 175 | session.protocol.uninterceptProtocol('https'); 176 | resolve(decodeURIComponent(url.substring(url.indexOf("=") + 1))); 177 | } else if(!url.startsWith('https://' + host + '/')) { 178 | modalwindow = null; 179 | w.destroy(); 180 | session.protocol.uninterceptProtocol('https'); 181 | reject(new Error("Webpage tries to access invalid url")); 182 | } 183 | } 184 | modalwindow.webContents.on('will-navigate', list); 185 | modalwindow.webContents.on('will-redirect', list); 186 | //modalwindow.webContents.openDevTools(); 187 | 188 | var session = modalwindow.webContents.session; 189 | 190 | session.on('will-download', e => { 191 | e.preventDefault(); 192 | }); 193 | 194 | session.setPermissionRequestHandler((webContents, permission, callback) => { 195 | callback(false); 196 | }); 197 | 198 | modalwindow.on('close', () => { 199 | if (!modalwindow) return; 200 | var w = modalwindow; 201 | modalwindow = null; 202 | w.destroy(); 203 | session.protocol.uninterceptProtocol('https'); 204 | resolve(null); 205 | }); 206 | modalwindow.loadURL('https://' + host + '/recaptcha'); 207 | }); 208 | } 209 | 210 | app.on('ready', () => { 211 | var window = new BrowserWindow({ 212 | autoHideMenuBar: true, 213 | webPreferences: { 214 | contextIsolation: false, 215 | nodeIntegration: true, 216 | devTools: true 217 | } 218 | }); 219 | window.webContents.on('ipc-message', (e, channel, data) => { 220 | if(channel == 'microsoft-auth') { 221 | if (authWindow) return; 222 | var now = new Date(); 223 | 224 | authWindow = new BrowserWindow({ 225 | autoHideMenuBar: true, 226 | webPreferences: { 227 | sandbox: true, 228 | nodeIntegration: false, 229 | contextIsolation: true, 230 | disableHtmlFullscreenWindowResize: true, 231 | spellcheck: false, 232 | webSecurity: true, 233 | allowRunningInsecureContent: false, 234 | partition: now.toJSON() + " " + now.getTime() 235 | } 236 | }); 237 | 238 | var list = (ev, url) => { 239 | if (!authWindow) return; 240 | if (url.startsWith("https://login.live.com/oauth20_desktop.srf?code=")) { 241 | var w = authWindow; 242 | authWindow = null; 243 | w.destroy(); 244 | var code = url.substring(url.indexOf("=") + 1, url.indexOf("&")).trim(); 245 | window.webContents.send('auth-success', code); 246 | } else if (url.startsWith("https://login.live.com/oauth20_desktop.srf?error=")) { 247 | var w = authWindow; 248 | authWindow = null; 249 | w.destroy(); 250 | var type = url.substring(url.indexOf('=') + 1).trim(); 251 | var pr = '&error_description='; 252 | var message = type.substring(type.indexOf(pr) + pr.length); 253 | type = type.substring(0, type.indexOf('&')); 254 | var ind = message.indexOf('&'); 255 | if (ind > 0) message = message.substring(0, ind); 256 | message = decodeURIComponent(message); 257 | if (type == 'access_denied') { 258 | window.webContents.send('auth-failed', ''); 259 | } else { 260 | window.webContents.send('auth-failed', type + ': ' + message); 261 | } 262 | } 263 | }; 264 | authWindow.webContents.on('will-navigate', list); 265 | authWindow.webContents.on('will-redirect', list); 266 | 267 | 268 | var authSession = authWindow.webContents.session; 269 | 270 | authSession.on('will-download', e => { 271 | e.preventDefault(); 272 | }); 273 | 274 | authSession.setPermissionRequestHandler((webContents, permission, callback) => { 275 | callback(false); 276 | }); 277 | 278 | authWindow.on('close', () => { 279 | if (!authWindow) return; 280 | var w = authWindow; 281 | authWindow = null; 282 | w.destroy(); 283 | window.webContents.send('auth-failed', ''); 284 | }); 285 | authWindow.webContents.loadURL("https://login.live.com/oauth20_authorize.srf?client_id=00000000402b5328&response_type=code&scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL&redirect_uri=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf"); 286 | } else if(channel == 'prompt-captcha') { 287 | retrieveUserCaptchaCode(data.host, data.sitekey).catch(ex => { console.error(ex); return ''; }).then(res => window.webContents.send('captcha-result', typeof res == 'string' ? res : '')); 288 | 289 | } 290 | }); 291 | window.webContents.loadFile(path.join(__dirname, 'index.html')); 292 | }); -------------------------------------------------------------------------------- /mc-multicast.js: -------------------------------------------------------------------------------- 1 | const dgram = require('dgram'); 2 | 3 | function bindMulticastClient(port, motd, bindAddress = '0.0.0.0', onSetup) { 4 | var multicastClient = dgram.createSocket({ type: 'udp4', reuseAddr: true }); 5 | multicastClient.bind(0, bindAddress, () => { 6 | try { 7 | var int; 8 | multicastClient.on('close', () => { 9 | if (int) clearInterval(int); 10 | int = null; 11 | }); 12 | multicastClient.on('error', () => { 13 | if (int) clearInterval(int); 14 | int = null; 15 | }); 16 | multicastClient.addMembership('224.0.2.60'); //MC multicast IP 17 | multicastClient.setBroadcast(true); 18 | if(onSetup) onSetup(); 19 | int = setInterval(() => { 20 | try { 21 | var msg = Buffer.from('[MOTD]' + motd + '[/MOTD][AD]' + port + '[/AD]'); 22 | multicastClient.send(msg, 0, msg.length, 4445, '224.0.2.60'); 23 | } catch(ex) { 24 | console.error(ex); 25 | } 26 | }, 1500); 27 | } catch(ex) { 28 | console.error(ex); 29 | } 30 | }); 31 | return multicastClient; 32 | } 33 | 34 | module.exports = { bindMulticastClient }; -------------------------------------------------------------------------------- /mc-proxy.js: -------------------------------------------------------------------------------- 1 | const { Buffer } = require('buffer'); 2 | const net = require('net'); 3 | const crypto = require('crypto'); 4 | const https = require('https'); 5 | const dns = require('dns'); 6 | const { createInflate, deflate } = require('zlib'); 7 | const { Duplex, Readable, Writable } = require('stream'); 8 | 9 | /** 10 | * Base class for a reader/writer 11 | */ 12 | class BaseDataParser { 13 | /** 14 | * The stream 15 | * @param {Readable | Writable | Duplex} stream 16 | */ 17 | constructor(stream) { 18 | this.stream = stream; 19 | this.error = null; 20 | this.stream.on('error', ex => this.error = ex); 21 | this.waitlist = Promise.resolve(true); 22 | this.canEnd = false; 23 | this.hasEnded = false; 24 | stream.once(stream._read ? 'end' : 'finish', () => this.hasEnded = true); 25 | stream.once('error', () => this.hasEnded = true); 26 | } 27 | 28 | /** 29 | * Returns the 'same' promise, however the returned promise will reject if the streams errors. 30 | * And if the given promise rejects, the stream will destroy with that error 31 | * @param {Promise} promise 32 | * @returns {Promise} 33 | */ 34 | streamPromise(promise, canEnd) { 35 | var stack = new Error("stack"); 36 | return new Promise((resolve, reject) => { 37 | if(this.error) { 38 | reject(this.error); 39 | return; 40 | } 41 | if(canEnd == null) canEnd = this.canEnd; 42 | var listener = ex => reject(ex); 43 | this.stream.once('error', listener); 44 | var listener2 = () => canEnd ? resolve(null) : reject(new Error("Operation while stream has ended")); 45 | var eventName = this.stream._read ? 'end' : 'finish'; 46 | this.stream.once(eventName, listener2) 47 | Promise.resolve(promise) 48 | .finally(() => { 49 | this.stream.removeListener(eventName, listener2); 50 | this.stream.removeListener('error', listener); 51 | }) 52 | .catch(ex => { 53 | try{this.stream.destroy(ex);}catch(_){} 54 | reject(ex) 55 | }) 56 | .then(value => resolve(value)) 57 | .catch(ex => reject(ex)) 58 | }).catch(ex => { 59 | if(ex && ex.stack && stack.stack) ex.stack += "\n" + stack.stack; 60 | throw ex; 61 | }); 62 | } 63 | 64 | /** 65 | * Resolves if ALL tasks have been completed 66 | * @returns {Promise} 67 | */ 68 | streamReady() { 69 | if(this.error) return Promise.reject(this.error); 70 | return this.waitlist; 71 | } 72 | 73 | /** 74 | * Add a task to the waitlist 75 | * This task will be run if all other tasks have completed, and the next task will be run if this task has been completed. 76 | * @param {(() => Promise) | Promise} promise A function that can be invoked after all previous tasks have been done 77 | * @returns 78 | */ 79 | addWaitlist(promise) { 80 | var canEnd = this.canEnd; 81 | this.waitlist = this.streamReady().then(() => this.streamPromise(typeof promise == 'function' ? promise() : promise, canEnd)); 82 | return this.waitlist; 83 | } 84 | } 85 | 86 | /** 87 | * A reader can parse minecraft packets from a Readable stream. 88 | */ 89 | class ReadableDataParser extends BaseDataParser { 90 | /** 91 | * The stream 92 | * @param {Readable} stream 93 | */ 94 | constructor(stream) { 95 | super(stream); 96 | this.index = 0; 97 | 98 | } 99 | 100 | 101 | /** 102 | * Resolves with a promise if it is save to read from this stream 103 | * @param {bool} canEnd if true, it will return (as if the stream was readable) if the stream ended otherwise it will throw an Error 104 | * @returns {Promise} a promise that resolves when it is safe to read 105 | */ 106 | onReadable(canEnd) { 107 | return this.streamPromise(new Promise(callback => this.stream.once('readable', () => callback())), canEnd); 108 | } 109 | 110 | 111 | /** 112 | * Read bytes from the stream 113 | * @param {number} size 114 | * @returns {Promise} 115 | */ 116 | readBytes(size) { 117 | var canEnd = this.canEnd; 118 | return this.addWaitlist(async () => { 119 | if(size < 1) return Buffer.alloc(0); 120 | this.index += size; 121 | var bytes; 122 | var firstTry = true; 123 | while(1) { 124 | /*if(this.hasEnded) { 125 | if(canEnd) return null; 126 | throw new Error("Read while stream has ended"); 127 | }*/ 128 | var nbytes = this.stream.read(size - (bytes ? bytes.length : 0)); 129 | if(!nbytes && (!firstTry || this.hasEnded)) { 130 | if(canEnd) return null; 131 | throw new Error("Read while stream has ended"); 132 | } 133 | if(nbytes) bytes = bytes ? Buffer.concat([bytes, nbytes]) : nbytes; 134 | nbytes = null; 135 | if(bytes != null && bytes.length >= size) { 136 | return bytes.slice(0, size); 137 | } 138 | await this.onReadable(canEnd); 139 | firstTry = false; 140 | } 141 | }); 142 | } 143 | 144 | /** 145 | * Read a single byte 146 | * @returns {Promise} 147 | */ 148 | readByte() { 149 | return this.readBytes(1).then(buff => buff ? buff[0] : null); 150 | } 151 | 152 | /** 153 | * Read a MC varInt 154 | * @param {bool} saveRead If canEnd = true, to store the already readed data and return that. 155 | * If false, ended while reading a varInt (no matter what canEnd) it will throw an Error. 156 | * @returns {Promise} A promise that resolves with the varint 157 | */ 158 | async readVarInt(saveRead = false) { 159 | var numRead = 0; 160 | var result = 0; 161 | var start = true; 162 | var readed = saveRead ? [] : null; 163 | do { 164 | var read = await this.readByte(); 165 | if(read == null && start) return null; 166 | if(read == null) { 167 | if(readed) return readed; 168 | throw new Error("EoF in var int"); 169 | } 170 | if(readed) readed.push(read); 171 | start = false; 172 | var value = (read & 0b01111111); 173 | result |= (value << (7 * numRead)); 174 | 175 | numRead++; 176 | if (numRead > 5) { 177 | throw new Error("VarInt is too big"); 178 | } 179 | } while ((read & 0b10000000) != 0); 180 | 181 | return result; 182 | } 183 | 184 | /** 185 | * Read a unsigned short (2 bytes) in BE 186 | * @returns {Promise} value of the Unsigned Short 187 | */ 188 | readUnsignedShort() { 189 | return this.readBytes(2).then(buff => buff ? buff.readUInt16BE(0) : null); 190 | } 191 | 192 | /** 193 | * Read a signed short (2 bytes) in BE 194 | * @returns {Promise} value of the short 195 | */ 196 | readShort() { 197 | return this.readBytes(2).then(buff => buff ? buff.readInt16BE(0) : null); 198 | } 199 | 200 | /** 201 | * Read a signed int (4 bytes) in BE 202 | * @returns {Promise} value of the int 203 | */ 204 | readInt() { 205 | return this.readBytes(4).then(buff => buff ? buff.readInt32BE(0) : null); 206 | } 207 | 208 | /** 209 | * Read a signed long (8 bytes) in BE 210 | * @returns {Promise} value of signed long as a BigInt 211 | */ 212 | readLong() { 213 | return this.readBytes(8).then(buff => buff ? buff.readBigInt64BE(0) : null); 214 | } 215 | 216 | /** 217 | * Read a string (up to maxLength) prefixed by a length as a VarInt 218 | * @param {number} maxLength max length to read, if the string is longer an Error will be thrown 219 | * @returns {Promise} the string value 220 | */ 221 | async readString(maxLength) { 222 | var len = await this.readVarInt(); 223 | if(len == null) return null; 224 | if(maxLength && len > maxLength) { 225 | var err = new RangeError("String too big: " + len + " > " + maxLength); 226 | try{this.stream.destroy(err)} catch(_) {} 227 | throw err; 228 | } 229 | var data = await this.readBytes(len); 230 | if(data == null) throw new Error("EoF in string"); 231 | return data.toString('utf-8'); 232 | } 233 | } 234 | 235 | /** 236 | * A WritableDataBuffer can contain Minecraft data/packets 237 | * that can be written to a WritableDataParser 238 | */ 239 | class WritableDataBuffer { 240 | constructor(size, parser, block) { 241 | if(!size) size = 50; 242 | this.buffer = Buffer.alloc(size); 243 | this.length = 0; 244 | if(parser && !(parser instanceof WritableDataParser)) throw new TypeError("parser must be a writable data parser") 245 | this.parser = parser || null; 246 | var oldCallback = (push, pending) => { 247 | if(!parser) throw new TypeError("No parser given"); 248 | if(push) return pending ? this.parser.appendPending(this.toBuffer()) : this.parser.writeBytes(this.toBuffer()); 249 | else throw new TypeError("Cannot abort because buffer is not waiting"); 250 | };; 251 | if(!block) { 252 | this.isBlocked = false; 253 | this.onPushed = Promise.resolve(true); 254 | this._callback = oldCallback; 255 | } else { 256 | this.isBlocked = true; 257 | this.aborted = false; 258 | var didAbort = false; 259 | var running = false; 260 | var callbackWait = new Promise(resolve => this._callback = (push, pending) => { 261 | this._callback = (push, pending) => { 262 | if(push) oldCallback(push, pending); 263 | else { 264 | this.aborted = true; 265 | didAbort = true; 266 | } 267 | }; 268 | resolve([push, pending]); 269 | }); 270 | 271 | this.onPushed = parser.addWaitlist(() => new Promise((resolve, reject) => callbackWait.then(([push, pending]) => { 272 | this._callback = (push, pending) => { 273 | if(push) oldCallback(push, pending); 274 | else { 275 | this.aborted = true; 276 | resolve(false); 277 | } 278 | }; 279 | push = didAbort ? false : push; 280 | if(push) { 281 | running = true; 282 | if(pending) { 283 | this.parser.pendingWriteBuffer = Buffer.concat([this.parser.pendingWriteBuffer, this.toBuffer()]); 284 | resolve(true); 285 | } else { 286 | var buff = this.toBuffer(); 287 | if(this.parser.pendingWriteBuffer && this.parser.pendingWriteBuffer.length > 0) { 288 | buff = Buffer.concat([this.parser.pendingWriteBuffer, buff]); 289 | this.parser.pendingWriteBuffer = Buffer.alloc(0); 290 | } 291 | this.parser.stream.write(buff, err => { 292 | this.isBlocked = false; 293 | if(err) reject(err); 294 | else resolve(true); 295 | }); 296 | } 297 | } else { 298 | this.aborted = true; 299 | resolve(false); 300 | } 301 | }))).catch(() => false); 302 | } 303 | } 304 | 305 | /** 306 | * Convert this class to a Node.JS Buffer 307 | * @returns {Buffer} the buffer containg the contents of this class 308 | */ 309 | toBuffer() { 310 | var fixed = Buffer.alloc(this.length); 311 | this.buffer.copy(fixed, 0, 0, this.length); 312 | return fixed; 313 | } 314 | 315 | /** 316 | * Resize the length of the WritableDataBuffer. 317 | * If necessary, the capacity will be resized too 318 | * @param {number} minimal_size Minimal required length. 319 | * @returns {undefined} 320 | */ 321 | resizeBuffer(minimal_size) { 322 | if(minimal_size < this.length) return; 323 | this.length = minimal_size; 324 | if(minimal_size < this.buffer.length) return; 325 | var newSize = this.buffer.length * 2; 326 | if(newSize < minimal_size) newSize = minimal_size; 327 | var nbuff = Buffer.alloc(newSize); 328 | this.buffer.copy(nbuff, 0, 0, this.buffer.length); 329 | this.buffer = nbuff; 330 | 331 | } 332 | 333 | /** 334 | * Write a single unsigned byte to this buffer 335 | * @param {number} value the unsigned byte value 336 | * @param {number | null} index the index to write at, or null (default) to write at the end 337 | * @returns {{index: number, length: number}} 338 | */ 339 | writeByte(value, index) { 340 | if(index == null) index = this.length; 341 | this.resizeBuffer(index + 1); 342 | this.buffer[index] = value; 343 | return { index, length: 1 }; 344 | } 345 | 346 | /** 347 | * Write multiple bytes to this buffer 348 | * @param {Buffer} buffer buffer containing the data to write 349 | * @param {number | null} index the index to write at, or null (default) to write at the end 350 | * @returns {{index: number, length: number}} 351 | */ 352 | writeBytes(buffer, index) { 353 | if(!(buffer instanceof Buffer)) buffer = Buffer.from(buffer); 354 | if(buffer.length < 1) return { index, length: 0 } 355 | if(index == null) index = this.length; 356 | this.resizeBuffer(index + buffer.length); 357 | buffer.copy(this.buffer, index, 0, buffer.length); 358 | return { index, length: buffer.length } 359 | } 360 | 361 | /** 362 | * Write a MC varint to this buffer 363 | * @param {number} value The varint to write (as a number value) 364 | * @param {number | null} index the index to write at, or null (defualt) to write at the end. 365 | * @returns {{index: number, length: number}} 366 | */ 367 | writeVarInt(value, index) { 368 | if(index == null) index = this.length; 369 | var start = index; 370 | do { 371 | var temp = value & 0b01111111; 372 | value >>= 7; 373 | if (value != 0) { 374 | temp |= 0b10000000; 375 | } 376 | index = this.writeByte(temp, index).index + 1; 377 | } while (value != 0); 378 | return { length: index - start, index: start }; 379 | } 380 | 381 | /** 382 | * Write an unsigned short (2 bytes) in BE 383 | * @param {number} value The unsigned short 384 | * @param {number | null} index The index to write at or null (default) to write at the end 385 | * @returns {{index: number, length: number}} 386 | */ 387 | writeUnsignedShort(value, index) { 388 | if(index == null) index = this.length; 389 | this.resizeBuffer(index + 2); 390 | this.buffer.writeUInt16BE(value, index); 391 | return { index, length: 2 }; 392 | } 393 | 394 | writeShort(value, index) { 395 | if(index == null) index = this.length; 396 | this.resizeBuffer(index + 2); 397 | this.buffer.writeInt16BE(value, index); 398 | return { index, length: 2 }; 399 | } 400 | 401 | writeLong(value, index) { 402 | if(index == null) index = this.length; 403 | this.resizeBuffer(index + 8); 404 | this.buffer.writeBigInt64BE(value, index); 405 | return { index, length: 8 }; 406 | } 407 | 408 | writeString(value, index) { 409 | var buff = Buffer.from(value, 'utf-8'); 410 | var ind = this.writeVarInt(buff.length, index); 411 | var ind2 = this.writeBytes(buff, ind.index + ind.length); 412 | return { index: ind.index, length: ind.length + ind2.length }; 413 | } 414 | 415 | /** 416 | * Push the buffer directly to the underlying stream of the parser, flushing any internal buffers if needed. 417 | * @param {WritableDataParser} parser the parser to push, null if default (the one that created this parser) 418 | * @returns {Promise} 419 | */ 420 | push(parser) { 421 | if(parser === this.parser) parser = null; 422 | if(parser) { 423 | return parser.writeBytes(this.toBuffer()); 424 | } 425 | if(!this.parser) throw new TypeError("There is no parser"); 426 | return this._callback(true, false); 427 | } 428 | 429 | /** 430 | * Append the buffer to the internal buffer of the parser (for a later write call) instead of writing it directly to the underling stream 431 | * 432 | * This improves perfomance, especially with TCP because TCP usually makes a TCP packet for every write(2) call. 433 | * @param {WritableDataParser} parser the parser to append internal buffer, null if default (the one that created this parser) 434 | * @returns {Promise} 435 | */ 436 | appendPending(parser) { 437 | if(parser === this.parser) parser = null; 438 | if(parser) { 439 | return parser.appendPending(this.toBuffer()); 440 | } 441 | if(!this.parser) throw new TypeError("There is no parser"); 442 | return this._callback(true, true); 443 | } 444 | 445 | /** 446 | * If this buffer was created as a blocking checkpoint, you can unblock it (without pushing) with this function 447 | * @returns {Promise} 448 | */ 449 | abort() { 450 | return this._callback(false, false); 451 | } 452 | } 453 | 454 | /** 455 | * A WritableDataParser can send minecraft packets using a WritableDataBuffer. 456 | */ 457 | class WritableDataParser extends BaseDataParser { 458 | /** 459 | * The stream 460 | * @param {Writable} stream 461 | */ 462 | constructor(stream) { 463 | super(stream); 464 | this.pendingWriteBuffer = Buffer.alloc(0); 465 | } 466 | 467 | /** 468 | * Write multiple bytes directly to the underlying stream 469 | * @param {Buffer} buffer the buffer to write 470 | * @returns {Promise} a promise that resolves when the data is written. 471 | */ 472 | writeBytes(buffer) { 473 | return this.addWaitlist(() => new Promise((resolve, reject) => { 474 | var buff = (this.pendingWriteBuffer && this.pendingWriteBuffer.length > 0) ? Buffer.concat([this.pendingWriteBuffer, buffer]) : buffer; 475 | if(buff !== buffer) this.pendingWriteBuffer = Buffer.alloc(0); 476 | if(buff.length < 1) { 477 | resolve(); 478 | return; 479 | } 480 | this.stream.write(buff, err => { 481 | if(err) reject(err); 482 | resolve(); 483 | }); 484 | })); 485 | } 486 | 487 | /** 488 | * Flush any internal buffers to write the data to the underlying Node.JS stream. 489 | * 490 | * There are only internal buffers if appendPending() is used, 491 | * buffers are always flushed if writeBytes() on this class or push() on a WritableDataBuffer (created without appendPending()) is used. 492 | * @returns {Promise} a promise that resolves when all data is written. 493 | */ 494 | flush() { 495 | return this.writeBytes(Buffer.alloc(0)); 496 | } 497 | 498 | /** 499 | * Pushes the buffer to the internal buffer of this class instead writing it to the underlying stream. 500 | * 501 | * This improves perfomance, especially with TCP because TCP usually makes a TCP packet for every write(2) call. 502 | * @param {Buffer} buffer the buffer to write 503 | * @returns {Promise} A promise that resolves when the data is pushed 504 | */ 505 | appendPending(buffer) { 506 | return this.addWaitlist(() => new Promise(callback => { 507 | if(buffer.length < 1) { 508 | callback(); 509 | return; 510 | } 511 | this.pendingWriteBuffer = Buffer.concat([this.pendingWriteBuffer, buffer]); 512 | callback(); 513 | })); 514 | } 515 | 516 | /** 517 | * Create a buffer that can push data to this parser 518 | * @param {number} size Initial capacity for the buffer, should be big enough (not required) so that it won't reallocate the buffer. 519 | * @returns {WritableDataBuffer} The newly created data buffer 520 | */ 521 | createBuffer(size) { 522 | return new WritableDataBuffer(size, this, false); 523 | } 524 | 525 | /** 526 | * Create a buffer that blocks this parses until it pushes all the data to the underlying stream (or cancels) 527 | * @param {number} size Initial capacity for the buffer, should be big enough (not required) so that it won't reallocate the buffer. 528 | * @returns {WritableDataBuffer} The newly created data buffer 529 | */ 530 | createCheckpoint(size) { 531 | return new WritableDataBuffer(size, this, true); 532 | } 533 | } 534 | 535 | 536 | /** 537 | * Validate the UUID and convert any UUID (that has dashes or not) to a UUID with dashes 538 | * @param {string} id the UUID (with dashes or not) to validate and convert 539 | * @returns {string} an UUID always with dashes 540 | */ 541 | function uuidWithDashes(id) { 542 | return [.../([a-z0-9]{8})\-?([a-z0-9]{4})\-?([a-z0-9]{4})\-?([a-z0-9]{4})\-?([a-z0-9]{12})/.exec(id)].slice(1).join('-') 543 | } 544 | 545 | /** 546 | * Validate the UUID and convert any UUID (that has dashes or not) to a UUID without dashes 547 | * @param {*} id the UUID (with dashes or not) to validate and convert 548 | * @returns {string} an UUID always without dashes 549 | */ 550 | function uuidWithoutDashes(id) { 551 | return [.../([a-z0-9]{8})\-?([a-z0-9]{4})\-?([a-z0-9]{4})\-?([a-z0-9]{4})\-?([a-z0-9]{12})/.exec(id)].slice(1).join('') 552 | } 553 | 554 | /** 555 | * Convert a binary buffer to a MC HEX string (e.g used to join servers.) 556 | * 557 | * Usually the binary buffer is the output of a SHA1 hash 558 | * @param {string | Buffer} str the binary data 559 | * @returns {string} A MC hex digest (which may start with a - instead of a valid hex character) 560 | */ 561 | function mcHexDigest(str) { 562 | var hash = Buffer.from(str, 'binary'); 563 | // check for negative hashes 564 | var negative = hash.readInt8(0) < 0; 565 | if (negative) performTwosCompliment(hash); 566 | var digest = hash.toString('hex'); 567 | // trim leading zeroes 568 | digest = digest.replace(/^0+/g, ''); 569 | if (negative) digest = '-' + digest; 570 | return digest; 571 | 572 | } 573 | 574 | //Help function for mcHexDigest if output is negative 575 | function performTwosCompliment(buffer) { 576 | var carry = true; 577 | var i, newByte, value; 578 | for (i = buffer.length - 1; i >= 0; --i) { 579 | value = buffer.readUInt8(i); 580 | newByte = ~value & 0xff; 581 | if (carry) { 582 | carry = newByte === 0xff; 583 | buffer.writeUInt8(carry ? 0 : newByte + 1, i); 584 | } else { 585 | buffer.writeUInt8(newByte, i); 586 | } 587 | } 588 | } 589 | 590 | /** 591 | * Generate a pseudo random SECURE 16 bytes secret for AES encryption 592 | * @returns {Promise} Resolves with a 16 bytes buffer secure random data or an Error if secure random data cannot be generated. 593 | */ 594 | function generateMCSharedSecret() { 595 | return new Promise((resolve, reject) => { 596 | crypto.randomFill(Buffer.alloc(16), (err, buff) => { 597 | if(err) reject(err); 598 | else resolve(buff); 599 | }); 600 | }); 601 | } 602 | 603 | var supportsMCChiper = crypto.getCiphers().includes('aes-128-cfb8'); 604 | 605 | /** 606 | * Create an encryption stream to encrypt 607 | * @param {Buffer} sharedSecret the 128-bits key (both key and IV) for the chiper stream 608 | * @returns {Duplex} A duplex where you can write plain text data to get encrypted data out. 609 | */ 610 | function createMCChiperStream(sharedSecret) { 611 | if(supportsMCChiper) { 612 | let duplex = crypto.createCipheriv('aes-128-cfb8', sharedSecret, sharedSecret); 613 | duplex.secret = sharedSecret; 614 | return duplex; 615 | } else { 616 | /* 617 | We do NOT want to implement AES in pure JS because AES natively is way faster. 618 | Also some CPU's have AES instructions so that is even faster. 619 | 620 | We use the aes-128-ecb to create our stream chiper for MC. 621 | */ 622 | var ecb = crypto.createCipheriv('aes-128-ecb', sharedSecret, Buffer.alloc(0)); 623 | var register = Buffer.alloc(16); 624 | sharedSecret.copy(register, 0, 0, register.length); 625 | var duplex; 626 | var onReady = null; 627 | duplex = new Duplex({ 628 | write(chunk, encoding, callback) { 629 | chunk = Buffer.from(chunk, encoding); 630 | var toSent = Buffer.allocUnsafe(chunk.length); 631 | var i = 0; 632 | for(var byte of chunk) { 633 | try { 634 | var block = ecb.update(register); 635 | var be = byte ^ block[0] 636 | register.copy(register, 0, 1, register.length); 637 | register[register.length - 1] = be; 638 | toSent[i++] = be; 639 | } catch(ex) { 640 | try{duplex.destroy(ex)}catch(_){} 641 | callback(ex); 642 | return; 643 | } 644 | } 645 | var ret = true; 646 | if(!duplex.push(toSent)) { 647 | onReady = () => duplex.emit('drain'); 648 | ret = false; 649 | } else { 650 | onReady = null; 651 | } 652 | callback(); 653 | return ret; 654 | }, 655 | read(size) { 656 | if(onReady) { 657 | onReady(); 658 | onReady = null; 659 | } 660 | } 661 | }); 662 | duplex.secret = sharedSecret; 663 | duplex.ecb = ecb; 664 | return duplex; 665 | } 666 | } 667 | 668 | /** 669 | * Create a decryption stream 670 | * @param {Buffer} sharedSecret the 128-bits key (both key and IV) for the chiper stream 671 | * @returns {Duplex} A duplex where you can write encrypted data to get unencrypted data. 672 | */ 673 | function createMCDechiperStream(sharedSecret) { 674 | if( supportsMCChiper) { 675 | let duplex = crypto.createDecipheriv('aes-128-cfb8', sharedSecret, sharedSecret); 676 | duplex.secret = sharedSecret; 677 | return duplex; 678 | } else { 679 | /* 680 | We do NOT want to implement AES in pure JS because AES natively is way faster. 681 | Also some CPU's have AES instructions so that is even faster. 682 | 683 | We use the aes-128-ecb to create our stream dechiper for MC. 684 | Note that the block chiper here is also encrypt 685 | */ 686 | var ecb = crypto.createCipheriv('aes-128-ecb', sharedSecret, Buffer.alloc(0)); 687 | var register = Buffer.alloc(16); 688 | sharedSecret.copy(register, 0, 0, register.length); 689 | var duplex; 690 | var onReady = null; 691 | duplex = new Duplex({ 692 | write(chunk, encoding, callback) { 693 | chunk = Buffer.from(chunk, encoding); 694 | var toSent = Buffer.allocUnsafe(chunk.length); 695 | var i = 0; 696 | for(var byte of chunk) { 697 | try { 698 | var block = ecb.update(register); 699 | var be = byte ^ block[0] 700 | register.copy(register, 0, 1, register.length); 701 | register[register.length - 1] = byte; 702 | toSent[i++] = be; 703 | } catch(ex) { 704 | try{duplex.destroy(ex)}catch(_){} 705 | callback(ex); 706 | return; 707 | } 708 | } 709 | if(duplex.push(toSent)) { 710 | callback(); 711 | } else { 712 | onReady = callback; 713 | } 714 | }, 715 | read(size) { 716 | if(onReady) { 717 | onReady(); 718 | onReady = null; 719 | } 720 | } 721 | }); 722 | duplex.secret = sharedSecret; 723 | duplex.ecb = ecb; 724 | return duplex; 725 | } 726 | } 727 | 728 | function createReadableFromBuffer(buffer) { 729 | var str = new Readable(); 730 | str.push(buffer); 731 | str.push(null); 732 | return str; 733 | } 734 | 735 | function createWritableToBuffer() { 736 | var ch = []; 737 | return [new Writable({ 738 | write(chunk, encoding, callback) { 739 | chunk = encoding === 'buffer' ? chunk : Buffer.from(chunk, encoding); 740 | ch.push(chunk); 741 | callback(); 742 | }, 743 | writev(chunks, callback) { 744 | ch.push(...chunks.map(x => x.encoding === 'buffer' ? x.chunk : Buffer.from(x.chunk, x.encoding))); 745 | callback(); 746 | } 747 | }), () => Buffer.concat(ch)]; 748 | } 749 | 750 | 751 | /** 752 | * 753 | * @param {(request: { 754 | * state: 'login' | 'status', 755 | * legacy: boolean, 756 | * host: string, port: number, 757 | * version: number, 758 | * username?: string, 759 | * remoteClient: net.Socket, 760 | * proxyServer: net.Server 761 | * }) => Promise<{ 762 | * host: string, 763 | * port: number, 764 | * cracked?: boolean, 765 | * displayHost?: string | null, 766 | * displayPort?: number | null, 767 | * version?: number | null, 768 | * status?: object | null, 769 | * disconnectMessage?: string | object | null, 770 | * verifyLogin?: (uuid: string, response: object) => Promise | boolean | null, 771 | * getDisconnectMessage: ('auth-failed' | 'user-denied' | 'cracked-not-allowed' | 'no-credentials' | 'login-failed' | 'connection-failed') => Promise | string | object | null, 772 | * getUser?: () => Promise<{ 773 | * username: string, 774 | * uuid: string, 775 | * sharedSecret?: Buffer, 776 | * crackedLogin: () => Promise | boolean | null, 777 | * getCredentials: () => Promise<{ accessToken: string, uuid: string }> | { accessToken: string, uuid: string } | null, 778 | * joinServer?: (serverId: string) => Promise | false | null 779 | * } | null> | { 780 | * username: string, 781 | * uuid: string, 782 | * sharedSecret?: Buffer, 783 | * crackedLogin: () => Promise | boolean | null, 784 | * getCredentials: () => Promise<{ accessToken: string, uuid: string }> | { accessToken: string, uuid: string } | null, 785 | * joinServer?: (serverId: string) => Promise | false | null 786 | * } | null 787 | * }> | { 788 | * host: string, 789 | * port: number, 790 | * cracked?: boolean, 791 | * displayHost?: string | null, 792 | * displayPort?: number | null, 793 | * version?: number | null, 794 | * status?: object | null, 795 | * disconnectMessage?: string | object | null, 796 | * verifyLogin?: (uuid: string, response: object) => Promise | boolean | null, 797 | * getDisconnectMessage: ('auth-failed' | 'user-denied' | 'cracked-not-allowed' | 'no-credentials' | 'login-failed' | 'connection-failed') => Promise | string | object | null, 798 | * getUser?: () => Promise<{ 799 | * username: string, 800 | * sharedSecret?: Buffer, 801 | * crackedLogin: () => Promise | boolean | null, 802 | * getCredentials: () => Promise<{ accessToken: string, uuid: string }> | { accessToken: string, uuid: string } | null, 803 | * joinServer?: (serverId: string) => Promise | false | null 804 | * } | null> | { 805 | * username: string, 806 | * sharedSecret?: Buffer, 807 | * crackedLogin: () => Promise | boolean | null, 808 | * getCredentials: () => Promise<{ accessToken: string, uuid: string }> | { accessToken: string, uuid: string } | null, 809 | * joinServer?: (serverId: string) => Promise | false | null 810 | * } | null 811 | * } 812 | * } getServer 813 | * @returns 814 | */ 815 | function createProxyServer(getServer) { 816 | var keyPromise = new Promise((resolve, reject) => { 817 | crypto.generateKeyPair('rsa', { 818 | modulusLength: 1024, 819 | publicKeyEncoding: { 820 | type: 'spki', 821 | format: 'der' 822 | }, 823 | privateKeyEncoding: { 824 | type: 'pkcs8', 825 | format: 'der', 826 | } 827 | }, (err, publicKey, privateKey) => { 828 | if(err) reject(err); 829 | else resolve({ publicKey, privateKey }); 830 | }); 831 | }); 832 | var proxyServer = new net.Server(async cl => { 833 | try { 834 | //proxyServer.emit('connection', cl); 835 | var serverAddr, serverPort; 836 | var socket = null; 837 | var remoteAddr = cl.remoteAddress.toString(); //ooit voor logging 838 | /** @type {{ 839 | * host: string, 840 | * port: number, 841 | * cracked?: boolean, 842 | * displayHost?: string | null, 843 | * displayPort?: number | null, 844 | * version?: number | null, 845 | * status?: object | null, 846 | * verifyLogin?: (uuid: string, response: object) => Promise | boolean | null, 847 | * getDisconnectMessage: ('auth-failed' | 'user-denied' | 'cracked-not-allowed' | 'no-credentials' | 'login-failed' | 'connection-failed') => Promise | string | object | null, 848 | * getUser?: () => Promise<{ 849 | * username: string, 850 | * sharedSecret?: Buffer, 851 | * crackedLogin: () => Promise | boolean | null, 852 | * getCredentials: () => Promise<{ accessToken: string, uuid: string }> | { accessToken: string, uuid: string } | null, 853 | * joinServer?: (serverId: string) => Promise | false | null 854 | * } | null> | { 855 | * username: string, 856 | * sharedSecret?: Buffer, 857 | * crackedLogin: () => Promise | boolean | null, 858 | * getCredentials: () => Promise<{ accessToken: string, uuid: string }> | { accessToken: string, uuid: string } | null, 859 | * joinServer?: (serverId: string) => Promise | false | null 860 | * } | null 861 | * } | null} */ 862 | var serverInfo = null; 863 | /** @type {{host: string, port: number, version: number}} */ 864 | var serverData = null; 865 | var reader = new ReadableDataParser(cl); 866 | var writer = new WritableDataParser(cl); 867 | /** @type {ReadableDataParser} */ 868 | var socketReader = null; 869 | /** @type {WritableDataParser} */ 870 | var socketWriter = null; 871 | var encryptionStage = 0; 872 | var playState = false; 873 | var compression = -1; 874 | var loginStatus = null; 875 | var username; 876 | var userID; 877 | var state = 0; 878 | var emittedError = false; 879 | var verifyToken = null; 880 | 881 | var chiperClient; 882 | var dechiperClient; 883 | var chiperServer; 884 | var dechiperServer; 885 | 886 | async function pipeStreams() { 887 | var onTerminate = null; 888 | var terminated = false; 889 | function terminate(ex) { 890 | if(terminated) 891 | return; 892 | terminated = true; 893 | var err = ex || new Error("Invalid data from client/server"); 894 | try {cl.destroy(err);} catch(_) {} 895 | try {socket.destroy(err);} catch(_) {} 896 | if(chiperServer) 897 | try {chiperServer.destroy(err);} catch(_) {} 898 | if(chiperClient) 899 | try {chiperClient.destroy(err);} catch(_) {} 900 | if(dechiperServer) 901 | try {dechiperServer.destroy(err);} catch(_) {} 902 | if(dechiperClient) 903 | try {dechiperClient.destroy(err);} catch(_) {} 904 | if(onTerminate) 905 | onTerminate(ex); 906 | throw err; 907 | } 908 | 909 | cl.on('error', ex => terminate(ex)); 910 | socket.on('error', ex => terminate(ex)); 911 | if(chiperServer) 912 | chiperServer.on('error', ex => terminate(ex)); 913 | if(chiperClient) 914 | chiperClient.on('error', ex => terminate(ex)); 915 | if(dechiperServer) 916 | dechiperServer.on('error', ex => terminate(ex)); 917 | if(dechiperClient) 918 | dechiperClient.on('error', ex => terminate(ex)); 919 | 920 | function createInflateFromBuffer(buffer) { 921 | var str = createReadableFromBuffer(buffer); 922 | var inf = createInflate(); 923 | str.on('error', ex => { try {inf.destroy(ex); }catch(_){} }); 924 | inf.on('error', ex => { try {str.destroy(ex); }catch(_){} }); 925 | str.pipe(inf); 926 | return inf; 927 | } 928 | 929 | async function readPacket(reader) { 930 | var compressedLength = await reader.readVarInt(); 931 | var index = reader.index; 932 | 933 | if(compressedLength > 2097151 || compressedLength < 1) 934 | return terminate(); 935 | var packetLength = (compression >= 0) ? await reader.readVarInt() : 0; 936 | var hasCompression = packetLength > 0; 937 | if(!hasCompression) 938 | packetLength = compressedLength; 939 | if(packetLength > 2097151 || packetLength < 1) 940 | return terminate(); 941 | var originalData = await reader.readBytes(compressedLength - (reader.index - index)); 942 | var readStream = new ReadableDataParser(hasCompression ? createInflateFromBuffer(originalData) : createReadableFromBuffer(originalData)); 943 | readStream.stream.on('error', () => {}); //errors should be handled by the read operation. 944 | return { packetLength, hasCompression, originalData, readStream } 945 | } 946 | 947 | async function processServer() { 948 | if(!playState) { 949 | while(!playState) { 950 | //we must process the packets until we reach play state. from that moment we can just pipe the streams 951 | //this must be done because of 2 things: 1. set compression 2. packet processing in processClient() is different when we are in state play 952 | var { packetLength, hasCompression, originalData, readStream } = await readPacket(socketReader); 953 | var packetID = await readStream.readVarInt(); 954 | var canCompress = compression >= 0; 955 | if(packetID == 0x03) { 956 | compression = await readStream.readVarInt(); 957 | } else if(packetID == 0x02) { 958 | playState = true; 959 | } 960 | if(hasCompression) { 961 | var buff1 = writer.createCheckpoint(5); 962 | var buff2 = writer.createCheckpoint(5); 963 | buff2.writeVarInt(packetLength); 964 | buff1.writeVarInt(buff2.length + originalData.length); 965 | buff1.appendPending(); 966 | buff2.appendPending(); 967 | writer.writeBytes(originalData); 968 | } else if(canCompress) { 969 | var buff = writer.createBuffer(6); 970 | buff.writeVarInt(originalData.length + 1); 971 | buff.writeByte(0); 972 | buff.appendPending(); 973 | writer.writeBytes(originalData); 974 | } else { 975 | var buff = writer.createBuffer(); 976 | buff.writeVarInt(originalData.length); 977 | buff.appendPending(); 978 | writer.writeBytes(originalData); 979 | } 980 | } 981 | 982 | await writer.flush(); 983 | await socketReader.streamReady(); 984 | await writer.streamReady(); 985 | } 986 | 987 | socketReader = null; 988 | writer = null; 989 | if(dechiperServer && chiperClient) { 990 | dechiperServer.pipe(chiperClient); 991 | } else if(dechiperServer) { 992 | dechiperServer.pipe(cl); 993 | } else if(chiperClient) { 994 | socket.pipe(chiperClient); 995 | } else { 996 | socket.pipe(cl); 997 | } 998 | 999 | return new Promise(() => {}); 1000 | } 1001 | 1002 | async function processClient() { 1003 | while(1) { 1004 | var { packetLength, hasCompression, originalData, readStream } = await readPacket(reader); 1005 | var packetID = await readStream.readVarInt(); 1006 | /** @type {WritableDataParser} */ 1007 | var responseStream = null; 1008 | /** @type {WritableDataBuffer} */ 1009 | var response = null; 1010 | var getResponseBytes = null; 1011 | var ended = false; 1012 | function createResponse() { 1013 | if(responseStream) 1014 | return; 1015 | var arr = createWritableToBuffer(); 1016 | responseStream = new WritableDataParser(arr[0]); 1017 | response = responseStream.createCheckpoint(packetLength); 1018 | getResponseBytes = arr[1]; 1019 | arr[0].on('error', ex => !ended && terminate(ex)); 1020 | response.writeVarInt(packetID); 1021 | } 1022 | if(playState && packetID == 0x04) { 1023 | var command = await readStream.readString(); 1024 | var timestamp = await readStream.readLong(); 1025 | await readStream.readLong(); //salt 1026 | var arrlength = await readStream.readVarInt(); 1027 | for(var i = 0; i < arrlength; i++) { 1028 | await readStream.readString(); //argument name 1029 | await readStream.readBytes(await readStream.readVarInt()); //byte array length + byte array 1030 | } 1031 | await readStream.readByte(); //signed preview 1032 | createResponse(); 1033 | response.writeString(command); 1034 | response.writeLong(timestamp); 1035 | response.writeLong(0n); 1036 | response.writeVarInt(0); 1037 | response.writeByte(0); 1038 | response.writeByte(0); 1039 | response.writeByte(0); 1040 | response.push(); 1041 | } else if(playState && packetID == 0x05) { 1042 | var message = await readStream.readString(); 1043 | var timestamp = await readStream.readLong(); 1044 | await readStream.readLong(); //salt 1045 | await readStream.readBytes(await readStream.readVarInt()); //signate length and signature 1046 | await readStream.readByte(); //signed preview 1047 | createResponse(); 1048 | response.writeString(message); 1049 | response.writeLong(timestamp); 1050 | response.writeLong(0n); 1051 | response.writeVarInt(0); 1052 | response.writeByte(0); 1053 | response.writeByte(0); 1054 | response.writeByte(0); 1055 | response.push(); 1056 | } 1057 | originalData = getResponseBytes ? await new Promise((resolve, reject) => { 1058 | (async () => { 1059 | await responseStream.flush(); 1060 | await responseStream.streamReady(); 1061 | })().then(() => { 1062 | responseStream.stream.once('error', ex => reject(ex)); 1063 | responseStream.stream.once('finish', () => { 1064 | ended = true; 1065 | var bytes = getResponseBytes(); 1066 | hasCompression = false; 1067 | if(compression >= 0 && bytes.length >= compression) { 1068 | hasCompression = true; 1069 | packetLength = bytes.length; 1070 | deflate(bytes, (ex, res) => ex ? reject(ex) : resolve(res)); 1071 | } else 1072 | resolve(bytes); 1073 | }); 1074 | responseStream.stream.end(); 1075 | }).catch(ex => reject(ex)); 1076 | }) : originalData; 1077 | if(hasCompression) { 1078 | var buff1 = socketWriter.createCheckpoint(5); 1079 | var buff2 = socketWriter.createCheckpoint(5); 1080 | buff2.writeVarInt(packetLength); 1081 | buff1.writeVarInt(buff2.length + originalData.length); 1082 | buff1.appendPending(); 1083 | buff2.appendPending(); 1084 | socketWriter.writeBytes(originalData); 1085 | } else if(compression >= 0) { 1086 | var buff = socketWriter.createBuffer(6); 1087 | buff.writeVarInt(originalData.length + 1); 1088 | buff.writeByte(0); 1089 | buff.appendPending(); 1090 | socketWriter.writeBytes(originalData); 1091 | } else { 1092 | var buff = socketWriter.createBuffer(); 1093 | buff.writeVarInt(originalData.length); 1094 | buff.appendPending(); 1095 | socketWriter.writeBytes(originalData); 1096 | } 1097 | await socketWriter.flush(); 1098 | } 1099 | } 1100 | 1101 | return Promise.race([processClient(), processServer(), new Promise((_, rej) => onTerminate = rej)]).catch(ex => { 1102 | terminate(ex); 1103 | throw ex; 1104 | }); 1105 | } 1106 | 1107 | function toUTF16Be(str) { 1108 | str = String(str); 1109 | var buff = Buffer.alloc(str.length * 2); 1110 | for(var i = 0; i < str.length; i++) { 1111 | buff.writeUInt16BE(str.charCodeAt(i), i * 2); 1112 | } 1113 | return buff; 1114 | } 1115 | 1116 | function fromUTF16Be(buff) { 1117 | var str = ''; 1118 | for(var i = 0; i < buff.length; i += 2) { 1119 | str += String.fromCharCode(buff.readUInt16BE(i)); 1120 | } 1121 | return str; 1122 | } 1123 | 1124 | 1125 | async function sendLegacyPing(pingData) { 1126 | var versionName = String(pingData.version.name); 1127 | var onlinePlayers = String(Number(pingData.players.online)); 1128 | var maxPlayers = String(Number(pingData.players.max)); 1129 | var del = String.fromCharCode(167); 1130 | var motd = String(pingData.description.text).split('\0').join(' ').split(del).join(' '); 1131 | var sendBuff = toUTF16Be(state == 4 ? motd + del + onlinePlayers + del + maxPlayers : del + '1\0' + '127\0' + versionName + '\0' + motd + '\0' + onlinePlayers + '\0' + maxPlayers); 1132 | buff = writer.createCheckpoint(3 + sendBuff.length); 1133 | buff.writeByte(0xFF); 1134 | buff.writeShort(sendBuff.length / 2); 1135 | buff.writeBytes(sendBuff); 1136 | await buff.push(); 1137 | await writer.flush(); 1138 | await writer.streamReady(); 1139 | cl.end(); 1140 | } 1141 | 1142 | function createConnection() { 1143 | return new Promise((resolve, reject) => { 1144 | var sock = net.connect({ 1145 | host: serverInfo.host, 1146 | port: serverInfo.port 1147 | }, () => (async () => { 1148 | try { 1149 | socket = sock; 1150 | cl.serverSocket = socket; 1151 | cl.emit('server', socket); 1152 | socketReader = new ReadableDataParser(socket); 1153 | socketWriter = new WritableDataParser(socket); 1154 | var hostBuff = Buffer.from(serverInfo.displayHost, 'utf-8'); 1155 | var buff1 = socketWriter.createCheckpoint(5); 1156 | var buff2 = socketWriter.createCheckpoint(10 + hostBuff.length); 1157 | buff2.writeVarInt(0); 1158 | buff2.writeVarInt(serverInfo.version); 1159 | buff2.writeVarInt(hostBuff.length); 1160 | buff2.writeBytes(hostBuff); 1161 | buff2.writeUnsignedShort(serverInfo.displayPort); 1162 | buff2.writeVarInt(state == 2 ? 2 : 1); 1163 | buff1.writeVarInt(buff2.length); 1164 | buff1.appendPending(); 1165 | if(state == 2 || state == 4 || state == 5) buff2.appendPending() 1166 | else await buff2.push(); 1167 | var user; 1168 | if(state == 4 || state == 5) { 1169 | //legacy ping 1170 | var buff = socketWriter.createCheckpoint(2); 1171 | buff.writeVarInt(1); 1172 | buff.writeVarInt(0); 1173 | await buff.push(); 1174 | 1175 | var length = await socketReader.readVarInt(); 1176 | var index = socketReader.index; 1177 | var id = await socketReader.readVarInt(); 1178 | if(length < 2 || id != 0) throw new RangeError("Invalid ID ping"); 1179 | var pingData = JSON.parse(await socketReader.readString(32767)); 1180 | if(index + length != socketReader.index) throw new RangeError("Invalid length"); 1181 | await sendLegacyPing(pingData); 1182 | resolve(sock); 1183 | return; 1184 | } else if(state == 2) { 1185 | user = await serverInfo.getUser(); 1186 | var userBuff = Buffer.from(user.username, 'utf-8'); 1187 | var buff1 = socketWriter.createCheckpoint(5); 1188 | var buff2 = socketWriter.createCheckpoint(4 + userBuff.length); 1189 | buff2.writeVarInt(0); 1190 | buff2.writeVarInt(userBuff.length); 1191 | buff2.writeBytes(userBuff); 1192 | buff2.writeByte(0); //no secure chat system 1193 | var hex = null; 1194 | try { 1195 | var uuid = ''; 1196 | if(user.uuid) 1197 | uuid = uuidWithoutDashes(user.uuid); 1198 | if(uuid == "00000000000000000000000000000000") 1199 | uuid = ''; 1200 | if(uuid) 1201 | hex = Buffer.from(uuid, 'hex'); 1202 | } catch(ex) { 1203 | hex = null; 1204 | } 1205 | if(hex && hex.length == 16) { 1206 | buff2.writeByte(1); 1207 | buff2.writeBytes(hex); 1208 | } else 1209 | buff2.writeByte(0); 1210 | buff1.writeVarInt(buff2.length); 1211 | buff1.appendPending(); 1212 | await buff2.push(); 1213 | } 1214 | 1215 | if(state == 1) { 1216 | resolve(sock); 1217 | } 1218 | 1219 | while(1) { 1220 | await socketWriter.flush(); 1221 | socketReader.canEnd = true; 1222 | var length = await socketReader.readVarInt(); 1223 | socketReader.canEnd = false; 1224 | var index = socketReader.index; 1225 | if(length == null) return; //TODO: kick other connection?? 1226 | var id = await socketReader.readVarInt(); 1227 | if(state == 1) { 1228 | if(length > 65535) throw new RangeError("Packet too big for status"); 1229 | if(id != 0x0 && id != 0x01) throw new TypeError("Unknown not supported ping ID: " + id); 1230 | if(id == 0x01 && length != 9) throw new TypeError("Invalid length for ping ID 0x01"); 1231 | if(id == 0x0 && length < 2) throw new TypeError("Invalid length for ping id 0x0"); 1232 | var data = await socketReader.readBytes(length - (socketReader.index - index)); 1233 | var buff1 = writer.createCheckpoint(5); 1234 | var buff2 = writer.createCheckpoint(4 + data.length); 1235 | buff2.writeVarInt(id); 1236 | buff2.writeBytes(data); 1237 | buff1.writeVarInt(buff2.length); 1238 | buff1.appendPending(); 1239 | await buff2.push(); 1240 | continue; 1241 | } else if(state != 2) { 1242 | throw new Error("Unknown state: " + state); 1243 | } 1244 | //weird plugin requests/responses without encryption... 1245 | if(id == 0x04) { 1246 | //forward plugin request to client 1247 | if(length > 524288) throw new RangeError("Packet too big for plugin"); 1248 | var messageID = await socketReader.readVarInt(); 1249 | var name = await socketReader.readString(32767); 1250 | if(socketReader.index - index > length) throw new Error("Unexpected eof in packet"); 1251 | var data = await socketReader.readBytes(length - (socketReader.index - index)); 1252 | var buff1 = writer.createCheckpoint(5); 1253 | var buff2 = writer.createCheckpoint(5 + name.length + data.length); 1254 | buff2.writeVarInt(0x04); 1255 | buff2.writeVarInt(messageID); 1256 | buff2.writeString(name); 1257 | buff2.writeBytes(data); 1258 | buff1.writeVarInt(buff2.length); 1259 | buff1.appendPending(); 1260 | await buff2.push(); 1261 | 1262 | //forward incomming plugin response to server 1263 | reader.canEnd = true; 1264 | var length2 = await reader.readVarInt(); 1265 | reader.canEnd = false; 1266 | if(length2 < 1) throw new RangeError("EOF for plugin response"); 1267 | var index2 = reader.index; 1268 | var id2 = await reader.readVarInt(); 1269 | if(id2 != 0x02) throw new RangeError("Expected a plugin response"); 1270 | var responseBytes = await reader.readBytes(length2 - (reader.index - index2)); 1271 | buff1 = socketWriter.createCheckpoint(5); 1272 | buff2 = socketWriter.createCheckpoint(2 + responseBytes.length); 1273 | buff2.writeVarInt(0x02); 1274 | buff2.writeBytes(responseBytes); 1275 | buff1.writeVarInt(buff2.length); 1276 | buff1.appendPending(); 1277 | await buff2.push(); 1278 | continue; 1279 | } 1280 | if(id == 0x00 || id == 0x03 || id == 0x02) { 1281 | //0x00: the client is disconnected/kicked 1282 | //(0x03 || 0x02) its seems that we receive login success or compression. meaning that the server is cracked 1283 | if(length > 65535) throw new TypeError("Packet too big"); 1284 | if(id != 0x00 && (user.crackedLogin && !(await user.crackedLogin()))) { 1285 | try{socket.end()} catch(_){} 1286 | var disconnectBuff = Buffer.from(JSON.stringify((await serverInfo.getDisconnectMessage('cracked-not-allowed')) || {text: 'Remote server is in offline mode, not allowed'}), 'utf-8'); 1287 | var buff1 = writer.createCheckpoint(5); 1288 | var buff2 = writer.createCheckpoint(5 + disconnectBuff.length); 1289 | buff2.writeVarInt(0); 1290 | buff2.writeVarInt(disconnectBuff.length); 1291 | buff2.writeBytes(disconnectBuff); 1292 | buff1.writeVarInt(buff2.length); 1293 | buff1.appendPending(); 1294 | await buff2.push(); 1295 | cl.end(); 1296 | return; 1297 | } 1298 | var buff1 = await writer.createCheckpoint(5); 1299 | var buff2 = await writer.createCheckpoint(4 + length); 1300 | buff2.writeVarInt(id); 1301 | if(id == 0x03) { 1302 | compression = socketReader.readVarInt(); 1303 | buff2.writeVarInt(compression); 1304 | } 1305 | buff2.writeBytes(await socketReader.readBytes(length - (socketReader.index - index))); 1306 | buff1.writeVarInt(buff2.length); 1307 | buff1.appendPending(); 1308 | await buff2.push(); 1309 | await writer.flush(); 1310 | await writer.streamReady(); 1311 | await reader.streamReady(); 1312 | playState = id == 0x02; 1313 | if(id == 0x03 || id == 0x02) { 1314 | await socketWriter.flush(); 1315 | await socketWriter.streamReady(); 1316 | await socketReader.streamReady(); 1317 | await pipeStreams(); 1318 | return; 1319 | } else { 1320 | try{socket.end();}catch(_){} 1321 | try{cl.end();}catch(_){} 1322 | return; 1323 | } 1324 | } 1325 | if(id != 0x01) throw new TypeError("Type must be 0x01, " + id); 1326 | var serverName = await socketReader.readString(20); 1327 | var pubKeyLen = await socketReader.readVarInt(); 1328 | if(pubKeyLen > 256) throw new TypeError("Public key too long"); 1329 | var pubKey = await socketReader.readBytes(pubKeyLen); 1330 | var verifyTokenLen = await socketReader.readVarInt(); 1331 | if(verifyToken > 256) throw new TypeError("Too long verify token"); 1332 | var verifyToken = await socketReader.readBytes(verifyTokenLen); 1333 | var sharedSecret = user.sharedSecret ? user.sharedSecret : await generateMCSharedSecret(); 1334 | if(!(sharedSecret instanceof Buffer)) throw new Error("Shared secret must be a buffer"); 1335 | var serverID = mcHexDigest(crypto.createHash('sha1').update(Buffer.from(serverName, 'utf-8')).update(sharedSecret).update(pubKey).digest()); 1336 | var succ; 1337 | var hasCredentials = false; 1338 | if(user.joinServer) { 1339 | hasCredentials = true; 1340 | succ = await user.joinServer(serverID, { serverName, sharedSecret, pubKey }); 1341 | } 1342 | if(!succ && succ !== false && user.getCredentials) { 1343 | var credentials = await user.getCredentials(serverID, { serverName, sharedSecret, pubKey }); 1344 | succ = await new Promise((resolve, reject) => { 1345 | if(!credentials) { 1346 | resolve(false); 1347 | return; 1348 | } 1349 | hasCredentials = true; 1350 | var body = Buffer.from(JSON.stringify({ 1351 | accessToken: credentials.accessToken, 1352 | selectedProfile: uuidWithoutDashes(credentials.uuid), 1353 | serverId: serverID 1354 | }), 'utf-8'); 1355 | var req = https.request({ 1356 | method: "POST", 1357 | host: 'sessionserver.mojang.com', 1358 | port: 443, 1359 | path: '/session/minecraft/join', 1360 | headers: { 1361 | 'Content-Type': 'application/json;utf-8', 1362 | 'Content-Length': body.length 1363 | } 1364 | }, res => { 1365 | try { 1366 | res.on('error', ex => reject(ex)); 1367 | res.resume(); 1368 | resolve(res.statusCode == 204 || res.statusCode == 200); 1369 | } catch(ex) { 1370 | reject(ex); 1371 | } 1372 | }).on('error', ex => reject(ex)); 1373 | req.write(body, err => err ? reject(err) : void 0); 1374 | req.end(); 1375 | }); 1376 | } 1377 | if(!succ) { 1378 | try{socket.end()} catch(_){} 1379 | var disconnectBuff = Buffer.from(JSON.stringify((await serverInfo.getDisconnectMessage(hasCredentials ? 'login-failed' : 'no-credentials')) || {text: 'Failed to sign in to remote server'}), 'utf-8'); 1380 | var buff1 = writer.createCheckpoint(5); 1381 | var buff2 = writer.createCheckpoint(5 + disconnectBuff.length); 1382 | buff2.writeVarInt(0); 1383 | buff2.writeVarInt(disconnectBuff.length); 1384 | buff2.writeBytes(disconnectBuff); 1385 | buff1.writeVarInt(buff2.length); 1386 | buff1.appendPending(); 1387 | await buff2.push(); 1388 | await writer.flush(); 1389 | await reader.streamReady(); 1390 | await writer.streamReady(); 1391 | await socketWriter.flush(); 1392 | await socketReader.streamReady(); 1393 | await socketWriter.streamReady(); 1394 | cl.end(); 1395 | return; 1396 | } 1397 | var encryptKey = crypto.createPublicKey({ 1398 | key: pubKey, 1399 | format: 'der', 1400 | type: 'spki' 1401 | }) 1402 | var encryptedSecret = crypto.publicEncrypt({ 1403 | key: encryptKey, 1404 | padding: crypto.constants.RSA_PKCS1_PADDING 1405 | }, sharedSecret); 1406 | var encryptedVerifyToken = crypto.publicEncrypt({ 1407 | key: encryptKey, 1408 | padding: crypto.constants.RSA_PKCS1_PADDING 1409 | }, verifyToken); 1410 | var buff1 = socketWriter.createCheckpoint(5); 1411 | var buff2 = socketWriter.createCheckpoint(10 + encryptedSecret.length + encryptedVerifyToken.length); 1412 | buff2.writeVarInt(0x01); 1413 | buff2.writeVarInt(encryptedSecret.length); 1414 | buff2.writeBytes(encryptedSecret); 1415 | buff2.writeVarInt(0x01); 1416 | buff2.writeVarInt(encryptedVerifyToken.length); 1417 | buff2.writeBytes(encryptedVerifyToken); 1418 | buff1.writeVarInt(buff2.length); 1419 | buff1.appendPending(); 1420 | await buff2.push(); 1421 | await writer.flush(); 1422 | await reader.streamReady(); 1423 | await writer.streamReady(); 1424 | await socketWriter.flush(); 1425 | await socketReader.streamReady(); 1426 | await socketWriter.streamReady(); 1427 | socketReader = null; 1428 | socketWriter = null; 1429 | chiperServer = createMCChiperStream(sharedSecret); 1430 | dechiperServer = createMCDechiperStream(sharedSecret); 1431 | socket.emit('encryption', { chiper: chiperServer, dechiper: dechiperServer }); 1432 | socket.pipe(dechiperServer); 1433 | chiperServer.pipe(socket); 1434 | socketReader = new ReadableDataParser(dechiperServer); 1435 | socketWriter = new WritableDataParser(chiperServer); 1436 | await pipeStreams(); 1437 | return; 1438 | } 1439 | } catch(ex) { 1440 | if(!emittedError) try{proxyServer.emit('client-error', ex, cl);}catch(_){} 1441 | emittedError = true; 1442 | if(socket) try{socket.destroy(ex)} catch(_){} 1443 | try{cl.destroy(ex)} catch(_) {} 1444 | return false; 1445 | } 1446 | })().finally(val => {if(val === false) return; try {cl.end()} catch(ex) {}})); 1447 | sock.on('error', async ex => { 1448 | try{sock.end()} catch(_){} 1449 | if(reader && writer) { 1450 | try { 1451 | var disconnectBuff = Buffer.from(JSON.stringify((await serverInfo.getDisconnectMessage('connection-failed', ex)) || {text: 'Failed to connect to remote server: ' + ex.message}), 'utf-8'); 1452 | var buff1 = writer.createCheckpoint(5); 1453 | var buff2 = writer.createCheckpoint(5 + disconnectBuff.length); 1454 | buff2.writeVarInt(0); 1455 | buff2.writeVarInt(disconnectBuff.length); 1456 | buff2.writeBytes(disconnectBuff); 1457 | buff1.writeVarInt(buff2.length); 1458 | buff1.appendPending(); 1459 | await buff2.push(); 1460 | await writer.flush(); 1461 | await reader.streamReady(); 1462 | await writer.streamReady(); 1463 | cl.end(); 1464 | } catch(ex) {} 1465 | } 1466 | reject(ex); //double rejecting does nothing 1467 | try{cl.destroy(ex)} catch(_){} 1468 | }); 1469 | }); 1470 | } 1471 | 1472 | while(1) { 1473 | await writer.flush(); 1474 | reader.canEnd = true; 1475 | var length = await reader.readVarInt(state == 0); 1476 | reader.canEnd = false; 1477 | if(length == null) break; 1478 | var index = reader.index; 1479 | //length == 254, var int for 0xFE 0x01 1480 | var probLegacy = state == 0 && ((length instanceof Array && length.length == 1 && length[0] == 0xFE) || length === 254) 1481 | var id = null; 1482 | 1483 | if(probLegacy) { 1484 | cl.allowHalfOpen = true; 1485 | //legacy ping 1486 | if(length === 254) { 1487 | reader.canEnd = true; 1488 | id = await reader.readVarInt(); 1489 | reader.canEnd = false; 1490 | if(id == 122) { 1491 | var nbyte = await reader.readByte(); 1492 | if(nbyte != 0x0B) throw new TypeError("Invalid legacy message " + nbyte); 1493 | await reader.readBytes(22); 1494 | var rlen = await reader.readShort(); 1495 | if(rlen < 0 || rlen > 4096) throw new RangeError("Too big legacy data"); 1496 | var version = await reader.readByte(); 1497 | var hostlen = await reader.readShort(); 1498 | if(hostlen * 2 != rlen - 7) throw new RangeError("Invalig host len"); 1499 | var host = fromUTF16Be(await reader.readBytes(hostlen * 2)); 1500 | var port = await reader.readInt(); 1501 | serverInfo = await getServer({state: 'status', legacy: true, host, port, version, remoteClient: cl, proxyServer}); 1502 | state = 5; 1503 | } else if(id == null) { 1504 | serverInfo = await getServer({state: 'status', legacy: true, host: '', port: 0, version: 0, remoteClient: cl, proxyServer}); 1505 | state = 5; 1506 | } else { 1507 | probLegacy = false; 1508 | } 1509 | 1510 | } else if(length instanceof Array) { 1511 | serverInfo = await getServer({state: 'status', legacy: true, host: '', port: 0, version: 0, remoteClient: cl, proxyServer}); 1512 | state = 4; 1513 | } else { 1514 | throw new Error("Unknown legacy ID: " + id); 1515 | } 1516 | if(probLegacy) { 1517 | if(serverInfo.status) { 1518 | await sendLegacyPing(serverInfo.status); 1519 | } else { 1520 | if(!serverInfo.displayHost) serverInfo.displayHost = serverInfo.host; 1521 | if(!serverInfo.version) serverInfo.version = version; 1522 | if(!serverInfo.displayPort) serverInfo.displayPort = serverInfo.port; 1523 | await createConnection(); 1524 | } 1525 | return; 1526 | } 1527 | } 1528 | cl.allowHalfOpen = false; 1529 | if(length < 1) throw new Error("length may not be lower then 1"); 1530 | if(id == null) id = await reader.readVarInt(); 1531 | 1532 | if(state == 0) { 1533 | if(id != 0) throw new Error("Expected handshake packet"); 1534 | var version = await reader.readVarInt(); 1535 | var host = await reader.readString(255); 1536 | var port = await reader.readUnsignedShort(); 1537 | var nextState = await reader.readVarInt(); 1538 | if(index + length != reader.index) throw new RangeError("Unexpected EOF in packet"); 1539 | if(nextState == 1) { 1540 | serverInfo = await getServer({state: 'status', legacy: false, host, port, version, remoteClient: cl, proxyServer}); 1541 | if(!serverInfo.displayHost) serverInfo.displayHost = serverInfo.host; 1542 | if(!serverInfo.version) serverInfo.version = version; 1543 | if(!serverInfo.displayPort) serverInfo.displayPort = serverInfo.port; 1544 | if(!serverInfo) { 1545 | cl.end(); 1546 | return; 1547 | } 1548 | if(serverInfo.status) { 1549 | state = 3; 1550 | } else { 1551 | state = 1; 1552 | await createConnection(); 1553 | } 1554 | } else if(nextState == 2) { 1555 | serverData = {host, port, version}; 1556 | state = 2; 1557 | encryptionStage = 1; 1558 | } else throw new Error("Unknown state: " + nextState); 1559 | continue; 1560 | } else if(state == 3) { 1561 | if(id == 0) { 1562 | if(index + length != reader.index) throw new RangeError("Unexpected EOF in packet"); 1563 | var str = Buffer.from(JSON.stringify(serverInfo.status), 'utf-8'); 1564 | var buff1 = writer.createCheckpoint(5); 1565 | var buff2 = writer.createCheckpoint(10 + str.length); 1566 | buff2.writeVarInt(0); 1567 | buff2.writeVarInt(str.length); 1568 | buff2.writeBytes(str); 1569 | buff1.writeVarInt(buff2.length); 1570 | buff1.appendPending(); 1571 | await buff2.push(); 1572 | } else if(id == 1) { 1573 | var val = reader.readLong(); 1574 | if(index + length != reader.index) throw new RangeError("Unexpected EOF in packet"); 1575 | var buff = writer.createCheckpoint(10); 1576 | buff.writeVarInt(9); 1577 | buff.writeVarInt(1); 1578 | buff.writeLong(val); 1579 | await buff.push(); 1580 | } else { 1581 | throw new Error("Unknown packet ID (state = status): " + id); 1582 | } 1583 | continue; 1584 | } else if(state == 1) { 1585 | if(length > 65535) throw new RangeError("Packet too big for ping"); 1586 | if(id != 0x0 && id != 0x01) throw new TypeError("Unknown not supported ping ID: " + id); 1587 | if(id == 0x01 && length != 9) throw new TypeError("Invalid length for ping ID 0x01"); 1588 | if(id == 0x00 && length != 1) throw new TypeError("Invalid length for ping ID 0x0"); 1589 | var data = await reader.readBytes(length - (reader.index - index)); 1590 | var buff1 = socketWriter.createCheckpoint(5); 1591 | var buff2 = socketWriter.createCheckpoint(4 + data.length); 1592 | buff2.writeVarInt(id); 1593 | buff2.writeBytes(data); 1594 | buff1.writeVarInt(buff2.length); 1595 | buff1.appendPending(); 1596 | await buff2.push(); 1597 | continue; 1598 | } else if(state != 2) { 1599 | throw new Error("Unknown state: " + state); 1600 | } 1601 | if(encryptionStage == 1) { 1602 | if(id != 0) throw new TypeError("Invalid ID for login start"); 1603 | username = await reader.readString(16); 1604 | await reader.readBytes(length - (reader.index - index)); 1605 | if(reader.index != index + length) throw new TypeError("Invalide EOF of packet"); 1606 | serverInfo = await getServer({state: 'login', legacy: false, host: serverData.host, port: serverData.port, version: serverData.version, username, remoteClient: cl, proxyServer}); 1607 | if(!serverInfo.displayHost) serverInfo.displayHost = serverInfo.host; 1608 | if(!serverInfo.version) serverInfo.version = version; 1609 | if(!serverInfo.displayPort) serverInfo.displayPort = serverInfo.port; 1610 | if(!serverInfo.getDisconnectMessage) serverInfo.getDisconnectMessage = () => null; 1611 | if(serverInfo.cracked) { 1612 | encryptionStage = 0; 1613 | return await createConnection(); 1614 | } 1615 | var { publicKey } = await keyPromise; 1616 | var buff1 = writer.createCheckpoint(5); 1617 | var buff2 = writer.createCheckpoint(20 + publicKey.length); 1618 | verifyToken = await new Promise((resolve, reject) => { 1619 | crypto.randomFill(Buffer.alloc(4), (err, buf) => { 1620 | if(err) reject(err); 1621 | else resolve(buf); 1622 | }); 1623 | }); 1624 | 1625 | 1626 | buff2.writeVarInt(1); 1627 | buff2.writeString(''); 1628 | buff2.writeVarInt(publicKey.length); 1629 | buff2.writeBytes(publicKey); 1630 | buff2.writeVarInt(verifyToken.length); 1631 | buff2.writeBytes(verifyToken); 1632 | buff1.writeVarInt(buff2.length); 1633 | buff1.appendPending(); 1634 | await buff2.push(); 1635 | encryptionStage = 2; 1636 | continue; 1637 | } else if(encryptionStage == 2) { 1638 | if(id == 0x02) { 1639 | //plugin response??? 1640 | //we do not even have encryption. ignore this 1641 | await reader.readBytes(length - (reader.index - index)); 1642 | continue; 1643 | } 1644 | if(id != 0x01) throw new TypeError("Invalid ID for encryption response: " + id + " length: " + length); 1645 | var sharedSecretLen = await reader.readVarInt(); 1646 | if(sharedSecretLen < 0 || sharedSecretLen > 256) throw new RangeError("Too big shared secret"); 1647 | var encryptedSharedSecret = await reader.readBytes(sharedSecretLen); 1648 | var hasVerifyToken = await reader.readByte(); 1649 | var { publicKey, privateKey } = await keyPromise; 1650 | var decryptKey = crypto.createPrivateKey({ 1651 | key: privateKey, 1652 | format: 'der', 1653 | type: 'pkcs8' 1654 | }); 1655 | if(hasVerifyToken > 0) { 1656 | var verifyTokenLen = await reader.readVarInt(); 1657 | if(verifyTokenLen < 0 || verifyTokenLen > 256) throw new TypeError("Too big verify token"); 1658 | var clientVerifyToken = await reader.readBytes(verifyTokenLen); 1659 | var toVerify = crypto.privateDecrypt({ 1660 | key: decryptKey, 1661 | padding: crypto.constants.RSA_PKCS1_PADDING 1662 | }, clientVerifyToken); 1663 | if(toVerify.length != verifyToken.length || !crypto.timingSafeEqual(toVerify, verifyToken)) throw new TypeError("verify token does not match"); 1664 | } 1665 | await reader.readBytes(length - (reader.index - index)); 1666 | var sharedSecret = crypto.privateDecrypt({ 1667 | key: decryptKey, 1668 | padding: crypto.constants.RSA_PKCS1_PADDING 1669 | }, encryptedSharedSecret); 1670 | await writer.flush(); 1671 | await reader.streamReady(); 1672 | await writer.streamReady(); 1673 | chiperClient = createMCChiperStream(sharedSecret); 1674 | dechiperClient = createMCDechiperStream(sharedSecret); 1675 | cl.emit('encryption', { chiper: chiperClient, dechiper: dechiperClient }); 1676 | chiperClient.on('error', ex => { 1677 | try{cl.destroy(ex)} catch(_){} 1678 | try{dechiperClient.destroy(ex)} catch(_){} 1679 | }) 1680 | dechiperClient.on('error', ex => { 1681 | try{cl.destroy(ex)} catch(_){} 1682 | try{chiperClient.destroy(ex)} catch(_){} 1683 | }); 1684 | cl.on('error', ex => { 1685 | try{chiperClient.destroy(ex)} catch(_){} 1686 | try{dechiperClient.destroy(ex)} catch(_){} 1687 | }); 1688 | cl.pipe(dechiperClient); 1689 | chiperClient.pipe(cl); 1690 | reader = new ReadableDataParser(dechiperClient); 1691 | writer = new WritableDataParser(chiperClient); 1692 | cl.resume(); 1693 | var serverID = mcHexDigest(crypto.createHash('sha1').update(Buffer.from('', 'utf-8')).update(sharedSecret).update(publicKey).digest()); 1694 | var success; 1695 | var disconnectMessage = null; 1696 | try { 1697 | success = await new Promise((resolve, reject) => { 1698 | https.request({ 1699 | method: "GET", 1700 | host: "sessionserver.mojang.com", 1701 | port: 443, 1702 | path: '/session/minecraft/hasJoined?username=' + encodeURIComponent(username) + '&serverId=' + encodeURIComponent(serverID) 1703 | }, res => { 1704 | try { 1705 | res.on('error', ex => reject(ex)); 1706 | if(res.statusCode != 200) { 1707 | resolve(false); 1708 | res.destroy(); 1709 | } 1710 | var len = 0; 1711 | var data = []; 1712 | res.on('data', d => { 1713 | len += d.length; 1714 | if(len > 65535) { 1715 | res.destroy(new Error("Cannot read more then 65535 bytes of data from response.")); 1716 | return; 1717 | } 1718 | data.push(d); 1719 | }); 1720 | res.on('end', async () => { 1721 | try { 1722 | var json = JSON.parse(Buffer.concat(data).toString('utf-8')); 1723 | if(typeof json != 'object' || json.name !== username || !json.id || typeof json.id != 'string') { 1724 | resolve(false); 1725 | return; 1726 | } 1727 | var res = true; 1728 | //add dashes 1729 | var id = uuidWithDashes(json.id); 1730 | userID = id; 1731 | if(serverInfo.verifyLogin) res = await serverInfo.verifyLogin(id, json); 1732 | if(res === false) { 1733 | disconnectMessage = await serverInfo.getDisconnectMessage('user-denied') || {text: "You are not allowed to login"}; 1734 | resolve(false); 1735 | } 1736 | else resolve(true); 1737 | } catch(ex) { 1738 | reject(ex); 1739 | } 1740 | }); 1741 | } catch(ex) { 1742 | reject(ex); 1743 | } 1744 | }).on('error', ex => reject(ex)).end(); 1745 | }); 1746 | } catch(ex) { 1747 | if(!emittedError) try{proxyServer.emit('client-error', ex, cl);}catch(_){} 1748 | emittedError = true; 1749 | if(socket) try{socket.destroy(ex)} catch(_){} 1750 | try{cl.destroy(ex)} catch(_) {} 1751 | return; 1752 | } 1753 | if(!success) { 1754 | if(!disconnectMessage) { 1755 | disconnectMessage = (await serverInfo.getDisconnectMessage('auth-failed')) || {text: "Cannot verify user"}; 1756 | } 1757 | //disconnect is encrypted 1758 | var disconnectBuff = Buffer.from(JSON.stringify(disconnectMessage), 'utf-8'); 1759 | var buff1 = writer.createCheckpoint(5); 1760 | var buff2 = writer.createCheckpoint(5 + disconnectBuff.length); 1761 | buff2.writeVarInt(0); 1762 | buff2.writeVarInt(disconnectBuff.length); 1763 | buff2.writeBytes(disconnectBuff); 1764 | buff1.writeVarInt(buff2.length); 1765 | buff1.appendPending(); 1766 | await buff2.push(); 1767 | await writer.flush(); 1768 | await reader.streamReady(); 1769 | await writer.streamReady(); 1770 | cl.end(); 1771 | return; 1772 | } 1773 | return await createConnection(); 1774 | } else { 1775 | throw new Error("Unknown encryption stage: " + encryptionStage); 1776 | } 1777 | } 1778 | } catch(ex) { 1779 | if(!emittedError) try{proxyServer.emit('client-error', ex, cl);}catch(_){} 1780 | emittedError = true; 1781 | if(socket) try{socket.destroy(ex)} catch(_){} 1782 | try{cl.destroy(ex)} catch(_) {} 1783 | } 1784 | }); 1785 | proxyServer.privateKey = keyPromise.then(x => x.privateKey); 1786 | proxyServer.publicKey = keyPromise.then(x => x.publicKey); 1787 | return proxyServer; 1788 | } 1789 | 1790 | /** 1791 | * Get status of the destination server 1792 | * @param {{protocolVersion?: string, host: string, port: number, displayHost?: string, displayPort?: number}} param0 Options for the destination server 1793 | * @returns {Promise<{data: object, ping: number}>} A promise that resolves with an object containing the JSON response data and the MS ping delay or rejects with an Error 1794 | */ 1795 | async function getServerStatus({ protocolVersion, host, port, displayHost, displayPort }) { 1796 | if(!displayHost) displayHost = host; 1797 | if(!displayPort) displayPort = port; 1798 | if(!protocolVersion) protocolVersion = 65535; 1799 | var socket = await new Promise((resolve, reject) => { 1800 | var errListener = ex => reject(ex); 1801 | var sock = net.connect({ 1802 | host, 1803 | port 1804 | }, () => { 1805 | sock.removeListener('error', errListener); 1806 | resolve(sock); 1807 | }).once('error', errListener); 1808 | }); 1809 | var data; 1810 | var ping; 1811 | try { 1812 | var writer = new WritableDataParser(socket); 1813 | var reader = new ReadableDataParser(socket); 1814 | 1815 | var hostBuff = Buffer.from(displayHost, 'utf-8'); 1816 | var buff1 = writer.createCheckpoint(5); 1817 | var buff2 = writer.createCheckpoint(20 + hostBuff.length); 1818 | buff2.writeVarInt(0); 1819 | buff2.writeVarInt(protocolVersion); 1820 | buff2.writeVarInt(hostBuff.length); 1821 | buff2.writeBytes(hostBuff); 1822 | buff2.writeUnsignedShort(port); 1823 | buff2.writeVarInt(1); 1824 | buff1.writeVarInt(buff2.length); 1825 | buff2.writeVarInt(1); 1826 | buff2.writeVarInt(0); 1827 | buff1.appendPending(); 1828 | await buff2.push(); //one write call, so one TCP packet for 2 MC packets. 1829 | 1830 | var length = await reader.readVarInt(); 1831 | var index = reader.index; 1832 | if(length < 1 || length > 65535) throw new Error("Too much data"); 1833 | var id = await reader.readVarInt(); 1834 | if(id != 0) throw new Error("Invalid ID for status response"); 1835 | data = JSON.parse(await reader.readString(32767)); 1836 | if(reader.index !== index + length) throw new Error("Length does not match with reader index"); 1837 | 1838 | buff1 = writer.createCheckpoint(10); 1839 | buff1.writeVarInt(9); 1840 | buff1.writeVarInt(1); 1841 | var now = BigInt(Date.now()); 1842 | buff1.writeLong(now); 1843 | await buff1.push(); 1844 | 1845 | var length = await reader.readVarInt(); 1846 | if(length != 9) throw new Error("Unexpected length for pong"); 1847 | var id = await reader.readVarInt(); 1848 | if(id != 1) throw new Error("Invalid ID for pong"); 1849 | var val = await reader.readLong(); 1850 | if(val != now) throw new Error("Invalid pong response (does not match)"); 1851 | ping = Date.now() - Number(now); 1852 | 1853 | await writer.flush(); 1854 | await reader.streamReady(); 1855 | await writer.streamReady(); 1856 | 1857 | } catch(ex) { 1858 | try {socket.destroy(ex)}catch(_){} 1859 | throw ex; 1860 | } 1861 | socket.on('error', () => {}); 1862 | try{socket.destroy();}catch(_){} 1863 | return { data, ping: ping }; 1864 | } 1865 | 1866 | /** 1867 | * Get the Public key (and a function to sign server join hashes) of the destination server 1868 | * @param {{protocolVersion?: string, host: string, port: number, displayHost?: string, displayPort?: number, username?: string}} param0 Options for the destination server 1869 | * @returns {Promise<{status: 'disconnect' | 'cracked' | 'online', message?: object, serverName?: string, publicKey?: Buffer, createHash?: (sharedSecret: Buffer) => string}>} A Promise that resolves with the response status. 1870 | * if online, then you get the serverName (max 20 bytes), the public key as a Buffer in DER format and a function that can create a MC hex digest for signing in if you give the missing SharedSecret. It rejects with an Error if the connection failed. 1871 | * Some servers reject the connection if the protocolVersion is not correct. You can get the protocolVersion with getServerStatus and use (await getServerStatus(...)).data.version.protocol for protocolVersion. 1872 | */ 1873 | async function getServerPublicKey({ protocolVersion, host, port, displayHost, displayPort, username }) { 1874 | if(!displayHost) displayHost = host; 1875 | if(!displayPort) displayPort = port; 1876 | if(!protocolVersion) protocolVersion = 65535; 1877 | if(!username) username = '_'; 1878 | 1879 | var socket = await new Promise((resolve, reject) => { 1880 | var errListener = ex => reject(ex); 1881 | var sock = net.connect({ 1882 | host, 1883 | port 1884 | }, () => { 1885 | sock.removeListener('error', errListener); 1886 | resolve(sock); 1887 | }).once('error', errListener); 1888 | }); 1889 | 1890 | var response = null; 1891 | 1892 | try { 1893 | var writer = new WritableDataParser(socket); 1894 | var reader = new ReadableDataParser(socket); 1895 | 1896 | var hostBuff = Buffer.from(displayHost, 'utf-8'); 1897 | var buff1 = writer.createCheckpoint(5); 1898 | var buff2 = writer.createCheckpoint(20 + hostBuff.length); 1899 | buff2.writeVarInt(0); 1900 | buff2.writeVarInt(protocolVersion); 1901 | buff2.writeVarInt(hostBuff.length); 1902 | buff2.writeBytes(hostBuff); 1903 | buff2.writeUnsignedShort(port); 1904 | buff2.writeVarInt(2); 1905 | buff1.writeVarInt(buff2.length); 1906 | buff1.appendPending(); 1907 | buff2.appendPending(); 1908 | 1909 | var usernameBuff = Buffer.from(username, 'utf-8'); 1910 | 1911 | buff1 = writer.createCheckpoint(5); 1912 | buff2 = writer.createCheckpoint(10 + usernameBuff.length); 1913 | buff2.writeVarInt(0); 1914 | buff2.writeVarInt(usernameBuff.length); 1915 | buff2.writeBytes(usernameBuff); 1916 | buff1.writeVarInt(buff2.length); 1917 | buff1.appendPending(); 1918 | await buff2.push(); //one write call, so one TCP packet for 2 MC packets. 1919 | 1920 | var id, length, index; 1921 | 1922 | while(1) { 1923 | length = await reader.readVarInt(); 1924 | if(length < 1 || length > 65535) throw new Error("Too much data"); 1925 | index = reader.index; 1926 | id = await reader.readVarInt(); 1927 | switch(id) { 1928 | case 0: 1929 | response = {status: 'disconnect', message: JSON.parse(await reader.readString(32767))}; 1930 | if(reader.index !== index + length) throw new Error("index does not match with length for disconnect"); 1931 | break; 1932 | case 1: 1933 | break; 1934 | case 2: //Login success 1935 | case 3: //set compression 1936 | response = {status: 'cracked'}; 1937 | break; 1938 | case 4: //Plugin request, we always response that we do not support the addition (same as te Notchian client) 1939 | var messageID = await reader.readVarInt(); 1940 | await reader.readBytes(length - (reader.index - index)); 1941 | buff1 = writer.createCheckpoint(5); 1942 | buff2 = writer.createCheckpoint(10); 1943 | buff2.writeVarInt(2); 1944 | buff2.writeVarInt(messageID); 1945 | buff2.writeByte(0); //success = false 1946 | buff1.writeVarInt(buff2.length); 1947 | buff1.appendPending(); 1948 | await buff2.push(); 1949 | continue; 1950 | default: 1951 | throw new Error("Unknown packet ID in login: " + id); 1952 | } 1953 | break; 1954 | } 1955 | if(id == 1) { 1956 | var serverName = await reader.readString(20); 1957 | var pubKeyLength = await reader.readVarInt(); 1958 | if(pubKeyLength > 256) throw new Error("Too long public key"); 1959 | var publicKey = await reader.readBytes(pubKeyLength); 1960 | var verifyTokenLength = await reader.readVarInt(); 1961 | if(verifyTokenLength > 256) throw new Error("Too long verify token"); 1962 | var verifyToken = await reader.readBytes(verifyTokenLength); 1963 | if(reader.index !== index + length) throw new Error("index does not match with length for encryption request"); 1964 | var serverNameBuff = Buffer.from(serverName, 'utf-8'); 1965 | response = {status: 'online', publicKey, verifyToken, serverName, createHash(sharedSecret) { 1966 | return mcHexDigest(crypto.createHash('sha1').update(serverNameBuff).update(sharedSecret).update(publicKey).digest()); 1967 | }}; 1968 | } 1969 | await writer.flush(); 1970 | await reader.streamReady(); 1971 | await writer.streamReady(); 1972 | if(!response) throw new Error("No response"); 1973 | 1974 | } catch(ex) { 1975 | try {socket.destroy(ex)}catch(_){} 1976 | throw ex; 1977 | } 1978 | socket.on('error', () => {}); 1979 | try{socket.destroy();}catch(_){} 1980 | return response; 1981 | } 1982 | 1983 | /** 1984 | * Resolve SRV records that points to the real address of a server. 1985 | * @param {String} host the host to resolve 1986 | * @returns {Promise<{name: string, port: number}>} a promise that resolves with the original host (and port is null) or a new name (and port is the port in the SRV record). 1987 | */ 1988 | function resolveMCSrvRecord(host) { 1989 | return new Promise(resolve => { 1990 | dns.resolveSrv('_minecraft._tcp.' + host, (err, addr) => { 1991 | if(addr) addr = addr[0]; 1992 | if(err || !addr || !addr.name) resolve({name: host, port: null}); 1993 | else resolve({name: addr.name, port: addr.port}); 1994 | }) 1995 | }); 1996 | } 1997 | 1998 | /** 1999 | * Convert chat objects (from disconnect or server ping) to a string. 2000 | * @param {object} chat The chat object to conver 2001 | * @returns {string} A string with all the formatting removed. 2002 | */ 2003 | function chatObjectToString(chat) { 2004 | if(typeof chat == 'object') { 2005 | var txt = String(chat.text || ''); 2006 | if(typeof chat.extra == 'object' && chat.extra && chat.extra instanceof Array) { 2007 | for(var item of chat.extra) { 2008 | txt += chatObjectToString(item); 2009 | } 2010 | } 2011 | return txt; 2012 | } else return String(chat); 2013 | } 2014 | 2015 | /** 2016 | * Parse a motd chat object to an array of string. each item in the array is a line for multiplayer. 2017 | * It always returns an array with 2 items (because Motd can have 2 lines). One of these lines can be empty and none of these lines is longer then 45 characters. 2018 | * @param {object} chat The chat object from server ping to convert. It is located at the 'description' property from the status object returned by server ping. 2019 | * @returns {[string, string]} An array with 2 items, for each motd line. 2020 | */ 2021 | function parsePingMotdObject(chat) { 2022 | var totalMotd = chatObjectToString(chat); 2023 | var splitIndex = totalMotd.indexOf('\n'); 2024 | 2025 | if(splitIndex < 0 || splitIndex > 45) splitIndex = 45; 2026 | return [totalMotd.substr(0, splitIndex), totalMotd.substr(splitIndex, 45).split('\n')[0]]; 2027 | 2028 | } 2029 | 2030 | module.exports = { 2031 | //Basics 2032 | createProxyServer, 2033 | uuidWithDashes, 2034 | uuidWithoutDashes, 2035 | getServerStatus, 2036 | resolveMCSrvRecord, 2037 | chatObjectToString, 2038 | parsePingMotdObject, 2039 | 2040 | //Advanced 2041 | getServerPublicKey, 2042 | generateMCSharedSecret, 2043 | mcHexDigest, 2044 | 2045 | //Own implementation 2046 | BaseDataParser, 2047 | ReadableDataParser, 2048 | WritableDataParser, 2049 | WritableDataBuffer, 2050 | createMCChiperStream, 2051 | createMCDechiperStream 2052 | }; 2053 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mc-proxy", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "main.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "electron": "^13.1.9", 9 | "electron-builder": "^22.11.7" 10 | }, 11 | "scripts": { 12 | "start": "electron ." 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC" 17 | } 18 | -------------------------------------------------------------------------------- /screenshots/mc-proxy-altening.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SqueezedSlime/mc-proxy/07e182221d9c558ead1aec998ee6075ee3fbce76/screenshots/mc-proxy-altening.PNG -------------------------------------------------------------------------------- /screenshots/minecraft-multiplayer-lan-world.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SqueezedSlime/mc-proxy/07e182221d9c558ead1aec998ee6075ee3fbce76/screenshots/minecraft-multiplayer-lan-world.PNG -------------------------------------------------------------------------------- /screenshots/minehut-with-altening-using-proxy.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SqueezedSlime/mc-proxy/07e182221d9c558ead1aec998ee6075ee3fbce76/screenshots/minehut-with-altening-using-proxy.PNG -------------------------------------------------------------------------------- /screenshots/saving-alts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SqueezedSlime/mc-proxy/07e182221d9c558ead1aec998ee6075ee3fbce76/screenshots/saving-alts.png --------------------------------------------------------------------------------