2022-12-18 05:39:38 -08:00
import WebSocket from "ws"
import {
encodeULEB128 as encodeVarInt ,
decodeULEB128 as decodeVarInt ,
encodeSLEB128 as encodeSVarInt ,
decodeSLEB128 as decodeSVarInt
} from "@thi.ng/leb128"
2022-12-20 11:29:33 -08:00
import { DisconnectReason , EAGLERCRAFT_SKIN_CHANNEL_NAME , EaglerPacketId , MAGIC_BUILTIN_SKIN_BYTES , MAGIC_ENDING_IDENTIFY_S_BYTES } from "./eaglerPacketDef.js"
2022-12-18 05:39:38 -08:00
import { Logger } from "./logger.js"
2022-12-20 11:29:33 -08:00
import { ChannelMessageType , Chat , ChatColor , ProxiedPlayer , State , UUID } from "./types.js"
import { toBuffer , toString as uuidToString } from "uuid-buffer"
2022-12-18 05:39:38 -08:00
import * as mc from "minecraft-protocol"
import { config } from "./config.js"
2022-12-20 11:29:33 -08:00
import sharp from "sharp"
import { createHash , randomUUID } from "crypto"
import { encodeSSkinDl , encodeSSkinDlBuiltin , packChannelMessage , processClientReqPacket } from "./eaglerSkin.js"
2022-12-18 05:39:38 -08:00
const logger = new Logger ( "LoginHandler" )
const USERNAME_REGEX = /[^0-9^a-z^A-Z^_]/gi
export function genUUID ( user : string ) : string {
2022-12-20 11:29:33 -08:00
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 )
2022-12-18 05:39:38 -08:00
}
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." )
}
2022-12-20 11:29:33 -08:00
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 ) {
2022-12-18 05:39:38 -08:00
if ( player . state == State . POST_HANDSHAKE ) {
2022-12-20 11:29:33 -08:00
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 ) ] )
2022-12-18 05:39:38 -08:00
player . ws . send ( d )
player . ws . close ( )
} else {
2022-12-20 11:29:33 -08:00
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 ) ] )
2022-12-18 05:39:38 -08:00
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 ) = > {
2022-12-20 11:29:33 -08:00
let blockedSuccessLogin = false
2022-12-18 05:39:38 -08:00
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
2022-12-20 11:29:33 -08:00
mcClient . on ( 'end' , ( ) = > {
client . ws . close ( )
} )
2022-12-18 05:39:38 -08:00
logger . info ( ` Player ${ client . username } has been connected to the server. ` )
res ( )
} )
mcClient . on ( 'raw' , p = > {
2022-12-20 11:29:33 -08:00
// block the login success packet to fix the bug that prints the UUID in chat on join
2022-12-21 01:44:27 -08:00
if ( p [ 0 ] == 0x03 && mcClient . state == mc . states . LOGIN )
return
2022-12-20 11:29:33 -08:00
if ( p [ 0 ] == 0x02 && blockedSuccessLogin ) {
2022-12-18 05:39:38 -08:00
client . ws . send ( p )
2022-12-20 11:29:33 -08:00
} else if ( p [ 0 ] == 0x02 ) {
blockedSuccessLogin = ! blockedSuccessLogin
2022-12-18 05:39:38 -08:00
} 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. ` )
} )
2022-12-20 11:29:33 -08:00
if ( PROXY . players . size + 1 > PROXY . config . maxPlayers ) {
disconnect ( client , ChatColor . YELLOW + "The proxy is full!" , DisconnectReason . CUSTOM )
2022-12-18 05:39:38 -08:00
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
2022-12-20 11:29:33 -08:00
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 ) ] )
2022-12-18 05:39:38 -08:00
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 ] )
2022-12-20 11:29:33 -08:00
const usernameLen = decodeVarInt ( login . subarray ( Iid [ 1 ] ) )
2022-12-18 05:39:38 -08:00
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 ( )
2022-12-20 11:29:33 -08:00
2022-12-18 05:39:38 -08:00
client . username = loginP . username
client . uuid = genUUID ( client . username )
try { validateUsername ( client . username ) }
catch ( err ) {
2022-12-20 11:29:33 -08:00
disconnect ( client , ChatColor . RED + err . message , DisconnectReason . CUSTOM )
2022-12-18 05:39:38 -08:00
return
}
if ( PROXY . players . has ( client . username ) ) {
2022-12-20 11:29:33 -08:00
disconnect ( client , ` Duplicate username: ${ client . username } . Please connect under a different username. ` , DisconnectReason . CUSTOM )
2022-12-18 05:39:38 -08:00
return
}
PROXY . players . set ( client . username , client )
if ( true ) {
const usernameLen = encodeVarInt ( client . username . length ) , username = client . username
2022-12-20 11:29:33 -08:00
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 ) ] )
2022-12-18 05:39:38 -08:00
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. ` )
2022-12-20 11:29:33 -08:00
disconnect ( client , ChatColor . RED + "Received bad packet." , DisconnectReason . CUSTOM )
2022-12-18 05:39:38 -08:00
client . ws . close ( )
return
}
2022-12-20 11:29:33 -08:00
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
}
}
2022-12-18 05:39:38 -08:00
const buff = Buffer . alloc ( 1 )
buff . set ( [ EaglerPacketId . COMPLETE_HANDSHAKE ] )
client . ws . send ( buff )
client . state = State . POST_HANDSHAKE
2022-12-20 11:29:33 -08:00
logger . info ( ` Client [/ ${ client . ip } : ${ client . remotePort } ] authenticated as player ${ client . username } ( ${ client . uuid } ) and passed handshake. Connecting! ` )
2022-12-18 05:39:38 -08:00
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 } ` )
2022-12-20 11:29:33 -08:00
disconnect ( client , ChatColor . RED + "Failed to connect to server. Please try again later." , DisconnectReason . CUSTOM )
2022-12-18 05:39:38 -08:00
client . state = State . DISCONNECTED
client . ws . close ( )
return
}
2022-12-20 11:29:33 -08:00
if ( client . queuedEaglerSkinPackets . length > 0 ) {
for ( const packet of client . queuedEaglerSkinPackets ) {
processClientReqPacket ( packet , client )
}
}
2022-12-18 05:39:38 -08:00
}
}
} else {
logger . error ( ` Client [/ ${ client . ip } : ${ client . remotePort } ] sent an unexpected packet! Disconnecting. ` )
2022-12-20 11:29:33 -08:00
disconnect ( client , ChatColor . RED + "Received bad packet" , DisconnectReason . CUSTOM )
2022-12-18 05:39:38 -08:00
client . ws . close ( )
}
2022-12-20 11:29:33 -08:00
}
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 ( )
2022-12-18 05:39:38 -08:00
}