implement multiplayer direct connect, implement network manager, implement handshake and login packets, implement ByteBuf, implement RSA and AES encryption, bump version to 1.1.0

This commit is contained in:
LabyStudio
2022-06-17 06:34:09 +02:00
parent 22872f55d6
commit 410346427f
35 changed files with 2513 additions and 10 deletions
@@ -0,0 +1,169 @@
import ByteBuf from "./util/ByteBuf.js";
import PacketRegistry from "./PacketRegistry.js";
import ProtocolState from "./ProtocolState.js";
import {aesjs} from "../../../../../../libraries/aes.js";
export default class NetworkManager {
constructor(minecraft) {
this.minecraft = minecraft;
this.socket = null;
this.connected = false;
this.networkHandler = null;
this.registry = new PacketRegistry();
this.protocolState = ProtocolState.HANDSHAKE;
this.readBuffer = new ByteBuf();
this.expectedLength = -1;
this.queue = [];
}
setNetworkHandler(networkHandler) {
this.networkHandler = networkHandler;
}
connect(address, port, proxy) {
this.socket = new WebSocket("ws://" + proxy.address + ":" + proxy.port);
this.socket.binaryType = "arraybuffer";
this.socket.onopen = e => this._onOpen(e);
this.socket.onclose = e => this._onClose(e);
this.socket.onmessage = e => this._onMessage(e);
this.socket.onerror = e => this._onError(e);
this.address = address;
this.port = port;
}
_onOpen() {
this.connected = true;
// Send proxy handshake
let object = {
"address": this.address,
"port": this.port,
};
this.socket.send(JSON.stringify(object));
// Handle connect event
this.networkHandler.onConnect();
// Flush packet queue
this.flushPacketQueue();
}
sendPacket(packet) {
if (this.connected) {
this._sendPacketImmediately(packet);
} else {
this.queue.push(packet);
}
}
_sendPacketImmediately(packet) {
// Switch packet state
let packetState = this.registry.getPacketState(packet);
if (packetState !== this.protocolState) {
if (packetState === null) {
console.error("[Network] Tried to send unknown packet: " + packet);
return;
}
console.log("[Network] Switching protocol state from " + ProtocolState.getName(this.protocolState) + " to " + ProtocolState.getName(packetState));
this.protocolState = packetState;
}
// Write packet to buffer
let buffer = new ByteBuf();
buffer.writeByte(this.registry.getClientBoundPacketId(this.protocolState, packet));
packet.write(buffer);
// Write chunk header
let array = buffer.getArray();
let wrapper = new ByteBuf();
wrapper.writeVarInt(array.length);
wrapper.write(array);
let chunk = wrapper.getArray().buffer;
// Decrypt chunk
if (this.isEncrypted) {
chunk = this.encryption.encrypt(new Uint8Array(chunk));
}
// Send chunk
this.socket.send(chunk);
console.log("[Network] [OUT] " + packet.constructor.name);
}
_onMessage(event) {
let chunk = new Int8Array(event.data);
// Decrypt chunk
if (this.isEncrypted) {
chunk = this.encryption.decrypt(new Uint8Array(event.data));
}
// Read packet header
if (this.expectedLength === -1) {
let buf = new ByteBuf(chunk);
this.expectedLength = buf.readVarInt(); // Read packet length
this.readBuffer.setPosition(0);
chunk = chunk.slice(buf.getPosition());
}
// Fill packet content
this.readBuffer.write(chunk);
// Handle packet
if (this.readBuffer.getPosition() >= this.expectedLength) {
this.expectedLength = -1;
this.readBuffer.setPosition(0);
this.onPacketBufferReceived(this.readBuffer);
}
}
onPacketBufferReceived(buffer) {
let packetId = buffer.readByte(); // Read packet id
let clazz = this.registry.getServerBoundById(this.protocolState, packetId);
if (clazz === null) {
console.log("[Network] Unknown packet id: " + packetId);
return;
}
let packet = new clazz;
console.log("[Network] [IN] " + packet.constructor.name);
packet.read(buffer, buffer.length);
packet.handle(this.networkHandler);
}
_onError(event) {
}
_onClose(event) {
if (this.connected) {
this.networkHandler.onDisconnect("Disconnected from server");
}
this.connected = false;
}
close() {
this.connected = false;
this.socket.close();
}
flushPacketQueue() {
this.queue.forEach(packet => this.sendPacket(packet));
this.queue = [];
}
enableEncryption(secretKey) {
this.isEncrypted = true;
this.encryption = new aesjs.ModeOfOperation.cfb(secretKey, secretKey, 1);
}
}
@@ -0,0 +1,19 @@
export default class Packet {
constructor() {
}
write(buffer) {
}
read(buffer) {
}
handle(packetHandler) {
}
}
@@ -0,0 +1,18 @@
export default class PacketHandler {
onConnect() {
}
handleStatusResponse(packet) {
}
handleEncryptionRequest(packet) {
}
onDisconnect() {
}
}
@@ -0,0 +1,95 @@
import ProtocolState from "./ProtocolState.js";
import HandshakePacket from "./packet/handshake/client/HandshakePacket.js";
import StatusQueryPacket from "./packet/status/client/StatusQueryPacket.js";
import LoginStartPacket from "./packet/login/client/LoginStartPacket.js";
import StatusResponsePacket from "./packet/status/server/StatusResponsePacket.js";
import EncryptionRequestPacket from "./packet/login/server/EncryptionRequestPacket.js";
import EncryptionResponsePacket from "./packet/login/client/EncryptionResponsePacket.js";
import LoginDisconnectPacket from "./packet/login/server/LoginDisconnectPacket.js";
export default class PacketRegistry {
constructor() {
this.packetsClient = [];
this.packetsServer = [];
// Register handshake
this.registerClient(ProtocolState.HANDSHAKE, 0x00, HandshakePacket);
// Register server status
this.registerClient(ProtocolState.STATUS, 0x00, StatusQueryPacket);
this.registerServer(ProtocolState.STATUS, 0x00, StatusResponsePacket);
// Register login
this.registerServer(ProtocolState.LOGIN, 0x00, LoginDisconnectPacket);
this.registerServer(ProtocolState.LOGIN, 0x01, EncryptionRequestPacket);
this.registerClient(ProtocolState.LOGIN, 0x00, LoginStartPacket);
this.registerClient(ProtocolState.LOGIN, 0x01, EncryptionResponsePacket);
}
registerClient(state, id, packet) {
this._register(this.packetsClient, state, id, packet);
}
registerServer(state, id, packet) {
this._register(this.packetsServer, state, id, packet);
}
_register(registry, state, id, packet) {
if (typeof registry[state] === "undefined") {
registry[state] = [];
}
registry[state][id] = packet;
}
getServerBoundById(state, id) {
if (typeof this.packetsServer[state][id] === "undefined") {
return null;
}
return this.packetsServer[state][id];
}
getClientBoundById(state, id) {
if (typeof this.packetsClient[state][id] === "undefined") {
return null;
}
return this.packetsClient[state][id];
}
getClientBoundPacketId(state, packet) {
for (let id in this.packetsClient[state]) {
if (this.packetsClient[state][id] === packet.constructor) {
return id;
}
}
return null;
}
getServerBoundPacketId(state, packet) {
for (let id in this.packetsServer[state]) {
if (this.packetsServer[state][id] === packet.constructor) {
return id;
}
}
return null;
}
getPacketState(packet) {
for (const [state, value] of Object.entries(this.packetsClient)) {
for (let id in value) {
if (value[id] === packet.constructor) {
return parseInt(state);
}
}
}
for (const [state, value] of Object.entries(this.packetsServer)) {
for (let id in value) {
if (value[id] === packet.constructor) {
return parseInt(state);
}
}
}
return null;
}
}
@@ -0,0 +1,21 @@
export default class ProtocolState {
static HANDSHAKE = -1;
static PLAY = 0;
static STATUS = 1;
static LOGIN = 2;
static getName(state) {
switch (state) {
case ProtocolState.HANDSHAKE:
return "HANDSHAKE";
case ProtocolState.LOGIN:
return "LOGIN";
case ProtocolState.PLAY:
return "PLAY";
case ProtocolState.STATUS:
return "STATUS";
default:
return "UNKNOWN";
}
}
}
@@ -0,0 +1,31 @@
import PacketHandler from "../PacketHandler.js";
import EncryptionResponsePacket from "../packet/login/client/EncryptionResponsePacket.js";
import CryptManager from "../util/CryptManager.js";
import GuiDisconnected from "../../gui/screens/GuiDisconnected.js";
export default class NetworkLoginHandler extends PacketHandler {
constructor(networkManager) {
super();
this.networkManager = networkManager;
}
handleEncryptionRequest(packet) {
let secretKey = CryptManager.createNewSharedKey();
this.networkManager.sendPacket(new EncryptionResponsePacket(secretKey, packet.publicKey, packet.verifyToken));
// Enable encryption
this.networkManager.enableEncryption(secretKey);
}
handleLoginDisconnect(packet) {
console.log("[Network] Disconnected from server: " + packet.message);
this.networkManager.minecraft.displayScreen(new GuiDisconnected(packet.message));
}
onDisconnect() {
}
}
@@ -0,0 +1,22 @@
import Packet from "../../../Packet.js";
export default class HandshakePacket extends Packet {
constructor(version, nextState) {
super();
this.version = version;
this.nextState = nextState;
}
write(buffer) {
buffer.writeVarInt(this.version); // Protocol version
buffer.writeString("localhost"); // Server address
buffer.writeShort(25565); // Server port
buffer.writeVarInt(this.nextState); // Next state
}
read(buffer) {
}
}
@@ -0,0 +1,22 @@
import Packet from "../../../Packet.js";
import CryptManager from "../../../util/CryptManager.js";
export default class EncryptionResponsePacket extends Packet {
constructor(secretKey, publicKey, verifyToken) {
super();
this.secretKeyEncrypted = CryptManager.encryptRSA(publicKey, secretKey);
this.verifyTokenEncrypted = CryptManager.encryptRSA(publicKey, verifyToken);
}
write(buffer) {
buffer.writeByteArray(this.secretKeyEncrypted);
buffer.writeByteArray(this.verifyTokenEncrypted);
}
read(buffer) {
}
}
@@ -0,0 +1,18 @@
import Packet from "../../../Packet.js";
export default class LoginStartPacket extends Packet {
constructor(username) {
super();
this.username = username;
}
write(buffer) {
buffer.writeString(this.username);
}
read(buffer) {
this.username = buffer.readString();
}
}
@@ -0,0 +1,22 @@
import Packet from "../../../Packet.js";
export default class EncryptionRequestPacket extends Packet {
constructor() {
super();
}
write(buffer) {
}
read(buffer) {
this.serverId = buffer.readString();
this.publicKey = buffer.readByteArray();
this.verifyToken = buffer.readByteArray();
}
handle(handler) {
handler.handleEncryptionRequest(this);
}
}
@@ -0,0 +1,23 @@
import Packet from "../../../Packet.js";
import {format} from "../../../../../../../../../libraries/chat.js";
export default class LoginDisconnectPacket extends Packet {
constructor(message) {
super();
this.message = message;
}
write(buffer) {
buffer.writeString(this.message);
}
read(buffer) {
this.message = format(JSON.parse(buffer.readString()));
}
handle(handler) {
handler.handleLoginDisconnect(this);
}
}
@@ -0,0 +1,14 @@
import Packet from "../../../Packet.js";
export default class StatusQueryPacket extends Packet {
constructor() {
super();
}
write(buffer) {
}
read(buffer) {
}
}
@@ -0,0 +1,20 @@
import Packet from "../../../Packet.js";
export default class StatusResponsePacket extends Packet {
constructor() {
super();
}
write(buffer) {
}
read(buffer) {
this.object = JSON.parse(buffer.readString());
}
handle(packetHandler) {
packetHandler.handleStatusResponse(this)
}
}
@@ -0,0 +1,21 @@
import PacketHandler from "../PacketHandler.js";
import GuiDisconnected from "../../gui/screens/GuiDisconnected.js";
export default class NetworkStatusHandler extends PacketHandler {
constructor(minecraft, callback) {
super();
this.minecraft = minecraft;
this.callback = callback;
}
handleStatusResponse(packet) {
this.callback(packet.object);
}
onDisconnect() {
this.minecraft.displayScreen(new GuiDisconnected("NetworkManager lost"));
}
}
@@ -0,0 +1,26 @@
import NetworkManager from "../NetworkManager.js";
import NetworkStatusHandler from "./NetworkStatusHandler.js";
import Minecraft from "../../Minecraft.js";
import HandshakePacket from "../packet/handshake/client/HandshakePacket.js";
import ProtocolState from "../ProtocolState.js";
import StatusQueryPacket from "../packet/status/client/StatusQueryPacket.js";
export default class ServerPinger {
constructor(minecraft) {
this.minecraft = minecraft;
}
ping(address, port, callback) {
// Connect to server
this.connection = new NetworkManager(this.minecraft);
this.connection.setNetworkHandler(new NetworkStatusHandler(this.minecraft, callback));
this.connection.connect(address, port, Minecraft.PROXY);
// Request status
this.connection.sendPacket(new HandshakePacket(Minecraft.PROTOCOL_VERSION, ProtocolState.STATUS));
this.connection.sendPacket(new StatusQueryPacket());
}
}
@@ -0,0 +1,172 @@
export default class ByteBuf {
static SEGMENT_BITS = 0x7F;
static CONTINUE_BIT = 0x80;
constructor(array = new Uint8Array(0)) {
this.array = array;
this.pos = 0;
}
getPosition() {
return this.pos;
}
setPosition(pos) {
this.pos = pos;
}
length() {
return this.array.length;
}
getArray() {
return this.array;
}
readByte() {
return this.array[this.pos++];
}
readShort() {
return this.array[this.pos++] << 8 | this.array[this.pos++];
}
readInt() {
return this.array[this.pos++] << 24
| this.array[this.pos++] << 16
| this.array[this.pos++] << 8
| this.array[this.pos++];
}
readLong() {
return this.array[this.pos++] << 56
| this.array[this.pos++] << 48
| this.array[this.pos++] << 40
| this.array[this.pos++] << 32
| this.array[this.pos++] << 24
| this.array[this.pos++] << 16
| this.array[this.pos++] << 8
| this.array[this.pos++];
}
readFloat() {
return this.readInt() / (1 << 24);
}
readDouble() {
return this.readLong() / (1 << 53);
}
readString() {
let len = this.readVarInt();
let array = new Uint8Array(len);
this.read(array, len);
return new TextDecoder().decode(array);
}
writeByte(value) {
this.extendIfNeeded(1);
this.array[this.pos++] = value;
}
writeShort(value) {
this.extendIfNeeded(2);
this.array[this.pos++] = value >> 8;
this.array[this.pos++] = value;
}
writeInt(value) {
this.extendIfNeeded(4);
this.array[this.pos++] = value >> 24;
this.array[this.pos++] = value >> 16;
this.array[this.pos++] = value >> 8;
this.array[this.pos++] = value;
}
writeLong(value) {
this.extendIfNeeded(8);
this.array[this.pos++] = value >> 56;
this.array[this.pos++] = value >> 48;
this.array[this.pos++] = value >> 40;
this.array[this.pos++] = value >> 32;
this.array[this.pos++] = value >> 24;
this.array[this.pos++] = value >> 16;
this.array[this.pos++] = value >> 8;
this.array[this.pos++] = value;
}
writeFloat(value) {
this.writeInt(value * (1 << 24));
}
writeDouble(value) {
this.writeLong(value * (1 << 53));
}
writeString(value) {
let array = new TextEncoder().encode(value);
this.writeVarInt(array.length);
this.write(array);
}
writeVarInt(value) {
while (true) {
if ((value & ~ByteBuf.SEGMENT_BITS) === 0) {
this.writeByte(value);
return;
}
this.writeByte((value & ByteBuf.SEGMENT_BITS) | ByteBuf.CONTINUE_BIT);
// Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone
value >>>= 7;
}
}
write(array) {
this.extendIfNeeded(array.length);
for (let i = 0; i < array.length; i++) {
this.array[this.pos++] = array[i];
}
}
extendIfNeeded(bytes) {
if (this.pos + bytes > this.array.length) {
let newArray = new Uint8Array(this.array.length + bytes);
newArray.set(this.array);
this.array = newArray;
}
}
read(array, length) {
for (let i = 0; i < length; i++) {
array[i] = this.array[this.pos++];
}
}
readVarInt() {
let result = 0;
let shift = 0;
while (true) {
let b = this.readByte();
if ((b & ByteBuf.CONTINUE_BIT) === 0) {
return result | (b << shift);
}
result |= (b & ByteBuf.SEGMENT_BITS) << shift;
shift += 7;
}
}
readByteArray() {
let length = this.readVarInt();
let array = [];
this.read(array, length);
return new Int8Array(array);
}
writeByteArray(array) {
this.writeVarInt(array.length);
this.write(array);
}
}
@@ -0,0 +1,56 @@
import Random from "../../../util/Random.js";
import ByteBuf from "./ByteBuf.js";
import {bigIntToBytes, bytesToBigInt, modPow} from "../../../../../../../libraries/modpow.js";
import {parseAsn1} from "../../../../../../../libraries/asn1.js";
export default class CryptManager {
static createNewSharedKey() {
let key = new Uint8Array(16);
window.crypto.getRandomValues(key);
return key;
}
static encryptRSA(publicKey, data) {
// Parse asn1 public key
let asn1 = parseAsn1(new Uint8Array(publicKey))
// Extract n an e of the public key
let n = bytesToBigInt(asn1.children[1].children[0].children[0].value);
let e = bytesToBigInt(asn1.children[1].children[0].children[1].value);
// Check length of public key
let length = (n.toString(2).length + 7) >> 3;
if (length < data.length + 11) {
throw new Error("Data is too long to encrypt");
}
let buffer = new ByteBuf(new Uint8Array(data).reverse());
buffer.setPosition(buffer.length());
// Add padding
buffer.writeByte(0x0);
let random = new Random();
let x = [];
while (buffer.length() < length - 2) {
x[0] = 0;
while (x[0] === 0) random.nextBytes(x, x.length);
buffer.writeByte(x[0]);
}
buffer.writeByte(0x2);
buffer.writeByte(0x0);
// Reverse data
let reversed = buffer.getArray().reverse();
// Convert to bigint
let bigInt = bytesToBigInt(reversed);
// Encrypt
bigInt = modPow(bigInt, e, n);
// Convert to bytes
return bigIntToBytes(bigInt);
}
}