├── .gitignore ├── mcpews ├── lib │ ├── version.js │ ├── index.js │ ├── encrypt.js │ ├── app.js │ ├── client.js │ ├── server.js │ └── index.d.ts ├── .eslintrc.js ├── package.json ├── LICENSE ├── README.md └── src │ ├── mitm.js │ └── repl.js ├── package.json ├── LICENSE ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /mcpews/lib/version.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | V1: 1, 3 | V2: 16842752 4 | }; 5 | -------------------------------------------------------------------------------- /mcpews/lib/index.js: -------------------------------------------------------------------------------- 1 | const WSServer = require("./server"); 2 | const WSClient = require("./client"); 3 | const WSApp = require("./app"); 4 | const Version = require("./version"); 5 | const { ClientEncryption, ServerEncryption } = require("./encrypt"); 6 | 7 | module.exports = { 8 | WSServer, WSClient, WSApp, Version, ClientEncryption, ServerEncryption 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CodeConnectFix", 3 | "version": "1.1.2", 4 | "description": "Allow use 'Code Connection for Minecraft' v1.50 with Minecraft Bedrock v1.19.x", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "build": "pkg package.json", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "bin": "index.js", 12 | "pkg": { 13 | "targets": [ 14 | "node16-win-x64" 15 | ], 16 | "outputPath": "dist" 17 | }, 18 | "author": "Rocher Laurent", 19 | "license": "MIT", 20 | "dependencies": { 21 | "uuid": "^9.0.0", 22 | "ws": "^8.9.0" 23 | }, 24 | "devDependencies": { 25 | "pkg": "^5.8.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /mcpews/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true 6 | }, 7 | extends: ["airbnb-base"], 8 | parserOptions: { 9 | ecmaVersion: "latest" 10 | }, 11 | rules: { 12 | indent: ["error", 4, { SwitchCase: 1 }], 13 | "linebreak-style": ["error", "windows"], 14 | quotes: ["error", "double"], 15 | semi: ["error", "always"], 16 | "max-classes-per-file": ["error", 3], 17 | "no-bitwise": ["error", { allow: ["<<", "&"] }], 18 | "no-param-reassign": ["error", { props: false }], 19 | "comma-dangle": ["error", "never"], 20 | "max-len": ["error", { code: 120 }] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /mcpews/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcpews", 3 | "version": "3.0.2", 4 | "description": "A library that supports MCPE Websocket Protocol", 5 | "main": "lib/index.js", 6 | "bin": { 7 | "mcpews": "./src/repl.js", 8 | "mcpewsmitm": "./src/mitm.js" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "lint": "eslint --fix ." 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/XeroAlpha/mcpews.git" 17 | }, 18 | "keywords": [ 19 | "minecraft", 20 | "bedrock", 21 | "websocket" 22 | ], 23 | "author": "ProjectXero", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/XeroAlpha/mcpews/issues" 27 | }, 28 | "homepage": "https://github.com/XeroAlpha/mcpews", 29 | "dependencies": { 30 | "uuid": "^8.3.2", 31 | "ws": "^8.6.0" 32 | }, 33 | "devDependencies": { 34 | "eslint": "^8.15.0", 35 | "eslint-config-airbnb-base": "^15.0.0", 36 | "eslint-plugin-import": "^2.26.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Rocher Laurent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mcpews/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ProjectXero 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mcpews/lib/encrypt.js: -------------------------------------------------------------------------------- 1 | const crypto = require("crypto"); 2 | 3 | const ecdhCurve = "secp384r1"; 4 | const blockSize = 16; 5 | const cipherAlgorithm = "aes-256-cfb8"; 6 | const hashAlgorithm = "sha256"; 7 | 8 | const asn1Header = Buffer.from("3076301006072a8648ce3d020106052b81040022036200", "hex"); 9 | function asOpenSSLPubKey(pubKeyBuffer) { 10 | return Buffer.concat([asn1Header, pubKeyBuffer]); 11 | } 12 | function asNodejsPubKey(pubKeyBuffer) { 13 | return pubKeyBuffer.slice(asn1Header.length); 14 | } 15 | 16 | function hashBuffer(algorithm, buffer) { 17 | const hash = crypto.createHash(algorithm); 18 | hash.update(buffer); 19 | return hash.digest(); 20 | } 21 | 22 | class Encryption { 23 | constructor() { 24 | this.ecdh = crypto.createECDH(ecdhCurve); 25 | this.pubKey = this.ecdh.generateKeys(); 26 | } 27 | 28 | initializeCipher(secretKey, salt) { 29 | const key = hashBuffer(hashAlgorithm, Buffer.concat([salt, secretKey])); 30 | const initialVector = key.slice(0, blockSize); 31 | this.cipher = crypto.createCipheriv(cipherAlgorithm, key, initialVector); 32 | this.decipher = crypto.createDecipheriv(cipherAlgorithm, key, initialVector); 33 | this.cipher.setAutoPadding(false); 34 | this.decipher.setAutoPadding(false); 35 | } 36 | 37 | encrypt(str) { 38 | return this.cipher.update(str, "utf8"); 39 | } 40 | 41 | decrypt(buffer) { 42 | return this.decipher.update(buffer).toString("utf8"); 43 | } 44 | } 45 | 46 | class ServerEncryption extends Encryption { 47 | beginKeyExchange() { 48 | this.salt = crypto.randomBytes(blockSize); 49 | return { 50 | publicKey: asOpenSSLPubKey(this.pubKey).toString("base64"), 51 | salt: this.salt.toString("base64") 52 | }; 53 | } 54 | 55 | completeKeyExchange(clientPubKeyStr) { 56 | const clientPubKey = asNodejsPubKey(Buffer.from(clientPubKeyStr, "base64")); 57 | this.initializeCipher(this.ecdh.computeSecret(clientPubKey), this.salt); 58 | } 59 | } 60 | 61 | class ClientEncryption extends Encryption { 62 | beginKeyExchange() { 63 | return { 64 | publicKey: asOpenSSLPubKey(this.pubKey).toString("base64") 65 | }; 66 | } 67 | 68 | completeKeyExchange(serverPubKeyStr, saltStr) { 69 | const serverPubKey = asNodejsPubKey(Buffer.from(serverPubKeyStr, "base64")); 70 | const salt = Buffer.from(saltStr, "base64"); 71 | this.initializeCipher(this.ecdh.computeSecret(serverPubKey), salt); 72 | } 73 | } 74 | 75 | module.exports = { 76 | implementName: "com.microsoft.minecraft.wsencrypt", 77 | ServerEncryption, 78 | ClientEncryption 79 | }; 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeConnectFix 2 | 3 | Allow use 'Code Connection for Minecraft' v1.50 with Minecraft Bedrock >= v1.19.x. 4 | It's a 'Man In The Middle' WebSocketSerwer for simulate a compatible version of Minecraft Bedrock to 'Code Connection for Minecraft'. 5 | 6 | ## Install 7 | 8 | ### Windows Executable - Easy Way - 9 | 10 | Download [CodeConnectFix.exe](https://github.com/lrocher/CodeConnectFix/releases/download/v1.1.2/CodeConnectFix.exe) and copy file on your Desktop. 11 | 12 | ### From Source - Developper Way - 13 | 14 | Requirement : Install Node.Js v16 LTS (https://nodejs.org/en/download/) 15 | 16 | 1) Download Source and extract archive in a directory (or Git clone project) 17 | 2) Open a shell (cmd.exe) an navigate to project directory 18 | 3) Execute 'npm install' for download dependencies 19 | 4) Execute 'npm start' or 'node index.js' for run server 20 | 5) Execute 'npm run build' for generate executable 21 | 22 | ## Usage 23 | 24 | On same computer : 25 | 26 | 1) Start 'Code Connection for Minecraft'. 27 | 2) Start 'CodeConnectFix' by run executable or execute server with NPM/Node. 28 | Note: First execution, require to add a firewall rule with an administrative account. 29 | 3) Start 'Minecraft Bedrock'. 30 | 1) Create a new world, Activate Cheat Mode, Start game. 31 | 2) In game, open chat (Press Enter or / key). 32 | 3) Execute command '/connect localhost:19135' and check confirmation message. 33 | 4) Put your game in pause (Press Escape). 34 | 4) Switch to 'Code Connection for Minecraft' and choose an editor. 35 | 36 | On different computers, you need to adjust computer address. 37 | 38 | CodeConnectFix [address:port] [port] 39 | 40 | - [address:port] : Value copied from 'Code Connection for Minecraft' after \connect. (Default: localhost:19131) 41 | - [port] : Value for 'CodeConnectFix' (Default: 19135) 42 | 43 | ## Troubleshooting 44 | 45 | - If 'Code Connection for Minecraft' interface don't change after execute /connect command. Restart 'CodeConnectFix' and retry 'connect' commande. 46 | - If 'CodeConnectFix' window close, try to run 'CodeConnectFix' from command line to check what wrong. 47 | 48 | ## Debug 49 | 50 | For additionnal debug message in console, define a NODE_ENV environment variable to 'development' before start 'CodeConnectFix'. 51 | 52 | On windows, Open a cmd.exe shell (use Windows+R) 53 | > cd [CodeConnectFix Directory] 54 | > SET NODE_ENV=development 55 | > CodeConnectFix.exe 56 | 57 | ## Code Connection Installer 58 | 59 | Code Connection Installer is not availlable on minecraft makecode website. 60 | But, it's possible to retreive it using webarchive : 61 | - http://web.archive.org/web/20231004075455/https://minecraft.makecode.com/setup/minecraft-windows10 62 | 63 | ## Credits 64 | 65 | This tools use a modified version of 'mcpews' v3.0.1 (A library that supports MCPE Websocket Protocol) made by XeroAlpha. 66 | 67 | - https://github.com/XeroAlpha/mcpews 68 | -------------------------------------------------------------------------------- /mcpews/README.md: -------------------------------------------------------------------------------- 1 | # MCPEWS 2 | 3 | A library that supports MCPE Websocket Protocol. 4 | 5 | ## Usage 6 | 7 | Server-side: 8 | ```javascript 9 | const { WSServer, Version } = require("mcpews"); 10 | const server = new WSServer(19134); // port 11 | 12 | server.on("client", session => { 13 | // someone type "/connect :19134" in the game console 14 | 15 | // execute a command 16 | session.sendCommand("say Connected!"); 17 | 18 | // execute a command and receive the response 19 | session.sendCommand("list", ({ body }) => { 20 | console.log("currentPlayerCount = " + body.currentPlayerCount); 21 | }); 22 | 23 | // subscribe a event 24 | session.subscribe("PlayerMessage", (event) => { 25 | // when event triggered 26 | const { body, version } = event; 27 | let message, messageType; 28 | if (version === Version.V2) { 29 | message = body.message; 30 | messageType = body.type; 31 | } else { 32 | message = body.properties.Message; 33 | messageType = body.properties.MessageType; 34 | } 35 | if (message === "close") { 36 | // disconnect from the game 37 | session.disconnect(); 38 | } else if (messageType === "chat") { 39 | session.sendCommand("say You just said " + message); 40 | } 41 | }); 42 | 43 | // enable encrypted connection 44 | session.enableEncryption(); 45 | }); 46 | ``` 47 | 48 | Client-side: 49 | ```javascript 50 | const { WSClient } = require("mcpews"); 51 | const client = new WSClient("ws://127.0.0.1:19134"); // address 52 | 53 | process.stdin.on("data", buffer => { 54 | // trigger a event (will be ignored if not subscribed) 55 | client.emitEvent("input", { 56 | data: buffer.toString() 57 | }); 58 | }); 59 | 60 | client.on("command", (event) => { 61 | const { requestId, commandLine } = event; 62 | 63 | // pass encryption handshake to client itself 64 | if (event.handleEncryptionHandshake()) return; 65 | 66 | // command received 67 | console.log("command: " + commandLine); 68 | 69 | // respond the command, must be called after handling 70 | event.respondCommand({ 71 | length: commandLine.length 72 | }); 73 | }); 74 | ``` 75 | 76 | WSApp, optimized for async/await: 77 | ```javascript 78 | const { WSApp } = require("mcpews"); 79 | 80 | const app = new WSApp(19134); 81 | app.on("session", async (session) => { 82 | const playerNames = (await session.command("testfor @a")).body.victim; 83 | const names = await Promise.all(playerNames.map(async (playerName) => { 84 | await session.command(`tell ${playerName} What's your name?`); 85 | try { 86 | const name = (await session.waitForEvent( 87 | "PlayerMessage", 88 | 30000, 89 | (ev) => ev.body.sender === playerName 90 | )).body.message; 91 | await session.command(`tell ${playerName} Your name is ${name}`); 92 | return name; 93 | } catch (err) { 94 | return playerName; 95 | } 96 | })); 97 | console.log(names); 98 | session.disconnect(); 99 | }); 100 | ``` 101 | 102 | REPL: 103 | ``` 104 | mcpews [] 105 | ``` 106 | 107 | MITM: 108 | ``` 109 | mcpewsmitm [] 110 | ``` -------------------------------------------------------------------------------- /mcpews/src/mitm.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable no-console */ 3 | 4 | const { WSServer, WSClient } = require("../lib"); 5 | 6 | function main(destAddress, sourcePort) { 7 | const wss = new WSServer(sourcePort); 8 | let clientCounter = 1; 9 | console.log(`Enter '/connect :${sourcePort}' to establish a connection.`); 10 | console.log(`If connection established, mitm will connect to ${destAddress} and forward messages`); 11 | wss.on("client", ({ session }) => { 12 | const client = new WSClient(`ws://${destAddress}`); 13 | const clientNo = clientCounter; 14 | clientCounter += 1; 15 | let serverVersion = NaN; 16 | let clientVersion = NaN; 17 | console.log(`<- [${clientNo}] connected`); 18 | client.on("command", (event) => { 19 | if (event.handleEncryptionHandshake()) { 20 | console.log(`-> [${clientNo}] keyExchange: ${event.requestId}`); 21 | session.enableEncryption(() => { 22 | console.log(`<- [${clientNo}] completeEncryption`); 23 | }); 24 | } else { 25 | const { requestId, commandLine } = event; 26 | session.sendCommandRaw(requestId, commandLine); 27 | console.log(`-> [${clientNo}] command: ${requestId} ${commandLine}`); 28 | } 29 | }); 30 | client.on("commandAgent", ({requestId, commandLine}) => { 31 | session.sendCommandAgentRaw(requestId, commandLine); 32 | console.log(`-> [${clientNo}] commandAgent: ${requestId} ${commandLine}`); 33 | }); 34 | client.on("commandLegacy", ({ 35 | requestId, commandName, overload, input 36 | }) => { 37 | session.sendCommandLegacyRaw(requestId, commandName, overload, input); 38 | console.log(`-> [${clientNo}] commandLegacy: ${requestId} ${commandName} ${overload}`, input); 39 | }); 40 | client.on("subscribe", ({ eventName }) => { 41 | session.subscribeRaw(eventName); 42 | console.log(`-> [${clientNo}] subscribe: ${eventName}`); 43 | }); 44 | client.on("unsubscribe", ({ eventName }) => { 45 | session.unsubscribeRaw(eventName); 46 | console.log(`-> [${clientNo}] unsubscribe: ${eventName}`); 47 | }); 48 | client.on("message", ({ version }) => { 49 | if (version !== clientVersion) { 50 | clientVersion = version; 51 | console.log(`-> [${clientNo}] version: ${clientVersion}`); 52 | } 53 | }); 54 | client.on("disconnect", () => { 55 | console.log(`-> [${clientNo}] disconnected from client`); 56 | session.disconnect(true); 57 | }); 58 | session.on("mcError", ({ requestId, statusCode, statusMessage }) => { 59 | client.sendError(statusCode, statusMessage, requestId); 60 | console.log(`<- [${clientNo}] error: ${statusMessage}`); 61 | }); 62 | session.on("event", ({ purpose, eventName, body }) => { 63 | client.publishEvent(eventName, body); 64 | console.log(`<- [${clientNo}] ${purpose}: ${eventName}`, body); 65 | }); 66 | session.on("commandResponse", ({ requestId, body }) => { 67 | client.respondCommand(requestId, body); 68 | console.log(`<- [${clientNo}] commandResponse: ${requestId}`, body); 69 | }); 70 | session.on("commandAgentResponse", ({ requestId, actionName, body }) => { 71 | client.respondCommandAgent(requestId, actionName, body); 72 | console.log(`<- [${clientNo}] commandAgentResponse: ${requestId} ${actionName}`); 73 | }); 74 | session.on("message", ({ version }) => { 75 | if (version !== serverVersion) { 76 | serverVersion = version; 77 | console.log(`<- [${clientNo}] version: ${serverVersion}`); 78 | } 79 | }); 80 | session.on("disconnect", () => { 81 | console.log(`<- [${clientNo}] disconnected from server`); 82 | client.disconnect(); 83 | }); 84 | }); 85 | } 86 | 87 | if (require.main === module) { 88 | main(process.argv[2], process.argv[3] || 19135); 89 | } else { 90 | module.exports = { main }; 91 | } 92 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable no-console */ 3 | 4 | const { WSServer, WSClient, Version } = require("./mcpews"); 5 | 6 | const ip = Object.values(require('os').networkInterfaces()).flat().find(i => i.family == 'IPv4' && !i.internal); 7 | const localhost = ( ip != null ? ip.address : 'localhost'); 8 | const debug = (process.env.NODE_ENV === 'development'); 9 | 10 | function main(destAddress, sourcePort) { 11 | const wss = new WSServer(sourcePort); 12 | let clientCounter = 1; 13 | 14 | console.log(`Enter '/connect ${localhost}:${sourcePort}' to establish a connection.`); 15 | console.log(`If connection established, CodeConnectFix will connect to ${destAddress} and forward messages`); 16 | 17 | wss.debug = debug; 18 | wss.on("client", ({ session }) => { 19 | const client = new WSClient(`ws://${destAddress}`, Version.V2); 20 | client.debug = debug; 21 | const clientNo = clientCounter; 22 | clientCounter += 1; 23 | 24 | const Requests = {}; 25 | let serverVersion = NaN; 26 | let clientVersion = NaN; 27 | console.log(`<- [${clientNo}] connected`); 28 | 29 | client.on("command", (event) => { 30 | if (event.handleEncryptionHandshake()) { 31 | console.log(`-> [${clientNo}] keyExchange: ${event.requestId}`); 32 | session.enableEncryption(() => { 33 | console.log(`<- [${clientNo}] completeEncryption`); 34 | }); 35 | } else { 36 | const { requestId, commandLine } = event; 37 | console.log(`-> [${clientNo}] command: ${requestId} ${commandLine}`); 38 | // Kept track of geteduclientinfo request 39 | if (commandLine === 'geteduclientinfo') { 40 | Requests[requestId] = commandLine; 41 | } 42 | session.sendCommandRaw(requestId, commandLine); 43 | 44 | } 45 | }); 46 | 47 | client.on("commandAgent", ({requestId, commandLine}) => { 48 | console.log(`-> [${clientNo}] commandAgent: ${requestId} ${commandLine}`); 49 | session.sendCommandAgentRaw(requestId, commandLine); 50 | }); 51 | 52 | client.on("commandLegacy", ({ 53 | requestId, commandName, overload, input 54 | }) => { 55 | console.log(`-> [${clientNo}] commandLegacy: ${requestId} ${commandName}`); 56 | // Not allow commandLegacy => It's force MakeCode to use V2 protocol 57 | client.sendError(2, "Error commandLegacy", requestId); 58 | }); 59 | 60 | client.on("subscribe", ({ eventName }) => { 61 | session.subscribeRaw(eventName); 62 | console.log(`-> [${clientNo}] subscribe: ${eventName}`); 63 | }); 64 | client.on("unsubscribe", ({ eventName }) => { 65 | session.unsubscribeRaw(eventName); 66 | console.log(`-> [${clientNo}] unsubscribe: ${eventName}`); 67 | }); 68 | client.on("message", ({ version }) => { 69 | if (version !== clientVersion) { 70 | clientVersion = version; 71 | console.log(`-> [${clientNo}] version: ${clientVersion}`); 72 | } 73 | }); 74 | client.on("disconnect", () => { 75 | console.log(`-> [${clientNo}] disconnected from client`); 76 | session.disconnect(true); 77 | }); 78 | session.on("mcError", ({ statusCode, statusMessage }) => { 79 | console.log(`<- [${clientNo}] error: ${statusMessage}`); 80 | client.sendError(statusCode, statusMessage); 81 | }); 82 | session.on("event", ({ purpose, eventName, body }) => { 83 | console.log(`<- [${clientNo}] ${purpose}: ${eventName}`); 84 | client.publishEvent(eventName, body); 85 | }); 86 | session.on("commandResponse", ({ requestId, body }) => { 87 | console.log(`<- [${clientNo}] commandResponse: ${requestId} ${body.statusCode}`); 88 | // Request is tracked ? 89 | let commandLine = Requests[requestId]; 90 | if ( commandLine ) { 91 | // If response of geteduclientinfo => Fake Code Connexion with companionProtocolVersion to v4 92 | // Last version of minecraft bedrock return his version number (16973824) 93 | if ( commandLine === 'geteduclientinfo' ) 94 | body.companionProtocolVersion = 4; 95 | delete Requests[requestId] 96 | } 97 | client.respondCommand(requestId, body); 98 | }); 99 | session.on("commandAgentResponse", ({ requestId, actionName, body }) => { 100 | console.log(`<- [${clientNo}] commandAgentResponse: ${requestId} ${actionName}`); 101 | client.respondCommandAgent(requestId, actionName, body); 102 | }); 103 | session.on("message", ({ version }) => { 104 | if (version !== serverVersion) { 105 | serverVersion = version; 106 | console.log(`<- [${clientNo}] version: ${serverVersion}`); 107 | } 108 | }); 109 | session.on("disconnect", () => { 110 | console.log(`<- [${clientNo}] disconnected from server`); 111 | client.disconnect(); 112 | }); 113 | }); 114 | } 115 | 116 | if (require.main === module) { 117 | main(process.argv[2] || 'localhost:19131', process.argv[3] || 19135); 118 | } else { 119 | module.exports = { main }; 120 | } 121 | -------------------------------------------------------------------------------- /mcpews/lib/app.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require("events"); 2 | const WSServer = require("./server"); 3 | 4 | const ERRORCODE_MASK = 1 << 31; 5 | 6 | function waitForEvent(emitter, eventName, timeout, filter) { 7 | return new Promise((resolve, reject) => { 8 | const listener = (event) => { 9 | if (filter && !filter(event)) return; 10 | resolve(event); 11 | emitter.removeListener(eventName, listener); 12 | }; 13 | emitter.addListener(eventName, listener); 14 | if (timeout) { 15 | setTimeout(() => { 16 | emitter.removeListener(eventName, listener); 17 | reject(new Error(`${eventName}: Timeout ${timeout} exceed.`)); 18 | }, timeout); 19 | } 20 | }); 21 | } 22 | 23 | const kWrappedListener = Symbol("wrappedListener"); 24 | function wrapListener(listener, wrapper) { 25 | const cached = listener[kWrappedListener]; 26 | if (cached) return cached; 27 | if (wrapper) { 28 | const wrapped = wrapper(listener); 29 | listener[kWrappedListener] = wrapped; 30 | return wrapped; 31 | } 32 | return null; 33 | } 34 | 35 | class AppSession { 36 | constructor(app, internalSession) { 37 | this.app = app; 38 | this.internalSession = internalSession; 39 | } 40 | 41 | enableEncryption() { 42 | return new Promise((resolve) => { 43 | this.internalSession.enableEncryption(resolve); 44 | }); 45 | } 46 | 47 | isEncrypted() { 48 | return this.internalSession.isEncrypted(); 49 | } 50 | 51 | on(event, listener) { 52 | const wrapped = wrapListener(listener, (l) => l.bind(this)); 53 | if (event === "Disconnect") { 54 | this.internalSession.on("disconnect", wrapped); 55 | } else { 56 | this.internalSession.subscribe(event, wrapped); 57 | } 58 | return this; 59 | } 60 | 61 | once(event, listener) { 62 | const holderListener = function doNothing() {}; // used to delay the unsubscribe request 63 | const wrappedListener = function wrapped(e) { 64 | this.off(event, wrappedListener); 65 | listener.call(this, e); 66 | this.off(event, holderListener); 67 | }; 68 | this.on(event, wrappedListener); 69 | this.on(event, holderListener); 70 | return this; 71 | } 72 | 73 | off(event, listener) { 74 | const wrapped = wrapListener(listener); 75 | if (wrapped) { 76 | this.internalSession.unsubscribe(event, wrapped); 77 | } 78 | return this; 79 | } 80 | 81 | addListener(event, listener) { 82 | return this.on(event, listener); 83 | } 84 | 85 | removeListener(event, listener) { 86 | return this.off(event, listener); 87 | } 88 | 89 | waitForEvent(event, timeout, filter) { 90 | return waitForEvent(this, event, timeout, filter); 91 | } 92 | 93 | withCatch(executor, timeout) { 94 | return new Promise((resolve, reject) => { 95 | let errorFrameCallback; 96 | let errorCallback; 97 | let timeoutId; 98 | const callback = (success, valueOrError) => { 99 | this.internalSession.off("mcError", errorFrameCallback); 100 | this.internalSession.off("error", errorCallback); 101 | if (timeoutId) { 102 | clearTimeout(timeoutId); 103 | } 104 | if (success) { 105 | resolve(valueOrError); 106 | } else { 107 | reject(valueOrError); 108 | } 109 | }; 110 | errorCallback = (error) => callback(false, error); 111 | errorFrameCallback = (frame) => callback(false, new Error(frame.statusMessage)); 112 | this.internalSession.once("mcError", errorFrameCallback); 113 | this.internalSession.once("error", reject); 114 | if (timeoutId > 0) { 115 | timeoutId = setTimeout(() => callback(false, new Error(`Timeout ${timeout} exceed.`)), timeout); 116 | } 117 | try { 118 | executor.call( 119 | this, 120 | (value) => callback(true, value), 121 | (reason) => callback(false, reason) 122 | ); 123 | } catch (err) { 124 | callback(false, err); 125 | } 126 | }); 127 | } 128 | 129 | command(command, timeout) { 130 | return this.withCatch((resolve, reject) => { 131 | this.internalSession.sendCommand(command, (event) => { 132 | if ((event.body.statusCode & ERRORCODE_MASK) === 0) { 133 | resolve(event); 134 | } else { 135 | reject(new Error(event.body.statusMessage)); 136 | } 137 | }); 138 | }, timeout); 139 | } 140 | 141 | commandLegacy(commandName, overload, input, timeout) { 142 | return this.withCatch((resolve, reject) => { 143 | this.internalSession.sendCommandLegacy(commandName, overload, input, (event) => { 144 | if ((event.body.statusCode & ERRORCODE_MASK) === 0) { 145 | resolve(event); 146 | } else { 147 | reject(new Error(event.body.statusMessage)); 148 | } 149 | }); 150 | }, timeout); 151 | } 152 | 153 | disconnect(timeout) { 154 | this.internalSession.disconnect(); 155 | return waitForEvent(this.internalSession, "disconnect", timeout); 156 | } 157 | } 158 | 159 | function onSession({ session }) { 160 | const appSession = new AppSession(this, session); 161 | this.sessions.push(appSession); 162 | this.emit("session", appSession); 163 | session.on("disconnect", () => { 164 | const sessionIndex = this.sessions.indexOf(session); 165 | if (sessionIndex >= 0) { 166 | this.sessions.splice(sessionIndex, 1); 167 | } 168 | }); 169 | } 170 | 171 | class WSApp extends EventEmitter { 172 | constructor(port, handleSession) { 173 | super(); 174 | this.internalServer = new WSServer(port, onSession.bind(this)); 175 | this.sessions = []; 176 | if (handleSession) { 177 | this.on("session", handleSession); 178 | } 179 | } 180 | 181 | async forEachSession(f) { 182 | await Promise.all(this.sessions.map(f)); 183 | } 184 | 185 | async mapSession(f) { 186 | return Promise.all(this.sessions.map(f)); 187 | } 188 | 189 | waitForSession(timeout) { 190 | return waitForEvent(this, "session", timeout); 191 | } 192 | } 193 | 194 | module.exports = WSApp; 195 | -------------------------------------------------------------------------------- /mcpews/lib/client.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require("events"); 2 | const WebSocket = require("ws"); 3 | const { ClientEncryption } = require("./encrypt"); 4 | const { V1, V2 } = require("./version"); 5 | 6 | function handleEncryptionHandshake() { 7 | return this.client.handleEncryptionHandshake(this.requestId, this.body.commandLine); 8 | } 9 | 10 | function respondCommandRequest(body) { 11 | return this.client.respondCommand(this.requestId, body); 12 | } 13 | 14 | function respondCommandAgentRequest(body) { 15 | return this.client.respondCommandAgent(this.requestId, this.actionName, body); 16 | } 17 | 18 | function onMessage(messageData) { 19 | let decryptedMessageData; 20 | if (this.encryption) { 21 | decryptedMessageData = this.encryption.decrypt(messageData); 22 | } else { 23 | decryptedMessageData = messageData; 24 | } 25 | const message = JSON.parse(decryptedMessageData); 26 | if (this.debug) 27 | console.log("Client receiveMessage : ", message); 28 | const { header, body } = message; 29 | const { messagePurpose: purpose, version } = header; 30 | const frameBase = { 31 | client: this, 32 | message, 33 | header, 34 | body, 35 | purpose, 36 | version 37 | }; 38 | switch (purpose) { 39 | case "subscribe": 40 | case "unsubscribe": { 41 | const { eventName } = body; 42 | const isEventListening = this.eventListenMap.get(eventName); 43 | if (purpose === "subscribe" && !isEventListening) { 44 | this.emit("subscribe", { 45 | ...frameBase, 46 | eventName 47 | }); 48 | this.eventListenMap.set(eventName, true); 49 | } else if (purpose === "unsubscribe" && isEventListening) { 50 | this.emit("unsubscribe", { 51 | ...frameBase, 52 | eventName 53 | }); 54 | this.eventListenMap.set(eventName, false); 55 | } 56 | break; 57 | } 58 | case "action:agent": 59 | if (body.commandLine) { 60 | this.emit("commandAgent", { 61 | ...frameBase, 62 | requestId: header.requestId, 63 | commandLine: body.commandLine, 64 | respond: respondCommandAgentRequest, 65 | handleEncryptionHandshake 66 | }); 67 | } else { 68 | frameBase.purpose = header.messagePurpose = 'commandRequest'; 69 | this.emit("commandLegacy", { 70 | ...frameBase, 71 | requestId: header.requestId, 72 | commandName: body.name, 73 | overload: body.overload, 74 | input: body.input, 75 | respond: respondCommandRequest 76 | }); 77 | } 78 | break; 79 | case "commandRequest": 80 | if (body.commandLine) { 81 | this.emit("command", { 82 | ...frameBase, 83 | requestId: header.requestId, 84 | commandLine: body.commandLine, 85 | respond: respondCommandRequest, 86 | handleEncryptionHandshake 87 | }); 88 | } else { 89 | this.emit("commandLegacy", { 90 | ...frameBase, 91 | requestId: header.requestId, 92 | commandName: body.name, 93 | overload: body.overload, 94 | input: body.input, 95 | respond: respondCommandRequest 96 | }); 97 | } 98 | break; 99 | default: 100 | this.emit("customFrame", frameBase); 101 | } 102 | this.emit("message", frameBase); 103 | } 104 | 105 | function onClose() { 106 | this.emit("disconnect", this); 107 | } 108 | 109 | function buildHeader(purpose, requestId, version, extraProperties) { 110 | return { 111 | version, 112 | requestId: requestId || "00000000-0000-0000-0000-000000000000", 113 | messagePurpose: purpose, 114 | ...extraProperties 115 | }; 116 | } 117 | 118 | class WSClient extends EventEmitter { 119 | constructor(address, version) { 120 | super(); 121 | this.socket = new WebSocket(address); 122 | this.eventListenMap = new Map(); 123 | this.version = version || V1; 124 | this.debug = false; 125 | this.socket.on("message", onMessage.bind(this)).on("close", onClose.bind(this)); 126 | } 127 | 128 | handleEncryptionHandshake(requestId, commandLine) { 129 | if (commandLine.startsWith("enableencryption ")) { 130 | const encryption = new ClientEncryption(); 131 | const keyExchangeParams = encryption.beginKeyExchange(); 132 | const args = commandLine.split(" "); 133 | encryption.completeKeyExchange(JSON.parse(args[1]), JSON.parse(args[2])); 134 | this.respondCommand(requestId, { 135 | publicKey: keyExchangeParams.publicKey, 136 | statusCode: 0 137 | }); 138 | this.encryption = encryption; 139 | this.emit("encryptionEnabled", { client: this }); 140 | return true; 141 | } 142 | return false; 143 | } 144 | 145 | isEncrypted() { 146 | return this.encryption != null; 147 | } 148 | 149 | sendMessage(message) { 150 | if (this.debug) 151 | console.log("Client sendMessage : ", message); 152 | let messageData = JSON.stringify(message); 153 | if (this.encryption) { 154 | messageData = this.encryption.encrypt(messageData); 155 | } 156 | this.socket.send(messageData); 157 | } 158 | 159 | sendFrame(messagePurpose, body, requestId, extraHeaders) { 160 | this.sendMessage({ 161 | header: buildHeader(messagePurpose, requestId, this.version, extraHeaders), 162 | body 163 | }); 164 | } 165 | 166 | sendError(statusCode, statusMessage, requestId) { 167 | this.sendFrame("error", { 168 | statusCode, 169 | statusMessage 170 | }, requestId); 171 | } 172 | 173 | sendEvent(eventName, body) { 174 | if (this.version === V2) { 175 | this.sendFrame("event", body, null, { eventName }); 176 | } else { 177 | this.sendFrame("event", { 178 | ...body, 179 | eventName 180 | }); 181 | } 182 | } 183 | 184 | publishEvent(eventName, body) { 185 | const isEventListening = this.eventListenMap.get(eventName); 186 | if (isEventListening) { 187 | this.sendEvent(eventName, body); 188 | } 189 | } 190 | 191 | respondCommand(requestId, body) { 192 | this.sendFrame("commandResponse", body, requestId); 193 | } 194 | 195 | respondCommandAgent(requestId, actionName, body) { 196 | this.sendFrame("action:agent", body, requestId, { actionName, action : actionIdFromactionName[actionName] }); 197 | } 198 | 199 | disconnect() { 200 | this.socket.close(); 201 | } 202 | } 203 | 204 | const actionIdFromactionName = { 205 | 'attack' : 1, 206 | 'collect' : 2, 207 | 'destroy' : 3, 208 | 'detectRedstone' : 4, 209 | 'detectObstacle' : 5, 210 | 'drop' : 6, 211 | 'dropAll' : 7, 212 | 'inspect' : 8, 213 | 'inspectItemCount' : 10, 214 | 'inspectItemDetail' : 11, 215 | 'inspectItemSpace' : 12, 216 | 'interact' : 13, 217 | 'move' : 14, 218 | 'placeBlock' : 15, 219 | 'till' : 16, 220 | 'transferItemTo' : 17, 221 | 'turn' : 18 222 | }; 223 | 224 | module.exports = WSClient; 225 | -------------------------------------------------------------------------------- /mcpews/lib/server.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require("events"); 2 | const WebSocket = require("ws"); 3 | const randomUUID = require("uuid").v4; 4 | const { implementName, ServerEncryption } = require("./encrypt"); 5 | const { V1, V2 } = require("./version"); 6 | 7 | function onMessage(messageData) { 8 | let decryptedMessageData; 9 | if (this.encryption) { 10 | decryptedMessageData = this.encryption.decrypt(messageData); 11 | } else { 12 | decryptedMessageData = messageData; 13 | } 14 | const message = JSON.parse(decryptedMessageData); 15 | if (this.debug) 16 | console.log("Server receiveMessage : ", message); 17 | const { header, body } = message; 18 | const { messagePurpose: purpose, version } = header; 19 | const frameBase = { 20 | server: this.server, 21 | session: this, 22 | message, 23 | header, 24 | body, 25 | purpose, 26 | version 27 | }; 28 | switch (purpose) { 29 | case "event": 30 | case "chat": 31 | if (version >= V2) { 32 | this.publishEvent(header.eventName, { 33 | ...frameBase, 34 | eventName: header.eventName 35 | }); 36 | } else { 37 | this.publishEvent(body.eventName, { 38 | ...frameBase, 39 | eventName: body.eventName 40 | }); 41 | } 42 | break; 43 | case "action:agent": 44 | this.respondCommandAgent(header.requestId, { 45 | ...frameBase, 46 | requestId: header.requestId, 47 | actionName: header.actionName 48 | }); 49 | break; 50 | case "commandResponse": 51 | this.respondCommand(header.requestId, { 52 | ...frameBase, 53 | requestId: header.requestId 54 | }); 55 | break; 56 | case "error": 57 | this.emit("mcError", { 58 | ...frameBase, 59 | requestId: header.requestId, 60 | statusCode: body.statusCode, 61 | statusMessage: body.statusMessage 62 | }); 63 | break; 64 | default: 65 | this.emit("customFrame", frameBase); 66 | } 67 | this.emit("message", frameBase); 68 | } 69 | 70 | function buildHeader(purpose, requestId, version, extraProperties) { 71 | return { 72 | version, 73 | requestId: requestId || "00000000-0000-0000-0000-000000000000", 74 | messagePurpose: purpose, 75 | messageType: "commandRequest", 76 | ...extraProperties 77 | }; 78 | } 79 | 80 | class Session extends EventEmitter { 81 | constructor(server, socket) { 82 | super(); 83 | this.server = server; 84 | this.debug = this.server.debug; 85 | this.socket = socket; 86 | this.version = V1; 87 | this.eventListeners = new Map(); 88 | this.responsors = new Map(); 89 | socket.on("message", onMessage.bind(this)); 90 | } 91 | 92 | enableEncryption(callback) { 93 | if (this.exchangingKey || this.encryption) { 94 | return false; 95 | } 96 | const encryption = new ServerEncryption(); 97 | const keyExchangeParams = encryption.beginKeyExchange(); 98 | this.exchangingKey = true; 99 | this.sendCommand( 100 | ["enableencryption", JSON.stringify(keyExchangeParams.publicKey), JSON.stringify(keyExchangeParams.salt)], 101 | (event) => { 102 | this.exchangingKey = false; 103 | encryption.completeKeyExchange(event.body.publicKey); 104 | this.encryption = encryption; 105 | const successEvent = { server: this.server, session: this, encryption }; 106 | if (callback) callback.call(this, successEvent); 107 | this.emit("encryptionEnabled", successEvent); 108 | } 109 | ); 110 | return true; 111 | } 112 | 113 | isEncrypted() { 114 | return this.encryption != null; 115 | } 116 | 117 | sendMessage(message) { 118 | if (this.debug) 119 | console.log("Server sendMessage : ", message); 120 | let messageData = JSON.stringify(message); 121 | if (this.encryption) { 122 | messageData = this.encryption.encrypt(messageData); 123 | } 124 | this.socket.send(messageData); 125 | } 126 | 127 | sendFrame(messagePurpose, body, requestId, extraHeaders) { 128 | this.sendMessage({ 129 | header: buildHeader(messagePurpose, requestId, this.version, extraHeaders), 130 | body 131 | }); 132 | } 133 | 134 | subscribeRaw(event) { 135 | this.sendFrame("subscribe", { 136 | eventName: event 137 | }); 138 | } 139 | 140 | subscribe(event, callback) { 141 | let listeners = this.eventListeners.get(event); 142 | if (!listeners) { 143 | listeners = new Set(); 144 | this.eventListeners.set(event, listeners); 145 | this.subscribeRaw(event); 146 | } 147 | listeners.add(callback); 148 | } 149 | 150 | unsubscribeRaw(event) { 151 | this.sendFrame("unsubscribe", { 152 | eventName: event 153 | }); 154 | } 155 | 156 | unsubscribe(event, callback) { 157 | const listeners = this.eventListeners.get(event); 158 | if (!listeners) { 159 | return; 160 | } 161 | listeners.delete(callback); 162 | if (listeners.size === 0) { 163 | this.eventListeners.delete(event); 164 | this.unsubscribeRaw(event); 165 | } 166 | } 167 | 168 | publishEvent(eventName, frame) { 169 | const listeners = this.eventListeners.get(eventName); 170 | if (listeners) { 171 | const listenersCopy = new Set(listeners); 172 | listenersCopy.forEach((e) => { 173 | try { 174 | e.call(this, frame); 175 | } catch (err) { 176 | this.emit("error", err); 177 | } 178 | }); 179 | } else { 180 | this.emit("event", frame); 181 | } 182 | } 183 | 184 | sendCommandRaw(requestId, command) { 185 | this.sendFrame( 186 | "commandRequest", 187 | { 188 | version: 1, 189 | commandLine: command, 190 | origin: { 191 | type: "player" 192 | } 193 | }, 194 | requestId 195 | ); 196 | } 197 | 198 | sendCommand(command, callback) { 199 | const requestId = randomUUID(); 200 | this.responsors.set(requestId, callback); 201 | this.sendCommandRaw(requestId, Array.isArray(command) ? command.join(" ") : command); 202 | return requestId; 203 | } 204 | 205 | sendCommandAgentRaw(requestId, command) { 206 | this.sendFrame( 207 | "action:agent", 208 | { 209 | version: 1, 210 | commandLine: command, 211 | }, 212 | requestId 213 | ); 214 | } 215 | 216 | sendCommandAgent(command, callback) { 217 | const requestId = randomUUID(); 218 | this.responsors.set(requestId, callback); 219 | this.sendCommandAgentRaw(requestId, Array.isArray(command) ? command.join(" ") : command); 220 | return requestId; 221 | } 222 | 223 | sendCommandLegacyRaw(requestId, commandName, overload, input) { 224 | this.sendFrame( 225 | "commandRequest", 226 | { 227 | version: 1, 228 | name: commandName, 229 | overload, 230 | input, 231 | origin: { type: "player" } 232 | }, 233 | requestId 234 | ); 235 | } 236 | 237 | sendCommandLegacy(commandName, overload, input, callback) { 238 | const requestId = randomUUID(); 239 | this.responsors.set(requestId, callback); 240 | this.sendCommandLegacyRaw(requestId, commandName, overload, input); 241 | return requestId; 242 | } 243 | 244 | respondCommand(requestId, frame) { 245 | const callback = this.responsors.get(requestId); 246 | this.responsors.delete(requestId); 247 | if (callback) { 248 | try { 249 | callback.call(this, frame); 250 | } catch (err) { 251 | this.emit("error", err); 252 | } 253 | } else { 254 | this.emit("commandResponse", frame); 255 | } 256 | } 257 | 258 | respondCommandAgent(requestId, frame) { 259 | const callback = this.responsors.get(requestId); 260 | this.responsors.delete(requestId); 261 | if (callback) { 262 | try { 263 | callback.call(this, frame); 264 | } catch (err) { 265 | this.emit("error", err); 266 | } 267 | } else { 268 | this.emit("commandAgentResponse", frame); 269 | } 270 | } 271 | 272 | disconnect(force) { 273 | if (force) { 274 | this.socket.close(); 275 | } else { 276 | this.sendCommand("closewebsocket", null); 277 | } 278 | } 279 | } 280 | 281 | function onConnection(socket, request) { 282 | const session = new Session(this, socket); 283 | this.sessions.add(session); 284 | this.emit("client", { server: this, session, request }); 285 | socket.on("close", () => { 286 | this.sessions.delete(this); 287 | session.emit("disconnect", { server: this.server, session: this }); 288 | }); 289 | } 290 | 291 | const kSecWebsocketKey = Symbol("sec-websocket-key"); 292 | 293 | class WSServer extends WebSocket.Server { 294 | constructor(port, handleClient) { 295 | super({ 296 | port, 297 | handleProtocols: (protocols) => protocols.has(implementName) 298 | }); 299 | this.debug = false; 300 | this.sessions = new Set(); 301 | this.on("connection", onConnection); 302 | if (handleClient) { 303 | this.on("client", handleClient); 304 | } 305 | } 306 | 307 | // overwrite handleUpgrade to skip sec-websocket-key format test 308 | // minecraft pe pre-1.2 use a shorter version of sec-websocket-key 309 | handleUpgrade(req, socket, head, cb) { 310 | const key = req.headers["sec-websocket-key"]; 311 | if (key && /^[+/0-9A-Za-z]{11}=$/.test(key)) { 312 | req.headers["sec-websocket-key"] = `skipkeytest${key}=`; 313 | req[kSecWebsocketKey] = key; 314 | } 315 | super.handleUpgrade(req, socket, head, cb); 316 | } 317 | 318 | // same reason as above 319 | completeUpgrade(extensions, key, protocols, req, socket, head, cb) { 320 | super.completeUpgrade(extensions, req[kSecWebsocketKey] || key, protocols, req, socket, head, cb); 321 | } 322 | 323 | broadcastCommand(command, callback) { 324 | this.sessions.forEach((e) => { 325 | e.sendCommand(command, callback); 326 | }); 327 | } 328 | 329 | broadcastCommandAgent(command, callback) { 330 | this.sessions.forEach((e) => { 331 | e.sendCommandAgent(command, callback); 332 | }); 333 | } 334 | 335 | broadcastSubscribe(event, callback) { 336 | this.sessions.forEach((e) => { 337 | e.subscribe(event, callback); 338 | }); 339 | } 340 | 341 | broadcastUnsubscribe(event, callback) { 342 | this.sessions.forEach((e) => { 343 | e.unsubscribe(event, callback); 344 | }); 345 | } 346 | 347 | disconnectAll(force) { 348 | this.sessions.forEach((e) => { 349 | e.disconnect(force); 350 | }); 351 | } 352 | } 353 | 354 | module.exports = WSServer; 355 | -------------------------------------------------------------------------------- /mcpews/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from "http"; 2 | import { WebSocket } from "ws"; 3 | 4 | export enum Version { 5 | V1 = 1, 6 | V2 = 16842752 7 | } 8 | 9 | declare class Encryption { 10 | constructor(); 11 | encrypt(str: string): Buffer; 12 | decrypt(buf: Buffer): string; 13 | } 14 | 15 | export class ServerEncryption extends Encryption { 16 | beginKeyExchange(): { 17 | publicKey: string; 18 | salt: string; 19 | }; 20 | completeKeyExchange(clientPublicKey: string): void; 21 | } 22 | 23 | export class ClientEncryption extends Encryption { 24 | beginKeyExchange(): { 25 | publicKey: string; 26 | }; 27 | completeKeyExchange(serverPublicKey: string, salt: string): void; 28 | } 29 | 30 | declare class TypedEventEmitter any }> { 31 | on(event: E, listener: EventMap[E]): this; 32 | once(event: E, listener: EventMap[E]): this; 33 | off(event: E, listener: EventMap[E]): this; 34 | addListener(event: E, listener: EventMap[E]): this; 35 | removeListener(event: E, listener: EventMap[E]): this; 36 | removeAllListeners(event: E): this; 37 | listeners(event: E): EventMap[E][]; 38 | rawListeners(event: E): EventMap[E][]; 39 | emit(event: E, ...args: Parameters): boolean; 40 | prependListener(event: E, listener: EventMap[E]): this; 41 | prependOnceListener(event: E, listener: EventMap[E]): this; 42 | 43 | on(event: string | symbol, listener: (...args: any[]) => void): this; 44 | once(event: string | symbol, listener: (...args: any[]) => void): this; 45 | off(event: string | symbol, listener: (...args: any[]) => void): this; 46 | addListener(event: string | symbol, listener: (...args: any[]) => void): this; 47 | removeListener(event: string | symbol, listener: (...args: any[]) => void): this; 48 | removeAllListeners(event: string | symbol): this; 49 | listeners(event: string | symbol): ((...args: any[]) => void)[]; 50 | rawListeners(event: string | symbol): ((...args: any[]) => void)[]; 51 | emit(event: string | symbol, ...args: any[]): boolean; 52 | prependListener(event: string | symbol, listener: (...args: any[]) => void): this; 53 | prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; 54 | 55 | listenerCount(event: E | string | symbol): number; 56 | eventNames(): (keyof EventMap)[]; 57 | } 58 | 59 | declare namespace ServerSessionEvent { 60 | interface Base { 61 | session: Session; 62 | server: WSServer; 63 | } 64 | 65 | interface EncryptionEnabled extends Base { 66 | encryption: Encryption; 67 | } 68 | 69 | interface Disconnect extends Base {} 70 | 71 | interface Message extends Base { 72 | message: any; 73 | } 74 | 75 | interface Frame extends Message { 76 | purpose: string; 77 | header: any; 78 | message: any; 79 | version: Version; 80 | } 81 | 82 | interface Event extends Frame { 83 | purpose: "event" | "chat"; 84 | eventName: string; 85 | body: any; 86 | } 87 | 88 | interface CommandResponse extends Frame { 89 | purpose: "commandResponse"; 90 | requestId: string; 91 | body: any; 92 | } 93 | 94 | interface CommandAgentResponse extends Frame { 95 | purpose: "action:agent"; 96 | requestId: string; 97 | actionName: string; 98 | body: any; 99 | } 100 | 101 | interface MCError extends Frame { 102 | purpose: "error"; 103 | requestId: string; 104 | statusCode?: number; 105 | statusMessage?: number; 106 | body: any; 107 | } 108 | 109 | interface Map { 110 | encryptionEnabled: (event: EncryptionEnabled) => void; 111 | error: (event: Error) => void; 112 | event: (event: Event) => void; 113 | commandResponse: (event: CommandResponse) => void; 114 | commandAgentResponse: (event: CommandAgentResponse) => void; 115 | mcError: (event: MCError) => void; 116 | customFrame: (event: Frame) => void; 117 | message: (event: Frame) => void; 118 | disconnect: (event: Disconnect) => void; 119 | } 120 | 121 | type EncryptionEnabledCallback = Map["encryptionEnabled"]; 122 | type EventCallback = Map["event"]; 123 | type CommandCallback = Map["commandResponse"]; 124 | type CommandAgentCallback = Map["commandResponse"]; 125 | } 126 | 127 | declare class Session extends TypedEventEmitter { 128 | readonly server: WSServer; 129 | encryption?: Encryption; 130 | 131 | constructor(server: WSServer, socket: WebSocket); 132 | enableEncryption(callback?: ServerSessionEvent.EncryptionEnabledCallback): void; 133 | isEncrypted(): boolean; 134 | sendMessage(message: any): void; 135 | sendFrame(messagePurpose: string, body: any, requestId: string, extraHeaders: any): void; 136 | subscribeRaw(event: string): void; 137 | subscribe(event: string, callback: ServerSessionEvent.EventCallback): void; 138 | unsubscribeRaw(event: string): void; 139 | unsubscribe(event: string, callback: ServerSessionEvent.EventCallback): void; 140 | sendCommandRaw(requestId: string, command: string): void; 141 | sendCommand(command: string | string[], callback?: ServerSessionEvent.CommandCallback): void; 142 | sendCommandAgentRaw(requestId: string, command: string): void; 143 | sendCommandAgent(command: string | string[], callback?: ServerSessionEvent.CommandAgentCallback): void; 144 | sendCommandLegacyRaw(requestId: string, commandName: string, overload: string, input: any): void; 145 | sendCommandLegacy( 146 | commandName: string, 147 | overload: string, 148 | input: any, 149 | callback?: ServerSessionEvent.CommandCallback 150 | ): void; 151 | disconnect(force?: boolean): void; 152 | } 153 | 154 | declare namespace ServerEvent { 155 | interface Client { 156 | server: WSServer; 157 | session: Session; 158 | request: IncomingMessage; 159 | } 160 | 161 | interface Map { 162 | client: (event: Client) => void; 163 | } 164 | 165 | type ClientCallback = Map["client"]; 166 | } 167 | 168 | export declare class WSServer extends TypedEventEmitter { 169 | readonly sessions: Set; 170 | 171 | constructor(port: number, handleClient?: ServerEvent.ClientCallback); 172 | broadcastCommand(command: string, callback?: ServerSessionEvent.CommandCallback): void; 173 | broadcastCommandAgent(command: string, callback?: ServerSessionEvent.CommandAgentCallback): void; 174 | broadcastSubscribe(event: string, callback: ServerSessionEvent.EventCallback): void; 175 | broadcastUnsubscribe(event: string, callback: ServerSessionEvent.EventCallback): void; 176 | disconnectAll(force?: boolean): void; 177 | } 178 | 179 | declare namespace ClientEvent { 180 | interface Base { 181 | client: WSClient; 182 | } 183 | 184 | interface EncryptionEnabled extends Base { 185 | encryption: Encryption; 186 | } 187 | 188 | interface Disconnect extends Base {} 189 | 190 | interface Message extends Base { 191 | message: any; 192 | } 193 | 194 | interface Frame extends Message { 195 | purpose: string; 196 | header: any; 197 | message: any; 198 | version: Version; 199 | } 200 | 201 | interface Subscribe extends Frame { 202 | purpose: "subscribe"; 203 | eventName: string; 204 | body: any; 205 | } 206 | 207 | interface Unsubscribe extends Frame { 208 | purpose: "unsubscribe"; 209 | eventName: string; 210 | body: any; 211 | } 212 | 213 | interface Command extends Frame { 214 | purpose: "command"; 215 | requestId: string; 216 | commandLine: string; 217 | body: any; 218 | respond(body: any): void; 219 | handleEncryptionHandshake(): boolean; 220 | } 221 | 222 | interface CommandAgent extends Frame { 223 | purpose: "action:agent"; 224 | requestId: string; 225 | commandLine: string; 226 | body: any; 227 | respond(body: any): void; 228 | handleEncryptionHandshake(): boolean; 229 | } 230 | 231 | interface LegacyCommand extends Frame { 232 | purpose: "command"; 233 | requestId: string; 234 | commandName: string; 235 | overload: string; 236 | input: any; 237 | body: any; 238 | respond(body: any): void; 239 | } 240 | 241 | interface Map { 242 | encryptionEnabled: (event: EncryptionEnabled) => void; 243 | error: (error: Error) => void; 244 | subscribe: (event: Subscribe) => void; 245 | unsubscribe: (event: Unsubscribe) => void; 246 | command: (event: Command) => void; 247 | commandAgent: (event: CommandAgent) => void; 248 | commandLegacy: (event: LegacyCommand) => void; 249 | customFrame: (event: Frame) => void; 250 | message: (event: Frame) => void; 251 | disconnect: (event: Disconnect) => void; 252 | } 253 | } 254 | 255 | export declare class WSClient extends TypedEventEmitter { 256 | encryption?: Encryption; 257 | 258 | constructor(address: string); 259 | handleEncryptionHandshake(requestId: string, commandLine: string): boolean; 260 | isEncrypted(): boolean; 261 | sendMessage(message: any): void; 262 | sendFrame(messagePurpose: string, body: any, requestId: string, extraHeaders: any): void; 263 | sendError(statusCode: number, statusMessage: string, requestId?: string): void; 264 | sendEvent(eventName: string, body: any): void; 265 | publishEvent(eventName: string, body: any): void; 266 | respondCommand(requestId: string, body: any): void; 267 | respondCommandAgent(requestId: string, actionName: string, body: any): void; 268 | disconnect(): void; 269 | } 270 | 271 | interface AppEvent extends ServerSessionEvent.Event { 272 | eventName: string; 273 | body: any; 274 | } 275 | 276 | interface AppCommandResponse extends ServerSessionEvent.CommandResponse { 277 | requestId: string; 278 | body: any; 279 | } 280 | 281 | type AppEventListener = (event: AppEvent) => void; 282 | type AppEventListenerNoData = () => void; 283 | 284 | declare class AppSession { 285 | readonly app: WSApp; 286 | readonly internalSession: Session; 287 | 288 | constructor(app: WSApp, impl: Session); 289 | enableEncryption(): Promise; 290 | isEncrypted(): boolean; 291 | on(eventName: "Disconnect", listener: AppEventListenerNoData): this; 292 | once(eventName: "Disconnect", listener: AppEventListenerNoData): this; 293 | off(eventName: "Disconnect", listener: AppEventListenerNoData): this; 294 | addListener(eventName: "Disconnect", listener: AppEventListenerNoData): this; 295 | removeListener(eventName: "Disconnect", listener: AppEventListenerNoData): this; 296 | waitForEvent(eventName: "Disconnect", timeout?: number): Promise; 297 | on(eventName: string, listener: AppEventListener): this; 298 | once(eventName: string, listener: AppEventListener): this; 299 | off(eventName: string, listener: AppEventListener): this; 300 | addListener(eventName: string, listener: AppEventListener): this; 301 | removeListener(eventName: string, listener: AppEventListener): this; 302 | waitForEvent(eventName: string, timeout?: number): Promise; 303 | command(command: string | string[], timeout?: number): Promise; 304 | commandLegacy(commandName: string, overload: string, input: any, timeout?: number): Promise; 305 | disconnect(timeout?: number): Promise; 306 | } 307 | 308 | interface WSAppEventMap { 309 | session: (session: AppSession) => void; 310 | } 311 | 312 | export declare class WSApp extends TypedEventEmitter { 313 | readonly internalServer: WSServer; 314 | readonly sessions: AppSession[]; 315 | 316 | constructor(port: number, handleSession?: (session: AppSession) => void); 317 | forEachSession(f: (session: AppSession, index: number, sessions: AppSession[]) => Promise): Promise; 318 | mapSession(f: (session: AppSession, index: number, sessions: AppSession[]) => Promise): Promise; 319 | waitForSession(timeout?: number): Promise; 320 | } 321 | -------------------------------------------------------------------------------- /mcpews/src/repl.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const EventEmitter = require("events"); 4 | const os = require("os"); 5 | const readline = require("readline"); 6 | const repl = require("repl"); 7 | const vm = require("vm"); 8 | const util = require("util"); 9 | const { WSServer } = require("../lib"); 10 | 11 | function sessionEventListener(eventName, { body }) { 12 | this.emit("event", eventName, body); 13 | } 14 | 15 | class SingleSessionServer extends EventEmitter { 16 | constructor(port) { 17 | super(); 18 | this.port = port; 19 | this.wsServer = new WSServer(port); 20 | this.eventListeners = new Map(); 21 | this.session = null; 22 | this.timeout = 3000; 23 | this.wsServer.on("client", ({ session: newSession, request }) => { 24 | if (this.session) { 25 | newSession.disconnect(); 26 | return; 27 | } 28 | const address = `${request.client.remoteAddress}:${request.client.remotePort}`; 29 | newSession.on("disconnect", () => { 30 | this.session = null; 31 | this.emit("offline", address); 32 | }); 33 | this.session = newSession; 34 | newSession.setMaxListeners(Infinity); 35 | this.emit("online", address); 36 | }); 37 | } 38 | 39 | isOnline() { 40 | return this.session != null; 41 | } 42 | 43 | getSession() { 44 | if (!this.session) throw new Error("Connection is not established."); 45 | return this.session; 46 | } 47 | 48 | withCatch(executor) { 49 | const session = this.getSession(); 50 | return new Promise((resolve, reject) => { 51 | let errorFrameCallback; 52 | let errorCallback; 53 | let timeoutId; 54 | const callback = (success, valueOrError) => { 55 | session.off("mcError", errorFrameCallback); 56 | session.off("error", errorCallback); 57 | if (timeoutId) { 58 | clearTimeout(timeoutId); 59 | } 60 | if (success) { 61 | resolve(valueOrError); 62 | } else { 63 | reject(valueOrError); 64 | } 65 | }; 66 | errorCallback = (error) => callback(false, error); 67 | errorFrameCallback = (frame) => callback(false, new Error(frame.statusMessage)); 68 | session.once("mcError", errorFrameCallback); 69 | session.once("error", reject); 70 | timeoutId = setTimeout(() => callback(false, new Error(`Timeout ${this.timeout} exceed.`)), this.timeout); 71 | try { 72 | executor.call( 73 | this, 74 | session, 75 | (value) => callback(true, value), 76 | (reason) => callback(false, reason) 77 | ); 78 | } catch (err) { 79 | callback(false, err); 80 | } 81 | }); 82 | } 83 | 84 | encrypt() { 85 | return this.withCatch((session, resolve) => { 86 | if (!session.enableEncryption(() => resolve(true))) { 87 | resolve(false); 88 | } 89 | }); 90 | } 91 | 92 | disconnect(force) { 93 | this.getSession().disconnect(force); 94 | } 95 | 96 | disconnectAll() { 97 | this.wsServer.disconnectAll(); 98 | } 99 | 100 | subscribe(eventName) { 101 | const session = this.getSession(); 102 | let listener = this.eventListeners.get(eventName); 103 | if (!listener) { 104 | listener = sessionEventListener.bind(this, eventName); 105 | session.subscribe(eventName, listener); 106 | this.eventListeners.set(eventName, listener); 107 | return true; 108 | } 109 | return false; 110 | } 111 | 112 | unsubscribe(eventName) { 113 | const session = this.getSession(); 114 | const listener = this.eventListeners.get(eventName); 115 | if (listener) { 116 | session.unsubscribe(eventName, listener); 117 | this.eventListeners.delete(eventName); 118 | return true; 119 | } 120 | return false; 121 | } 122 | 123 | sendCommand(cmd) { 124 | return this.withCatch((session, resolve) => { 125 | session.sendCommand(cmd, ({ body }) => resolve(body)); 126 | }); 127 | } 128 | 129 | sendCommandLegacy(commandName, overload, input) { 130 | return this.withCatch((session, resolve) => { 131 | session.sendCommandLegacy(commandName, overload, input, ({ body }) => resolve(body)); 132 | }); 133 | } 134 | 135 | allConnectCommands(externalOnly) { 136 | const interfaces = os.networkInterfaces(); 137 | const ips = []; 138 | Object.values(interfaces).forEach((devInfos) => { 139 | let infoList = devInfos.filter((niInfo) => niInfo.family === "IPv4"); 140 | if (externalOnly) { 141 | infoList = infoList.filter((niInfo) => !niInfo.internal && niInfo.address !== "127.0.0.1"); 142 | } 143 | ips.push(...infoList.map((niInfo) => niInfo.address)); 144 | }); 145 | if (ips.length === 0) ips.push("0.0.0.0"); 146 | return ips.map((ip) => `/connect ${ip}:${this.port}`); 147 | } 148 | 149 | connectCommand() { 150 | return this.allConnectCommands(true)[0]; 151 | } 152 | } 153 | 154 | const OFFLINE_PROMPT = "[Offline] > "; 155 | const ONLINE_PROMPT = "> "; 156 | class CommandReplServer extends repl.REPLServer { 157 | constructor(port) { 158 | super({ 159 | prompt: OFFLINE_PROMPT, 160 | eval: (cmd, context, file, callback) => { 161 | this.doEval(cmd, context, file, callback); 162 | } 163 | }); 164 | this.server = new SingleSessionServer(port); 165 | this.acceptUserInput = true; 166 | this.defineDefaultCommands(); 167 | this.on("reset", (context) => this.resetContextScope(context)).on("exit", () => this.server.disconnectAll()); 168 | this.resetContextScope(this.context); 169 | this.server 170 | .on("online", (address) => { 171 | this.printLine( 172 | `${OFFLINE_PROMPT}\nConnection established: ${address}.\nType ".help" for more information.`, 173 | true 174 | ); 175 | this.setPrompt(ONLINE_PROMPT); 176 | if (this.acceptUserInput) { 177 | this.displayPrompt(true); 178 | } 179 | }) 180 | .on("offline", (address) => { 181 | this.printLine(`Connection disconnected: ${address}.`, true); 182 | this.showOfflinePrompt(true); 183 | this.setPrompt(OFFLINE_PROMPT); 184 | if (this.acceptUserInput) { 185 | this.displayPrompt(true); 186 | } 187 | }) 188 | .on("event", (eventName, body) => { 189 | if (this.editorMode) return; 190 | this.printLine(util.format("[%s] %o", eventName, body), true); 191 | }); 192 | this.showOfflinePrompt(true); 193 | } 194 | 195 | printLine(str, rewriteLine) { 196 | if (rewriteLine) { 197 | readline.cursorTo(this.output, 0); 198 | readline.clearLine(this.output, 0); 199 | } 200 | this.output.write(`${str}\n`); 201 | if (this.acceptUserInput) { 202 | this.displayPrompt(true); 203 | } 204 | } 205 | 206 | showOfflinePrompt(singleLine) { 207 | if (singleLine) { 208 | this.printLine(`Type "${this.server.connectCommand()}" in the game console to connect.`, true); 209 | } else { 210 | this.printLine( 211 | `Type one of following commands in the game console to connect:\n${this.server 212 | .allConnectCommands() 213 | .join("\n")}`, 214 | true 215 | ); 216 | } 217 | } 218 | 219 | resetContextScope(context) { 220 | Object.defineProperties(context, { 221 | wss: { 222 | configurable: true, 223 | writable: false, 224 | value: this.server.wsServer 225 | }, 226 | session: { 227 | configurable: true, 228 | get: () => this.server.getSession() 229 | }, 230 | encrypt: { 231 | configurable: true, 232 | value: () => this.server.encrypt() 233 | }, 234 | disconnect: { 235 | configurable: true, 236 | value: () => this.server.disconnect() 237 | }, 238 | subscribe: { 239 | configurable: true, 240 | value: (eventName) => this.server.subscribe(eventName) 241 | }, 242 | unsubscribe: { 243 | configurable: true, 244 | value: (eventName) => this.server.unsubscribe(eventName) 245 | }, 246 | command: { 247 | configurable: true, 248 | value: (commandLine) => this.server.sendCommand(commandLine) 249 | }, 250 | commandLegacy: { 251 | configurable: true, 252 | value: (commandName, overload, input) => this.server.sendCommandLegacy(commandName, overload, input) 253 | } 254 | }); 255 | } 256 | 257 | defineDefaultCommands() { 258 | this.defineCommand("subscribe", { 259 | help: "Subscribe a event", 260 | action: (eventName) => { 261 | if (this.server.isOnline()) { 262 | if (this.server.subscribe(eventName)) { 263 | this.printLine(`Subscribed ${eventName}.`); 264 | } else { 265 | this.printLine(`Event ${eventName} is already subscribed.`); 266 | } 267 | } else { 268 | this.printLine("Connection is not established."); 269 | } 270 | } 271 | }); 272 | this.defineCommand("unsubscribe", { 273 | help: "Unsubscribe a event", 274 | action: (eventName) => { 275 | if (this.server.isOnline()) { 276 | if (this.server.unsubscribe(eventName)) { 277 | this.printLine(`Unsubscribed ${eventName}.`); 278 | } else { 279 | this.printLine(`Event ${eventName} is not subscribed.`); 280 | } 281 | } else { 282 | this.printLine("Connection is not established."); 283 | } 284 | } 285 | }); 286 | this.defineCommand("disconnect", { 287 | help: "Disconnect from all the clients", 288 | action: (arg) => { 289 | if (this.server.isOnline()) { 290 | if (arg === "force") { 291 | this.server.disconnect(true); 292 | } else { 293 | let disconnected = false; 294 | const timeout = setTimeout(() => { 295 | if (disconnected) return; 296 | this.printLine("Connection close request timeout."); 297 | this.server.disconnect(true); 298 | }, 10000); 299 | this.server.once("offline", () => { 300 | disconnected = true; 301 | clearTimeout(timeout); 302 | }); 303 | this.server.disconnect(false); 304 | } 305 | } else { 306 | this.printLine("Connection is not established."); 307 | } 308 | } 309 | }); 310 | this.defineCommand("encrypt", { 311 | help: "Encrypt the connection", 312 | action: () => { 313 | if (this.server.isOnline()) { 314 | this.server.encrypt().then(() => { 315 | this.printLine("Connection is encrypted.", true); 316 | }); 317 | } else { 318 | this.printLine("Connection is not established."); 319 | } 320 | } 321 | }); 322 | } 323 | 324 | doEval(cmd, context, file, callback) { 325 | let result; 326 | this.acceptUserInput = false; 327 | try { 328 | const trimmedCmd = cmd.trim(); 329 | if (trimmedCmd.startsWith("/") && !trimmedCmd.includes("\n")) { 330 | if (!this.server.isOnline() && trimmedCmd.startsWith("/connect")) { 331 | this.showOfflinePrompt(); 332 | this.acceptUserInput = true; 333 | callback(null); 334 | return; 335 | } 336 | result = this.server.sendCommand(trimmedCmd.slice(1)); 337 | } else if (trimmedCmd.length > 0) { 338 | result = vm.runInContext(cmd, context, { 339 | filename: file 340 | }); 341 | } else { 342 | this.acceptUserInput = true; 343 | callback(null); 344 | return; 345 | } 346 | if (result && result.then) { 347 | result 348 | .then( 349 | (res) => callback(null, res), 350 | (err) => callback(err) 351 | ) 352 | .finally(() => { 353 | this.acceptUserInput = true; 354 | }); 355 | } else { 356 | callback(null, result); 357 | this.acceptUserInput = true; 358 | } 359 | } catch (err) { 360 | callback(err); 361 | this.acceptUserInput = true; 362 | } 363 | } 364 | } 365 | 366 | function main(port) { 367 | const replServer = new CommandReplServer(port); 368 | replServer.on("exit", () => { 369 | process.exit(0); 370 | }); 371 | } 372 | 373 | if (require.main === module) { 374 | main(Number(process.argv[2]) || 19134); 375 | } else { 376 | module.exports = { CommandReplServer, main }; 377 | } 378 | --------------------------------------------------------------------------------