From 6e9c45a9ecd95e333a1e7647d2716d010f3727cf Mon Sep 17 00:00:00 2001 From: Ibrahim Ansari Date: Sun, 18 Sep 2022 04:15:09 +0530 Subject: [PATCH] Return connection immediately, add connect event. Resolving the connection ASAP allows the client to close it during a connection attempt. Instead, a connect event is used to signal success. --- .../modules/connection/ConnectionModule.kt | 71 +++-- src/minecraft/connection/index.ts | 3 +- src/minecraft/connection/javascript.ts | 253 +++++++++--------- src/minecraft/connection/native.ts | 7 +- src/screens/chat/ChatScreen.tsx | 2 +- 5 files changed, 178 insertions(+), 158 deletions(-) diff --git a/android/app/src/main/java/com/enderchat/modules/connection/ConnectionModule.kt b/android/app/src/main/java/com/enderchat/modules/connection/ConnectionModule.kt index 93e80b9..e67a3ae 100644 --- a/android/app/src/main/java/com/enderchat/modules/connection/ConnectionModule.kt +++ b/android/app/src/main/java/com/enderchat/modules/connection/ConnectionModule.kt @@ -125,18 +125,35 @@ class ConnectionModule(reactContext: ReactApplicationContext) // Start thread which handles creating the connection and then reads packets from it. // This avoids blocking the main thread on writeLock and keeps the UI thread responsive. scope.launch(Dispatchers.IO) { - lock.writeLock().lock() val socket: Socket val connectionId = UUID.randomUUID() try { - // Only one connection at a time. + lock.write { + // Only one connection at a time. + directlyCloseConnection() + + // Create socket and connection ID. + socket = Socket() + socket.soTimeout = 20 * 1000 + this@ConnectionModule.socket = socket + this@ConnectionModule.connectionId = connectionId + promise.resolve(connectionId.toString()) + } + } catch (e: Exception) { directlyCloseConnection() + promise.reject(e) + return@launch + } - // Create socket and connection ID. - socket = Socket() + try { + // Connect to the server. socket.connect(InetSocketAddress(host, port), 30 * 1000) - socket.soTimeout = 20 * 1000 - this@ConnectionModule.connectionId = connectionId + + // Send connect event. + val params = Arguments.createMap().apply { + putString("connectionId", connectionId.toString()) + } + sendEvent(reactContext = reactApplicationContext, "ecm:connect", params) // Create data to send in Handshake. val portBuf = ByteBuffer.allocate(2) @@ -154,15 +171,13 @@ class ConnectionModule(reactContext: ReactApplicationContext) // Send Login Start packet. val loginPacketData = Base64.decode(loginPacket, Base64.DEFAULT) socket.getOutputStream().write(Packet(0x00, loginPacketData).writePacket()) - - // Update the current socket and resolve/reject. - this@ConnectionModule.socket = socket - lock.writeLock().unlock() - promise.resolve(connectionId.toString()) } catch (e: Exception) { - directlyCloseConnection() - lock.writeLock().unlock() - promise.reject(e) + lock.write { + if (this@ConnectionModule.socket == socket) { + directlyCloseConnection() + sendErrorEvent(connectionId, e) + } else sendCloseEvent(connectionId) + } return@launch } @@ -252,12 +267,7 @@ class ConnectionModule(reactContext: ReactApplicationContext) } catch (e: Exception) { if (lockAcquired) lock.readLock().unlock() lock.write { if (this@ConnectionModule.socket == socket) directlyCloseConnection() } - val params = Arguments.createMap().apply { - putString("connectionId", connectionId.toString()) - putString("stackTrace", e.stackTraceToString()) - putString("message", e.message) - } - sendEvent(reactContext = reactApplicationContext, "ecm:error", params) + sendErrorEvent(connectionId, e) break } } @@ -265,11 +275,24 @@ class ConnectionModule(reactContext: ReactApplicationContext) // Dispatch close event to JS. // The only way this.socket != socket is if directlyCloseConnection was called. // If isInputStream returns -1, for now we assume the socket was closed too. - val params = Arguments.createMap().apply { - putString("connectionId", connectionId.toString()) - } - sendEvent(reactContext = reactApplicationContext, "ecm:close", params) + sendCloseEvent(connectionId) + } + } + + private fun sendErrorEvent(connectionId: UUID, e: Exception) { + val params = Arguments.createMap().apply { + putString("connectionId", connectionId.toString()) + putString("stackTrace", e.stackTraceToString()) + putString("message", e.message) + } + sendEvent(reactContext = reactApplicationContext, "ecm:error", params) + } + + private fun sendCloseEvent(connectionId: UUID) { + val params = Arguments.createMap().apply { + putString("connectionId", connectionId.toString()) } + sendEvent(reactContext = reactApplicationContext, "ecm:close", params) } private fun sendEvent(reactContext: ReactContext, eventName: String, params: WritableMap?) { diff --git a/src/minecraft/connection/index.ts b/src/minecraft/connection/index.ts index ff94f58..c79cfec 100644 --- a/src/minecraft/connection/index.ts +++ b/src/minecraft/connection/index.ts @@ -29,7 +29,8 @@ export interface ServerConnection extends events.EventEmitter { close: () => void - on: ((event: 'packet', listener: (packet: Packet) => void) => this) & + on: ((event: 'connect', listener: () => void) => this) & + ((event: 'packet', listener: (packet: Packet) => void) => this) & ((event: 'error', listener: (error: Error) => void) => this) & ((event: 'close', listener: () => void) => this) & ((event: string, listener: Function) => this) diff --git a/src/minecraft/connection/javascript.ts b/src/minecraft/connection/javascript.ts index a624b1b..2818767 100644 --- a/src/minecraft/connection/javascript.ts +++ b/src/minecraft/connection/javascript.ts @@ -83,140 +83,131 @@ export class JavaScriptServerConnection const initiateJavaScriptConnection = async (opts: ConnectionOptions) => { const [host, port] = await resolveHostname(opts.host, opts.port) - return await new Promise((resolve, reject) => { - const socket = net.createConnection({ host, port }) - const conn = new JavaScriptServerConnection(socket, opts) - let resolved = false - const { accessToken, selectedProfile } = opts - socket.on('connect', () => { - // Create data to send in Handshake. - const portBuf = Buffer.alloc(2) - portBuf.writeUInt16BE(port) - const handshakeData = [ - writeVarInt(opts.protocolVersion), - host, - portBuf, - writeVarInt(2) - ] - // Initialise Handshake with server. - socket.write(makeBasePacket(0x00, concatPacketData(handshakeData)), () => - // Send Login Start packet. - socket.write(makeBasePacket(0x00, getLoginPacket(opts)), () => { - resolved = true - resolve(conn) - }) - ) - }) - socket.on('close', () => { - conn.closed = true - conn.emit('close') - }) - socket.on('error', err => { - if (!resolved) reject(err) - else { - conn.disconnectReason = err.message - conn.emit('error', err) - } - }) - const lock = new Semaphore(1) - socket.on('data', newData => { - // Handle timeout after 20 seconds of no data. - if (conn.disconnectTimer) clearTimeout(conn.disconnectTimer) - conn.disconnectTimer = setTimeout(() => conn.close(), 20000) - // Run after interactions to improve user experience. - InteractionManager.runAfterInteractions(async () => { - await lock.acquire() - try { - // Note: the entire packet is encrypted, including the length fields and the packet's data. - // https://github.com/PrismarineJS/node-minecraft-protocol/blob/master/src/transforms/encryption.js - let finalData = newData - if (conn.aesDecipher) finalData = conn.aesDecipher.update(newData) - // Buffer data for read. - conn.bufferedData = Buffer.concat([conn.bufferedData, finalData]) - while (true) { - const packet = conn.compressionEnabled - ? await parseCompressedPacket(conn.bufferedData) - : parsePacket(conn.bufferedData) - if (packet) { - // Remove packet from buffered data. - conn.bufferedData = - conn.bufferedData.length <= packet.packetLength - ? Buffer.alloc(0) // Avoid errors shortening. - : conn.bufferedData.slice(packet.packetLength) - // Internally handle login packets. - const is1164 = - conn.options.protocolVersion >= protocolMap['1.16.4'] - const is117 = conn.options.protocolVersion >= protocolMap[1.17] - const is119 = conn.options.protocolVersion >= protocolMap[1.19] - const is1191 = - conn.options.protocolVersion >= protocolMap['1.19.1'] - if (packet.id === 0x03 && !conn.loggedIn /* Set Compression */) { - const [threshold] = readVarInt(packet.data) - conn.compressionThreshold = threshold - conn.compressionEnabled = threshold >= 0 - } else if (packet.id === 0x02 && !conn.loggedIn) { - conn.loggedIn = true // Login Success - } else if ( - // Keep Alive (clientbound) - (packet.id === 0x1f && is1164 && !is117) || - (packet.id === 0x21 && is117 && !is119) || - (packet.id === 0x1e && is119 && !is1191) || - (packet.id === 0x20 && is1191) - ) { - const id = is1191 ? 0x12 : is119 ? 0x11 : is117 ? 0x0f : 0x10 - conn - .writePacket(id, packet.data) - .catch(err => conn.emit('error', err)) - } else if ( - // Disconnect (login) or Disconnect (play) - (packet.id === 0x00 && !conn.loggedIn) || - (packet.id === 0x19 && conn.loggedIn && is1164 && !is117) || - (packet.id === 0x1a && conn.loggedIn && is117 && !is119) || - (packet.id === 0x17 && conn.loggedIn && is119 && !is1191) || - (packet.id === 0x19 && conn.loggedIn && is1191) - ) { - const [chatLength, chatVarIntLength] = readVarInt(packet.data) - conn.disconnectReason = packet.data - .slice(chatVarIntLength, chatVarIntLength + chatLength) - .toString('utf8') - } else if (packet.id === 0x04 && !conn.loggedIn) { - /* Login Plugin Request */ - const [msgId] = readVarInt(packet.data) - const rs = concatPacketData([writeVarInt(msgId), false]) - conn.writePacket(0x02, rs).catch(err => conn.emit('error', err)) - } else if (packet.id === 0x01 && !conn.loggedIn) { - /* Encryption Request */ - if (!accessToken || !selectedProfile) { - conn.disconnectReason = - '{"text":"This server requires a premium account to be logged in!"}' - conn.close() - continue - } - handleEncryptionRequest( - packet, - accessToken, - selectedProfile, - conn, - is119, - async (secret: Buffer, response: Buffer) => { - const AES_ALG = 'aes-128-cfb8' - conn.aesDecipher = createDecipheriv(AES_ALG, secret, secret) - await conn.writePacket(0x01, response) - conn.aesCipher = createCipheriv(AES_ALG, secret, secret) - } - ) + const socket = net.createConnection({ host, port }) + const conn = new JavaScriptServerConnection(socket, opts) + const { accessToken, selectedProfile } = opts + socket.on('connect', () => { + conn.emit('connect') + // Create data to send in Handshake. + const portBuf = Buffer.alloc(2) + portBuf.writeUInt16BE(port) + const handshakeData = [ + writeVarInt(opts.protocolVersion), + host, + portBuf, + writeVarInt(2) + ] + // Initialise Handshake with server. + socket.write(makeBasePacket(0x00, concatPacketData(handshakeData)), () => + // Send Login Start packet. + socket.write(makeBasePacket(0x00, getLoginPacket(opts))) + ) + }) + socket.on('close', () => { + conn.closed = true + conn.emit('close') + }) + socket.on('error', err => { + conn.disconnectReason = err.message + conn.emit('error', err) + }) + const lock = new Semaphore(1) + socket.on('data', newData => { + // Handle timeout after 20 seconds of no data. + if (conn.disconnectTimer) clearTimeout(conn.disconnectTimer) + conn.disconnectTimer = setTimeout(() => conn.close(), 20000) + // Run after interactions to improve user experience. + InteractionManager.runAfterInteractions(async () => { + await lock.acquire() + try { + // Note: the entire packet is encrypted, including the length fields and the packet's data. + // https://github.com/PrismarineJS/node-minecraft-protocol/blob/master/src/transforms/encryption.js + let finalData = newData + if (conn.aesDecipher) finalData = conn.aesDecipher.update(newData) + // Buffer data for read. + conn.bufferedData = Buffer.concat([conn.bufferedData, finalData]) + while (true) { + const packet = conn.compressionEnabled + ? await parseCompressedPacket(conn.bufferedData) + : parsePacket(conn.bufferedData) + if (packet) { + // Remove packet from buffered data. + conn.bufferedData = + conn.bufferedData.length <= packet.packetLength + ? Buffer.alloc(0) // Avoid errors shortening. + : conn.bufferedData.slice(packet.packetLength) + // Internally handle login packets. + const is1164 = conn.options.protocolVersion >= protocolMap['1.16.4'] + const is117 = conn.options.protocolVersion >= protocolMap[1.17] + const is119 = conn.options.protocolVersion >= protocolMap[1.19] + const is1191 = conn.options.protocolVersion >= protocolMap['1.19.1'] + if (packet.id === 0x03 && !conn.loggedIn /* Set Compression */) { + const [threshold] = readVarInt(packet.data) + conn.compressionThreshold = threshold + conn.compressionEnabled = threshold >= 0 + } else if (packet.id === 0x02 && !conn.loggedIn) { + conn.loggedIn = true // Login Success + } else if ( + // Keep Alive (clientbound) + (packet.id === 0x1f && is1164 && !is117) || + (packet.id === 0x21 && is117 && !is119) || + (packet.id === 0x1e && is119 && !is1191) || + (packet.id === 0x20 && is1191) + ) { + const id = is1191 ? 0x12 : is119 ? 0x11 : is117 ? 0x0f : 0x10 + conn + .writePacket(id, packet.data) + .catch(err => conn.emit('error', err)) + } else if ( + // Disconnect (login) or Disconnect (play) + (packet.id === 0x00 && !conn.loggedIn) || + (packet.id === 0x19 && conn.loggedIn && is1164 && !is117) || + (packet.id === 0x1a && conn.loggedIn && is117 && !is119) || + (packet.id === 0x17 && conn.loggedIn && is119 && !is1191) || + (packet.id === 0x19 && conn.loggedIn && is1191) + ) { + const [chatLength, chatVarIntLength] = readVarInt(packet.data) + conn.disconnectReason = packet.data + .slice(chatVarIntLength, chatVarIntLength + chatLength) + .toString('utf8') + } else if (packet.id === 0x04 && !conn.loggedIn) { + /* Login Plugin Request */ + const [msgId] = readVarInt(packet.data) + const rs = concatPacketData([writeVarInt(msgId), false]) + conn.writePacket(0x02, rs).catch(err => conn.emit('error', err)) + } else if (packet.id === 0x01 && !conn.loggedIn) { + /* Encryption Request */ + if (!accessToken || !selectedProfile) { + conn.disconnectReason = + '{"text":"This server requires a premium account to be logged in!"}' + conn.close() + continue } - conn.emit('packet', packet) - } else break - } - conn.emit('data', newData) - } catch (err) { - conn.emit('error', err) + handleEncryptionRequest( + packet, + accessToken, + selectedProfile, + conn, + is119, + async (secret: Buffer, response: Buffer) => { + const AES_ALG = 'aes-128-cfb8' + conn.aesDecipher = createDecipheriv(AES_ALG, secret, secret) + await conn.writePacket(0x01, response) + conn.aesCipher = createCipheriv(AES_ALG, secret, secret) + } + ) + } + conn.emit('packet', packet) + } else break } - lock.release() - }).then(() => {}, console.error) - }) + conn.emit('data', newData) + } catch (err) { + conn.emit('error', err) + } + lock.release() + }).then(() => {}, console.error) }) + return conn } export default initiateJavaScriptConnection diff --git a/src/minecraft/connection/native.ts b/src/minecraft/connection/native.ts index 052e33b..a46ba37 100644 --- a/src/minecraft/connection/native.ts +++ b/src/minecraft/connection/native.ts @@ -28,7 +28,8 @@ interface NativeErrorEvent extends NativeEvent { } export declare interface NativeServerConnection { - on: ((event: 'packet', listener: (packet: Packet) => void) => this) & + on: ((event: 'connect', listener: () => void) => this) & + ((event: 'packet', listener: (packet: Packet) => void) => this) & ((event: 'error', listener: (error: Error) => void) => this) & ((event: 'close', listener: () => void) => this) & ((event: string, listener: Function) => this) @@ -57,6 +58,9 @@ export class NativeServerConnection 'ecm:log', ({ log }: NativeEvent & { log: string }) => console.log(log) ) + this.eventEmitter.addListener('ecm:connect', (event: NativeEvent) => { + if (event.connectionId === this.id) this.emit('connect') + }) this.eventEmitter.addListener('ecm:packet', (event: NativePacketEvent) => { if (event.connectionId !== this.id) return // Run after interactions to improve user experience. @@ -143,6 +147,7 @@ export class NativeServerConnection if (this.closed) return this.closed = true if (closeConnection) ConnectionModule.closeConnection(this.id) + this.eventEmitter.removeAllListeners('ecm:connect') this.eventEmitter.removeAllListeners('ecm:packet') this.eventEmitter.removeAllListeners('ecm:error') this.eventEmitter.removeAllListeners('ecm:close') diff --git a/src/screens/chat/ChatScreen.tsx b/src/screens/chat/ChatScreen.tsx index 8100940..001e3eb 100644 --- a/src/screens/chat/ChatScreen.tsx +++ b/src/screens/chat/ChatScreen.tsx @@ -177,8 +177,8 @@ const ChatScreen = ({ navigation, route }: Props) => { if (typeof conn === 'string') { closeChatScreen({ server: serverName, reason: conn }) } else { + conn.connection.on('connect', () => setLoading('Logging in...')) setConnection(conn) - setLoading('Logging in...') } } else if (typeof conn !== 'string') conn.connection.close() }