mirror of
https://github.com/barkeser2002/node-minecraft-protocol.git
synced 2026-09-25 08:30:08 +03:00
Replace custom SOCKS/HTTP protocol implementations with simple wrapper
functions that leverage existing packages and examples:
- Use 'socks' package for SOCKS4/5 (like existing client_socks_proxy example)
- Use built-in http module for HTTP CONNECT (like existing client_http_proxy example)
- Auto-generate connect function instead of complex custom protocols
- Add socks and proxy-agent as dependencies
- Fix test SOCKS5 proxy to send proper response format
Benefits:
- Reduced from 267 to ~100 lines of code
- Leverages battle-tested external packages
- Reuses proven patterns from existing examples
- Same simple user API: proxy: { type: 'socks5', host: '...', port: 1080 }
- Better reliability and maintainability
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
449 lines
17 KiB
JavaScript
449 lines
17 KiB
JavaScript
/* eslint-env mocha */
|
|
|
|
const mc = require('../')
|
|
const os = require('os')
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
const assert = require('power-assert')
|
|
const util = require('util')
|
|
const applyClientHelpers = require('./common/clientHelpers')
|
|
const download = util.promisify(require('minecraft-wrap').download)
|
|
const { getPort } = require('./common/util')
|
|
|
|
const SURVIVE_TIME = 10000
|
|
const MC_SERVER_PATH = path.join(__dirname, 'server')
|
|
|
|
const Wrap = require('minecraft-wrap').Wrap
|
|
|
|
for (const supportedVersion of mc.supportedVersions) {
|
|
let PORT = null
|
|
const mcData = require('minecraft-data')(supportedVersion)
|
|
const version = mcData.version
|
|
const MC_SERVER_JAR_DIR = process.env.MC_SERVER_JAR_DIR || os.tmpdir()
|
|
const MC_SERVER_JAR = MC_SERVER_JAR_DIR + '/minecraft_server.' + version.minecraftVersion + '.jar'
|
|
const MC_SERVER_DIR = MC_SERVER_PATH + '_' + supportedVersion
|
|
const wrap = new Wrap(MC_SERVER_JAR, MC_SERVER_DIR, {
|
|
minMem: 1024,
|
|
maxMem: 1024
|
|
})
|
|
wrap.on('line', function (line) {
|
|
console.log(line)
|
|
})
|
|
|
|
describe('client ' + supportedVersion + 'v', function () {
|
|
this.timeout(10 * 60 * 1000)
|
|
|
|
before(async function () {
|
|
this.timeout(30 * 1000)
|
|
await download(version.minecraftVersion, MC_SERVER_JAR)
|
|
PORT = await getPort()
|
|
console.log(`Port chosen: ${PORT}`)
|
|
})
|
|
|
|
after(async () => {
|
|
await new Promise((resolve, reject) => {
|
|
wrap.deleteServerData(err => {
|
|
if (err) reject(err)
|
|
resolve()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('offline', function () {
|
|
this.timeout(240 * 1000)
|
|
before(async () => {
|
|
console.log(new Date() + 'starting server ' + version.minecraftVersion)
|
|
await new Promise((resolve, reject) => {
|
|
wrap.startServer({
|
|
'online-mode': 'false',
|
|
'server-port': PORT,
|
|
motd: 'test1234',
|
|
'max-players': 120,
|
|
'level-type': 'flat',
|
|
'generate-structures': 'false', // 12m
|
|
'use-native-transport': 'false' // java 16 throws errors without this, https://www.spigotmc.org/threads/unable-to-access-address-of-buffer.311602
|
|
}, (err) => {
|
|
if (err) reject(err)
|
|
resolve()
|
|
})
|
|
})
|
|
console.log(new Date() + 'started server ' + version.minecraftVersion)
|
|
})
|
|
|
|
after(function (done) {
|
|
console.log(new Date() + 'stopping server' + version.minecraftVersion)
|
|
wrap.stopServer(function (err) {
|
|
if (err) { console.log(err) }
|
|
console.log(new Date() + 'stopped server ' + version.minecraftVersion)
|
|
done(err)
|
|
})
|
|
})
|
|
|
|
it('pings the server', function (done) {
|
|
mc.ping({
|
|
version: version.minecraftVersion,
|
|
port: PORT
|
|
}, function (err, results) {
|
|
if (err) return done(err)
|
|
assert.ok(results.latency >= 0)
|
|
assert.ok(results.latency <= 1000)
|
|
delete results.latency
|
|
delete results.favicon // too lazy to figure it out
|
|
/* assert.deepEqual(results, {
|
|
version: {
|
|
name: '1.7.4',
|
|
protocol: 4
|
|
},
|
|
description: { text: "test1234" }
|
|
}); */
|
|
done()
|
|
})
|
|
})
|
|
|
|
// chat/Style.java
|
|
const CLICK_EVENT = mcData.version['>=']('1.21.5') ? 'click_event' : 'clickEvent'
|
|
it('connects successfully - offline mode', function (done) {
|
|
const client = applyClientHelpers(mc.createClient({
|
|
username: 'Player',
|
|
version: version.minecraftVersion,
|
|
port: PORT,
|
|
auth: 'offline'
|
|
}))
|
|
client.on('error', err => done(err))
|
|
|
|
client.on('state', (state) => {
|
|
console.log('Client now in state', state)
|
|
})
|
|
|
|
// ** Dump some server data **
|
|
fs.rmSync(MC_SERVER_DIR + '_registry_data.json', { force: true })
|
|
client.on('raw.registry_data', (buffer) => {
|
|
fs.writeFileSync(MC_SERVER_DIR + '_registry_data.bin', buffer)
|
|
})
|
|
client.on('registry_data', (json) => {
|
|
if (json.codec) { // Pre 1.20.5, codec is 1 json
|
|
fs.writeFileSync(MC_SERVER_DIR + '_registry_data.json', JSON.stringify(json))
|
|
} else { // 1.20.5+, codec is many nbt's each with their own ids, merge them
|
|
let currentData = {}
|
|
if (fs.existsSync(MC_SERVER_DIR + '_registry_data.json')) {
|
|
currentData = JSON.parse(fs.readFileSync(MC_SERVER_DIR + '_registry_data.json', 'utf8'))
|
|
}
|
|
currentData[json.id] = json
|
|
fs.writeFileSync(MC_SERVER_DIR + '_registry_data.json', JSON.stringify(currentData))
|
|
}
|
|
console.log('Wrote registry data')
|
|
})
|
|
client.on('login', (packet) => {
|
|
fs.writeFileSync(MC_SERVER_DIR + '_login.json', JSON.stringify(packet))
|
|
if (fs.existsSync(MC_SERVER_DIR + '_registry_data.json')) {
|
|
// generate a loginPacket.json for minecraft-data
|
|
const codec = JSON.parse(fs.readFileSync(MC_SERVER_DIR + '_registry_data.json'))
|
|
fs.writeFileSync(MC_SERVER_DIR + '_loginPacket.json', JSON.stringify({
|
|
...packet,
|
|
dimensionCodec: codec.codec || codec
|
|
}, null, 2))
|
|
console.log('Wrote loginPacket.json')
|
|
}
|
|
})
|
|
// ** End dumping code **
|
|
|
|
const lineListener = function (line) {
|
|
const match = line.match(/\[Server thread\/INFO\]: (?:\[Not Secure\] )?<(.+?)> (.+)/)
|
|
if (!match) return
|
|
assert.strictEqual(match[1], 'Player')
|
|
assert.strictEqual(match[2], 'hello everyone; I have logged in.')
|
|
wrap.writeServer('say hello\n')
|
|
wrap.off('line', lineListener)
|
|
}
|
|
wrap.on('line', lineListener)
|
|
let chatCount = 0
|
|
|
|
client.on('login', function (packet) {
|
|
if (packet.worldState) { // 1.20.5+
|
|
assert.strictEqual(packet.worldState.gamemode, 'survival')
|
|
} else {
|
|
assert.strictEqual(packet.gameMode, 0)
|
|
}
|
|
client.chat('hello everyone; I have logged in.')
|
|
})
|
|
client.on('playerChat', function (data) {
|
|
chatCount += 1
|
|
assert.ok(chatCount <= 2)
|
|
|
|
if (!mcData.supportFeature('clientsideChatFormatting')) {
|
|
const message = JSON.parse(data.formattedMessage)
|
|
if (chatCount === 1) {
|
|
assert.strictEqual(message.translate, 'chat.type.text')
|
|
assert.deepEqual(message.with[0].clickEvent, {
|
|
action: 'suggest_command',
|
|
value: mcData.version.version > 340 ? '/tell Player ' : '/msg Player '
|
|
})
|
|
assert.deepEqual(message.with[0].text, 'Player')
|
|
assert.strictEqual(message.with[1], 'hello everyone; I have logged in.')
|
|
} else if (chatCount === 2) {
|
|
assert.strictEqual(message.translate, 'chat.type.announcement')
|
|
assert.strictEqual(message.with[0].text ? message.with[0].text : message.with[0], 'Server')
|
|
assert.deepEqual(message.with[1].extra
|
|
? (message.with[1].extra[0].text
|
|
? message.with[1].extra[0].text
|
|
: message.with[1].extra[0])
|
|
: message.with[1].text, 'hello')
|
|
wrap.removeListener('line', lineListener)
|
|
client.end()
|
|
done()
|
|
}
|
|
} else {
|
|
// 1.19+
|
|
const sender = JSON.parse(data.senderName)
|
|
const msgPayload = data.formattedMessage ? JSON.parse(data.formattedMessage) : data.plainMessage
|
|
const plainMessage = client.parseMessage(msgPayload).toString()
|
|
|
|
if (chatCount === 1) {
|
|
assert.strictEqual(plainMessage, 'hello everyone; I have logged in.')
|
|
const clickEvent = sender[CLICK_EVENT]
|
|
assert.strictEqual(clickEvent.action, 'suggest_command')
|
|
assert.ok(['/tell Player ', '/msg Player '].includes(clickEvent.value || clickEvent.command))
|
|
assert.strictEqual(sender.text, 'Player')
|
|
} else if (chatCount === 2) {
|
|
const plainSender = client.parseMessage(sender).toString()
|
|
assert.strictEqual(plainMessage, 'hello')
|
|
assert.strictEqual(plainSender, 'Server')
|
|
wrap.removeListener('line', lineListener)
|
|
client.end()
|
|
done()
|
|
}
|
|
}
|
|
})
|
|
|
|
client.on('systemChat', function (data) {
|
|
// For 1.7.10
|
|
chatCount += 1
|
|
assert.ok(chatCount <= 2)
|
|
|
|
const message = JSON.parse(data.formattedMessage)
|
|
if (chatCount === 1) {
|
|
assert.strictEqual(message.translate, 'chat.type.text')
|
|
assert.deepEqual(message.with[0][CLICK_EVENT], {
|
|
action: 'suggest_command',
|
|
value: mcData.version.version > 340 ? '/tell Player ' : '/msg Player '
|
|
})
|
|
assert.deepEqual(message.with[0].text, 'Player')
|
|
assert.strictEqual(message.with[1], 'hello everyone; I have logged in.')
|
|
} else if (chatCount === 2) {
|
|
assert.strictEqual(message.translate, 'chat.type.announcement')
|
|
assert.strictEqual(message.with[0].text ? message.with[0].text : message.with[0], 'Server')
|
|
assert.deepEqual(message.with[1].extra
|
|
? (message.with[1].extra[0].text
|
|
? message.with[1].extra[0].text
|
|
: message.with[1].extra[0])
|
|
: message.with[1].text, 'hello')
|
|
wrap.removeListener('line', lineListener)
|
|
client.end()
|
|
done()
|
|
}
|
|
})
|
|
})
|
|
|
|
it('does not crash for ' + SURVIVE_TIME + 'ms', function (done) {
|
|
const client = applyClientHelpers(mc.createClient({
|
|
username: 'Player',
|
|
version: version.minecraftVersion,
|
|
port: PORT,
|
|
auth: 'offline'
|
|
}))
|
|
client.on('error', err => done(err))
|
|
client.on('login', function () {
|
|
client.chat('hello everyone; I have logged in.')
|
|
setTimeout(function () {
|
|
client.end()
|
|
done()
|
|
}, SURVIVE_TIME)
|
|
})
|
|
})
|
|
|
|
it('produce a decent error when connecting with the wrong version', function (done) {
|
|
if (process.platform === 'win32') return done()
|
|
const client = mc.createClient({
|
|
username: 'Player',
|
|
version: version.minecraftVersion === '1.8.8' ? '1.11.2' : '1.8.8',
|
|
port: PORT,
|
|
auth: 'offline'
|
|
})
|
|
client.once('error', function (err) {
|
|
if (err.message.startsWith('This server is version')) {
|
|
console.log(new Date() + 'Correctly got an error for wrong version : ' + err.message)
|
|
client.end()
|
|
done()
|
|
} else {
|
|
client.end()
|
|
done(err)
|
|
}
|
|
client.on('error', err => {
|
|
if (err.message.indexOf('ECONNRESET') === -1) {
|
|
done(err)
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
it('connects through SOCKS5 proxy', function (done) {
|
|
const net = require('net')
|
|
let proxyPort
|
|
let proxyServer
|
|
|
|
// Simple SOCKS5 proxy that forwards to MC server
|
|
const createProxy = () => {
|
|
return net.createServer((clientSocket) => {
|
|
let stage = 'auth'
|
|
|
|
clientSocket.on('data', (data) => {
|
|
if (stage === 'auth') {
|
|
if (data[0] === 0x05) {
|
|
clientSocket.write(Buffer.from([0x05, 0x00])) // No auth required
|
|
stage = 'connect'
|
|
}
|
|
} else if (stage === 'connect') {
|
|
if (data[0] === 0x05 && data[1] === 0x01) {
|
|
// Parse the connect request properly (for completeness)
|
|
// We don't actually need to parse it since we forward to fixed target
|
|
|
|
// Forward to MC server
|
|
const serverSocket = net.createConnection(PORT, 'localhost', () => {
|
|
// Send proper SOCKS5 success response
|
|
const response = Buffer.alloc(10)
|
|
response[0] = 0x05 // Version
|
|
response[1] = 0x00 // Success
|
|
response[2] = 0x00 // Reserved
|
|
response[3] = 0x01 // IPv4
|
|
response.writeUInt32BE(0x7f000001, 4) // 127.0.0.1
|
|
response.writeUInt16BE(PORT, 8) // Port
|
|
clientSocket.write(response)
|
|
|
|
clientSocket.pipe(serverSocket)
|
|
serverSocket.pipe(clientSocket)
|
|
})
|
|
serverSocket.on('error', () => clientSocket.destroy())
|
|
}
|
|
}
|
|
})
|
|
clientSocket.on('error', () => {})
|
|
})
|
|
}
|
|
|
|
// Start proxy server
|
|
const { getPort } = require('./common/util')
|
|
getPort().then(port => {
|
|
proxyPort = port
|
|
proxyServer = createProxy()
|
|
proxyServer.listen(proxyPort, () => {
|
|
// Connect through proxy
|
|
const client = mc.createClient({
|
|
username: 'ProxyUser',
|
|
version: version.minecraftVersion,
|
|
port: PORT,
|
|
auth: 'offline',
|
|
proxy: {
|
|
type: 'socks5',
|
|
host: 'localhost',
|
|
port: proxyPort
|
|
}
|
|
})
|
|
|
|
client.on('login', () => {
|
|
console.log('✓ Connected through SOCKS5 proxy!')
|
|
client.end()
|
|
proxyServer.close()
|
|
done()
|
|
})
|
|
|
|
client.on('error', (err) => {
|
|
proxyServer.close()
|
|
done(err)
|
|
})
|
|
})
|
|
}).catch(done)
|
|
})
|
|
})
|
|
|
|
describe.skip('online', function () {
|
|
before(function (done) {
|
|
console.log(new Date() + 'starting server ' + version.minecraftVersion)
|
|
wrap.startServer({
|
|
'online-mode': 'true',
|
|
'server-port': PORT,
|
|
'use-native-transport': 'false' // java 16 throws errors without this, https://www.spigotmc.org/threads/unable-to-access-address-of-buffer.311602
|
|
}, function (err) {
|
|
if (err) { console.log(err) }
|
|
console.log(new Date() + 'started server ' + version.minecraftVersion)
|
|
done(err)
|
|
})
|
|
})
|
|
|
|
after(function (done) {
|
|
console.log(new Date() + 'stopping server ' + version.minecraftVersion)
|
|
wrap.stopServer(function (err) {
|
|
if (err) { console.log(err) }
|
|
console.log(new Date() + 'stopped server ' + version.minecraftVersion)
|
|
done(err)
|
|
})
|
|
})
|
|
|
|
it('connects successfully - online mode', function (done) {
|
|
const client = applyClientHelpers(mc.createClient({
|
|
username: process.env.MC_USERNAME,
|
|
password: process.env.MC_PASSWORD,
|
|
version: version.minecraftVersion,
|
|
port: PORT
|
|
}))
|
|
client.on('error', err => done(err))
|
|
const lineListener = function (line) {
|
|
const match = line.match(/\[Server thread\/INFO\]: <(.+?)> (.+)/)
|
|
if (!match) return
|
|
assert.strictEqual(match[1], client.username)
|
|
assert.strictEqual(match[2], 'hello everyone; I have logged in.')
|
|
wrap.writeServer('say hello\n')
|
|
}
|
|
wrap.on('line', lineListener)
|
|
client.on('login', function (packet) {
|
|
assert.strictEqual(packet.levelType, 'default')
|
|
assert.strictEqual(packet.difficulty, 1)
|
|
assert.strictEqual(packet.dimension, 0)
|
|
assert.strictEqual(packet.gameMode, 0)
|
|
client.chat('hello everyone; I have logged in.')
|
|
})
|
|
let chatCount = 0
|
|
client.on('chat', function (packet) {
|
|
chatCount += 1
|
|
assert.ok(chatCount <= 2)
|
|
if (chatCount === 2) {
|
|
client.removeAllListeners('chat')
|
|
wrap.removeListener('line', lineListener)
|
|
client.end()
|
|
done()
|
|
}
|
|
})
|
|
})
|
|
|
|
it('gets kicked when no credentials supplied in online mode', function (done) {
|
|
const client = mc.createClient({
|
|
username: 'Player',
|
|
version: version.minecraftVersion,
|
|
port: PORT,
|
|
auth: 'offline'
|
|
})
|
|
client.on('error', err => done(err))
|
|
let gotKicked = false
|
|
client.on('disconnect', function (packet) {
|
|
assert.ok(packet.reason.indexOf('"Failed to verify username!"') !== -1 || packet.reason.indexOf('multiplayer.disconnect.unverified_username') !== -1)
|
|
gotKicked = true
|
|
})
|
|
client.on('end', function () {
|
|
assert.ok(gotKicked)
|
|
client.end()
|
|
done()
|
|
})
|
|
})
|
|
})
|
|
})
|
|
}
|