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 }