Skip to content

Commit

Permalink
Return connection immediately, add connect event.
Browse files Browse the repository at this point in the history
Resolving the connection ASAP allows the client to close it during a
connection attempt. Instead, a connect event is used to signal success.
  • Loading branch information
retrixe committed Sep 17, 2022
1 parent 1942c40 commit 6e9c45a
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 158 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}

Expand Down Expand Up @@ -252,24 +267,32 @@ 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
}
}

// 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?) {
Expand Down
3 changes: 2 additions & 1 deletion src/minecraft/connection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
253 changes: 122 additions & 131 deletions src/minecraft/connection/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ServerConnection>((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
Loading

0 comments on commit 6e9c45a

Please sign in to comment.