/ login-server / source / service / L2LoginClient.ts
L2LoginClient.ts
  1  import { Socket } from 'net'
  2  import { LoginCrypt } from '../security/crypt/LoginCrypt'
  3  import { SessionKeyHelper, SessionKeyProperties } from './SessionKey'
  4  import { LoginManager } from '../cache/LoginManager'
  5  import { AuthenticateSessionId } from '../packets/receive/AuthenticateSessionId'
  6  import { GameGuardSuccess } from '../packets/send/GameGuardSuccess'
  7  import { LoginFail, LoginFailReason } from '../packets/send/LoginFail'
  8  import { AuthenticatedLogin } from '../packets/manager/AuthenticatedLogin'
  9  import { RequestServerLogin } from '../packets/manager/RequestServerLogin'
 10  import { RequestServerList } from '../packets/manager/RequestServerList'
 11  import { InitialConfiguration } from '../packets/send/InitialConfiguration'
 12  import { ConfigManager } from '../config/ConfigManager'
 13  import { ServerList } from '../packets/send/ServerList'
 14  import { createKeyPair, L2KeyPair } from '../security/ScrambledKeyPair'
 15  import { ServerLog } from '../logger/Logger'
 16  import Timeout = NodeJS.Timeout
 17  import chalk from 'chalk'
 18  import crypto from 'node:crypto'
 19  
 20  enum LoginClientState {
 21      CONNECTED,
 22      AUTHED_GG,
 23      AUTHED_LOGIN,
 24  }
 25  
 26  export class L2LoginClient {
 27      readonly connection: Socket
 28      state: LoginClientState = LoginClientState.CONNECTED
 29      scrambledPair: L2KeyPair
 30      blowfishKey: Buffer = LoginManager.generateBlowfishKey()
 31      readonly sessionId: number = SessionKeyHelper.createSessionId()
 32      loginCrypt: LoginCrypt
 33      private readonly charactersOnServers: Map<number, number> = new Map<number, number>()
 34      private readonly charactersToDelete: Map<number, Array<number>> = new Map<number, Array<number>>()
 35      lastServerId: number = null
 36      accessLevel: number = null
 37      readonly connectionHash: string
 38      accountName: string
 39      joinedGS: boolean = false
 40      sessionKey: SessionKeyProperties
 41      enableServerListUpdates: boolean = false
 42  
 43      sendWelcomePacketTask: Timeout
 44      cleanUpTask: Timeout
 45      loginTimeExpiredTask: Timeout
 46  
 47      constructor( socket : Socket ) {
 48          /*
 49              Disabling Nagle's algorithm delay before sending packet data.
 50           */
 51          socket.setNoDelay( true )
 52  
 53          this.connection = socket
 54          this.connectionHash = [ this.connection.remoteAddress, this.connection.remotePort, this.sessionId.toString() ].join( '#' )
 55  
 56          this.connection.on( 'data', this.onProcessData.bind( this ) )
 57          this.connection.on( 'close', this.onProcessClose.bind( this ) )
 58          this.connection.on( 'error', this.onProcessError.bind( this ) )
 59  
 60          let delay: number = Math.max( ConfigManager.server.getLoginServerResponseDelay(), 500 )
 61          this.sendWelcomePacketTask = setTimeout( this.sendWelcomePacket.bind( this ), delay )
 62          this.loginTimeExpiredTask = setTimeout( this.terminateConnection.bind( this ), ConfigManager.server.getLoginServerConnectionDuration() * 1000 + delay )
 63      }
 64  
 65      sendWelcomePacket() : void {
 66          /*
 67              Delaying creation of resources until time it is necessary
 68              for us to use them. In case of frequent connections, when
 69              they are dropped, it is possible to create situation where
 70              resources will be constantly wasted.
 71           */
 72          this.scrambledPair = createKeyPair()
 73          this.blowfishKey = crypto.randomBytes( 16 )
 74          this.loginCrypt = new LoginCrypt( this.blowfishKey )
 75  
 76          this.sendPacket( InitialConfiguration( this.scrambledPair.modulus, this.blowfishKey, this.sessionId ) )
 77      }
 78  
 79      async onProcessData( incomingData: Buffer ): Promise<void> {
 80          if ( !this.loginCrypt ) {
 81              return
 82          }
 83  
 84          let packetSizeIndex : number = 0
 85  
 86          while ( packetSizeIndex < incomingData.length ) {
 87              let packetSize = incomingData.readUInt16LE( packetSizeIndex )
 88              let packetData = incomingData.subarray( packetSizeIndex + 2, packetSizeIndex + packetSize )
 89              packetSizeIndex += packetSize
 90  
 91              try {
 92                  await this.processIncomingPacket( this.loginCrypt.decrypt( packetData ) )
 93              } catch ( error ) {
 94                  ServerLog.fatal( error, 'Failed to process packet for account: %s', this.accountName )
 95                  this.closeConnection( LoginFail( LoginFailReason.noMessage ) )
 96              }
 97          }
 98      }
 99  
100      private async processIncomingPacket( decryptedPacket : Buffer ): Promise<void> {
101  
102          try {
103              const opcode : number = decryptedPacket.readUInt8( 0 )
104  
105              switch ( this.state ) {
106                  case LoginClientState.CONNECTED:
107                      return this.onConnected( opcode, decryptedPacket )
108  
109                  case LoginClientState.AUTHED_GG:
110                      return this.onAuthenticatedGameGuard( opcode, decryptedPacket )
111  
112                  case LoginClientState.AUTHED_LOGIN:
113                      return this.onAuthenticatedLogin( opcode, decryptedPacket )
114              }
115          } catch ( error: any ) {
116              ServerLog.error( error, 'Failed L2LoginClient.onProcessData' )
117          }
118  
119          this.connection.end()
120      }
121  
122      onConnected( operation: number, packet : Buffer ) : void {
123          if ( operation === 0x07 && AuthenticateSessionId( this.sessionId, packet ) ) {
124              if ( ConfigManager.diagnostic.showIncomingPackets() ) {
125                  ServerLog.info( '%s received %s with code %s', chalk.red( this.connection.remoteAddress ), chalk.magenta( AuthenticateSessionId.name ), chalk.yellow( this.packetSignatureToHex( operation ) ) )
126              }
127  
128              this.state = LoginClientState.AUTHED_GG
129              return this.sendPacket( GameGuardSuccess( this.sessionId ) )
130          }
131  
132          ServerLog.trace( 'onConnected operation \'%s\' is not recognized.', this.packetSignatureToHex( operation ) )
133          return this.closeConnection( LoginFail( LoginFailReason.accessFailed ) )
134      }
135  
136      onAuthenticatedGameGuard( operation: number, packet: Buffer ) : Promise<void> {
137          if ( operation === 0x00 ) {
138              if ( ConfigManager.diagnostic.showIncomingPackets() ) {
139                  ServerLog.info( '%s received %s with code %s', chalk.red( this.connection.remoteAddress ), chalk.magenta( 'AuthenticatedLogin' ), chalk.yellow( this.packetSignatureToHex( operation ) ) )
140              }
141  
142              return AuthenticatedLogin.process( this, packet )
143          }
144  
145          ServerLog.trace( 'L2LoginClient.onAuthenticatedGameGuard operation \'%s\' is not recognized.', this.packetSignatureToHex( operation ) )
146          this.closeConnection( LoginFail( LoginFailReason.accessFailed ) )
147      }
148  
149      onAuthenticatedLogin( operation: number, packet: Buffer ) : void {
150          if ( operation === 0x02 ) {
151              if ( ConfigManager.diagnostic.showIncomingPackets() ) {
152                  ServerLog.info( '%s received %s with code %s', chalk.red( this.accountName ), chalk.magenta( RequestServerLogin.name ), chalk.yellow( this.packetSignatureToHex( operation ) ) )
153              }
154  
155              return this.sendPacket( RequestServerLogin( this, packet ) )
156          }
157  
158          if ( operation === 0x05 ) {
159              if ( ConfigManager.diagnostic.showIncomingPackets() ) {
160                  ServerLog.info( '%s received %s with code %s', chalk.red( this.accountName ), chalk.magenta( RequestServerList.name ), chalk.yellow( this.packetSignatureToHex( operation ) ) )
161              }
162  
163              return this.sendPacket( RequestServerList( this, packet ) )
164          }
165  
166          ServerLog.trace( 'L2LoginClient.onAuthenticatedLogin operation \'%s\' is not recognized.', this.packetSignatureToHex( operation ) )
167          return this.closeConnection( LoginFail( LoginFailReason.accessFailed ) )
168      }
169  
170      onProcessClose() {
171          this.enableServerListUpdates = false
172  
173          /*
174              When client disconnects, login client would still need to be available since GS would need to reach to login
175              server and validate session keys in order for client to login into game server. Hence, login client should be silently cleaned up later.
176           */
177          ServerLog.trace( 'Connection to the login server is closed by: %s:%d', this.connection.remoteAddress, this.connection.remotePort )
178          this.stopCleanUpTask()
179          this.cleanUpTask = setTimeout( this.runCleanupTask.bind( this ), 2000 )
180      }
181  
182      protected packetSignatureToHex( code: number ) : string {
183          const hexCode = code.toString( 16 ).toUpperCase()
184          if ( hexCode.length === 1 ) {
185              return `0x0${hexCode}`
186          }
187  
188          return `0x${hexCode}`
189      }
190  
191      onProcessError() {
192          LoginManager.removeClient( this.connectionHash )
193      }
194  
195      stopCleanUpTask() {
196          if ( this.cleanUpTask ) {
197              clearTimeout( this.cleanUpTask )
198              this.cleanUpTask = null
199          }
200      }
201  
202      getConnectionHash() {
203          return this.connectionHash
204      }
205  
206      getConnectionIpAddress() : string {
207          return this.connection.remoteAddress
208      }
209  
210      getState() {
211          return this.state
212      }
213  
214      setStateLoginAuthenticated() {
215          this.state = LoginClientState.AUTHED_LOGIN
216      }
217  
218      getRSAKeyPair() : any {
219          return this.scrambledPair.value
220      }
221  
222      getAccountName() : string {
223          return this.accountName
224      }
225  
226      setAccountName( accountName : string ) : void {
227          this.accountName = accountName
228      }
229  
230      setAccessLevel( accessLevel ) {
231          this.accessLevel = accessLevel
232      }
233  
234      getAccessLevel() {
235          return this.accessLevel
236      }
237  
238      setLastServer( serverId : number ) : void {
239          this.lastServerId = serverId
240      }
241  
242      getLastServer() : number {
243          return this.lastServerId
244      }
245  
246      getSessionId() {
247          return this.sessionId
248      }
249  
250      setJoinedGS( value: boolean ) {
251          this.joinedGS = value
252      }
253  
254      setSessionKey( sessionKey: SessionKeyProperties ): void {
255          this.sessionKey = sessionKey
256      }
257  
258      getSessionKey(): SessionKeyProperties {
259          return this.sessionKey
260      }
261  
262      sendPacket( packet : Buffer ) : void {
263          if ( this.connection.destroyed || !this.loginCrypt ) {
264              return
265          }
266  
267          this.connection.write( this.loginCrypt.encrypt( packet ) )
268      }
269  
270      closeConnection( packet : Buffer ) : void {
271          if ( this.connection.destroyed || !this.loginCrypt ) {
272              return
273          }
274  
275          this.connection.end( this.loginCrypt.encrypt( packet ) )
276      }
277  
278      setCharactersOnServer( serverId: number, numberOfCharacters: number ) : void {
279          this.charactersOnServers.set( serverId, numberOfCharacters )
280      }
281  
282      getCharactersOnServer() : ReadonlyMap<number, number> {
283          return this.charactersOnServers
284      }
285  
286      setCharactersWaitingDeleteOnServer( serverId: number, timesToDelete: Array<number> ) : void {
287          this.charactersToDelete.set( serverId, timesToDelete )
288      }
289  
290      getCharactersWaitingDeleteOnServer() : ReadonlyMap<number, Array<number>> {
291          return this.charactersToDelete
292      }
293  
294      runCleanupTask() : void {
295          clearTimeout( this.sendWelcomePacketTask )
296          clearTimeout( this.loginTimeExpiredTask )
297  
298          LoginManager.removeClient( this.connectionHash )
299      }
300  
301      sendServerList() {
302          if ( !this.connection ) {
303              return
304          }
305  
306          this.enableServerListUpdates = true
307          this.sendPacket( ServerList( this ) )
308      }
309  
310      terminateConnection() : void {
311          this.closeConnection( LoginFail( LoginFailReason.noMessage ) )
312      }
313  }