ClientProxy.ts
1 import { LoginClientConnection } from './connection/LoginClientConnection' 2 import { LoginServerConnection } from './connection/LoginServerConnection' 3 import { Socket } from 'net' 4 import { LoginEventIndicator } from '../enum/LoginEventIndicators' 5 import { GameClientConnection } from './connection/GameClientConnection' 6 import { GameServerConnection } from './connection/GameServerConnection' 7 import { GameEventIndicator } from '../enum/GameEventIndicator' 8 import { ServerLog } from '../logger/Logger' 9 import { ClientCache } from '../cache/ClientCache' 10 import _ from 'lodash' 11 import { RequestProtocolVersion, ProtocolVersionEvent } from '../packets/game/receive/RequestProtocolVersion' 12 import chalk from 'chalk' 13 import { ListenerCache } from '../cache/ListenerCache' 14 import { PlayerSay, PlayerSayEvent } from '../packets/game/receive/PlayerSay' 15 import { ProxyCommands } from '../enum/ProxyCommands' 16 import { BypassToServer, BypassToServerEvent } from '../packets/game/receive/BypassToServer' 17 import { ConfigManager } from '../config/ConfigManager' 18 import { PacketEvent } from '../packets/PacketMethodTypes' 19 import { AuthenticatedLogin } from '../packets/game/send/AuthenticatedLogin' 20 21 export class ClientProxy { 22 loginClient: LoginClientConnection 23 loginServer: LoginServerConnection 24 25 gameClient: GameClientConnection 26 gameServer: GameServerConnection 27 28 disconnectTimeout: NodeJS.Timeout 29 joinClientsTimeout: NodeJS.Timeout 30 31 constructor( clientConnection: Socket, port: number, host: string, proxyHost : Array<number> ) { 32 let loginBlowfishKey = this.generateLoginClientBlowfishKey() 33 34 this.loginClient = new LoginClientConnection( this.onLoginClientEvent.bind( this ), clientConnection, loginBlowfishKey ) 35 this.loginServer = new LoginServerConnection( this.onLoginServerEvent.bind( this ), port, host, proxyHost, loginBlowfishKey ) 36 37 this.loginClient.proxyToConnection( this.loginServer ) 38 this.loginServer.proxyToConnection( this.loginClient ) 39 } 40 41 onLoginClientEvent( indicator: LoginEventIndicator ) : void { 42 switch ( indicator ) { 43 case LoginEventIndicator.ConnectionTerminated: 44 ServerLog.debug( 'Login Client closed connection' ) 45 return this.loginServer.abortConnection() 46 } 47 } 48 49 onLoginServerEvent( indicator: LoginEventIndicator ) : void { 50 switch ( indicator ) { 51 case LoginEventIndicator.ConnectionTerminated: 52 ServerLog.debug( 'Login Server closed connection' ) 53 return this.loginClient.abortConnection() 54 55 case LoginEventIndicator.FlowFinished: 56 return this.registerWaitingClient() 57 } 58 } 59 60 onGameServerEvent( indicator: GameEventIndicator ) : void { 61 switch ( indicator ) { 62 /* 63 We most likely are receiving event during packet process, so we cannot 64 directly join connections yet, since if done now packet from server 65 can be proxied to client. We must wait a fraction of time. 66 */ 67 case GameEventIndicator.ServerProxyInitialized: 68 this.joinClientsTimeout = setTimeout( this.joinGameConnections.bind( this ), 100 ) 69 return 70 71 case GameEventIndicator.ConnectionTerminated: 72 ServerLog.debug( 'Game Server closed connection' ) 73 this.gameClient.abortConnection() 74 return this.cleanUp() 75 } 76 } 77 78 onGameClientEvent( indicator: GameEventIndicator ) : void { 79 switch ( indicator ) { 80 case GameEventIndicator.ConnectionTerminated: 81 ServerLog.debug( 'Game Client closed connection' ) 82 this.gameServer.abortConnection() 83 return this.cleanUp() 84 } 85 } 86 87 /* 88 1. Register parameters to recognize incoming game client and wait for game client 89 - login parameters are used to recognize incoming client after first few packet interactions 90 */ 91 private registerWaitingClient() : void { 92 this.disconnectTimeout = setTimeout( this.terminateClient.bind( this ), ConfigManager.server.getWaitingClientTimeout(), 'Connections timed out' ) 93 return ClientCache.registerWaitingClient( this ) 94 } 95 96 /* 97 2. When connected L2 client is identified to have same login parameters, 98 it is possible now to join both client and server connections to exchange packets. 99 However, proxy must now relay same packet data to game server to ensure that protocol version 100 and login parameters are accepted. 101 */ 102 addGameClientConnection( connection : GameClientConnection ) : void { 103 this.gameClient = connection 104 105 const serverId = this.loginClient.serverLoginEvent.serverId 106 const serverData = this.loginServer.servers.availableServers.find( server => server.id === serverId ) 107 108 if ( !serverData ) { 109 return this.terminateClient( `Unable to find server with id=${serverId} in available servers (size=${this.loginServer.servers.availableServers.length})` ) 110 } 111 112 ServerLog.debug( `Proxy => Game Server : starting connection for serverId=${serverId} on ${serverData.ip}:${serverData.port}` ) 113 114 const protocolVersionEvent = this.gameClient.recordedPackets[ RequestProtocolVersion.name ].event as ProtocolVersionEvent 115 this.gameServer = new GameServerConnection( serverData.ip, serverData.port, protocolVersionEvent, this.onGameServerEvent.bind( this ), chalk.blueBright( 'Game Server => Proxy' ) ) 116 } 117 118 terminateClient( reason: string ) : void { 119 if ( reason ) { 120 ServerLog.info( `Terminating client due to: ${reason}` ) 121 } 122 123 if ( this.loginServer ) { 124 this.loginServer.abortConnection() 125 } 126 127 if ( this.loginClient ) { 128 this.loginClient.abortConnection() 129 } 130 131 if ( this.gameClient ) { 132 this.gameClient.abortConnection() 133 } 134 135 if ( this.gameServer ) { 136 this.gameServer.abortConnection() 137 } 138 139 return this.cleanUp() 140 } 141 142 private cleanUp() : void { 143 this.stopTimeouts() 144 ClientCache.removeClient( this ) 145 ClientCache.removeWaitingClient( this ) 146 } 147 148 private stopTimeouts() : void { 149 if ( this.disconnectTimeout ) { 150 clearTimeout( this.disconnectTimeout ) 151 this.disconnectTimeout = null 152 } 153 154 if ( this.joinClientsTimeout ) { 155 clearTimeout( this.joinClientsTimeout ) 156 this.joinClientsTimeout = null 157 } 158 } 159 160 /* 161 3. When both client and server connections are ready to act as proxy, 162 we establish link between processing of packets to facilitate proper game 163 server packet flow. The game flow is started by sending AuthenticatedLogin to game 164 server, with data obtained from login server packet flow. 165 - while it is possible to re-send ProtocolVersion packet, such action can be 166 used to identify proxy behavior, so it is easier to simply rely on fact that if communication 167 to game server cannot be accomplished (incompatible L2 client), server will reject 168 such data with normal (according to particular game server implementation) flow 169 */ 170 private joinGameConnections() : void { 171 this.gameServer.proxyToConnection( this.gameClient ) 172 this.gameClient.proxyToConnection( this.gameServer ) 173 174 this.gameClient.setCallback( this.onGameClientEvent.bind( this ) ) 175 this.gameServer.sendData( AuthenticatedLogin( this.gameClient.clientChronicle, this.gameClient.authenticatedLogin ) ) 176 177 this.stopTimeouts() 178 this.terminateLoginConnections() 179 ServerLog.trace( 'Game Client and Server connections are paired' ) 180 181 /* 182 Client packet modifiers allow us to build proxy-specific functionality, 183 such as voice commands and bypass handling. 184 */ 185 this.gameClient.setPacketModifier( this.onGameClientPacket.bind( this ) ) 186 } 187 188 private generateLoginClientBlowfishKey(): Buffer { 189 const numberSequence: Array<number> = _.times( 16, (): number => { 190 return _.random( 255 ) 191 } ) 192 193 return Buffer.from( numberSequence ) 194 } 195 196 private terminateLoginConnections() : void { 197 this.loginServer.abortConnection() 198 this.loginClient.abortConnection() 199 200 this.loginServer = null 201 this.loginClient = null 202 } 203 204 private onGameClientPacket( name: string, event: PacketEvent, rawPacket: Buffer ) : Buffer { 205 if ( !ConfigManager.server.isListenersEnabled() ) { 206 return rawPacket 207 } 208 209 if ( name === PlayerSay.name && ( event as PlayerSayEvent ).message.startsWith( ProxyCommands.VoicePrefix ) ) { 210 let commandChunks : Array<string> = ( event as PlayerSayEvent ).message.split( ' ' ) 211 212 if ( ListenerCache.hasListeners( commandChunks[ 0 ] ) ) { 213 return ListenerCache.processListeners( commandChunks[ 0 ], event, this, rawPacket ) 214 } 215 } 216 217 if ( name === BypassToServer.name && ( event as BypassToServerEvent ).command.startsWith( ProxyCommands.BypassPrefix ) ) { 218 let commandChunks : Array<string> = ( event as BypassToServerEvent ).command.split( ' ' ) 219 220 if ( ListenerCache.hasListeners( commandChunks[ 0 ] ) ) { 221 return ListenerCache.processListeners( commandChunks[ 0 ], event, this, rawPacket ) 222 } 223 } 224 225 return rawPacket 226 } 227 }