Skip to content

Commit

Permalink
Finish initial ConnectionModule (not working atm).
Browse files Browse the repository at this point in the history
  • Loading branch information
retrixe committed Aug 28, 2022
1 parent 175d6b2 commit 031c008
Show file tree
Hide file tree
Showing 5 changed files with 383 additions and 126 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@ import android.util.Base64
import com.enderchat.modules.connection.datatypes.Packet
import com.enderchat.modules.connection.datatypes.VarInt
import com.enderchat.modules.connection.datatypes.writeString
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.*
import com.facebook.react.modules.core.DeviceEventManagerModule
import kotlinx.coroutines.*
import java.io.ByteArrayOutputStream
import java.net.Socket
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.UUID
import java.util.concurrent.locks.ReentrantReadWriteLock
import javax.crypto.Cipher
import kotlin.concurrent.read
import kotlin.concurrent.thread
import kotlin.concurrent.write
Expand All @@ -25,25 +24,64 @@ class ConnectionModule(reactContext: ReactApplicationContext)
private val lock = ReentrantReadWriteLock()
private var socket: Socket? = null
private var connectionId: UUID? = null
private var readThread: Thread? = null
private var writeThread: Thread? = null
private var compressionThreshold = -1
private var compressionEnabled = false
// TODO: AES ciphers for reading and writing.
// TODO: Use AES ciphers for reading and writing. Maybe JS can initialise these? Or do we handle Encryption Request?
private var aesDecipher: Cipher? = null
private var aesCipher: Cipher? = null
private var loggedIn = false

override fun getName() = "ConnectionModule"

private fun directlyCloseConnection() {
if (socket?.isClosed == false) socket!!.close() // We hold a lock lol
try {
if (socket?.isClosed == false) socket!!.close() // We hold a lock, this won't mutate.
} catch (_: Exception) {}
socket = null
readThread = null
writeThread = null
connectionId = null
compressionThreshold = -1
compressionEnabled = false
aesDecipher = null
aesCipher = null
loggedIn = false
}

@ReactMethod fun closeConnection() = lock.write { directlyCloseConnection() }
private fun directlyWriteToConnection(id: Int, data: ByteArray): Boolean {
val packet = Packet(id, data)
var asBytes =
if (compressionEnabled) packet.writeCompressedPacket(compressionThreshold)
else packet.writePacket()
val socket = socket!!
val socketIsOpen = !socket.isClosed && !socket.isOutputShutdown
if (socketIsOpen) {
if (aesCipher != null) {
asBytes = aesCipher!!.update(asBytes)
}
socket.getOutputStream().write(asBytes)
}
return socketIsOpen
}

@ReactMethod fun writeToConnection(
connId: String, packetId: Int, data: String, promise: Promise
) = runBlocking {
launch(Dispatchers.Default) {
lock.read {
if (connId == connectionId.toString()) {
try {
val dataBytes = Base64.decode(data, Base64.DEFAULT)
promise.resolve(directlyWriteToConnection(packetId, dataBytes))
} catch (e: Exception) {
promise.reject(e)
}
} else promise.resolve(false)
}
}
}

@ReactMethod fun closeConnection(id: String) = lock.write {
if (id == connectionId.toString()) directlyCloseConnection()
}

@ReactMethod fun openConnection(opts: ReadableMap, promise: Promise) {
val host = opts.getString("host")!!
Expand All @@ -62,15 +100,14 @@ class ConnectionModule(reactContext: ReactApplicationContext)

lock.writeLock().lock()
val socket: Socket
val connectionId = UUID.randomUUID()
try {
// Only one connection at a time.
directlyCloseConnection()

// Create socket, connection ID, read thread and write thread.
// Create socket and connection ID.
socket = Socket(host, port)
connectionId = UUID.randomUUID()
readThread = Thread.currentThread()
// TODO: What about the writeThread? Implement the ability to write from JS.
this.connectionId = UUID.randomUUID()

// Create data to send in Handshake.
val portBuf = ByteBuffer.allocate(2)
Expand All @@ -91,39 +128,122 @@ class ConnectionModule(reactContext: ReactApplicationContext)
// Update the current socket and resolve/reject.
this.socket = socket
lock.writeLock().unlock()
promise.resolve(true)
promise.resolve(connectionId.toString())
} catch (e: Exception) {
directlyCloseConnection()
lock.writeLock().unlock()
promise.reject(e)
return@thread
}

// TODO: Dispatch error events to JS when errors are encountered.
// Calculate the necessary packet IDs.
val is1164 = protocolVersion >= PROTOCOL_VERSION_1164
val is117 = protocolVersion >= PROTOCOL_VERSION_117
val is119 = protocolVersion >= PROTOCOL_VERSION_119
val is1191 = protocolVersion >= PROTOCOL_VERSION_1191
val keepAliveClientBoundId =
if (is1191) 0x20
else if (is119) 0x1e
else if (is117) 0x21
else if (is1164) 0x1f
else 0x1f
val keepAliveServerBoundId =
if (is1191) 0x12
else if (is119) 0x11
else if (is117) 0x0f
else if (is1164) 0x10
else 0x10
val loginSuccessId = 0x02
val setCompressionId = 0x03

// Re-use the current thread, start reading from the socket.
val buffer = ByteArrayOutputStream()
val buf = ByteArray(1024)
while (lock.read { this.socket == socket }) {
val n = socket.getInputStream().read(buf)
if (n == -1)
break
buffer.write(buf, 0, n)

// Read packet.
val bytes = buffer.toByteArray()
val packet =
if (compressionEnabled) Packet.readCompressed(bytes) ?: continue
else Packet.read(bytes) ?: continue
// Reset the buffer, we've been reading byte-by-byte so this is fine to do.
buffer.reset()
// We know packet.totalLength exists for read/readCompressed.
buffer.write(bytes, packet.totalLength!!, bytes.size - packet.totalLength)

// TODO: We can handle Keep Alive, Login Success and Set Compression.
// TODO: Forward the rest to JavaScript. (Actually, forward Login Success too.)
val buf = ByteArray(4096)
var aesDecipher: Cipher?
while (lock.read {
aesDecipher = this.aesDecipher
return@read this.socket == socket
}) {
try {
val n = socket.getInputStream().read(buf)
if (n == -1)
break

// Decrypt if necessary.
if (aesDecipher != null) {
buffer.write(aesDecipher!!.update(buf, 0, n))
} else {
buffer.write(buf, 0, n)
}

// Read packet.
val bytes = buffer.toByteArray()
val packet =
if (compressionEnabled) Packet.readCompressed(bytes) ?: continue
else Packet.read(bytes) ?: continue
// Reset the buffer, we've been reading byte-by-byte so this is fine to do.
buffer.reset() // We know packet.totalLength exists for read/readCompressed.
buffer.write(bytes, packet.totalLength!!, bytes.size - packet.totalLength)

// We can handle Keep Alive, Login Success and Set Compression.
// TODO: Maybe handle Keep Alive in JS. The overhead is minimal and would take away the disconnect timer from here.
if (packet.id.value == keepAliveClientBoundId) {
directlyWriteToConnection(keepAliveServerBoundId, packet.data)
continue
} else if (packet.id.value == setCompressionId && !loggedIn) {
val threshold = VarInt.read(packet.data)?.value ?: 0
compressionThreshold = threshold
compressionEnabled = threshold >= 0
} else if (packet.id.value == loginSuccessId && !loggedIn) {
loggedIn = true // Login Success
}

// Forward the packet to JavaScript.
val packetLengthLength =
packet.totalLength - packet.data.size - packet.id.data.size
val params = Arguments.createMap().apply {
putString("connectionId", connectionId.toString())
putDouble("id", packet.id.value.toDouble())
putString("data", Base64.encodeToString(packet.data, Base64.DEFAULT))
putBoolean("compressed", compressionEnabled)
putDouble("idLength", packet.id.data.size.toDouble())
putDouble("dataLength", packet.data.size.toDouble())
putDouble("packetLength", packet.totalLength.toDouble())
putDouble("lengthLength", packetLengthLength.toDouble())
}
sendEvent(reactContext = reactApplicationContext, "packet", params)
} catch (e: Exception) {
lock.write { directlyCloseConnection() }
val params = Arguments.createMap().apply {
putString("connectionId", connectionId.toString())
putString("stackTrace", e.stackTraceToString())
putString("message", e.message)
}
sendEvent(reactContext = reactApplicationContext, "error", params)
}
}

// 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())
}
buffer.close()
// TODO: Dispatch close event to JS.
sendEvent(reactContext = reactApplicationContext, "close", params)
}
}

private fun sendEvent(reactContext: ReactContext, eventName: String, params: WritableMap?) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(eventName, params)
}

@ReactMethod fun addListener(/* eventName: String */) {
// Set up any upstream listeners or background tasks as necessary
}

@ReactMethod fun removeListeners(/* count: Int */) {
// Remove upstream listeners, stop unnecessary background tasks
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import java.io.ByteArrayOutputStream
import java.util.zip.Deflater
import java.util.zip.Inflater

const val PROTOCOL_VERSION_1164 = 754
const val PROTOCOL_VERSION_117 = 755
const val PROTOCOL_VERSION_119 = 759
const val PROTOCOL_VERSION_1191 = 760

fun compressData(bytes: ByteArray): ByteArray {
ByteArrayOutputStream(bytes.size).use {
val deflater = Deflater().apply {
Expand Down
49 changes: 1 addition & 48 deletions src/minecraft/connection/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from '../utils'
import { joinMinecraftSession } from '../api/mojang'
import { ServerConnection, ConnectionOptions } from '.'
import { getLoginPacket, parseEncryptionRequestPacket } from './shared'

export declare interface JavaScriptServerConnection {
on: ((event: 'packet', listener: (packet: Packet) => void) => this) &
Expand Down Expand Up @@ -91,37 +92,6 @@ export class JavaScriptServerConnection
}
}

const getLoginPacket = (opts: ConnectionOptions) => {
const data: PacketDataTypes[] = [opts.username]
if (opts.protocolVersion >= protocolMap[1.19]) {
data.push(!!opts.certificate)
if (opts.certificate) {
let buf = Buffer.alloc(8)
buf.writeIntBE(new Date(opts.certificate.expiresAt).getTime(), 2, 6) // writeBigInt64BE
data.push(buf)
const publicKeyBase64Data = opts.certificate.keyPair.publicKey
.replace(/\n/g, '')
.replace('-----BEGIN RSA PUBLIC KEY-----', '')
.replace('-----END RSA PUBLIC KEY-----', '')
.trim()
buf = Buffer.from(publicKeyBase64Data, 'base64')
data.push(writeVarInt(buf.byteLength))
data.push(buf)
buf = Buffer.from(opts.certificate.publicKeySignature, 'base64')
data.push(writeVarInt(buf.byteLength))
data.push(buf)
}
if (opts.protocolVersion >= protocolMap['1.19.1']) {
if (opts.selectedProfile) {
const msb = Buffer.from(opts.selectedProfile.substring(0, 16), 'hex')
const lsb = Buffer.from(opts.selectedProfile.substring(16), 'hex')
data.push(concatPacketData([true, msb, lsb]))
} else data.push(concatPacketData([false]))
}
}
return concatPacketData(data)
}

const initiateJavaScriptConnection = async (opts: ConnectionOptions) => {
const [host, port] = await resolveHostname(opts.host, opts.port)
return await new Promise<ServerConnection>((resolve, reject) => {
Expand Down Expand Up @@ -294,21 +264,4 @@ const initiateJavaScriptConnection = async (opts: ConnectionOptions) => {
})
}

const parseEncryptionRequestPacket = (packet: Packet) => {
// ASCII encoding of the server id string
let data = packet.data
const [sidLen, sidLenLen] = readVarInt(data)
const serverId = data.slice(sidLenLen, sidLenLen + sidLen)
// Server's encoded public key
data = data.slice(sidLen + sidLenLen)
const [pkLen, pkLenLen] = readVarInt(data)
const publicKey = data.slice(pkLenLen, pkLen + pkLenLen)
// Server's randomly generated verify token
data = data.slice(pkLen + pkLenLen)
const [vtLen, vtLenLen] = readVarInt(data)
const verifyToken = data.slice(vtLenLen, vtLenLen + vtLen)

return [serverId, publicKey, verifyToken]
}

export default initiateJavaScriptConnection
Loading

0 comments on commit 031c008

Please sign in to comment.