first commit

This commit is contained in:
q13x 2022-12-18 05:39:38 -08:00
commit 0bef5be008
26 changed files with 2833 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

49
README.md Normal file
View 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
View File

@ -0,0 +1,2 @@
export class ProxiedPlayer {
}

15
classes.ts Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,7 @@
import { ProxyGlobals } from "./types.js"
declare global {
var PROXY: ProxyGlobals
}
export {}

67
index.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

15
package.json Normal file
View 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
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"module": "esnext",
"esModuleInterop": true,
"target": "es2017",
"moduleResolution": "node",
"sourceMap": false
}
}

6
types.js Normal file
View 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
View 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
View 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
View 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()
}
}