/ proxy / source / service / ClientProxy.ts
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  }