mirror of
https://github.com/WorldEditAxe/eaglerproxy.git
synced 2024-11-21 21:16:05 -08:00
first commit
This commit is contained in:
commit
0bef5be008
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
49
README.md
Normal file
49
README.md
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# EaglercraftX (1.8.9) WebSocket Proxy
|
||||||
|
## What is this?
|
||||||
|
A very primitive and small Node.js based alternative to the custom BungeeCord servers for Eaglercraft 1.8.9. Until the developers officially release the BungeeCord server, this is the only way you can create a EaglercraftX server.
|
||||||
|
## Issues
|
||||||
|
* Generic and vague "End of stream" error when disconnected by the proxy, not the server
|
||||||
|
* Inability to set a server icon
|
||||||
|
* Skins don't work
|
||||||
|
## Setup Guide
|
||||||
|
### Prerequisites
|
||||||
|
* Node.js v12 and up
|
||||||
|
* A 1.8.9-compatible Minecraft server or proxy
|
||||||
|
### Setup Guide
|
||||||
|
1. Download and extract this repository to a folder on your computer.
|
||||||
|
2. Open a terminal and go to the folder of the repository. Run `npm i`.
|
||||||
|
3. Edit `config.js` to configure your proxy. Below is a small breakdown of the configuration file.
|
||||||
|
```js
|
||||||
|
export const config = {
|
||||||
|
// The name of the proxy. Does nothing.
|
||||||
|
name: "BasedProxy",
|
||||||
|
// The port you want to run the proxy on.
|
||||||
|
port: 80,
|
||||||
|
// The amount of players that can join and use this proxy simultaneously.
|
||||||
|
maxPlayers: 20,
|
||||||
|
motd: {
|
||||||
|
// Does nothing. (icons do not work)
|
||||||
|
iconURL: null,
|
||||||
|
// The first line of the MOTD.
|
||||||
|
l1: "hi",
|
||||||
|
// The second line of the MOTD.
|
||||||
|
l2: "lol"
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
// The IP/domain of the server you want the proxy to point to.
|
||||||
|
// Remember, the server HAS to be offline, or you can't connect.
|
||||||
|
host: "127.0.0.1",
|
||||||
|
// The port the server is running on.
|
||||||
|
port: 25565
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
// Whether or not encryption should be enabled.
|
||||||
|
// If you are using Repl.it, this should be left off.
|
||||||
|
enabled: false,
|
||||||
|
// The key issued to you by your certificate authority (CA).
|
||||||
|
key: null,
|
||||||
|
// The certificate issued to you by your certificate authority (CA).
|
||||||
|
cert: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
2
classes.js
Normal file
2
classes.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export class ProxiedPlayer {
|
||||||
|
}
|
15
classes.ts
Normal file
15
classes.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Client } from "minecraft-protocol"
|
||||||
|
import { WebSocket } from "ws"
|
||||||
|
import { State } from "./types.js"
|
||||||
|
|
||||||
|
export class ProxiedPlayer {
|
||||||
|
public username: string
|
||||||
|
public uuid: string
|
||||||
|
public clientBrand: string
|
||||||
|
public state: State
|
||||||
|
public ws: WebSocket
|
||||||
|
public ip: string
|
||||||
|
public remotePort: number
|
||||||
|
public remoteConnection: Client
|
||||||
|
public compressionThreshold: number
|
||||||
|
}
|
19
config.js
Normal file
19
config.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export const config = {
|
||||||
|
name: "BasedProxy",
|
||||||
|
port: 80,
|
||||||
|
maxPlayers: 20,
|
||||||
|
motd: {
|
||||||
|
iconURL: null,
|
||||||
|
l1: "hi",
|
||||||
|
l2: "lol"
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 25565
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
enabled: false,
|
||||||
|
key: null,
|
||||||
|
cert: null
|
||||||
|
}
|
||||||
|
};
|
21
config.ts
Normal file
21
config.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Config } from "./types.js";
|
||||||
|
|
||||||
|
export const config: Config = {
|
||||||
|
name: "BasedProxy",
|
||||||
|
port: 80, // 443 if using TLS
|
||||||
|
maxPlayers: 20,
|
||||||
|
motd: {
|
||||||
|
iconURL: null,
|
||||||
|
l1: "hi",
|
||||||
|
l2: "lol"
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 25565
|
||||||
|
},
|
||||||
|
security: { // provide path to key & cert if you want to enable encryption/secure websockets
|
||||||
|
enabled: false,
|
||||||
|
key: null,
|
||||||
|
cert: null
|
||||||
|
}
|
||||||
|
}
|
22
eaglerPacketDef.js
Normal file
22
eaglerPacketDef.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export var EaglerPacketId;
|
||||||
|
(function (EaglerPacketId) {
|
||||||
|
EaglerPacketId[EaglerPacketId["IDENTIFY_CLIENT"] = 1] = "IDENTIFY_CLIENT";
|
||||||
|
EaglerPacketId[EaglerPacketId["IDENTIFY_SERVER"] = 2] = "IDENTIFY_SERVER";
|
||||||
|
EaglerPacketId[EaglerPacketId["LOGIN"] = 4] = "LOGIN";
|
||||||
|
EaglerPacketId[EaglerPacketId["LOGIN_ACK"] = 5] = "LOGIN_ACK";
|
||||||
|
EaglerPacketId[EaglerPacketId["SKIN"] = 7] = "SKIN";
|
||||||
|
EaglerPacketId[EaglerPacketId["C_READY"] = 8] = "C_READY";
|
||||||
|
EaglerPacketId[EaglerPacketId["COMPLETE_HANDSHAKE"] = 9] = "COMPLETE_HANDSHAKE";
|
||||||
|
EaglerPacketId[EaglerPacketId["DISCONNECT"] = 255] = "DISCONNECT";
|
||||||
|
})(EaglerPacketId || (EaglerPacketId = {}));
|
||||||
|
export var DisconnectReason;
|
||||||
|
(function (DisconnectReason) {
|
||||||
|
DisconnectReason[DisconnectReason["UNEXPECTED_PACKET"] = 1] = "UNEXPECTED_PACKET";
|
||||||
|
DisconnectReason[DisconnectReason["DUPLICATE_USERNAME"] = 2] = "DUPLICATE_USERNAME";
|
||||||
|
DisconnectReason[DisconnectReason["BAD_USERNAME"] = 3] = "BAD_USERNAME";
|
||||||
|
DisconnectReason[DisconnectReason["SERVER_DISCONNECT"] = 4] = "SERVER_DISCONNECT";
|
||||||
|
DisconnectReason[DisconnectReason["CUSTOM"] = 8] = "CUSTOM";
|
||||||
|
})(DisconnectReason || (DisconnectReason = {}));
|
||||||
|
export const MAGIC_BUILTIN_SKIN_BYTES = [0x00, 0x05, 0x01, 0x00, 0x00, 0x00];
|
||||||
|
export const MAGIC_ENDING_IDENTIFYS_BYTES = [0x00, 0x00, 0x00];
|
||||||
|
// Afterwards, forward 0x01 (Join Game, Clientbound) and everything after that
|
41
eaglerPacketDef.ts
Normal file
41
eaglerPacketDef.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { UUID } from "./types.js"
|
||||||
|
|
||||||
|
export enum EaglerPacketId {
|
||||||
|
IDENTIFY_CLIENT = 0x01,
|
||||||
|
IDENTIFY_SERVER = 0x2,
|
||||||
|
LOGIN = 0x4,
|
||||||
|
LOGIN_ACK = 0x05,
|
||||||
|
SKIN = 0x07,
|
||||||
|
C_READY = 0x08,
|
||||||
|
COMPLETE_HANDSHAKE = 0x09,
|
||||||
|
DISCONNECT = 0xff
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DisconnectReason {
|
||||||
|
UNEXPECTED_PACKET = 0x1,
|
||||||
|
DUPLICATE_USERNAME = 0x2,
|
||||||
|
BAD_USERNAME = 0x3,
|
||||||
|
SERVER_DISCONNECT = 0x4,
|
||||||
|
CUSTOM = 0x8
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: get skins + server icon working
|
||||||
|
export type Bitmap = unknown
|
||||||
|
export const MAGIC_BUILTIN_SKIN_BYTES = [0x00, 0x05, 0x01, 0x00, 0x00, 0x00]
|
||||||
|
export const MAGIC_ENDING_IDENTIFYS_BYTES = [0x00, 0x00, 0x00]
|
||||||
|
|
||||||
|
// NOTE: unless explicitly marked, a number (VarInt) preceding a string is a string
|
||||||
|
|
||||||
|
export type IdentifyC = [EaglerPacketId.IDENTIFY_CLIENT, 0x01, 0x2f, number, string, number, string]
|
||||||
|
export type IdentifyS = [EaglerPacketId.IDENTIFY_SERVER, 0x01, number, string, number, string]
|
||||||
|
export type Login = [EaglerPacketId.LOGIN, number, string, number, "default", 0x0]
|
||||||
|
export type LoginAck = [EaglerPacketId.LOGIN_ACK, number, string, UUID]
|
||||||
|
export type BaseSkin = [EaglerPacketId.SKIN, number, string, ...typeof MAGIC_BUILTIN_SKIN_BYTES | [number]]
|
||||||
|
// IF base skin packet ends with magic bytes...
|
||||||
|
export type SkinBuiltIn = [EaglerPacketId.SKIN, number, string, ...typeof MAGIC_BUILTIN_SKIN_BYTES, number]
|
||||||
|
export type SkinCustom = [EaglerPacketId.SKIN, number, string, number, Bitmap]
|
||||||
|
export type ClientReady = [EaglerPacketId.C_READY]
|
||||||
|
export type Joined = [EaglerPacketId.COMPLETE_HANDSHAKE]
|
||||||
|
export type Disconnect = [EaglerPacketId.DISCONNECT, number, string, DisconnectReason]
|
||||||
|
|
||||||
|
// Afterwards, forward 0x01 (Join Game, Clientbound) and everything after that
|
7
globals.d.ts
vendored
Normal file
7
globals.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { ProxyGlobals } from "./types.js"
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var PROXY: ProxyGlobals
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
67
index.js
Normal file
67
index.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import * as https from "https";
|
||||||
|
import { WebSocketServer } from "ws";
|
||||||
|
import { ProxiedPlayer } from "./classes.js";
|
||||||
|
import { config } from "./config.js";
|
||||||
|
import { handlePacket } from "./listener.js";
|
||||||
|
import { Logger } from "./logger.js";
|
||||||
|
import { BRANDING, NETWORK_VERSION, VERSION } from "./meta.js";
|
||||||
|
import { State } from "./types.js";
|
||||||
|
import { genUUID } from "./utils.js";
|
||||||
|
const logger = new Logger("EagXProxy");
|
||||||
|
const connectionLogger = new Logger("ConnectionHandler");
|
||||||
|
global.PROXY = {
|
||||||
|
brand: BRANDING,
|
||||||
|
version: VERSION,
|
||||||
|
MOTDVersion: NETWORK_VERSION,
|
||||||
|
serverName: config.name,
|
||||||
|
secure: false,
|
||||||
|
proxyUUID: genUUID(config.name),
|
||||||
|
MOTD: {
|
||||||
|
icon: null,
|
||||||
|
motd: [config.motd.l1, config.motd.l2]
|
||||||
|
},
|
||||||
|
playerStats: {
|
||||||
|
max: config.maxPlayers,
|
||||||
|
onlineCount: 0
|
||||||
|
},
|
||||||
|
wsServer: null,
|
||||||
|
players: new Map(),
|
||||||
|
logger: logger,
|
||||||
|
config: config
|
||||||
|
};
|
||||||
|
PROXY.playerStats.onlineCount = PROXY.players.size;
|
||||||
|
let server;
|
||||||
|
if (PROXY.config.security.enabled) {
|
||||||
|
logger.info(`Starting SECURE WebSocket proxy on port ${config.port}...`);
|
||||||
|
if (process.env.REPL_SLUG) {
|
||||||
|
logger.warn("You appear to be running the proxy on Repl.it with encryption enabled. Please note that Repl.it by default provides encryption, and enabling encryption may or may not prevent you from connecting to the server.");
|
||||||
|
}
|
||||||
|
server = new WebSocketServer({
|
||||||
|
server: https.createServer({
|
||||||
|
key: readFileSync(config.security.key),
|
||||||
|
cert: readFileSync(config.security.cert)
|
||||||
|
}).listen(config.port)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.info(`Starting INSECURE WebSocket proxy on port ${config.port}...`);
|
||||||
|
server = new WebSocketServer({
|
||||||
|
port: config.port
|
||||||
|
});
|
||||||
|
}
|
||||||
|
PROXY.wsServer = server;
|
||||||
|
server.addListener('connection', c => {
|
||||||
|
connectionLogger.debug(`[CONNECTION] New inbound WebSocket connection from [/${c._socket.remoteAddress}:${c._socket.remotePort}]. (${c._socket.remotePort} -> ${config.port})`);
|
||||||
|
const plr = new ProxiedPlayer();
|
||||||
|
plr.ws = c;
|
||||||
|
plr.ip = c._socket.remoteAddress;
|
||||||
|
plr.remotePort = c._socket.remotePort;
|
||||||
|
plr.state = State.PRE_HANDSHAKE;
|
||||||
|
c.on('message', msg => {
|
||||||
|
handlePacket(msg, plr);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
server.on('listening', () => {
|
||||||
|
logger.info(`Successfully started${config.security.enabled ? " [secure]" : ""} WebSocket proxy on port ${config.port}!`);
|
||||||
|
});
|
76
index.ts
Normal file
76
index.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import * as http from "http"
|
||||||
|
import * as https from "https"
|
||||||
|
import { WebSocketServer } from "ws";
|
||||||
|
import { ProxiedPlayer } from "./classes.js";
|
||||||
|
import { config } from "./config.js";
|
||||||
|
import { handlePacket } from "./listener.js";
|
||||||
|
import { Logger } from "./logger.js";
|
||||||
|
import { BRANDING, NETWORK_VERSION, VERSION } from "./meta.js";
|
||||||
|
import { State } from "./types.js";
|
||||||
|
import { genUUID } from "./utils.js";
|
||||||
|
|
||||||
|
const logger = new Logger("EagXProxy")
|
||||||
|
const connectionLogger = new Logger("ConnectionHandler")
|
||||||
|
|
||||||
|
global.PROXY = {
|
||||||
|
brand: BRANDING,
|
||||||
|
version: VERSION,
|
||||||
|
MOTDVersion: NETWORK_VERSION,
|
||||||
|
|
||||||
|
serverName: config.name,
|
||||||
|
secure: false,
|
||||||
|
proxyUUID: genUUID(config.name),
|
||||||
|
MOTD: {
|
||||||
|
icon: null,
|
||||||
|
motd: [config.motd.l1, config.motd.l2]
|
||||||
|
},
|
||||||
|
|
||||||
|
playerStats: {
|
||||||
|
max: config.maxPlayers,
|
||||||
|
onlineCount: 0
|
||||||
|
},
|
||||||
|
wsServer: null,
|
||||||
|
players: new Map(),
|
||||||
|
logger: logger,
|
||||||
|
config: config
|
||||||
|
}
|
||||||
|
PROXY.playerStats.onlineCount = PROXY.players.size
|
||||||
|
|
||||||
|
let server: WebSocketServer
|
||||||
|
|
||||||
|
if (PROXY.config.security.enabled) {
|
||||||
|
logger.info(`Starting SECURE WebSocket proxy on port ${config.port}...`)
|
||||||
|
if (process.env.REPL_SLUG) {
|
||||||
|
logger.warn("You appear to be running the proxy on Repl.it with encryption enabled. Please note that Repl.it by default provides encryption, and enabling encryption may or may not prevent you from connecting to the server.")
|
||||||
|
}
|
||||||
|
server = new WebSocketServer({
|
||||||
|
server: https.createServer({
|
||||||
|
key: readFileSync(config.security.key),
|
||||||
|
cert: readFileSync(config.security.cert)
|
||||||
|
}).listen(config.port)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
logger.info(`Starting INSECURE WebSocket proxy on port ${config.port}...`)
|
||||||
|
server = new WebSocketServer({
|
||||||
|
port: config.port
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
PROXY.wsServer = server
|
||||||
|
|
||||||
|
server.addListener('connection', c => {
|
||||||
|
connectionLogger.debug(`[CONNECTION] New inbound WebSocket connection from [/${(c as any)._socket.remoteAddress}:${(c as any)._socket.remotePort}]. (${(c as any)._socket.remotePort} -> ${config.port})`)
|
||||||
|
const plr = new ProxiedPlayer()
|
||||||
|
plr.ws = c
|
||||||
|
plr.ip = (c as any)._socket.remoteAddress
|
||||||
|
plr.remotePort = (c as any)._socket.remotePort
|
||||||
|
plr.state = State.PRE_HANDSHAKE
|
||||||
|
c.on('message', msg => {
|
||||||
|
handlePacket(msg as Buffer, plr)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
server.on('listening', () => {
|
||||||
|
logger.info(`Successfully started${config.security.enabled ? " [secure]" : ""} WebSocket proxy on port ${config.port}!`)
|
||||||
|
})
|
26
listener.js
Normal file
26
listener.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { Logger } from "./logger.js";
|
||||||
|
import { handleMotd } from "./motd.js";
|
||||||
|
import { State } from "./types.js";
|
||||||
|
import { doHandshake } from "./utils.js";
|
||||||
|
const logger = new Logger("PacketHandler");
|
||||||
|
export function handlePacket(packet, client) {
|
||||||
|
if (client.state == State.PRE_HANDSHAKE) {
|
||||||
|
if (packet.toString() === "Accept: MOTD") {
|
||||||
|
handleMotd(client);
|
||||||
|
}
|
||||||
|
else if (!client._handled) {
|
||||||
|
;
|
||||||
|
client._handled = true;
|
||||||
|
doHandshake(client, packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (client.state == State.POST_HANDSHAKE) {
|
||||||
|
if (!client.remoteConnection || client.remoteConnection.socket.closed) {
|
||||||
|
logger.warn(`Player ${client.username} is marked as post handshake, but is disconnected from the game server? Disconnecting due to illegal state.`);
|
||||||
|
client.ws.close();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
client.remoteConnection.writeRaw(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
listener.ts
Normal file
25
listener.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { ProxiedPlayer } from "./classes.js";
|
||||||
|
import { Logger } from "./logger.js";
|
||||||
|
import { handleMotd } from "./motd.js";
|
||||||
|
import { State } from "./types.js";
|
||||||
|
import { doHandshake } from "./utils.js";
|
||||||
|
|
||||||
|
const logger = new Logger("PacketHandler")
|
||||||
|
|
||||||
|
export function handlePacket(packet: Buffer, client: ProxiedPlayer) {
|
||||||
|
if (client.state == State.PRE_HANDSHAKE) {
|
||||||
|
if (packet.toString() === "Accept: MOTD") {
|
||||||
|
handleMotd(client)
|
||||||
|
} else if (!(client as any)._handled) {
|
||||||
|
;(client as any)._handled = true
|
||||||
|
doHandshake(client, packet)
|
||||||
|
}
|
||||||
|
} else if (client.state == State.POST_HANDSHAKE) {
|
||||||
|
if (!client.remoteConnection || client.remoteConnection.socket.closed) {
|
||||||
|
logger.warn(`Player ${client.username} is marked as post handshake, but is disconnected from the game server? Disconnecting due to illegal state.`)
|
||||||
|
client.ws.close()
|
||||||
|
} else {
|
||||||
|
client.remoteConnection.writeRaw(packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
logger.js
Normal file
54
logger.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { Chalk } from "chalk";
|
||||||
|
const color = new Chalk({ level: 2 });
|
||||||
|
let global_verbose = false;
|
||||||
|
export function verboseLogging(newVal) {
|
||||||
|
global_verbose = (newVal !== null && newVal !== void 0 ? newVal : global_verbose) ? false : true;
|
||||||
|
}
|
||||||
|
function jsonLog(type, message) {
|
||||||
|
return JSON.stringify({
|
||||||
|
type: type,
|
||||||
|
message: message
|
||||||
|
}) + "\n";
|
||||||
|
}
|
||||||
|
export class Logger {
|
||||||
|
constructor(name, verbose) {
|
||||||
|
this.jsonLog = process.argv.includes("--json") || process.argv.includes("-j");
|
||||||
|
this.loggerName = name;
|
||||||
|
if (verbose)
|
||||||
|
this.verbose = verbose;
|
||||||
|
else
|
||||||
|
this.verbose = global_verbose;
|
||||||
|
}
|
||||||
|
info(s) {
|
||||||
|
if (!this.jsonLog)
|
||||||
|
process.stdout.write(`${color.green("I")} ${color.gray(new Date().toISOString())} ${color.reset(`${color.yellow(`${this.loggerName}:`)} ${s}`)}\n`);
|
||||||
|
else
|
||||||
|
process.stdout.write(jsonLog("info", s));
|
||||||
|
}
|
||||||
|
warn(s) {
|
||||||
|
if (!this.jsonLog)
|
||||||
|
process.stdout.write(`${color.yellow("W")} ${color.gray(new Date().toISOString())} ${color.yellow(`${color.yellow(`${this.loggerName}:`)} ${s}`)}\n`);
|
||||||
|
else
|
||||||
|
process.stderr.write(jsonLog("warn", s));
|
||||||
|
}
|
||||||
|
error(s) {
|
||||||
|
if (!this.jsonLog)
|
||||||
|
process.stderr.write(`* ${color.red("E")} ${color.gray(new Date().toISOString())} ${color.redBright(`${color.red(`${this.loggerName}:`)} ${s}`)}\n`);
|
||||||
|
else
|
||||||
|
process.stderr.write(jsonLog("error", s));
|
||||||
|
}
|
||||||
|
fatal(s) {
|
||||||
|
if (!this.jsonLog)
|
||||||
|
process.stderr.write(`** ${color.red("F!")} ${color.gray(new Date().toISOString())} ${color.bgRedBright(color.redBright(`${color.red(`${this.loggerName}:`)} ${s}`))}\n`);
|
||||||
|
else
|
||||||
|
process.stderr.write(jsonLog("fatal", s));
|
||||||
|
}
|
||||||
|
debug(s) {
|
||||||
|
if (this.verbose || global_verbose) {
|
||||||
|
if (!this.jsonLog)
|
||||||
|
process.stderr.write(`${color.gray("D")} ${color.gray(new Date().toISOString())} ${color.gray(`${color.gray(`${this.loggerName}:`)} ${s}`)}\n`);
|
||||||
|
else
|
||||||
|
process.stderr.write(jsonLog("debug", s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
logger.ts
Normal file
61
logger.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { Chalk } from "chalk"
|
||||||
|
|
||||||
|
const color = new Chalk({ level: 2 })
|
||||||
|
|
||||||
|
let global_verbose: boolean = false
|
||||||
|
|
||||||
|
type JsonLogType = "info" | "warn" | "error" | "fatal" | "debug"
|
||||||
|
type JsonOutput = {
|
||||||
|
type: JsonLogType,
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verboseLogging(newVal?: boolean) {
|
||||||
|
global_verbose = newVal ?? global_verbose ? false : true
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonLog(type: JsonLogType, message: string): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
type: type,
|
||||||
|
message: message
|
||||||
|
}) + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Logger {
|
||||||
|
loggerName: string
|
||||||
|
verbose: boolean
|
||||||
|
private jsonLog: boolean = process.argv.includes("--json") || process.argv.includes("-j")
|
||||||
|
|
||||||
|
constructor(name: string, verbose?: boolean) {
|
||||||
|
this.loggerName = name
|
||||||
|
if (verbose) this.verbose = verbose
|
||||||
|
else this.verbose = global_verbose
|
||||||
|
}
|
||||||
|
|
||||||
|
info(s: string) {
|
||||||
|
if (!this.jsonLog) process.stdout.write(`${color.green("I")} ${color.gray(new Date().toISOString())} ${color.reset(`${color.yellow(`${this.loggerName}:`)} ${s}`)}\n`)
|
||||||
|
else process.stdout.write(jsonLog("info", s))
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(s: string) {
|
||||||
|
if (!this.jsonLog) process.stdout.write(`${color.yellow("W")} ${color.gray(new Date().toISOString())} ${color.yellow(`${color.yellow(`${this.loggerName}:`)} ${s}`)}\n`)
|
||||||
|
else process.stderr.write(jsonLog("warn", s))
|
||||||
|
}
|
||||||
|
|
||||||
|
error(s: string) {
|
||||||
|
if (!this.jsonLog) process.stderr.write(`* ${color.red("E")} ${color.gray(new Date().toISOString())} ${color.redBright(`${color.red(`${this.loggerName}:`)} ${s}`)}\n`)
|
||||||
|
else process.stderr.write(jsonLog("error", s))
|
||||||
|
}
|
||||||
|
|
||||||
|
fatal(s: string) {
|
||||||
|
if (!this.jsonLog) process.stderr.write(`** ${color.red("F!")} ${color.gray(new Date().toISOString())} ${color.bgRedBright(color.redBright(`${color.red(`${this.loggerName}:`)} ${s}`))}\n`)
|
||||||
|
else process.stderr.write(jsonLog("fatal", s))
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(s: string) {
|
||||||
|
if (this.verbose || global_verbose) {
|
||||||
|
if (!this.jsonLog) process.stderr.write(`${color.gray("D")} ${color.gray(new Date().toISOString())} ${color.gray(`${color.gray(`${this.loggerName}:`)} ${s}`)}\n`)
|
||||||
|
else process.stderr.write(jsonLog("debug", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
meta.js
Normal file
3
meta.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const BRANDING = Object.freeze("EaglerXProxy");
|
||||||
|
export const VERSION = "1.0.0";
|
||||||
|
export const NETWORK_VERSION = Object.freeze(BRANDING + "/" + VERSION);
|
3
meta.ts
Normal file
3
meta.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const BRANDING: Readonly<string> = Object.freeze("EaglerXProxy")
|
||||||
|
export const VERSION: Readonly<string> = "1.0.0"
|
||||||
|
export const NETWORK_VERSION: Readonly<string> = Object.freeze(BRANDING + "/" + VERSION)
|
34
motd.js
Normal file
34
motd.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
export function handleMotd(player) {
|
||||||
|
const names = [];
|
||||||
|
for (const [username, player] of PROXY.players) {
|
||||||
|
if (names.length > 0) {
|
||||||
|
names.push(`(and ${PROXY.players.size - names.length} more)`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
names.push(username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
player.ws.send(JSON.stringify({
|
||||||
|
brand: PROXY.brand,
|
||||||
|
cracked: true,
|
||||||
|
data: {
|
||||||
|
cache: true,
|
||||||
|
icon: PROXY.MOTD.icon ? true : false,
|
||||||
|
max: PROXY.playerStats.max,
|
||||||
|
motd: PROXY.MOTD.motd,
|
||||||
|
online: PROXY.playerStats.onlineCount,
|
||||||
|
players: names
|
||||||
|
},
|
||||||
|
name: PROXY.serverName,
|
||||||
|
secure: false,
|
||||||
|
time: Date.now(),
|
||||||
|
type: "motd",
|
||||||
|
uuid: PROXY.proxyUUID,
|
||||||
|
vers: PROXY.MOTDVersion
|
||||||
|
}));
|
||||||
|
if (PROXY.MOTD.icon) {
|
||||||
|
player.ws.send(PROXY.MOTD.icon);
|
||||||
|
}
|
||||||
|
player.ws.close();
|
||||||
|
}
|
66
motd.ts
Normal file
66
motd.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import { WebSocket } from "ws";
|
||||||
|
import { ProxiedPlayer } from "./classes.js";
|
||||||
|
import { UUID } from "./types.js"
|
||||||
|
|
||||||
|
export type MotdPlayer = {
|
||||||
|
name: string,
|
||||||
|
id: UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MotdJSONRes = {
|
||||||
|
brand: string,
|
||||||
|
cracked: true,
|
||||||
|
data: {
|
||||||
|
cache: true,
|
||||||
|
icon: boolean,
|
||||||
|
max: number,
|
||||||
|
motd: [string, string],
|
||||||
|
online: number,
|
||||||
|
players: string[],
|
||||||
|
},
|
||||||
|
name: string,
|
||||||
|
secure: false,
|
||||||
|
time: ReturnType<typeof Date.now>,
|
||||||
|
type: "motd",
|
||||||
|
uuid: ReturnType<typeof randomUUID>,
|
||||||
|
vers: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// a 16384 byte array
|
||||||
|
export type MotdServerLogo = Int8Array
|
||||||
|
|
||||||
|
export function handleMotd(player: Partial<ProxiedPlayer>) {
|
||||||
|
const names = []
|
||||||
|
for (const [username, player] of PROXY.players) {
|
||||||
|
if (names.length > 0) {
|
||||||
|
names.push(`(and ${PROXY.players.size - names.length} more)`)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
names.push(username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
player.ws.send(JSON.stringify({
|
||||||
|
brand: PROXY.brand,
|
||||||
|
cracked: true,
|
||||||
|
data: {
|
||||||
|
cache: true,
|
||||||
|
icon: PROXY.MOTD.icon ? true : false,
|
||||||
|
max: PROXY.playerStats.max,
|
||||||
|
motd: PROXY.MOTD.motd,
|
||||||
|
online: PROXY.playerStats.onlineCount,
|
||||||
|
players: names
|
||||||
|
},
|
||||||
|
name: PROXY.serverName,
|
||||||
|
secure: false,
|
||||||
|
time: Date.now(),
|
||||||
|
type: "motd",
|
||||||
|
uuid: PROXY.proxyUUID,
|
||||||
|
vers: PROXY.MOTDVersion
|
||||||
|
} as MotdJSONRes))
|
||||||
|
if (PROXY.MOTD.icon) {
|
||||||
|
player.ws.send(PROXY.MOTD.icon)
|
||||||
|
}
|
||||||
|
player.ws.close()
|
||||||
|
}
|
1681
package-lock.json
generated
Normal file
1681
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
package.json
Normal file
15
package.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@thi.ng/leb128": "^3.0.1",
|
||||||
|
"@types/node": "^18.11.16",
|
||||||
|
"@types/uuid": "^9.0.0",
|
||||||
|
"@types/ws": "^8.5.3",
|
||||||
|
"chalk": "^5.2.0",
|
||||||
|
"minecraft-protocol": "^1.36.2",
|
||||||
|
"tsc": "^2.0.4",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
|
"uuid-buffer": "^1.0.3",
|
||||||
|
"ws": "^8.11.0"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
|
}
|
9
tsconfig.json
Normal file
9
tsconfig.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "esnext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"target": "es2017",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"sourceMap": false
|
||||||
|
}
|
||||||
|
}
|
6
types.js
Normal file
6
types.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export var State;
|
||||||
|
(function (State) {
|
||||||
|
State[State["PRE_HANDSHAKE"] = 0] = "PRE_HANDSHAKE";
|
||||||
|
State[State["POST_HANDSHAKE"] = 1] = "POST_HANDSHAKE";
|
||||||
|
State[State["DISCONNECTED"] = 2] = "DISCONNECTED";
|
||||||
|
})(State || (State = {}));
|
60
types.ts
Normal file
60
types.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import { randomUUID } from "crypto"
|
||||||
|
import { WebSocketServer } from "ws"
|
||||||
|
import { ProxiedPlayer } from "./classes.js"
|
||||||
|
import { Logger } from "./logger.js"
|
||||||
|
import { BRANDING, NETWORK_VERSION, VERSION } from "./meta.js"
|
||||||
|
|
||||||
|
export type UUID = ReturnType<typeof randomUUID>
|
||||||
|
|
||||||
|
export enum State {
|
||||||
|
PRE_HANDSHAKE,
|
||||||
|
POST_HANDSHAKE,
|
||||||
|
DISCONNECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MOTD = {
|
||||||
|
icon?: Int8Array, // 16384
|
||||||
|
motd: [string, string]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlayerStats = {
|
||||||
|
max: number,
|
||||||
|
onlineCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProxyGlobals = {
|
||||||
|
brand: typeof BRANDING,
|
||||||
|
version: typeof VERSION,
|
||||||
|
MOTDVersion: typeof NETWORK_VERSION,
|
||||||
|
|
||||||
|
serverName: string,
|
||||||
|
secure: false,
|
||||||
|
proxyUUID: UUID,
|
||||||
|
MOTD: MOTD,
|
||||||
|
|
||||||
|
playerStats: PlayerStats,
|
||||||
|
wsServer: WebSocketServer,
|
||||||
|
players: Map<string, ProxiedPlayer>,
|
||||||
|
logger: Logger,
|
||||||
|
config: Config
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Config = {
|
||||||
|
name: string,
|
||||||
|
port: number,
|
||||||
|
maxPlayers: number,
|
||||||
|
motd: {
|
||||||
|
iconURL?: string,
|
||||||
|
l1: string,
|
||||||
|
l2: string
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: string,
|
||||||
|
port: number
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
enabled: boolean
|
||||||
|
key: string,
|
||||||
|
cert: string
|
||||||
|
}
|
||||||
|
}
|
229
utils.js
Normal file
229
utils.js
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
import { v3 } from "uuid";
|
||||||
|
import { encodeULEB128 as encodeVarInt, decodeULEB128 as decodeVarInt, decodeSLEB128 as decodeSVarInt } from "@thi.ng/leb128";
|
||||||
|
import { DisconnectReason, EaglerPacketId, MAGIC_ENDING_IDENTIFYS_BYTES } from "./eaglerPacketDef.js";
|
||||||
|
import { Logger } from "./logger.js";
|
||||||
|
import { State } from "./types.js";
|
||||||
|
import { toBuffer } from "uuid-buffer";
|
||||||
|
import * as mc from "minecraft-protocol";
|
||||||
|
import { config } from "./config.js";
|
||||||
|
const MAGIC_UUID = "a7e774bc-7ea4-11ed-9a58-1f9e14304a59";
|
||||||
|
const logger = new Logger("LoginHandler");
|
||||||
|
const USERNAME_REGEX = /[^0-9^a-z^A-Z^_]/gi;
|
||||||
|
export function genUUID(user) {
|
||||||
|
return v3(user, MAGIC_UUID);
|
||||||
|
}
|
||||||
|
export function bufferizeUUID(uuid) {
|
||||||
|
return toBuffer(uuid);
|
||||||
|
}
|
||||||
|
export function validateUsername(user) {
|
||||||
|
if (user.length > 20)
|
||||||
|
throw new Error("Username is too long!");
|
||||||
|
if (!!user.match(USERNAME_REGEX))
|
||||||
|
throw new Error("Invalid username. Username can only contain alphanumeric characters, and the underscore (_) character.");
|
||||||
|
}
|
||||||
|
export function disconnect(player, message, code) {
|
||||||
|
if (player.state == State.POST_HANDSHAKE) {
|
||||||
|
const messageLen = encodeVarInt(message.length);
|
||||||
|
const d = Buffer.alloc(1 + messageLen.length + message.length);
|
||||||
|
d.set([0x40, ...messageLen, ...Buffer.from(message)]);
|
||||||
|
player.ws.send(d);
|
||||||
|
player.ws.close();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const messageLen = encodeVarInt(message.length), codeEnc = encodeVarInt(code !== null && code !== void 0 ? code : DisconnectReason.CUSTOM);
|
||||||
|
const d = Buffer.alloc(1 + codeEnc.length + messageLen.length + message.length);
|
||||||
|
d.set([0xff, ...codeEnc, ...messageLen, ...Buffer.from(message)]);
|
||||||
|
player.ws.send(d);
|
||||||
|
player.ws.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function awaitPacket(ws, id) {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
let resolved = false;
|
||||||
|
const msgCb = (msg) => {
|
||||||
|
if (id != null && msg[0] == id) {
|
||||||
|
resolved = true;
|
||||||
|
ws.removeEventListener('message', msgCb);
|
||||||
|
ws.removeEventListener('close', discon);
|
||||||
|
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2);
|
||||||
|
res(msg);
|
||||||
|
}
|
||||||
|
else if (id == null) {
|
||||||
|
resolved = true;
|
||||||
|
ws.removeEventListener('message', msgCb);
|
||||||
|
ws.removeEventListener('close', discon);
|
||||||
|
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2);
|
||||||
|
res(msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const discon = () => {
|
||||||
|
resolved = true;
|
||||||
|
ws.removeEventListener('message', msgCb);
|
||||||
|
ws.removeEventListener('close', discon);
|
||||||
|
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2);
|
||||||
|
rej("Connection closed");
|
||||||
|
};
|
||||||
|
ws.setMaxListeners(ws.getMaxListeners() + 2);
|
||||||
|
ws.on('message', msgCb);
|
||||||
|
ws.on('close', discon);
|
||||||
|
setTimeout(() => {
|
||||||
|
ws.removeEventListener('message', msgCb);
|
||||||
|
ws.removeEventListener('close', discon);
|
||||||
|
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2);
|
||||||
|
rej("Timed out");
|
||||||
|
}, 10000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function loginServer(ip, port, client) {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
let receivedCompression = false;
|
||||||
|
const mcClient = mc.createClient({
|
||||||
|
host: ip,
|
||||||
|
port: port,
|
||||||
|
auth: 'offline',
|
||||||
|
version: '1.8.8',
|
||||||
|
username: client.username
|
||||||
|
});
|
||||||
|
mcClient.on('error', err => {
|
||||||
|
mcClient.end();
|
||||||
|
rej(err);
|
||||||
|
});
|
||||||
|
mcClient.on('end', () => {
|
||||||
|
client.ws.close();
|
||||||
|
});
|
||||||
|
mcClient.on('connect', () => {
|
||||||
|
client.remoteConnection = mcClient;
|
||||||
|
logger.info(`Player ${client.username} has been connected to the server.`);
|
||||||
|
res();
|
||||||
|
});
|
||||||
|
mcClient.on('raw', p => {
|
||||||
|
if (p[0] == 0x03 && !receivedCompression) {
|
||||||
|
receivedCompression = true;
|
||||||
|
const compT = {
|
||||||
|
id: null,
|
||||||
|
thres: null
|
||||||
|
};
|
||||||
|
const id = decodeVarInt(p);
|
||||||
|
compT.id = Number(id[0]);
|
||||||
|
const thres = decodeSVarInt(p.subarray(id[1]));
|
||||||
|
compT.thres = thres[0];
|
||||||
|
client.compressionThreshold = compT.thres;
|
||||||
|
client.ws.send(p);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
client.ws.send(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export async function doHandshake(client, initialPacket) {
|
||||||
|
client.ws.on('close', () => {
|
||||||
|
client.state = State.DISCONNECTED;
|
||||||
|
if (client.remoteConnection) {
|
||||||
|
client.remoteConnection.end();
|
||||||
|
}
|
||||||
|
PROXY.players.delete(client.username);
|
||||||
|
PROXY.playerStats.onlineCount -= 1;
|
||||||
|
logger.info(`Client [/${client.ip}:${client.remotePort}]${client.username ? ` (${client.username})` : ""} disconnected from the server.`);
|
||||||
|
});
|
||||||
|
if (PROXY.players.size + 1 > PROXY.playerStats.max) {
|
||||||
|
disconnect(client, "The proxy is full!", DisconnectReason.CUSTOM);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const identifyC = {
|
||||||
|
id: null,
|
||||||
|
brandingLen: null,
|
||||||
|
branding: null,
|
||||||
|
verLen: null,
|
||||||
|
ver: null
|
||||||
|
};
|
||||||
|
if (true) {
|
||||||
|
// save namespace by nesting func declarations in a if true statement
|
||||||
|
const Iid = decodeVarInt(initialPacket);
|
||||||
|
identifyC.id = Number(Iid[0]);
|
||||||
|
const brandingLen = decodeVarInt(initialPacket.subarray(Iid[1] + 2));
|
||||||
|
identifyC.brandingLen = Number(brandingLen[0]);
|
||||||
|
identifyC.branding = initialPacket.subarray(brandingLen[1] + Iid[1] + 2, brandingLen[1] + Iid[1] + Number(brandingLen[0]) + 2).toString();
|
||||||
|
const verLen = decodeVarInt(initialPacket.subarray(brandingLen[1] + Iid[1] + Number(brandingLen[0]) + 2));
|
||||||
|
identifyC.verLen = Number(verLen[0]);
|
||||||
|
identifyC.ver = initialPacket.subarray(brandingLen[1] + Number(brandingLen[0]) + Iid[1] + verLen[1] + 2).toString();
|
||||||
|
}
|
||||||
|
if (true) {
|
||||||
|
const brandingLen = encodeVarInt(PROXY.brand.length), brand = PROXY.brand;
|
||||||
|
const verLen = encodeVarInt(PROXY.version.length), version = PROXY.version;
|
||||||
|
const buff = Buffer.alloc(2 + MAGIC_ENDING_IDENTIFYS_BYTES.length + brandingLen.length + brand.length + verLen.length + version.length);
|
||||||
|
buff.set([EaglerPacketId.IDENTIFY_SERVER, 0x01, ...brandingLen, ...Buffer.from(brand), ...verLen, ...Buffer.from(version), ...Buffer.from(MAGIC_ENDING_IDENTIFYS_BYTES)]);
|
||||||
|
client.ws.send(buff);
|
||||||
|
}
|
||||||
|
client.clientBrand = identifyC.branding;
|
||||||
|
const login = await awaitPacket(client.ws);
|
||||||
|
const loginP = {
|
||||||
|
id: null,
|
||||||
|
usernameLen: null,
|
||||||
|
username: null,
|
||||||
|
randomStrLen: null,
|
||||||
|
randomStr: null,
|
||||||
|
nullByte: null
|
||||||
|
};
|
||||||
|
if (login[0] === EaglerPacketId.LOGIN) {
|
||||||
|
const Iid = decodeVarInt(login);
|
||||||
|
loginP.id = Number(Iid[0]);
|
||||||
|
const usernameLen = decodeVarInt(login.subarray(loginP[1]));
|
||||||
|
loginP.usernameLen = Number(usernameLen[0]);
|
||||||
|
loginP.username = login.subarray(Iid[1] + usernameLen[1], Iid[1] + usernameLen[1] + loginP.usernameLen).toString();
|
||||||
|
const randomStrLen = decodeVarInt(login.subarray(Iid[1] + usernameLen[1] + loginP.usernameLen));
|
||||||
|
loginP.randomStrLen = Number(randomStrLen[0]);
|
||||||
|
loginP.randomStr = login.subarray(Iid[1] + usernameLen[1] + loginP.usernameLen + randomStrLen[1], Iid[1] + usernameLen[1] + loginP.usernameLen + randomStrLen[1] + loginP.randomStrLen).toString();
|
||||||
|
client.username = loginP.username;
|
||||||
|
client.uuid = genUUID(client.username);
|
||||||
|
try {
|
||||||
|
validateUsername(client.username);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
disconnect(client, err.message, DisconnectReason.BAD_USERNAME);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (PROXY.players.has(client.username)) {
|
||||||
|
disconnect(client, `Duplicate username: ${client.username}. Please connect under a different username.`, DisconnectReason.DUPLICATE_USERNAME);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PROXY.players.set(client.username, client);
|
||||||
|
if (true) {
|
||||||
|
const usernameLen = encodeVarInt(client.username.length), username = client.username;
|
||||||
|
const uuidLen = encodeVarInt(client.uuid.length), uuid = client.uuid;
|
||||||
|
const buff = Buffer.alloc(1 + usernameLen.length + username.length + uuidLen.length + uuid.length);
|
||||||
|
buff.set([EaglerPacketId.LOGIN_ACK, ...usernameLen, ...Buffer.from(username), ...uuidLen, ...Buffer.from(uuid)]);
|
||||||
|
client.ws.send(buff);
|
||||||
|
if (true) {
|
||||||
|
const [skin, ready] = await Promise.all([awaitPacket(client.ws, EaglerPacketId.SKIN), awaitPacket(client.ws, EaglerPacketId.C_READY)]);
|
||||||
|
if (ready[0] != 0x08) {
|
||||||
|
logger.error(`Client [/${client.ip}:${client.remotePort}] sent an unexpected packet! Disconnecting.`);
|
||||||
|
disconnect(client, "Received bad packet", DisconnectReason.UNEXPECTED_PACKET);
|
||||||
|
client.ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const buff = Buffer.alloc(1);
|
||||||
|
buff.set([EaglerPacketId.COMPLETE_HANDSHAKE]);
|
||||||
|
client.ws.send(buff);
|
||||||
|
client.state = State.POST_HANDSHAKE;
|
||||||
|
PROXY.playerStats.onlineCount += 1;
|
||||||
|
logger.info(`Client [/${client.ip}:${client.remotePort}] authenticated as player "${client.username}" and passed handshake. Connecting!`);
|
||||||
|
try {
|
||||||
|
await loginServer(config.server.host, config.server.port, client);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
logger.error(`Could not connect to remote server at [/${config.server.host}:${config.server.port}]: ${err}`);
|
||||||
|
disconnect(client, "Failed to connect to server. Please try again later.", DisconnectReason.CUSTOM);
|
||||||
|
client.state = State.DISCONNECTED;
|
||||||
|
client.ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.error(`Client [/${client.ip}:${client.remotePort}] sent an unexpected packet! Disconnecting.`);
|
||||||
|
disconnect(client, "Received bad packet", DisconnectReason.UNEXPECTED_PACKET);
|
||||||
|
client.ws.close();
|
||||||
|
}
|
||||||
|
}
|
241
utils.ts
Normal file
241
utils.ts
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
import { v3 } from "uuid"
|
||||||
|
import WebSocket from "ws"
|
||||||
|
import { ProxiedPlayer } from "./classes.js"
|
||||||
|
import {
|
||||||
|
encodeULEB128 as encodeVarInt,
|
||||||
|
decodeULEB128 as decodeVarInt,
|
||||||
|
encodeSLEB128 as encodeSVarInt,
|
||||||
|
decodeSLEB128 as decodeSVarInt
|
||||||
|
} from "@thi.ng/leb128"
|
||||||
|
import { DisconnectReason, EaglerPacketId, MAGIC_ENDING_IDENTIFYS_BYTES } from "./eaglerPacketDef.js"
|
||||||
|
import { Logger } from "./logger.js"
|
||||||
|
import { State } from "./types.js"
|
||||||
|
import { toBuffer } from "uuid-buffer"
|
||||||
|
import * as mc from "minecraft-protocol"
|
||||||
|
import { config } from "./config.js"
|
||||||
|
|
||||||
|
const MAGIC_UUID = "a7e774bc-7ea4-11ed-9a58-1f9e14304a59"
|
||||||
|
const logger = new Logger("LoginHandler")
|
||||||
|
const USERNAME_REGEX = /[^0-9^a-z^A-Z^_]/gi
|
||||||
|
|
||||||
|
export function genUUID(user: string): string {
|
||||||
|
return v3(user, MAGIC_UUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bufferizeUUID(uuid: string): Buffer {
|
||||||
|
return toBuffer(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateUsername(user: string): void | never {
|
||||||
|
if (user.length > 20)
|
||||||
|
throw new Error("Username is too long!")
|
||||||
|
if (!!user.match(USERNAME_REGEX))
|
||||||
|
throw new Error("Invalid username. Username can only contain alphanumeric characters, and the underscore (_) character.")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disconnect(player: ProxiedPlayer, message: string, code?: DisconnectReason) {
|
||||||
|
if (player.state == State.POST_HANDSHAKE) {
|
||||||
|
const messageLen = encodeVarInt(message.length)
|
||||||
|
const d = Buffer.alloc(1 + messageLen.length + message.length)
|
||||||
|
d.set([0x40, ...messageLen, ...Buffer.from(message)])
|
||||||
|
player.ws.send(d)
|
||||||
|
player.ws.close()
|
||||||
|
} else {
|
||||||
|
const messageLen = encodeVarInt(message.length), codeEnc = encodeVarInt(code ?? DisconnectReason.CUSTOM)
|
||||||
|
const d = Buffer.alloc(1 + codeEnc.length + messageLen.length + message.length)
|
||||||
|
d.set([0xff,...codeEnc, ...messageLen, ...Buffer.from(message)])
|
||||||
|
player.ws.send(d)
|
||||||
|
player.ws.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function awaitPacket(ws: WebSocket, id?: EaglerPacketId): Promise<Buffer> {
|
||||||
|
return new Promise<Buffer>((res, rej) => {
|
||||||
|
let resolved = false
|
||||||
|
const msgCb = (msg: any) => {
|
||||||
|
if (id != null && msg[0] == id) {
|
||||||
|
resolved = true
|
||||||
|
ws.removeEventListener('message', msgCb)
|
||||||
|
ws.removeEventListener('close', discon)
|
||||||
|
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2)
|
||||||
|
res(msg)
|
||||||
|
} else if (id == null) {
|
||||||
|
resolved = true
|
||||||
|
ws.removeEventListener('message', msgCb)
|
||||||
|
ws.removeEventListener('close', discon)
|
||||||
|
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2)
|
||||||
|
res(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const discon = () => {
|
||||||
|
resolved = true
|
||||||
|
ws.removeEventListener('message', msgCb)
|
||||||
|
ws.removeEventListener('close', discon)
|
||||||
|
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2)
|
||||||
|
rej("Connection closed")
|
||||||
|
}
|
||||||
|
ws.setMaxListeners(ws.getMaxListeners() + 2)
|
||||||
|
ws.on('message', msgCb)
|
||||||
|
ws.on('close', discon)
|
||||||
|
setTimeout(() => {
|
||||||
|
ws.removeEventListener('message', msgCb)
|
||||||
|
ws.removeEventListener('close', discon)
|
||||||
|
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2)
|
||||||
|
rej("Timed out")
|
||||||
|
}, 10000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loginServer(ip: string, port: number, client: ProxiedPlayer) {
|
||||||
|
return new Promise<void>((res, rej) => {
|
||||||
|
let receivedCompression = false
|
||||||
|
const mcClient = mc.createClient({
|
||||||
|
host: ip,
|
||||||
|
port: port,
|
||||||
|
auth: 'offline',
|
||||||
|
version: '1.8.8',
|
||||||
|
username: client.username
|
||||||
|
})
|
||||||
|
mcClient.on('error', err => {
|
||||||
|
mcClient.end()
|
||||||
|
rej(err)
|
||||||
|
})
|
||||||
|
mcClient.on('end', () => {
|
||||||
|
client.ws.close()
|
||||||
|
})
|
||||||
|
mcClient.on('connect', () => {
|
||||||
|
client.remoteConnection = mcClient
|
||||||
|
logger.info(`Player ${client.username} has been connected to the server.`)
|
||||||
|
res()
|
||||||
|
})
|
||||||
|
mcClient.on('raw', p => {
|
||||||
|
if (p[0] == 0x03 && !receivedCompression) {
|
||||||
|
receivedCompression = true
|
||||||
|
const compT = {
|
||||||
|
id: null,
|
||||||
|
thres: null
|
||||||
|
}
|
||||||
|
const id = decodeVarInt(p)
|
||||||
|
compT.id = Number(id[0])
|
||||||
|
const thres = decodeSVarInt(p.subarray(id[1]))
|
||||||
|
compT.thres = thres[0]
|
||||||
|
|
||||||
|
client.compressionThreshold = compT.thres
|
||||||
|
client.ws.send(p)
|
||||||
|
} else {
|
||||||
|
client.ws.send(p)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function doHandshake(client: ProxiedPlayer, initialPacket: Buffer) {
|
||||||
|
client.ws.on('close', () => {
|
||||||
|
client.state = State.DISCONNECTED
|
||||||
|
if (client.remoteConnection) {
|
||||||
|
client.remoteConnection.end()
|
||||||
|
}
|
||||||
|
PROXY.players.delete(client.username)
|
||||||
|
PROXY.playerStats.onlineCount -= 1
|
||||||
|
logger.info(`Client [/${client.ip}:${client.remotePort}]${client.username ? ` (${client.username})` : ""} disconnected from the server.`)
|
||||||
|
})
|
||||||
|
if (PROXY.players.size + 1 > PROXY.playerStats.max) {
|
||||||
|
disconnect(client, "The proxy is full!", DisconnectReason.CUSTOM)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const identifyC = {
|
||||||
|
id: null,
|
||||||
|
brandingLen: null,
|
||||||
|
branding: null,
|
||||||
|
verLen: null,
|
||||||
|
ver: null
|
||||||
|
}
|
||||||
|
if (true) {
|
||||||
|
// save namespace by nesting func declarations in a if true statement
|
||||||
|
const Iid = decodeVarInt(initialPacket)
|
||||||
|
identifyC.id = Number(Iid[0])
|
||||||
|
const brandingLen = decodeVarInt(initialPacket.subarray(Iid[1] + 2))
|
||||||
|
identifyC.brandingLen = Number(brandingLen[0])
|
||||||
|
identifyC.branding = initialPacket.subarray(brandingLen[1] + Iid[1] + 2, brandingLen[1] + Iid[1] + Number(brandingLen[0]) + 2).toString()
|
||||||
|
const verLen = decodeVarInt(initialPacket.subarray(brandingLen[1] + Iid[1] + Number(brandingLen[0]) + 2))
|
||||||
|
identifyC.verLen = Number(verLen[0])
|
||||||
|
identifyC.ver = initialPacket.subarray(brandingLen[1] + Number(brandingLen[0]) + Iid[1] + verLen[1] + 2).toString()
|
||||||
|
}
|
||||||
|
if (true) {
|
||||||
|
const brandingLen = encodeVarInt(PROXY.brand.length), brand = PROXY.brand
|
||||||
|
const verLen = encodeVarInt(PROXY.version.length), version = PROXY.version
|
||||||
|
const buff = Buffer.alloc(2 + MAGIC_ENDING_IDENTIFYS_BYTES.length + brandingLen.length + brand.length + verLen.length + version.length)
|
||||||
|
buff.set([EaglerPacketId.IDENTIFY_SERVER, 0x01, ...brandingLen, ...Buffer.from(brand), ...verLen, ...Buffer.from(version), ...Buffer.from(MAGIC_ENDING_IDENTIFYS_BYTES)])
|
||||||
|
client.ws.send(buff)
|
||||||
|
}
|
||||||
|
client.clientBrand = identifyC.branding
|
||||||
|
const login = await awaitPacket(client.ws)
|
||||||
|
const loginP = {
|
||||||
|
id: null,
|
||||||
|
usernameLen: null,
|
||||||
|
username: null,
|
||||||
|
randomStrLen: null,
|
||||||
|
randomStr: null,
|
||||||
|
nullByte: null
|
||||||
|
}
|
||||||
|
if (login[0] === EaglerPacketId.LOGIN) {
|
||||||
|
const Iid = decodeVarInt(login)
|
||||||
|
loginP.id = Number(Iid[0])
|
||||||
|
const usernameLen = decodeVarInt(login.subarray(loginP[1]))
|
||||||
|
loginP.usernameLen = Number(usernameLen[0])
|
||||||
|
loginP.username = login.subarray(Iid[1] + usernameLen[1], Iid[1] + usernameLen[1] + loginP.usernameLen).toString()
|
||||||
|
const randomStrLen = decodeVarInt(login.subarray(Iid[1] + usernameLen[1] + loginP.usernameLen))
|
||||||
|
loginP.randomStrLen = Number(randomStrLen[0])
|
||||||
|
loginP.randomStr = login.subarray(Iid[1] + usernameLen[1] + loginP.usernameLen + randomStrLen[1], Iid[1] + usernameLen[1] + loginP.usernameLen + randomStrLen[1] + loginP.randomStrLen).toString()
|
||||||
|
client.username = loginP.username
|
||||||
|
client.uuid = genUUID(client.username)
|
||||||
|
try { validateUsername(client.username) }
|
||||||
|
catch (err) {
|
||||||
|
disconnect(client, err.message, DisconnectReason.BAD_USERNAME)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (PROXY.players.has(client.username)) {
|
||||||
|
disconnect(client, `Duplicate username: ${client.username}. Please connect under a different username.`, DisconnectReason.DUPLICATE_USERNAME)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
PROXY.players.set(client.username, client)
|
||||||
|
if (true) {
|
||||||
|
const usernameLen = encodeVarInt(client.username.length), username = client.username
|
||||||
|
const uuidLen = encodeVarInt(client.uuid.length), uuid = client.uuid
|
||||||
|
const buff = Buffer.alloc(1 + usernameLen.length + username.length + uuidLen.length + uuid.length)
|
||||||
|
buff.set([EaglerPacketId.LOGIN_ACK, ...usernameLen, ...Buffer.from(username), ...uuidLen, ...Buffer.from(uuid)])
|
||||||
|
client.ws.send(buff)
|
||||||
|
|
||||||
|
if (true) {
|
||||||
|
const [skin, ready] = await Promise.all([awaitPacket(client.ws, EaglerPacketId.SKIN), awaitPacket(client.ws, EaglerPacketId.C_READY)])
|
||||||
|
if (ready[0] != 0x08) {
|
||||||
|
logger.error(`Client [/${client.ip}:${client.remotePort}] sent an unexpected packet! Disconnecting.`)
|
||||||
|
disconnect(client, "Received bad packet", DisconnectReason.UNEXPECTED_PACKET)
|
||||||
|
client.ws.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const buff = Buffer.alloc(1)
|
||||||
|
buff.set([EaglerPacketId.COMPLETE_HANDSHAKE])
|
||||||
|
client.ws.send(buff)
|
||||||
|
|
||||||
|
client.state = State.POST_HANDSHAKE
|
||||||
|
PROXY.playerStats.onlineCount += 1
|
||||||
|
|
||||||
|
logger.info(`Client [/${client.ip}:${client.remotePort}] authenticated as player "${client.username}" and passed handshake. Connecting!`)
|
||||||
|
try { await loginServer(config.server.host, config.server.port, client) }
|
||||||
|
catch (err) {
|
||||||
|
logger.error(`Could not connect to remote server at [/${config.server.host}:${config.server.port}]: ${err}`)
|
||||||
|
disconnect(client, "Failed to connect to server. Please try again later.", DisconnectReason.CUSTOM)
|
||||||
|
client.state = State.DISCONNECTED
|
||||||
|
client.ws.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.error(`Client [/${client.ip}:${client.remotePort}] sent an unexpected packet! Disconnecting.`)
|
||||||
|
disconnect(client, "Received bad packet", DisconnectReason.UNEXPECTED_PACKET)
|
||||||
|
client.ws.close()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user