mirror of
https://github.com/WorldEditAxe/eaglerproxy.git
synced 2024-11-09 07:16:05 -08:00
Wipe
This commit is contained in:
parent
a908e16697
commit
9784b72bd0
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,3 +0,0 @@
|
||||||
node_modules
|
|
||||||
insecureSocks
|
|
||||||
build
|
|
61
README.md
61
README.md
|
@ -1,61 +0,0 @@
|
||||||
# EaglercraftX (1.8.9) WebSocket Proxy
|
|
||||||
## This repository has been archived as EaglerX's client and bungee [source code](https://gitlab.com/lax1dude/eaglercraftx-1.8) has been released. Please migrate to it if you haven't already!
|
|
||||||
### Demo: `wss://eaglerx-server.worldeditaxe.repl.co/server` ([EaglerX 1.8.9 client](https://web.arch.lol/mc/1.8.8/) only)
|
|
||||||
![Two EaglerX clients connected to the same server](./assets/demo.png)
|
|
||||||
## What is this?
|
|
||||||
A WebSocket proxy that allows EaglercraftX 1.8 clients to connect to an offline vanilla Minecraft server with (mostly working) Eaglercraft skin support. This is meant to be a replacement for the unreleased official EaglercraftX bungee until it releases. It supports all offline 1.8.9 servers and even online servers when modified!
|
|
||||||
**Note:** Don't expect magic. Some things may or may not work. While the proxy has shown to be stable and working during testing, you may encounter some bugs.
|
|
||||||
## Setup Guide
|
|
||||||
### Prerequisites
|
|
||||||
* Node.js v12 and up
|
|
||||||
* An **OFFLINE** 1.8.9-compatible Minecraft server or proxy
|
|
||||||
### Setup
|
|
||||||
#### If Repl.it is acceptable, fork the [demo](https://replit.com/@WorldEditAxe/eaglerx-server) and connect to it. All proxy files will be under the `proxy` folder.
|
|
||||||
1. Download the latest package available on this repository.
|
|
||||||
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. (unused, required for MOTD)
|
|
||||||
name: "Proxy",
|
|
||||||
// The address to bind the WebSocket server to.
|
|
||||||
bindHost: "0.0.0.0",
|
|
||||||
// The port to bind the WebSocket server to.
|
|
||||||
// Use 80 if security.enabled is false, and 443 when it is true.
|
|
||||||
bindPort: 80,
|
|
||||||
// The max amount of concurrent connections allowed at once. (player cap)
|
|
||||||
maxPlayers: 20,
|
|
||||||
motd: {
|
|
||||||
// The file path that leads to an image.
|
|
||||||
// A 64x64 image is recommended, but any image with a 1:1 ratio will work.
|
|
||||||
iconURL: "./icon.webp",
|
|
||||||
// The first line of the MOTD.
|
|
||||||
l1: "hi",
|
|
||||||
// The second line of the MOTD.
|
|
||||||
l2: "lol"
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
// The hostname/IP of the remote server you want the proxy to connect to.
|
|
||||||
host: "127.0.0.1",
|
|
||||||
// The port of the remote server you want the proxy to connect to.
|
|
||||||
// On most Minecraft server installations, the default port is 25565.
|
|
||||||
port: 25565
|
|
||||||
},
|
|
||||||
security: {
|
|
||||||
// Set this to true to enable encryption (wss://).
|
|
||||||
// If you're using Repl.it, there's no need to enable encryption as it comes by default on all repls.
|
|
||||||
// You will need to obtain certificate files in order to enable encryption.
|
|
||||||
enabled: false,
|
|
||||||
// The private key file provided to you by your certificate authority.
|
|
||||||
key: null,
|
|
||||||
// The certificate file provided to you by your certificate authority.
|
|
||||||
cert: null
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
3. Start your proxy by running `node index.js`.
|
|
||||||
4. Connect to your server. For the server address, use the folllowing format: `ws://<IP>:<port>`. If you are using encryption, replace `ws://` with `wss://`.
|
|
||||||
## Creating Issues
|
|
||||||
When creating a new issue, please:
|
|
||||||
- Refrain from opening duplicate issues. If your issue is already there, please join in on the conversation there instead!
|
|
||||||
- Provide a brief description of the issue, what you expected, and what you got instead. Avoid unhelpful and overly short summaries - we need to know what's wrong!
|
|
BIN
assets/demo.png
BIN
assets/demo.png
Binary file not shown.
Before Width: | Height: | Size: 693 KiB |
26
config.ts
26
config.ts
|
@ -1,26 +0,0 @@
|
||||||
import { Config } from "./types.js";
|
|
||||||
|
|
||||||
export const config: Config = {
|
|
||||||
name: "MinecraftProxy",
|
|
||||||
bindHost: "0.0.0.0",
|
|
||||||
bindPort: 80, // 443 if using TLS
|
|
||||||
maxPlayers: 20,
|
|
||||||
motd: {
|
|
||||||
iconURL: null,
|
|
||||||
l1: "hi",
|
|
||||||
l2: "lol"
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
host: "localhost",
|
|
||||||
port: 25565
|
|
||||||
},
|
|
||||||
security: { // provide path to key & cert if you want to enable encryption/secure websockets
|
|
||||||
enabled: false,
|
|
||||||
key: null,
|
|
||||||
cert: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
|
@ -1,64 +0,0 @@
|
||||||
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 {
|
|
||||||
CUSTOM = 0x8
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: get skin fetching working
|
|
||||||
// Skins are raw Uint8 pixel data arrays.
|
|
||||||
// A pixel is represented by three bytes, each for each primary color: red, green and blue.
|
|
||||||
export type Skin = Buffer
|
|
||||||
// Prefixed skins are prefixed with their dimensions before being
|
|
||||||
export type PrefixedSkin = Buffer
|
|
||||||
export const MAGIC_BUILTIN_SKIN_BYTES = [0x00, 0x05, 0x01, 0x00, 0x00, 0x00]
|
|
||||||
export const MAGIC_ENDING_IDENTIFY_S_BYTES = [0x00, 0x00, 0x00]
|
|
||||||
export const MAGIC_ENDING_S_SKINDL_BI = [0x00, 0x00, 0x00]
|
|
||||||
export const EAGLERCRAFT_SKIN_CHANNEL_NAME = "EAG|Skins-1.8"
|
|
||||||
|
|
||||||
// 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, PrefixedSkin]
|
|
||||||
export type ClientReady = [EaglerPacketId.C_READY]
|
|
||||||
export type Joined = [EaglerPacketId.COMPLETE_HANDSHAKE]
|
|
||||||
export type Disconnect = [EaglerPacketId.DISCONNECT, number, string, DisconnectReason]
|
|
||||||
|
|
||||||
// EAGLERCRAFT SKIN PROTOCOL
|
|
||||||
// All Eaglercraft skin networking is done through plugin channels under the channel name EAG|Skins-1.8.
|
|
||||||
// Below are some packet defs.
|
|
||||||
|
|
||||||
export enum EaglerSkinPacketId {
|
|
||||||
C_FETCH_SKIN = 0x03,
|
|
||||||
S_SKIN_DL_BI = 0x04,
|
|
||||||
S_SKIN_DL = 0x05,
|
|
||||||
C_REQ_SKIN = 0x06
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SkinId = number
|
|
||||||
|
|
||||||
// A Vanilla plugin channel message packet.
|
|
||||||
// Every message is encapsulated through one of these packets.
|
|
||||||
export type CBaseChannelMessage = [0x17, number, string, Buffer]
|
|
||||||
export type SBaseChannelMessage = [0x3f, number, string, Buffer]
|
|
||||||
|
|
||||||
export type CFetchSkin = [EaglerSkinPacketId.C_FETCH_SKIN, UUID]
|
|
||||||
export type SSkinDlBi = [EaglerSkinPacketId.S_SKIN_DL_BI, UUID, ...typeof MAGIC_ENDING_S_SKINDL_BI, SkinId]
|
|
||||||
export type SSkinDl = [EaglerSkinPacketId.S_SKIN_DL, UUID, number, Skin]
|
|
||||||
export type CSkinReq = [EaglerSkinPacketId.C_REQ_SKIN, UUID, 0x00, number, string]
|
|
209
eaglerSkin.ts
209
eaglerSkin.ts
|
@ -1,209 +0,0 @@
|
||||||
import {
|
|
||||||
encodeULEB128 as encodeVarInt,
|
|
||||||
decodeULEB128 as decodeVarInt,
|
|
||||||
encodeSLEB128 as encodeSVarInt,
|
|
||||||
decodeSLEB128 as decodeSVarInt
|
|
||||||
} from "@thi.ng/leb128"
|
|
||||||
import { DecodedCFetchSkin, DecodedSSkinDl, DecodedCSkinReq, DecodedSSkinFetchBuiltin, UnpackedChannelMessage, ProxiedPlayer, ChannelMessageType } from "./types.js"
|
|
||||||
import uuidBuffer from "uuid-buffer"
|
|
||||||
import { bufferizeUUID } from "./utils.js"
|
|
||||||
import { EAGLERCRAFT_SKIN_CHANNEL_NAME, EaglerSkinPacketId, MAGIC_ENDING_S_SKINDL_BI } from "./eaglerPacketDef.js"
|
|
||||||
import sharp from "sharp"
|
|
||||||
import request from "request"
|
|
||||||
|
|
||||||
export function unpackChannelMessage(message: Buffer): UnpackedChannelMessage {
|
|
||||||
if (message[0] != 0x17 && message[0] != 0x3f)
|
|
||||||
throw new Error("Invalid packet ID detected")
|
|
||||||
const ret: UnpackedChannelMessage = {
|
|
||||||
channel: null,
|
|
||||||
data: null,
|
|
||||||
type: null
|
|
||||||
}
|
|
||||||
const Iid = decodeVarInt(message)
|
|
||||||
const channelNameLen = decodeVarInt(message.subarray(Iid[1])), channelName = message.subarray(Iid[1] + channelNameLen[1], Iid[1] + channelNameLen[1] + Number(channelNameLen[0])).toString()
|
|
||||||
ret.type = Number(Iid[0])
|
|
||||||
ret.channel = channelName
|
|
||||||
ret.data = message.subarray(Iid[1] + channelNameLen[1] + channelName.length)
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
export function packChannelMessage(channel: string, type: ChannelMessageType, message: Buffer): Buffer {
|
|
||||||
const channelNameLen = encodeVarInt(channel.length), buff = Buffer.alloc(1 + channelNameLen.length + channel.length + message.length)
|
|
||||||
buff.set([type,...channelNameLen, ...Buffer.from(channel), ...message])
|
|
||||||
return buff
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeCFetchSkin(message: Buffer): DecodedCFetchSkin {
|
|
||||||
const ret: DecodedCFetchSkin = {
|
|
||||||
id: null,
|
|
||||||
uuid: null
|
|
||||||
}
|
|
||||||
const Iid = decodeVarInt(message), uuid = uuidBuffer.toString(message.subarray(Iid[1]))
|
|
||||||
ret.id = Number(Iid[0])
|
|
||||||
ret.uuid = uuid
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
export function encodeFetchSkin(uuid: string | Buffer): Buffer {
|
|
||||||
uuid = typeof uuid == 'string' ? bufferizeUUID(uuid) : uuid
|
|
||||||
const buff = Buffer.alloc(1 + uuid.length)
|
|
||||||
buff.set([EaglerSkinPacketId.C_FETCH_SKIN, ...uuid])
|
|
||||||
return buff
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeSSkinDlBuiltin(message: Buffer): DecodedSSkinFetchBuiltin {
|
|
||||||
const ret: DecodedSSkinFetchBuiltin = {
|
|
||||||
id: null,
|
|
||||||
uuid: null,
|
|
||||||
skinId: null
|
|
||||||
}
|
|
||||||
const Iid = decodeVarInt(message), uuid = uuidBuffer.toString(message.subarray(Iid[1], Iid[1] + 16))
|
|
||||||
ret.id = Number(Iid[0])
|
|
||||||
ret.uuid = uuid
|
|
||||||
const skinId = decodeVarInt(message.subarray(Iid[1] + 16 + 3))
|
|
||||||
ret.skinId = Number(skinId)
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
export function encodeSSkinDlBuiltin(uuid: string | Buffer, skinId: number): Buffer {
|
|
||||||
uuid = typeof uuid == 'string' ? bufferizeUUID(uuid) : uuid
|
|
||||||
const encSkinId = encodeVarInt(skinId)
|
|
||||||
const buff = Buffer.alloc(1 + 16 + 3 + encSkinId.length)
|
|
||||||
buff.set([EaglerSkinPacketId.S_SKIN_DL_BI, ...uuid, ...Buffer.from(MAGIC_ENDING_S_SKINDL_BI), encSkinId[0]])
|
|
||||||
return buff
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeSSkinDl(message: Buffer): DecodedSSkinDl {
|
|
||||||
const ret: DecodedSSkinDl = {
|
|
||||||
id: null,
|
|
||||||
uuid: null,
|
|
||||||
skin: null
|
|
||||||
}
|
|
||||||
const Iid = decodeVarInt(message), uuid = uuidBuffer.toString(message.subarray(Iid[1], Iid[1] + 16))
|
|
||||||
ret.id = Number(Iid[0])
|
|
||||||
ret.uuid = uuid
|
|
||||||
const skin = message.subarray(Iid[1] + 16)
|
|
||||||
ret.skin = skin
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
export function encodeSSkinDl(uuid: string | Buffer, skin: Buffer, isFetched: boolean): Buffer {
|
|
||||||
uuid = typeof uuid == 'string' ? bufferizeUUID(uuid) : uuid
|
|
||||||
// eaglercraft clients always expect a 16385 byte long byte array for the skin
|
|
||||||
if (!isFetched) skin = skin.length !== 16384 ? skin.length < 16384 ? Buffer.concat([Buffer.alloc(16384 - skin.length), skin]) : skin.subarray(16383) : skin
|
|
||||||
else skin = skin.length !== 16384 ? skin.length < 16384 ? Buffer.concat([skin, Buffer.alloc(16384 - skin.length)]) : skin.subarray(16383) : skin
|
|
||||||
const buff = Buffer.alloc(1 + 16 + 1 + skin.length)
|
|
||||||
if (!isFetched) buff.set([EaglerSkinPacketId.S_SKIN_DL,...uuid, 0xff,...skin])
|
|
||||||
else buff.set([EaglerSkinPacketId.S_SKIN_DL, ...uuid, 0x00, ...skin])
|
|
||||||
return buff
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeCSkinReq(message: Buffer): DecodedCSkinReq {
|
|
||||||
const ret: DecodedCSkinReq = {
|
|
||||||
id: null,
|
|
||||||
uuid: null,
|
|
||||||
url: null
|
|
||||||
}
|
|
||||||
const Iid = decodeVarInt(message), uuid = uuidBuffer.toString(message.subarray(Iid[1], Iid[1] + 16))
|
|
||||||
ret.id = Number(Iid[0])
|
|
||||||
ret.uuid = uuid
|
|
||||||
const urlLen = decodeVarInt(message.subarray(Iid[1] + 16 + 1)), url = message.subarray(Iid[1] + 16 + 1 + urlLen[1]).toString()
|
|
||||||
ret.url = url
|
|
||||||
return ret
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function encodeCSkinReq(uuid: string | Buffer, url: string): Buffer {
|
|
||||||
uuid = typeof uuid == 'string' ? bufferizeUUID(uuid) : uuid
|
|
||||||
const urlLen = encodeVarInt(url.length), eUrl = Buffer.from(url)
|
|
||||||
const buff = Buffer.alloc(1 + 1 + 16 + urlLen.length + eUrl.length)
|
|
||||||
buff.set([EaglerSkinPacketId.C_REQ_SKIN, ...uuid, 0x00, ...urlLen, ...eUrl])
|
|
||||||
return buff
|
|
||||||
}
|
|
||||||
|
|
||||||
const SEG_SIZE = 3
|
|
||||||
|
|
||||||
function invert(buff: Buffer): Buffer {
|
|
||||||
let buffers: Buffer[] = [], i = 0
|
|
||||||
const newBuffer = Buffer.alloc(buff.length)
|
|
||||||
while (true) {
|
|
||||||
if (i >= buff.length)
|
|
||||||
break
|
|
||||||
newBuffer.set(buff.subarray(i, i + 4).reverse(), i)
|
|
||||||
i += 4
|
|
||||||
}
|
|
||||||
return newBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
async function genRGBAEagler(buff: Buffer): Promise<Buffer> {
|
|
||||||
const r = await sharp(buff).extractChannel('red').raw({ depth: 'uchar' }).toBuffer()
|
|
||||||
const g = await sharp(buff).extractChannel('green').raw({ depth: 'uchar' }).toBuffer()
|
|
||||||
const b = await sharp(buff).extractChannel('blue').raw({ depth: 'uchar' }).toBuffer()
|
|
||||||
const a = await sharp(buff).ensureAlpha().extractChannel(3).toColorspace('b-w').raw({ depth: 'uchar' }).toBuffer()
|
|
||||||
const newBuff = Buffer.alloc(64 ** 2 * 4)
|
|
||||||
for (let i = 1; i < 64 ** 2; i++) {
|
|
||||||
const bytePos = i * 4
|
|
||||||
newBuff[bytePos] = a[i]
|
|
||||||
newBuff[bytePos + 1] = b[i]
|
|
||||||
newBuff[bytePos + 2] = g[i]
|
|
||||||
newBuff[bytePos + 3] = r[i]
|
|
||||||
}
|
|
||||||
return newBuff
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toEaglerSkin(buff: Buffer): Promise<Buffer> {
|
|
||||||
return genRGBAEagler(buff)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchSkin(url: string, process?: boolean): Promise<Buffer> {
|
|
||||||
return new Promise<Buffer>((res, rej) => {
|
|
||||||
let body = []
|
|
||||||
request({ url: url, encoding: null }, (err, response, body) => {
|
|
||||||
if (err) {
|
|
||||||
rej(err)
|
|
||||||
} else {
|
|
||||||
toEaglerSkin(body).then(buff => res(buff))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPlayerWithUUID(uuid: string): ProxiedPlayer {
|
|
||||||
for (const [username, plr] of PROXY.players) {
|
|
||||||
if (plr.uuid == uuid)
|
|
||||||
return plr
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function processClientReqPacket(decodedMessage: UnpackedChannelMessage, client: ProxiedPlayer) {
|
|
||||||
if (decodedMessage.type == ChannelMessageType.SERVER)
|
|
||||||
throw new Error("Server message was passed to client message handler")
|
|
||||||
switch(decodedMessage.data[0] as EaglerSkinPacketId) {
|
|
||||||
default:
|
|
||||||
throw new Error("Unknown operation")
|
|
||||||
break
|
|
||||||
case EaglerSkinPacketId.C_REQ_SKIN:
|
|
||||||
const reqSkinPck = decodeCSkinReq(decodedMessage.data)
|
|
||||||
if (getPlayerWithUUID(reqSkinPck.uuid)) {
|
|
||||||
client.ws.send(packChannelMessage(EAGLERCRAFT_SKIN_CHANNEL_NAME, ChannelMessageType.SERVER, encodeSSkinDl(reqSkinPck.uuid, getPlayerWithUUID(reqSkinPck.uuid).skin.customSkin, false)))
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const skin = await fetchSkin(reqSkinPck.url)
|
|
||||||
const resPck = encodeSSkinDl(reqSkinPck.uuid, skin, true)
|
|
||||||
client.ws.send(packChannelMessage(EAGLERCRAFT_SKIN_CHANNEL_NAME, ChannelMessageType.SERVER, resPck))
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case EaglerSkinPacketId.C_FETCH_SKIN:
|
|
||||||
const fetchSkinPlrPck = decodeCFetchSkin(decodedMessage.data)
|
|
||||||
const plr = getPlayerWithUUID(fetchSkinPlrPck.uuid)
|
|
||||||
if (plr) {
|
|
||||||
if (plr.skin.type == 'BUILTIN') {
|
|
||||||
client.ws.send(packChannelMessage(EAGLERCRAFT_SKIN_CHANNEL_NAME, ChannelMessageType.SERVER, encodeSSkinDlBuiltin(plr.uuid, plr.skin.skinId)))
|
|
||||||
} else {
|
|
||||||
client.ws.send(packChannelMessage(EAGLERCRAFT_SKIN_CHANNEL_NAME, ChannelMessageType.SERVER, encodeSSkinDl(plr.uuid, plr.skin.customSkin, false)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
7
globals.d.ts
vendored
7
globals.d.ts
vendored
|
@ -1,7 +0,0 @@
|
||||||
import { ProxyGlobals } from "./types.js"
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var PROXY: ProxyGlobals
|
|
||||||
}
|
|
||||||
|
|
||||||
export {}
|
|
95
index.ts
95
index.ts
|
@ -1,95 +0,0 @@
|
||||||
import { readFileSync } from "fs";
|
|
||||||
import * as http from "http"
|
|
||||||
import * as https from "https"
|
|
||||||
import { WebSocketServer } from "ws";
|
|
||||||
import { BRANDING, config, NETWORK_VERSION, VERSION } from "./config.js";
|
|
||||||
import { handlePacket } from "./listener.js";
|
|
||||||
import { Logger } from "./logger.js";
|
|
||||||
import { disconnect, generateMOTDImage } from "./utils.js";
|
|
||||||
import { ChatColor, ProxiedPlayer, 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: config.motd.iconURL ? await generateMOTDImage(readFileSync(config.motd.iconURL)) : null,
|
|
||||||
motd: [config.motd.l1, config.motd.l2]
|
|
||||||
},
|
|
||||||
|
|
||||||
wsServer: null,
|
|
||||||
players: new Map(),
|
|
||||||
logger: logger,
|
|
||||||
config: config
|
|
||||||
}
|
|
||||||
|
|
||||||
let server: WebSocketServer
|
|
||||||
|
|
||||||
if (PROXY.config.security.enabled) {
|
|
||||||
logger.info(`Starting SECURE WebSocket proxy on port ${config.bindPort}...`)
|
|
||||||
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.bindPort, config.bindHost)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
logger.info(`Starting INSECURE WebSocket proxy on port ${config.bindPort}...`)
|
|
||||||
server = new WebSocketServer({
|
|
||||||
port: config.bindPort,
|
|
||||||
host: config.bindHost
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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.bindPort})`)
|
|
||||||
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
|
|
||||||
plr.queuedEaglerSkinPackets = []
|
|
||||||
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.bindPort}!`)
|
|
||||||
})
|
|
||||||
|
|
||||||
process.on('uncaughtException', err => {
|
|
||||||
logger.error(`An uncaught exception was caught! Exception: ${err.stack ?? err}`)
|
|
||||||
})
|
|
||||||
process.on('unhandledRejection', err => {
|
|
||||||
logger.error(`An unhandled promise rejection was caught! Rejection: ${(err != null ? (err as any).stack : err) ?? err}`)
|
|
||||||
})
|
|
||||||
process.on('SIGTERM', () => {
|
|
||||||
logger.info("Cleaning up before exiting...")
|
|
||||||
for (const [username, plr] of PROXY.players) {
|
|
||||||
if (plr.remoteConnection != null) plr.remoteConnection.end()
|
|
||||||
disconnect(plr, ChatColor.YELLOW + "Proxy is shutting down.")
|
|
||||||
}
|
|
||||||
process.exit(0)
|
|
||||||
})
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
logger.info("Cleaning up before exiting...")
|
|
||||||
for (const [username, plr] of PROXY.players) {
|
|
||||||
if (plr.remoteConnection != null) plr.remoteConnection.end()
|
|
||||||
disconnect(plr, ChatColor.YELLOW + "Proxy is shutting down.")
|
|
||||||
}
|
|
||||||
process.exit(0)
|
|
||||||
})
|
|
40
listener.ts
40
listener.ts
|
@ -1,40 +0,0 @@
|
||||||
import { EAGLERCRAFT_SKIN_CHANNEL_NAME } from "./eaglerPacketDef.js";
|
|
||||||
import { processClientReqPacket, unpackChannelMessage } from "./eaglerSkin.js";
|
|
||||||
import { Logger } from "./logger.js";
|
|
||||||
import { State, ProxiedPlayer } from "./types.js";
|
|
||||||
import { doHandshake, handleMotd } 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)
|
|
||||||
.catch(err => {
|
|
||||||
logger.warn(`Error occurred whilst handling handshake! Error: ${err.stack ?? err}`)
|
|
||||||
})
|
|
||||||
} else if (!(client as any)._handled && packet[0] == 0x17) {
|
|
||||||
const decoded = unpackChannelMessage(packet)
|
|
||||||
if (decoded.channel == EAGLERCRAFT_SKIN_CHANNEL_NAME) {
|
|
||||||
client.queuedEaglerSkinPackets.push(decoded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (client.state == State.POST_HANDSHAKE) {
|
|
||||||
if (!client.remoteConnection || client.remoteConnection.socket.closed) {
|
|
||||||
logger.warn(`Received packet from player ${client.username} that is marked as post handshake, but is disconnected from the game server? Disconnecting due to illegal state.`)
|
|
||||||
client.ws.close()
|
|
||||||
} else {
|
|
||||||
if (packet[0] == 0x17) {
|
|
||||||
const decoded = unpackChannelMessage(packet)
|
|
||||||
if (decoded.channel == EAGLERCRAFT_SKIN_CHANNEL_NAME) {
|
|
||||||
processClientReqPacket(decoded, client)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
client.remoteConnection.writeRaw(packet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
61
logger.ts
61
logger.ts
|
@ -1,61 +0,0 @@
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
3200
package-lock.json
generated
3200
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
|
@ -1,41 +0,0 @@
|
||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"@thi.ng/leb128": "^3.0.1",
|
|
||||||
"@types/axios": "^0.14.0",
|
|
||||||
"@types/node": "^18.11.16",
|
|
||||||
"@types/request": "^2.48.8",
|
|
||||||
"@types/sharp": "^0.31.0",
|
|
||||||
"@types/uuid": "^9.0.0",
|
|
||||||
"@types/ws": "^8.5.3",
|
|
||||||
"chalk": "^5.2.0",
|
|
||||||
"minecraft-protocol": "^1.36.2",
|
|
||||||
"request": "^2.88.2",
|
|
||||||
"sharp": "^0.31.2",
|
|
||||||
"tsc": "^2.0.4",
|
|
||||||
"uuid-buffer": "^1.0.3",
|
|
||||||
"ws": "^8.11.0"
|
|
||||||
},
|
|
||||||
"type": "module",
|
|
||||||
"name": "eaglerxbungee-reimpl",
|
|
||||||
"description": "Play on offline vanilla Minecraft 1.8.9 servers through EaglerX.",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "index.js",
|
|
||||||
"devDependencies": {},
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/WorldEditAxe/eaglerxbungee-reimpl.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"eaglercraftx",
|
|
||||||
"eaglercraftx-bungee"
|
|
||||||
],
|
|
||||||
"author": "WorldEditAxe",
|
|
||||||
"license": "MIT",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/WorldEditAxe/eaglerxbungee-reimpl/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/WorldEditAxe/eaglerxbungee-reimpl#readme"
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "esnext",
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"target": "es2017",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"sourceMap": false,
|
|
||||||
"outDir": "build"
|
|
||||||
}
|
|
||||||
}
|
|
159
types.ts
159
types.ts
|
@ -1,159 +0,0 @@
|
||||||
import { randomUUID } from "crypto"
|
|
||||||
import { Client } from "minecraft-protocol"
|
|
||||||
import { WebSocketServer, WebSocket } from "ws"
|
|
||||||
import { BRANDING, VERSION, NETWORK_VERSION } from "./config.js"
|
|
||||||
import { EaglerSkinPacketId, SkinId } from "./eaglerPacketDef.js"
|
|
||||||
import { Logger } from "./logger.js"
|
|
||||||
|
|
||||||
export type UUID = ReturnType<typeof randomUUID>
|
|
||||||
|
|
||||||
export enum State {
|
|
||||||
PRE_HANDSHAKE,
|
|
||||||
POST_HANDSHAKE,
|
|
||||||
DISCONNECTED
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MOTD = {
|
|
||||||
icon?: Buffer, // 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,
|
|
||||||
|
|
||||||
wsServer: WebSocketServer,
|
|
||||||
players: Map<string, ProxiedPlayer>,
|
|
||||||
logger: Logger,
|
|
||||||
config: Config
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Config = {
|
|
||||||
name: string,
|
|
||||||
bindPort: number,
|
|
||||||
bindHost: string,
|
|
||||||
maxPlayers: number,
|
|
||||||
motd: {
|
|
||||||
iconURL?: string,
|
|
||||||
l1: string,
|
|
||||||
l2: string
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
host: string,
|
|
||||||
port: number
|
|
||||||
},
|
|
||||||
security: {
|
|
||||||
enabled: boolean
|
|
||||||
key: string,
|
|
||||||
cert: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ChatColor {
|
|
||||||
BLACK = "§0",
|
|
||||||
DARK_BLUE = "§1",
|
|
||||||
DARK_GREEN = "§2",
|
|
||||||
DARK_CYAN = "§3",
|
|
||||||
DARK_RED = "§4",
|
|
||||||
PURPLE = "§5",
|
|
||||||
GOLD = "§6",
|
|
||||||
GRAY = "§7",
|
|
||||||
DARK_GRAY = "§8",
|
|
||||||
BLUE = "§9",
|
|
||||||
BRIGHT_GREEN = "§a",
|
|
||||||
CYAN = "§b",
|
|
||||||
RED = "§c",
|
|
||||||
PINK = "§d",
|
|
||||||
YELLOW = "§e",
|
|
||||||
WHITE = "§f",
|
|
||||||
// text styling
|
|
||||||
OBFUSCATED = '§k',
|
|
||||||
BOLD = '§l',
|
|
||||||
STRIKETHROUGH = '§m',
|
|
||||||
UNDERLINED = '§n',
|
|
||||||
ITALIC = '§o',
|
|
||||||
RESET = '§r'
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ChatExtra = {
|
|
||||||
text: string,
|
|
||||||
bold?: boolean,
|
|
||||||
italic?: boolean,
|
|
||||||
underlined?: boolean,
|
|
||||||
strikethrough?: boolean,
|
|
||||||
obfuscated?: boolean,
|
|
||||||
color?: ChatColor | 'reset'
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Chat = {
|
|
||||||
text?: string,
|
|
||||||
bold?: boolean,
|
|
||||||
italic?: boolean,
|
|
||||||
underlined?: boolean,
|
|
||||||
strikethrough?: boolean,
|
|
||||||
obfuscated?: boolean,
|
|
||||||
color?: ChatColor | 'reset',
|
|
||||||
extra?: ChatExtra[]
|
|
||||||
}
|
|
||||||
|
|
||||||
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 skin: {
|
|
||||||
type: "CUSTOM" | "BUILTIN",
|
|
||||||
skinId?: number,
|
|
||||||
customSkin?: Buffer
|
|
||||||
}
|
|
||||||
public queuedEaglerSkinPackets: UnpackedChannelMessage[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ChannelMessageType {
|
|
||||||
CLIENT = 0x17,
|
|
||||||
SERVER = 0x3f
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UnpackedChannelMessage = {
|
|
||||||
channel: string,
|
|
||||||
data: Buffer,
|
|
||||||
type: ChannelMessageType
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DecodedCFetchSkin = {
|
|
||||||
id: EaglerSkinPacketId.C_FETCH_SKIN,
|
|
||||||
uuid: UUID
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DecodedSSkinFetchBuiltin = {
|
|
||||||
id: EaglerSkinPacketId.S_SKIN_DL_BI,
|
|
||||||
uuid: UUID,
|
|
||||||
skinId: SkinId
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DecodedSSkinDl = {
|
|
||||||
id: EaglerSkinPacketId.S_SKIN_DL,
|
|
||||||
uuid: UUID,
|
|
||||||
skin: Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DecodedCSkinReq = {
|
|
||||||
id: EaglerSkinPacketId.C_REQ_SKIN,
|
|
||||||
uuid: UUID,
|
|
||||||
url: string
|
|
||||||
}
|
|
382
utils.ts
382
utils.ts
|
@ -1,382 +0,0 @@
|
||||||
import WebSocket from "ws"
|
|
||||||
import {
|
|
||||||
encodeULEB128 as encodeVarInt,
|
|
||||||
decodeULEB128 as decodeVarInt,
|
|
||||||
encodeSLEB128 as encodeSVarInt,
|
|
||||||
decodeSLEB128 as decodeSVarInt
|
|
||||||
} from "@thi.ng/leb128"
|
|
||||||
import { DisconnectReason, EAGLERCRAFT_SKIN_CHANNEL_NAME, EaglerPacketId, MAGIC_BUILTIN_SKIN_BYTES, MAGIC_ENDING_IDENTIFY_S_BYTES } from "./eaglerPacketDef.js"
|
|
||||||
import { Logger } from "./logger.js"
|
|
||||||
import { ChannelMessageType, Chat, ChatColor, ProxiedPlayer, State, UUID } from "./types.js"
|
|
||||||
import { toBuffer, toString as uuidToString } from "uuid-buffer"
|
|
||||||
import * as mc from "minecraft-protocol"
|
|
||||||
import { config } from "./config.js"
|
|
||||||
import sharp from "sharp"
|
|
||||||
import { createHash, randomUUID } from "crypto"
|
|
||||||
import { encodeSSkinDl, encodeSSkinDlBuiltin, packChannelMessage, processClientReqPacket } from "./eaglerSkin.js"
|
|
||||||
|
|
||||||
const logger = new Logger("LoginHandler")
|
|
||||||
const USERNAME_REGEX = /[^0-9^a-z^A-Z^_]/gi
|
|
||||||
|
|
||||||
export function genUUID(user: string): string {
|
|
||||||
const str = `OfflinePlayer:${user}`
|
|
||||||
let md5Bytes = createHash('md5').update(str).digest()
|
|
||||||
md5Bytes[6] &= 0x0f; /* clear version */
|
|
||||||
md5Bytes[6] |= 0x30; /* set to version 3 */
|
|
||||||
md5Bytes[8] &= 0x3f; /* clear variant */
|
|
||||||
md5Bytes[8] |= 0x80; /* set to IETF variant */
|
|
||||||
return uuidToString(md5Bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 chatToPlainString(chat: Chat): string {
|
|
||||||
let ret = ''
|
|
||||||
if (chat.text != null) ret += chat.text
|
|
||||||
if (chat.extra != null) {
|
|
||||||
chat.extra.forEach(extra => {
|
|
||||||
ret += extra.text
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
export function disconnect(player: ProxiedPlayer, message: Chat | string, code?: DisconnectReason) {
|
|
||||||
if (player.state == State.POST_HANDSHAKE) {
|
|
||||||
const message_m = (typeof message == 'string' ? JSON.stringify({ text: message }) : JSON.stringify(message)) + 0x0
|
|
||||||
const messageLen = encodeVarInt(message_m.length)
|
|
||||||
const d = Buffer.alloc([0x40, ...messageLen, ...Buffer.from(message_m)].length)
|
|
||||||
d.set([0x40, ...messageLen, ...Buffer.from(message_m)])
|
|
||||||
player.ws.send(d)
|
|
||||||
player.ws.close()
|
|
||||||
} else {
|
|
||||||
const message_m = (typeof message == 'string' ? message : chatToPlainString(message))
|
|
||||||
const messageLen = encodeVarInt(message_m.length), codeEnc = encodeVarInt(code ?? DisconnectReason.CUSTOM)
|
|
||||||
const d = Buffer.alloc([0xff,...codeEnc, ...messageLen, ...Buffer.from(message_m)].length)
|
|
||||||
d.set([0xff,...codeEnc, ...messageLen, ...Buffer.from(message_m)])
|
|
||||||
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 blockedSuccessLogin = 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('connect', () => {
|
|
||||||
client.remoteConnection = mcClient
|
|
||||||
mcClient.on('end', () => {
|
|
||||||
client.ws.close()
|
|
||||||
})
|
|
||||||
mcClient.on('kick_disconnect', kick => {
|
|
||||||
logger.warn(`Player ${client.username} was kicked from the server! Reason: ${kick.reason}`)
|
|
||||||
})
|
|
||||||
logger.info(`Player ${client.username} has been connected to the server.`)
|
|
||||||
res()
|
|
||||||
})
|
|
||||||
mcClient.on('raw', p => {
|
|
||||||
// block the login success packet to fix the bug that prints the UUID in chat on join
|
|
||||||
if (p[0] == 0x03 && mcClient.state == mc.states.LOGIN)
|
|
||||||
return
|
|
||||||
if (p[0] == 0x02 && blockedSuccessLogin) {
|
|
||||||
client.ws.send(p)
|
|
||||||
} else if (p[0] == 0x02) {
|
|
||||||
blockedSuccessLogin = !blockedSuccessLogin
|
|
||||||
} 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)
|
|
||||||
logger.info(`Client [/${client.ip}:${client.remotePort}]${client.username ? ` (${client.username})` : ""} disconnected from the server.`)
|
|
||||||
})
|
|
||||||
if (PROXY.players.size + 1 > PROXY.config.maxPlayers) {
|
|
||||||
disconnect(client, ChatColor.YELLOW + "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_IDENTIFY_S_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_IDENTIFY_S_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(Iid[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, ChatColor.RED + err.message, DisconnectReason.CUSTOM)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (PROXY.players.has(client.username)) {
|
|
||||||
disconnect(client, `Duplicate username: ${client.username}. Please connect under a different username.`, DisconnectReason.CUSTOM)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
PROXY.players.set(client.username, client)
|
|
||||||
if (true) {
|
|
||||||
const usernameLen = encodeVarInt(client.username.length), username = client.username
|
|
||||||
const uuid = bufferizeUUID(client.uuid)
|
|
||||||
const buff = Buffer.alloc(1 + usernameLen.length + username.length + uuid.length)
|
|
||||||
buff.set([EaglerPacketId.LOGIN_ACK, ...usernameLen, ...Buffer.from(username), ...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, ChatColor.RED + "Received bad packet.", DisconnectReason.CUSTOM)
|
|
||||||
client.ws.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (true) {
|
|
||||||
const skinP = {
|
|
||||||
id: null,
|
|
||||||
skinVerLen: null,
|
|
||||||
skinVer: null, // skin_v1
|
|
||||||
type: null, // CUSTOM or BUILTIN
|
|
||||||
skinId: null,
|
|
||||||
skinDimens: null,
|
|
||||||
skin: null
|
|
||||||
}
|
|
||||||
const Iid = decodeVarInt(skin)
|
|
||||||
skinP.id = Number(Iid[0])
|
|
||||||
const skinVerLen = decodeVarInt(skin.subarray(Iid[1]))
|
|
||||||
skinP.skinVerLen = Number(skinVerLen[0])
|
|
||||||
skinP.skinVer = skin.subarray(Iid[1] + skinVerLen[1], Iid[1] + skinVerLen[1] + skinP.skinVerLen).toString()
|
|
||||||
const typebuff = skin.subarray(Iid[1] + skinVerLen[1] + skinP.skinVerLen, Iid[1] + skinVerLen[1] + skinP.skinVerLen + MAGIC_BUILTIN_SKIN_BYTES.length)
|
|
||||||
if (typebuff.compare(Buffer.from(MAGIC_BUILTIN_SKIN_BYTES)) == 0) {
|
|
||||||
skinP.type = "BUILTIN"
|
|
||||||
skinP.skinId = Number(decodeVarInt(skin.subarray(Iid[1] + skinVerLen[1] + skinP.skinVerLen + MAGIC_BUILTIN_SKIN_BYTES.length))[0])
|
|
||||||
} else {
|
|
||||||
skinP.type = "CUSTOM"
|
|
||||||
const skinSqrt = decodeVarInt(skin.subarray(Iid[1] + skinVerLen[1] + skinP.skinVerLen)), dimensions = Number(skinSqrt[0]) * Number(skinSqrt[0]) * 3
|
|
||||||
skinP.skinDimens = dimensions
|
|
||||||
skinP.skin = skin.subarray(Iid[1] + skinVerLen[1] + skinP.skinVerLen + skinSqrt[1] + 16)
|
|
||||||
if (skinP.skin.length > 16385) {
|
|
||||||
disconnect(client, ChatColor.RED + "Invalid skin received!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
client.skin = {
|
|
||||||
type: skinP.type,
|
|
||||||
skinId: skinP.skinId,
|
|
||||||
customSkin: skinP.skin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const buff = Buffer.alloc(1)
|
|
||||||
buff.set([EaglerPacketId.COMPLETE_HANDSHAKE])
|
|
||||||
client.ws.send(buff)
|
|
||||||
|
|
||||||
client.state = State.POST_HANDSHAKE
|
|
||||||
|
|
||||||
logger.info(`Client [/${client.ip}:${client.remotePort}] authenticated as player ${client.username} (${client.uuid}) 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, ChatColor.RED + "Failed to connect to server. Please try again later.", DisconnectReason.CUSTOM)
|
|
||||||
client.state = State.DISCONNECTED
|
|
||||||
client.ws.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client.queuedEaglerSkinPackets.length > 0) {
|
|
||||||
for (const packet of client.queuedEaglerSkinPackets) {
|
|
||||||
processClientReqPacket(packet, client)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.error(`Client [/${client.ip}:${client.remotePort}] sent an unexpected packet! Disconnecting.`)
|
|
||||||
disconnect(client, ChatColor.RED + "Received bad packet", DisconnectReason.CUSTOM)
|
|
||||||
client.ws.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = Buffer
|
|
||||||
|
|
||||||
const ICON_SQRT = 64
|
|
||||||
|
|
||||||
export function generateMOTDImage(file: Buffer): Promise<MotdServerLogo> {
|
|
||||||
return new Promise<MotdServerLogo>((res, rej) => {
|
|
||||||
sharp(file)
|
|
||||||
.resize(ICON_SQRT, ICON_SQRT, {
|
|
||||||
kernel: 'nearest'
|
|
||||||
})
|
|
||||||
.raw({
|
|
||||||
depth: 'uchar'
|
|
||||||
})
|
|
||||||
.toBuffer()
|
|
||||||
.then(buff => {
|
|
||||||
for (const pixel of buff) {
|
|
||||||
if ((pixel & 0xFFFFFF) == 0) {
|
|
||||||
buff[buff.indexOf(pixel)] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res(buff)
|
|
||||||
})
|
|
||||||
.catch(rej)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleMotd(player: Partial<ProxiedPlayer>) {
|
|
||||||
const names = []
|
|
||||||
for (const [username, player] of PROXY.players) {
|
|
||||||
if (names.length > 0) {
|
|
||||||
names.push(`${ChatColor.GRAY}${ChatColor.ITALIC}(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.config.maxPlayers,
|
|
||||||
motd: PROXY.MOTD.motd,
|
|
||||||
online: PROXY.players.size,
|
|
||||||
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()
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user