MVP / PoC

This commit is contained in:
Dinhero21 2024-01-07 05:26:32 -03:00
parent ff3c30a482
commit 8a58ee0ae5
16 changed files with 627 additions and 101 deletions

View file

@ -15,5 +15,22 @@
"sourceType": "module",
"project": "tsconfig.json"
},
"rules": {}
"rules": {
"import/order": [
"error",
{
"groups": [
"type",
"index",
"sibling",
"parent",
"internal",
"builtin",
"external",
"object"
]
}
],
"no-case-declarations": "off"
}
}

13
package-lock.json generated
View file

@ -22,6 +22,7 @@
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-n": "^16.6.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-require-extensions": "^0.1.3",
"nodemon": "^3.0.2",
"typescript": "^5.3.3"
}
@ -2039,6 +2040,18 @@
"eslint": "^7.0.0 || ^8.0.0"
}
},
"node_modules/eslint-plugin-require-extensions": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-require-extensions/-/eslint-plugin-require-extensions-0.1.3.tgz",
"integrity": "sha512-T3c1PZ9PIdI3hjV8LdunfYI8gj017UQjzAnCrxuo3wAjneDbTPHdE3oNWInOjMA+z/aBkUtlW5vC0YepYMZIug==",
"dev": true,
"engines": {
"node": ">=16"
},
"peerDependencies": {
"eslint": "*"
}
},
"node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",

View file

@ -32,10 +32,11 @@
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-n": "^16.6.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-require-extensions": "^0.1.3",
"nodemon": "^3.0.2",
"typescript": "^5.3.3"
},
"dependencies": {
"minecraft-protocol": "^1.26.5"
}
}
}

View file

@ -1,112 +1,21 @@
import { createClient, createServer, states } from 'minecraft-protocol'
import Instance from './instance/index.js'
import { createServer } from 'minecraft-protocol'
interface RawPacket {
name: string
data: unknown
}
// See: https://nodejs.org/api/worker_threads.html#considerations-when-transferring-typedarrays-and-buffers
Object.assign(Uint8Array.prototype, Buffer.prototype)
const VERSION = '1.19.4'
const SERVER = {
export const SERVER_OPTIONS = {
host: '127.0.0.1',
port: 25565,
keepAlive: false,
version: VERSION
}
const TARGET = {
host: 'kaboom.pw',
port: 25565,
keepAlive: false,
username: 'Player137',
version: VERSION
}
const server = createServer(SERVER)
export const server = createServer(SERVER_OPTIONS)
server.on('login', client => {
const target = createClient(TARGET)
client.on('end', reason => {
console.info('Client disconnected:', reason)
target.end(reason)
})
target.on('end', reason => {
console.info('Target disconnected:', reason)
client.end(reason)
})
client.on('error', error => {
console.error('Client error:', error)
})
target.on('error', error => {
console.error('Target error:', error)
})
client.on('packet', (data, meta) => {
if (meta.state !== states.PLAY) return
sendTargetPacket({
name: meta.name,
data
})
})
target.on('packet', (data, meta) => {
if (meta.state !== states.PLAY) return
sendClientPacket({
name: meta.name,
data
})
})
const CLIENT_QUEUE: RawPacket[] = []
const TARGET_QUEUE: RawPacket[] = []
client.on('state', state => {
if (state !== states.PLAY) return
for (const packet of CLIENT_QUEUE) client.write(packet.name, packet.data)
CLIENT_QUEUE.length = 0
})
target.on('state', state => {
if (state !== states.PLAY) return
for (const packet of TARGET_QUEUE) target.write(packet.name, packet.data)
TARGET_QUEUE.length = 0
})
function sendClientPacket (packet: RawPacket): void {
if (client.state !== states.PLAY) {
CLIENT_QUEUE.push(packet)
return
}
client.write(
packet.name,
packet.data
)
}
function sendTargetPacket (packet: RawPacket): void {
if (target.state !== states.PLAY) {
TARGET_QUEUE.push(packet)
return
}
target.write(
packet.name,
packet.data
)
}
// eslint-disable-next-line no-new
new Instance(client)
})

91
src/instance/index.ts Normal file
View file

@ -0,0 +1,91 @@
import { type Message } from '../worker/parent.js'
import { importModulesGenerator } from '../util/import-modules.js'
import { Channel } from '../util/channel.js'
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import { Worker } from 'worker_threads'
import { createClient, type ServerClient } from 'minecraft-protocol'
if (!('require' in globalThis)) {
globalThis.__filename = fileURLToPath(import.meta.url)
globalThis.__dirname = dirname(__filename)
}
const WORKER_PATH = resolve(__dirname, '../worker')
const MODULE_DIR_PATH = resolve(__dirname, '../module')
const VERSION = '1.19.4'
export const TARGET_OPTIONS = {
host: 'grandma-does.tech',
port: 25565,
keepAlive: false,
username: 'Player137',
version: VERSION
}
export class Instance {
public readonly client
public readonly server
public readonly worker
constructor (client: ServerClient) {
this.client = client
const target = createClient(TARGET_OPTIONS)
this.server = target
target.on('error', error => {
console.error('Target error:', error)
})
const worker = new Worker(WORKER_PATH)
this.worker = worker
worker.on('error', error => {
console.error('Worker error:', error)
})
void this._importModules()
}
private async _importModules (): Promise<void> {
for await (const module of importModulesGenerator(MODULE_DIR_PATH, 'global.js')) {
if (module === null) throw new Error('Expected module not to be null')
if (typeof module !== 'object') throw new Error('Expected module to be an object')
if (!('default' in module)) throw new Error('Expected default export')
const f = module.default
if (typeof f !== 'function') throw new Error('Expected default export to be a function')
await (f as (instance: Instance) => Promise<void>)(this)
}
}
protected postMessage (channel: string, data: any): void {
this.worker.postMessage({
channel,
data
} satisfies Message)
}
public createChannel<T> (id: string): Channel<T> {
const channel = new Channel<T>(id)
channel._subscribe(data => {
this.postMessage(id, data)
})
this.worker.on('message', (message: Message<T>) => {
if (message.channel !== id) return
channel._write(message.data)
})
return channel
}
}
export default Instance

122
src/module/proxy/global.ts Normal file
View file

@ -0,0 +1,122 @@
import type Instance from '../../instance/index.js'
import { type Message } from './shared.js'
import { type RawPacket } from '../../util/packet.js'
import { states } from 'minecraft-protocol'
export default async function (instance: Instance): Promise<void> {
const clientQueue: RawPacket[] = []
const serverQueue: RawPacket[] = []
const channel = instance.createChannel<Message>('proxy')
const client = instance.client
const server = instance.server
const worker = instance.worker
client.on('end', reason => {
console.info('Client disconnected:', reason)
server.end(reason)
void worker.terminate()
})
server.on('end', reason => {
console.info('Target disconnected:', reason)
client.end(reason)
void worker.terminate()
})
client.on('error', error => {
console.error('Client error:', error)
})
server.on('error', error => {
console.error('Target error:', error)
})
client.on('packet', (data, meta) => {
if (meta.state !== states.PLAY) return
channel.write({
side: 'client',
packet: {
name: meta.name,
data
}
})
})
server.on('packet', (data, meta) => {
if (meta.state !== states.PLAY) return
channel.write({
side: 'server',
packet: {
name: meta.name,
data
}
})
})
client.on('state', state => {
if (state !== states.PLAY) return
const queue = clientQueue
for (const packet of queue) client.write(packet.name, packet.data)
queue.length = 0
})
server.on('state', state => {
if (state !== states.PLAY) return
const queue = serverQueue
for (const packet of queue) server.write(packet.name, packet.data)
queue.length = 0
})
channel.subscribe(({ side, packet }) => {
switch (side) {
case 'client':
writeClientPacket(packet)
break
case 'server':
writeServerPacket(packet)
break
default:
throw new Error(`Invalid side: ${side as any}`)
}
})
function writeClientPacket (packet: RawPacket): void {
if (client.state !== states.PLAY) {
clientQueue.push(packet)
return
}
client.write(
packet.name,
packet.data
)
}
function writeServerPacket (packet: RawPacket): void {
if (server.state !== states.PLAY) {
serverQueue.push(packet)
return
}
server.write(
packet.name,
packet.data
)
}
}

68
src/module/proxy/local.ts Normal file
View file

@ -0,0 +1,68 @@
import { type Side, type Message } from './shared.js'
import { type AsyncVoid, EventHandler } from '../../util/events.js'
import { createChannel } from '../../worker/parent.js'
import { Packet, type RawPacket } from '../../util/packet.js'
export type EventMap = Record<string, (packet: Packet) => AsyncVoid>
export class EventEmitter extends EventHandler<EventMap> {
public async emit (name: string, packet: Packet): Promise<void> {
await this._emit(name, packet)
}
}
// ? Should I export the channel
export const channel = createChannel<Message>('proxy')
export class Proxy {
public readonly client = new EventEmitter()
public readonly server = new EventEmitter()
constructor () {
channel.subscribe(({ side, packet: raw }: Message) => {
void (async () => {
const emitter = this[side]
const packet = new Packet(raw.name, raw.data)
await emitter.emit('packet', packet)
await emitter.emit(packet.name, packet)
if (packet.canceled) return
switch (side) {
case 'client':
side = 'server'
break
case 'server':
side = 'client'
break
default:
throw new Error(`Invalid side: ${side as any}`)
}
channel.write({
side,
packet
})
})()
})
}
protected write (side: Side, packet: RawPacket): void {
channel.write({
side,
packet
})
}
public writeClient (name: string, data: unknown): void {
this.write('client', { name, data })
}
public writeServer (name: string, data: unknown): void {
this.write('server', { name, data })
}
}
export default new Proxy()

View file

@ -0,0 +1,8 @@
import { type RawPacket } from '../../util/packet.js'
export type Side = 'client' | 'server'
export interface Message {
side: Side
packet: RawPacket
}

41
src/util/channel.ts Normal file
View file

@ -0,0 +1,41 @@
// _x -> x
// x -> _x
export class Channel<T> {
public readonly id
constructor (id: string) {
this.id = id
}
private readonly listeners = new Set<(data: T) => void>()
public subscribe (listener: (data: T) => void): void {
this.listeners.add(listener)
}
public unsubscribe (listener: (data: T) => void): void {
this.listeners.delete(listener)
}
public write (data: T): void {
for (const listener of this._listeners) {
listener(data)
}
}
private readonly _listeners = new Set<(data: T) => void>()
public _subscribe (listener: (data: T) => void): void {
this._listeners.add(listener)
}
public _unsubscribe (listener: (data: T) => void): void {
this._listeners.delete(listener)
}
public _write (data: T): void {
for (const listener of this.listeners) {
listener(data)
}
}
}

42
src/util/events.ts Normal file
View file

@ -0,0 +1,42 @@
export type AsyncVoid = void | Promise<void>
export type EventMap<T> = { [K in keyof T]: (...args: any[]) => AsyncVoid }
export class EventHandler<T extends EventMap<T>> {
protected map = new Map<keyof T, Set<T[keyof T]>>()
protected async _emit<E extends keyof T> (name: E, ...data: Parameters<T[E]>): Promise<void> {
const map = this.map
const set = map.get(name)
if (set === undefined) return
for (const listener of set) await listener(...data)
}
public on<E extends keyof T> (name: E, callback: T[E]): void {
const map = this.map
const set = map.get(name) ?? new Set()
map.set(name, set)
set.add(callback)
}
public off<E extends keyof T> (name: E, callback: T[E]): void {
const map = this.map
const set = map.get(name)
if (set === undefined) return
set.delete(callback)
}
public clear (): void {
const map = this.map
map.clear()
}
}

22
src/util/file.ts Normal file
View file

@ -0,0 +1,22 @@
import fs, { access, constants } from 'fs/promises'
import { resolve } from 'path'
export async function * getAllFiles (dir: string): AsyncIterable<string> {
const entries = await fs.readdir(dir, { withFileTypes: true })
for (const entry of entries) {
const res = resolve(dir, entry.name)
if (entry.isDirectory()) {
yield * getAllFiles(res)
} else {
yield res
}
}
}
export async function exists (path: string): Promise<boolean> {
return await access(path, constants.F_OK)
.then(() => true)
.catch(() => false)
}

View file

@ -0,0 +1,26 @@
// export async function * importStarGenerator (directory: string): AsyncGenerator<unknown> {
// for await (const file of getAllFiles(directory)) {
// yield await import(file)
// }
// }
import { exists } from './file.js'
import { readdir } from 'fs/promises'
import { resolve } from 'path'
export async function * importModulesGenerator (directory: string, index: string): AsyncGenerator<unknown> {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const path = resolve(entry.path, entry.name, index)
if (!entry.isDirectory()) throw new Error(`Expected ${path} to be a directory`)
if (!await exists(path)) continue
yield await import(path)
}
}
export async function importModules (directory: string, index: string): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const _ of importModulesGenerator(directory, index));
}

16
src/util/packet.ts Normal file
View file

@ -0,0 +1,16 @@
export interface RawPacket {
name: string
data: unknown
}
export class Packet<T = unknown> {
public name
public data
public canceled: boolean = false
constructor (name: string, data: T) {
this.name = name
this.data = data
}
}

12
src/worker/index.ts Normal file
View file

@ -0,0 +1,12 @@
import { importModules } from '../util/import-modules.js'
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
if (!('require' in globalThis)) {
globalThis.__filename = fileURLToPath(import.meta.url)
globalThis.__dirname = dirname(__filename)
}
const MODULE_DIR_PATH = resolve(__dirname, '../module')
await importModules(MODULE_DIR_PATH, 'local.js')

104
src/worker/module/chat.ts Normal file
View file

@ -0,0 +1,104 @@
// import { type UUID } from 'crypto'
// import { type AsyncVoid, EventHandler } from '../../util/events.js'
// import { type Packet } from '../util/packet.js'
// import proxy from './proxy.js'
// export type i64 = [number, number]
// export interface ClientChatPacketData {
// timestamp: i64
// salt: i64
// acknowledged: Uint8Array
// }
// export type Signature = Uint8Array
// export interface ClientChatMessagePacketData extends ClientChatPacketData {
// message: string
// signature: Signature | undefined
// offset: number
// }
// export interface ClientChatCommandPacketData extends ClientChatPacketData {
// command: string
// argumentSignatures: Signature[]
// messageCount: number
// }
// export interface ProfileLessChatPacketData {
// message: string
// type: number
// name: string
// // TODO: find out what target is supposed to be
// target: undefined
// }
// export interface PlayerChatPacketData {
// senderUuid: UUID
// index: number
// signature: Signature | undefined
// plainMessage: string
// timestamp: i64
// salt: i64
// // TODO: find out what previousMessages is supposed to be
// previousMessages: never[]
// unsignedChatContent: string
// filterType: number
// // TODO: Find out what filterTypeMask is supposed to be
// filterTypeMask: undefined
// type: number
// networkName: string
// // TODO: find out what networkTargetName is supposed to be
// networkTargetName: undefined
// }
// export interface SystemChatPacketData {
// content: string
// isActionBar: boolean
// }
// export interface ChatEventMap {
// 'client.command': (packet: Packet<ClientChatCommandPacketData>) => AsyncVoid
// 'client.message': (packet: Packet<ClientChatMessagePacketData>) => AsyncVoid
// 'server.profiless': (packet: Packet<ProfileLessChatPacketData>) => AsyncVoid
// 'server.player': (packet: Packet<PlayerChatPacketData>) => AsyncVoid
// 'server.system': (packet: Packet<SystemChatPacketData>) => AsyncVoid
// }
// export class Chat extends EventHandler<ChatEventMap> {
// constructor () {
// super()
// proxy.client.on('chat_command', async packet => {
// await this._emit('client.command', packet as Packet<ClientChatCommandPacketData>)
// })
// proxy.client.on('chat_message', async packet => {
// await this._emit('client.message', packet as Packet<ClientChatMessagePacketData>)
// })
// proxy.server.on('profileless_chat', async packet => {
// await this._emit('server.profiless', packet as Packet<ProfileLessChatPacketData>)
// })
// proxy.server.on('player_chat', async packet => {
// await this._emit('server.player', packet as Packet<PlayerChatPacketData>)
// })
// proxy.server.on('system_chat', async packet => {
// await this._emit('server.system', packet as Packet<SystemChatPacketData>)
// })
// }
// // // TODO: Fix
// // public writeClientCommand (command: string): void {
// // proxy.writeClient('chat_command', { command })
// // }
// // public writeClientMessage (message: string): void {
// // proxy.writeClient('chat_message', { message })
// // }
// }
// export default new Chat()

34
src/worker/parent.ts Normal file
View file

@ -0,0 +1,34 @@
import { Channel } from '../util/channel.js'
import { parentPort } from 'worker_threads'
if (parentPort === null) throw new Error('Must run in worker thread')
const port = parentPort
export interface Message<T = any> {
channel: string
data: T
}
function postMessage (channel: string, data: any): void {
port.postMessage({
channel,
data
})
}
export function createChannel<T> (id: string): Channel<T> {
const channel = new Channel<T>(id)
channel._subscribe(message => {
postMessage(id, message)
})
port.on('message', (message: Message<T>) => {
if (message.channel !== id) return
channel._write(message.data)
})
return channel
}