Skip to content

Commit 6e9c45a

Browse files
committed
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.
1 parent 1942c40 commit 6e9c45a

File tree

5 files changed

+178
-158
lines changed

5 files changed

+178
-158
lines changed

android/app/src/main/java/com/enderchat/modules/connection/ConnectionModule.kt

+47-24
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,35 @@ class ConnectionModule(reactContext: ReactApplicationContext)
125125
// Start thread which handles creating the connection and then reads packets from it.
126126
// This avoids blocking the main thread on writeLock and keeps the UI thread responsive.
127127
scope.launch(Dispatchers.IO) {
128-
lock.writeLock().lock()
129128
val socket: Socket
130129
val connectionId = UUID.randomUUID()
131130
try {
132-
// Only one connection at a time.
131+
lock.write {
132+
// Only one connection at a time.
133+
directlyCloseConnection()
134+
135+
// Create socket and connection ID.
136+
socket = Socket()
137+
socket.soTimeout = 20 * 1000
138+
this@ConnectionModule.socket = socket
139+
this@ConnectionModule.connectionId = connectionId
140+
promise.resolve(connectionId.toString())
141+
}
142+
} catch (e: Exception) {
133143
directlyCloseConnection()
144+
promise.reject(e)
145+
return@launch
146+
}
134147

135-
// Create socket and connection ID.
136-
socket = Socket()
148+
try {
149+
// Connect to the server.
137150
socket.connect(InetSocketAddress(host, port), 30 * 1000)
138-
socket.soTimeout = 20 * 1000
139-
this@ConnectionModule.connectionId = connectionId
151+
152+
// Send connect event.
153+
val params = Arguments.createMap().apply {
154+
putString("connectionId", connectionId.toString())
155+
}
156+
sendEvent(reactContext = reactApplicationContext, "ecm:connect", params)
140157

141158
// Create data to send in Handshake.
142159
val portBuf = ByteBuffer.allocate(2)
@@ -154,15 +171,13 @@ class ConnectionModule(reactContext: ReactApplicationContext)
154171
// Send Login Start packet.
155172
val loginPacketData = Base64.decode(loginPacket, Base64.DEFAULT)
156173
socket.getOutputStream().write(Packet(0x00, loginPacketData).writePacket())
157-
158-
// Update the current socket and resolve/reject.
159-
this@ConnectionModule.socket = socket
160-
lock.writeLock().unlock()
161-
promise.resolve(connectionId.toString())
162174
} catch (e: Exception) {
163-
directlyCloseConnection()
164-
lock.writeLock().unlock()
165-
promise.reject(e)
175+
lock.write {
176+
if (this@ConnectionModule.socket == socket) {
177+
directlyCloseConnection()
178+
sendErrorEvent(connectionId, e)
179+
} else sendCloseEvent(connectionId)
180+
}
166181
return@launch
167182
}
168183

@@ -252,24 +267,32 @@ class ConnectionModule(reactContext: ReactApplicationContext)
252267
} catch (e: Exception) {
253268
if (lockAcquired) lock.readLock().unlock()
254269
lock.write { if (this@ConnectionModule.socket == socket) directlyCloseConnection() }
255-
val params = Arguments.createMap().apply {
256-
putString("connectionId", connectionId.toString())
257-
putString("stackTrace", e.stackTraceToString())
258-
putString("message", e.message)
259-
}
260-
sendEvent(reactContext = reactApplicationContext, "ecm:error", params)
270+
sendErrorEvent(connectionId, e)
261271
break
262272
}
263273
}
264274

265275
// Dispatch close event to JS.
266276
// The only way this.socket != socket is if directlyCloseConnection was called.
267277
// If isInputStream returns -1, for now we assume the socket was closed too.
268-
val params = Arguments.createMap().apply {
269-
putString("connectionId", connectionId.toString())
270-
}
271-
sendEvent(reactContext = reactApplicationContext, "ecm:close", params)
278+
sendCloseEvent(connectionId)
279+
}
280+
}
281+
282+
private fun sendErrorEvent(connectionId: UUID, e: Exception) {
283+
val params = Arguments.createMap().apply {
284+
putString("connectionId", connectionId.toString())
285+
putString("stackTrace", e.stackTraceToString())
286+
putString("message", e.message)
287+
}
288+
sendEvent(reactContext = reactApplicationContext, "ecm:error", params)
289+
}
290+
291+
private fun sendCloseEvent(connectionId: UUID) {
292+
val params = Arguments.createMap().apply {
293+
putString("connectionId", connectionId.toString())
272294
}
295+
sendEvent(reactContext = reactApplicationContext, "ecm:close", params)
273296
}
274297

275298
private fun sendEvent(reactContext: ReactContext, eventName: String, params: WritableMap?) {

src/minecraft/connection/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export interface ServerConnection extends events.EventEmitter {
2929

3030
close: () => void
3131

32-
on: ((event: 'packet', listener: (packet: Packet) => void) => this) &
32+
on: ((event: 'connect', listener: () => void) => this) &
33+
((event: 'packet', listener: (packet: Packet) => void) => this) &
3334
((event: 'error', listener: (error: Error) => void) => this) &
3435
((event: 'close', listener: () => void) => this) &
3536
((event: string, listener: Function) => this)

src/minecraft/connection/javascript.ts

+122-131
Original file line numberDiff line numberDiff line change
@@ -83,140 +83,131 @@ export class JavaScriptServerConnection
8383

8484
const initiateJavaScriptConnection = async (opts: ConnectionOptions) => {
8585
const [host, port] = await resolveHostname(opts.host, opts.port)
86-
return await new Promise<ServerConnection>((resolve, reject) => {
87-
const socket = net.createConnection({ host, port })
88-
const conn = new JavaScriptServerConnection(socket, opts)
89-
let resolved = false
90-
const { accessToken, selectedProfile } = opts
91-
socket.on('connect', () => {
92-
// Create data to send in Handshake.
93-
const portBuf = Buffer.alloc(2)
94-
portBuf.writeUInt16BE(port)
95-
const handshakeData = [
96-
writeVarInt(opts.protocolVersion),
97-
host,
98-
portBuf,
99-
writeVarInt(2)
100-
]
101-
// Initialise Handshake with server.
102-
socket.write(makeBasePacket(0x00, concatPacketData(handshakeData)), () =>
103-
// Send Login Start packet.
104-
socket.write(makeBasePacket(0x00, getLoginPacket(opts)), () => {
105-
resolved = true
106-
resolve(conn)
107-
})
108-
)
109-
})
110-
socket.on('close', () => {
111-
conn.closed = true
112-
conn.emit('close')
113-
})
114-
socket.on('error', err => {
115-
if (!resolved) reject(err)
116-
else {
117-
conn.disconnectReason = err.message
118-
conn.emit('error', err)
119-
}
120-
})
121-
const lock = new Semaphore(1)
122-
socket.on('data', newData => {
123-
// Handle timeout after 20 seconds of no data.
124-
if (conn.disconnectTimer) clearTimeout(conn.disconnectTimer)
125-
conn.disconnectTimer = setTimeout(() => conn.close(), 20000)
126-
// Run after interactions to improve user experience.
127-
InteractionManager.runAfterInteractions(async () => {
128-
await lock.acquire()
129-
try {
130-
// Note: the entire packet is encrypted, including the length fields and the packet's data.
131-
// https://github.com/PrismarineJS/node-minecraft-protocol/blob/master/src/transforms/encryption.js
132-
let finalData = newData
133-
if (conn.aesDecipher) finalData = conn.aesDecipher.update(newData)
134-
// Buffer data for read.
135-
conn.bufferedData = Buffer.concat([conn.bufferedData, finalData])
136-
while (true) {
137-
const packet = conn.compressionEnabled
138-
? await parseCompressedPacket(conn.bufferedData)
139-
: parsePacket(conn.bufferedData)
140-
if (packet) {
141-
// Remove packet from buffered data.
142-
conn.bufferedData =
143-
conn.bufferedData.length <= packet.packetLength
144-
? Buffer.alloc(0) // Avoid errors shortening.
145-
: conn.bufferedData.slice(packet.packetLength)
146-
// Internally handle login packets.
147-
const is1164 =
148-
conn.options.protocolVersion >= protocolMap['1.16.4']
149-
const is117 = conn.options.protocolVersion >= protocolMap[1.17]
150-
const is119 = conn.options.protocolVersion >= protocolMap[1.19]
151-
const is1191 =
152-
conn.options.protocolVersion >= protocolMap['1.19.1']
153-
if (packet.id === 0x03 && !conn.loggedIn /* Set Compression */) {
154-
const [threshold] = readVarInt(packet.data)
155-
conn.compressionThreshold = threshold
156-
conn.compressionEnabled = threshold >= 0
157-
} else if (packet.id === 0x02 && !conn.loggedIn) {
158-
conn.loggedIn = true // Login Success
159-
} else if (
160-
// Keep Alive (clientbound)
161-
(packet.id === 0x1f && is1164 && !is117) ||
162-
(packet.id === 0x21 && is117 && !is119) ||
163-
(packet.id === 0x1e && is119 && !is1191) ||
164-
(packet.id === 0x20 && is1191)
165-
) {
166-
const id = is1191 ? 0x12 : is119 ? 0x11 : is117 ? 0x0f : 0x10
167-
conn
168-
.writePacket(id, packet.data)
169-
.catch(err => conn.emit('error', err))
170-
} else if (
171-
// Disconnect (login) or Disconnect (play)
172-
(packet.id === 0x00 && !conn.loggedIn) ||
173-
(packet.id === 0x19 && conn.loggedIn && is1164 && !is117) ||
174-
(packet.id === 0x1a && conn.loggedIn && is117 && !is119) ||
175-
(packet.id === 0x17 && conn.loggedIn && is119 && !is1191) ||
176-
(packet.id === 0x19 && conn.loggedIn && is1191)
177-
) {
178-
const [chatLength, chatVarIntLength] = readVarInt(packet.data)
179-
conn.disconnectReason = packet.data
180-
.slice(chatVarIntLength, chatVarIntLength + chatLength)
181-
.toString('utf8')
182-
} else if (packet.id === 0x04 && !conn.loggedIn) {
183-
/* Login Plugin Request */
184-
const [msgId] = readVarInt(packet.data)
185-
const rs = concatPacketData([writeVarInt(msgId), false])
186-
conn.writePacket(0x02, rs).catch(err => conn.emit('error', err))
187-
} else if (packet.id === 0x01 && !conn.loggedIn) {
188-
/* Encryption Request */
189-
if (!accessToken || !selectedProfile) {
190-
conn.disconnectReason =
191-
'{"text":"This server requires a premium account to be logged in!"}'
192-
conn.close()
193-
continue
194-
}
195-
handleEncryptionRequest(
196-
packet,
197-
accessToken,
198-
selectedProfile,
199-
conn,
200-
is119,
201-
async (secret: Buffer, response: Buffer) => {
202-
const AES_ALG = 'aes-128-cfb8'
203-
conn.aesDecipher = createDecipheriv(AES_ALG, secret, secret)
204-
await conn.writePacket(0x01, response)
205-
conn.aesCipher = createCipheriv(AES_ALG, secret, secret)
206-
}
207-
)
86+
const socket = net.createConnection({ host, port })
87+
const conn = new JavaScriptServerConnection(socket, opts)
88+
const { accessToken, selectedProfile } = opts
89+
socket.on('connect', () => {
90+
conn.emit('connect')
91+
// Create data to send in Handshake.
92+
const portBuf = Buffer.alloc(2)
93+
portBuf.writeUInt16BE(port)
94+
const handshakeData = [
95+
writeVarInt(opts.protocolVersion),
96+
host,
97+
portBuf,
98+
writeVarInt(2)
99+
]
100+
// Initialise Handshake with server.
101+
socket.write(makeBasePacket(0x00, concatPacketData(handshakeData)), () =>
102+
// Send Login Start packet.
103+
socket.write(makeBasePacket(0x00, getLoginPacket(opts)))
104+
)
105+
})
106+
socket.on('close', () => {
107+
conn.closed = true
108+
conn.emit('close')
109+
})
110+
socket.on('error', err => {
111+
conn.disconnectReason = err.message
112+
conn.emit('error', err)
113+
})
114+
const lock = new Semaphore(1)
115+
socket.on('data', newData => {
116+
// Handle timeout after 20 seconds of no data.
117+
if (conn.disconnectTimer) clearTimeout(conn.disconnectTimer)
118+
conn.disconnectTimer = setTimeout(() => conn.close(), 20000)
119+
// Run after interactions to improve user experience.
120+
InteractionManager.runAfterInteractions(async () => {
121+
await lock.acquire()
122+
try {
123+
// Note: the entire packet is encrypted, including the length fields and the packet's data.
124+
// https://github.com/PrismarineJS/node-minecraft-protocol/blob/master/src/transforms/encryption.js
125+
let finalData = newData
126+
if (conn.aesDecipher) finalData = conn.aesDecipher.update(newData)
127+
// Buffer data for read.
128+
conn.bufferedData = Buffer.concat([conn.bufferedData, finalData])
129+
while (true) {
130+
const packet = conn.compressionEnabled
131+
? await parseCompressedPacket(conn.bufferedData)
132+
: parsePacket(conn.bufferedData)
133+
if (packet) {
134+
// Remove packet from buffered data.
135+
conn.bufferedData =
136+
conn.bufferedData.length <= packet.packetLength
137+
? Buffer.alloc(0) // Avoid errors shortening.
138+
: conn.bufferedData.slice(packet.packetLength)
139+
// Internally handle login packets.
140+
const is1164 = conn.options.protocolVersion >= protocolMap['1.16.4']
141+
const is117 = conn.options.protocolVersion >= protocolMap[1.17]
142+
const is119 = conn.options.protocolVersion >= protocolMap[1.19]
143+
const is1191 = conn.options.protocolVersion >= protocolMap['1.19.1']
144+
if (packet.id === 0x03 && !conn.loggedIn /* Set Compression */) {
145+
const [threshold] = readVarInt(packet.data)
146+
conn.compressionThreshold = threshold
147+
conn.compressionEnabled = threshold >= 0
148+
} else if (packet.id === 0x02 && !conn.loggedIn) {
149+
conn.loggedIn = true // Login Success
150+
} else if (
151+
// Keep Alive (clientbound)
152+
(packet.id === 0x1f && is1164 && !is117) ||
153+
(packet.id === 0x21 && is117 && !is119) ||
154+
(packet.id === 0x1e && is119 && !is1191) ||
155+
(packet.id === 0x20 && is1191)
156+
) {
157+
const id = is1191 ? 0x12 : is119 ? 0x11 : is117 ? 0x0f : 0x10
158+
conn
159+
.writePacket(id, packet.data)
160+
.catch(err => conn.emit('error', err))
161+
} else if (
162+
// Disconnect (login) or Disconnect (play)
163+
(packet.id === 0x00 && !conn.loggedIn) ||
164+
(packet.id === 0x19 && conn.loggedIn && is1164 && !is117) ||
165+
(packet.id === 0x1a && conn.loggedIn && is117 && !is119) ||
166+
(packet.id === 0x17 && conn.loggedIn && is119 && !is1191) ||
167+
(packet.id === 0x19 && conn.loggedIn && is1191)
168+
) {
169+
const [chatLength, chatVarIntLength] = readVarInt(packet.data)
170+
conn.disconnectReason = packet.data
171+
.slice(chatVarIntLength, chatVarIntLength + chatLength)
172+
.toString('utf8')
173+
} else if (packet.id === 0x04 && !conn.loggedIn) {
174+
/* Login Plugin Request */
175+
const [msgId] = readVarInt(packet.data)
176+
const rs = concatPacketData([writeVarInt(msgId), false])
177+
conn.writePacket(0x02, rs).catch(err => conn.emit('error', err))
178+
} else if (packet.id === 0x01 && !conn.loggedIn) {
179+
/* Encryption Request */
180+
if (!accessToken || !selectedProfile) {
181+
conn.disconnectReason =
182+
'{"text":"This server requires a premium account to be logged in!"}'
183+
conn.close()
184+
continue
208185
}
209-
conn.emit('packet', packet)
210-
} else break
211-
}
212-
conn.emit('data', newData)
213-
} catch (err) {
214-
conn.emit('error', err)
186+
handleEncryptionRequest(
187+
packet,
188+
accessToken,
189+
selectedProfile,
190+
conn,
191+
is119,
192+
async (secret: Buffer, response: Buffer) => {
193+
const AES_ALG = 'aes-128-cfb8'
194+
conn.aesDecipher = createDecipheriv(AES_ALG, secret, secret)
195+
await conn.writePacket(0x01, response)
196+
conn.aesCipher = createCipheriv(AES_ALG, secret, secret)
197+
}
198+
)
199+
}
200+
conn.emit('packet', packet)
201+
} else break
215202
}
216-
lock.release()
217-
}).then(() => {}, console.error)
218-
})
203+
conn.emit('data', newData)
204+
} catch (err) {
205+
conn.emit('error', err)
206+
}
207+
lock.release()
208+
}).then(() => {}, console.error)
219209
})
210+
return conn
220211
}
221212

222213
export default initiateJavaScriptConnection

0 commit comments

Comments
 (0)