pornofon
This commit is contained in:
parent
16b6f2f2d6
commit
8b31dfa980
174
ByteStream/ByteStream.js
Normal file
174
ByteStream/ByteStream.js
Normal file
@ -0,0 +1,174 @@
|
||||
class ByteStream {
|
||||
constructor(buffer = null) {
|
||||
this.buffer = buffer || Buffer.alloc(0)
|
||||
this.offset = 0
|
||||
}
|
||||
|
||||
readUInt8() {
|
||||
if (this.offset >= this.buffer.length) {
|
||||
throw new Error("ByteStream: Cannot read beyond buffer length")
|
||||
}
|
||||
const value = this.buffer.readUInt8(this.offset)
|
||||
this.offset += 1
|
||||
return value
|
||||
}
|
||||
|
||||
readUInt16() {
|
||||
if (this.offset + 2 > this.buffer.length) {
|
||||
throw new Error("ByteStream: Cannot read beyond buffer length")
|
||||
}
|
||||
const value = this.buffer.readUInt16BE(this.offset)
|
||||
this.offset += 2
|
||||
return value
|
||||
}
|
||||
|
||||
readUInt24() {
|
||||
if (this.offset + 3 > this.buffer.length) {
|
||||
throw new Error("ByteStream: Cannot read beyond buffer length")
|
||||
}
|
||||
const value =
|
||||
(this.buffer.readUInt8(this.offset) << 16) |
|
||||
(this.buffer.readUInt8(this.offset + 1) << 8) |
|
||||
this.buffer.readUInt8(this.offset + 2)
|
||||
this.offset += 3
|
||||
return value
|
||||
}
|
||||
|
||||
readUInt32() {
|
||||
if (this.offset + 4 > this.buffer.length) {
|
||||
throw new Error("ByteStream: Cannot read beyond buffer length")
|
||||
}
|
||||
const value = this.buffer.readUInt32BE(this.offset)
|
||||
this.offset += 4
|
||||
return value
|
||||
}
|
||||
|
||||
readInt32() {
|
||||
if (this.offset + 4 > this.buffer.length) {
|
||||
throw new Error("ByteStream: Cannot read beyond buffer length")
|
||||
}
|
||||
const value = this.buffer.readInt32BE(this.offset)
|
||||
this.offset += 4
|
||||
return value
|
||||
}
|
||||
|
||||
readString() {
|
||||
const length = this.readUInt32()
|
||||
if (length === 0) return ""
|
||||
|
||||
if (this.offset + length > this.buffer.length) {
|
||||
throw new Error("ByteStream: Cannot read beyond buffer length")
|
||||
}
|
||||
|
||||
const value = this.buffer.toString("utf8", this.offset, this.offset + length)
|
||||
this.offset += length
|
||||
return value
|
||||
}
|
||||
|
||||
readBytes(length) {
|
||||
if (this.offset + length > this.buffer.length) {
|
||||
throw new Error("ByteStream: Cannot read beyond buffer length")
|
||||
}
|
||||
const value = this.buffer.slice(this.offset, this.offset + length)
|
||||
this.offset += length
|
||||
return value
|
||||
}
|
||||
|
||||
writeUInt8(value) {
|
||||
const buf = Buffer.alloc(1)
|
||||
buf.writeUInt8(value, 0)
|
||||
this.buffer = Buffer.concat([this.buffer, buf])
|
||||
return this
|
||||
}
|
||||
|
||||
writeUInt16(value) {
|
||||
const buf = Buffer.alloc(2)
|
||||
buf.writeUInt16BE(value, 0)
|
||||
this.buffer = Buffer.concat([this.buffer, buf])
|
||||
return this
|
||||
}
|
||||
|
||||
writeUInt24(value) {
|
||||
const buf = Buffer.alloc(3)
|
||||
buf.writeUInt8((value >> 16) & 0xff, 0)
|
||||
buf.writeUInt8((value >> 8) & 0xff, 1)
|
||||
buf.writeUInt8(value & 0xff, 2)
|
||||
this.buffer = Buffer.concat([this.buffer, buf])
|
||||
return this
|
||||
}
|
||||
|
||||
writeUInt32(value) {
|
||||
const buf = Buffer.alloc(4)
|
||||
buf.writeUInt32BE(value, 0)
|
||||
this.buffer = Buffer.concat([this.buffer, buf])
|
||||
return this
|
||||
}
|
||||
|
||||
writeInt32(value) {
|
||||
const buf = Buffer.alloc(4)
|
||||
buf.writeInt32BE(value, 0)
|
||||
this.buffer = Buffer.concat([this.buffer, buf])
|
||||
return this
|
||||
}
|
||||
|
||||
writeString(value) {
|
||||
this.writeUInt32(value.length)
|
||||
if (value.length > 0) {
|
||||
const buf = Buffer.from(value, "utf8")
|
||||
this.buffer = Buffer.concat([this.buffer, buf])
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
writeBytes(bytes) {
|
||||
this.buffer = Buffer.concat([this.buffer, bytes])
|
||||
return this
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
getBuffer() {
|
||||
return this.buffer
|
||||
}
|
||||
|
||||
getLength() {
|
||||
return this.buffer.length
|
||||
}
|
||||
|
||||
getRemainingLength() {
|
||||
return this.buffer.length - this.offset
|
||||
}
|
||||
|
||||
getOffset() {
|
||||
return this.offset
|
||||
}
|
||||
|
||||
setOffset(offset) {
|
||||
if (offset < 0 || offset > this.buffer.length) {
|
||||
throw new Error("ByteStream: Invalid offset")
|
||||
}
|
||||
this.offset = offset
|
||||
return this
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.buffer = Buffer.alloc(0)
|
||||
this.offset = 0
|
||||
return this
|
||||
}
|
||||
|
||||
clone() {
|
||||
const cloned = new ByteStream(Buffer.from(this.buffer))
|
||||
cloned.offset = this.offset
|
||||
return cloned
|
||||
}
|
||||
|
||||
toHex() {
|
||||
return this.buffer.toString("hex")
|
||||
}
|
||||
|
||||
static fromHex(hexString) {
|
||||
return new ByteStream(Buffer.from(hexString, "hex"))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ByteStream
|
217
Client/BrawlClient.js
Normal file
217
Client/BrawlClient.js
Normal file
@ -0,0 +1,217 @@
|
||||
const net = require("net")
|
||||
const MessageFactory = require("../Protocol/MessageFactory")
|
||||
const { Crypto } = require("../Utils/Crypto")
|
||||
|
||||
class BrawlClient {
|
||||
constructor(config, logger) {
|
||||
this.config = config
|
||||
this.logger = logger
|
||||
this.socket = null
|
||||
this.connected = false
|
||||
|
||||
this.crypto = new Crypto(config.crypto)
|
||||
this.messageFactory = new MessageFactory(config, this.crypto, logger)
|
||||
|
||||
this.packetTypes = [10100, 10101, 20108, 0]
|
||||
this.currentPacketIndex = 0
|
||||
this.sequenceNumber = 0
|
||||
this.keepAliveCounter = 0
|
||||
|
||||
this.reconnectAttempts = 0
|
||||
this.keepAliveInterval = null
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.logger.info(`🌐 Connecting to ${this.config.server.host}:${this.config.server.port}...`)
|
||||
|
||||
this.socket = new net.Socket()
|
||||
this.socket.setTimeout(this.config.network.timeout)
|
||||
|
||||
this.socket.connect(this.config.server.port, this.config.server.host, () => {
|
||||
this.logger.connection("connected", {
|
||||
host: this.config.server.host,
|
||||
port: this.config.server.port,
|
||||
})
|
||||
|
||||
this.connected = true
|
||||
this.reconnectAttempts = 0
|
||||
this.startProtocolSequence()
|
||||
})
|
||||
|
||||
this.socket.on("data", (data) => {
|
||||
this.handleServerData(data)
|
||||
})
|
||||
|
||||
this.socket.on("close", () => {
|
||||
this.logger.connection("disconnected")
|
||||
this.connected = false
|
||||
this.stopKeepAlive()
|
||||
this.attemptReconnect()
|
||||
})
|
||||
|
||||
this.socket.on("error", (err) => {
|
||||
this.logger.error("Connection error:", err.message)
|
||||
this.connected = false
|
||||
this.stopKeepAlive()
|
||||
})
|
||||
|
||||
this.socket.on("timeout", () => {
|
||||
this.logger.warn("Connection timeout")
|
||||
this.socket.destroy()
|
||||
})
|
||||
}
|
||||
|
||||
handleServerData(data) {
|
||||
try {
|
||||
let decryptedData = data
|
||||
if (this.config.crypto.enabled) {
|
||||
decryptedData = this.crypto.decrypt(data)
|
||||
this.logger.debug("🔓 Packet decrypted")
|
||||
}
|
||||
|
||||
const message = this.messageFactory.parseMessage(decryptedData)
|
||||
|
||||
switch (message.type) {
|
||||
case 20100:
|
||||
this.logger.success("🔐 Server Hello received")
|
||||
break
|
||||
case 20103:
|
||||
this.logger.success("✅ Login OK received")
|
||||
this.startKeepAlive()
|
||||
break
|
||||
case 20104:
|
||||
this.logger.error("❌ Login Failed received")
|
||||
break
|
||||
case 20108:
|
||||
this.logger.debug("💓 Keep Alive response received")
|
||||
break
|
||||
default:
|
||||
this.logger.info(`📦 Unknown packet type: ${message.type}`)
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error("Error parsing server data:", error.message)
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(type, data = null) {
|
||||
if (!this.connected) {
|
||||
this.logger.warn("❌ Not connected to server")
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const message = this.messageFactory.createMessage(type, data)
|
||||
const payload = message.encode()
|
||||
const packet = this.messageFactory.createPacketHeader(type, payload)
|
||||
|
||||
let finalPacket = packet
|
||||
if (this.config.crypto.enabled) {
|
||||
finalPacket = this.crypto.encrypt(packet)
|
||||
this.logger.debug("🔐 Packet encrypted")
|
||||
} else {
|
||||
this.logger.debug("📤 Packet sent without encryption")
|
||||
}
|
||||
|
||||
this.socket.write(finalPacket)
|
||||
|
||||
const typeName = this.getMessageTypeName(type)
|
||||
this.logger.success(`✅ ${typeName} sent successfully`)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
this.logger.error(`💥 Error sending message ${type}:`, error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
getMessageTypeName(type) {
|
||||
const names = {
|
||||
10100: "ClientHelloMessage",
|
||||
10101: "LoginMessage",
|
||||
20108: "KeepAliveMessage",
|
||||
0: "HeartbeatMessage",
|
||||
}
|
||||
return names[type] || `Unknown(${type})`
|
||||
}
|
||||
|
||||
startProtocolSequence() {
|
||||
this.logger.info("🎯 Starting protocol sequence...")
|
||||
|
||||
setTimeout(() => this.sendMessage(10100), 100)
|
||||
setTimeout(() => this.sendMessage(10101), 1000)
|
||||
setTimeout(() => this.sendMessage(0, { sequence: this.sequenceNumber++ }), 2000)
|
||||
setTimeout(() => {
|
||||
this.startPeriodicSending()
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
startPeriodicSending() {
|
||||
setInterval(() => {
|
||||
if (this.connected) {
|
||||
const packetType = this.packetTypes[this.currentPacketIndex]
|
||||
let data = null
|
||||
|
||||
if (packetType === 0) {
|
||||
data = { sequence: this.sequenceNumber++ }
|
||||
} else if (packetType === 20108) {
|
||||
data = { counter: this.keepAliveCounter++ }
|
||||
}
|
||||
|
||||
this.sendMessage(packetType, data)
|
||||
this.currentPacketIndex = (this.currentPacketIndex + 1) % this.packetTypes.length
|
||||
}
|
||||
}, this.config.network.keepAliveInterval)
|
||||
}
|
||||
|
||||
startKeepAlive() {
|
||||
this.keepAliveInterval = setInterval(() => {
|
||||
if (this.connected) {
|
||||
this.sendMessage(20108, { counter: this.keepAliveCounter++ })
|
||||
}
|
||||
}, this.config.network.keepAliveInterval)
|
||||
}
|
||||
|
||||
stopKeepAlive() {
|
||||
if (this.keepAliveInterval) {
|
||||
clearInterval(this.keepAliveInterval)
|
||||
this.keepAliveInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
attemptReconnect() {
|
||||
if (this.reconnectAttempts < this.config.network.reconnectAttempts) {
|
||||
this.reconnectAttempts++
|
||||
this.logger.info(
|
||||
`🔄 Attempting reconnection ${this.reconnectAttempts}/${this.config.network.reconnectAttempts}...`,
|
||||
)
|
||||
|
||||
setTimeout(() => {
|
||||
this.connect()
|
||||
}, this.config.network.reconnectDelay)
|
||||
} else {
|
||||
this.logger.error("💀 Max reconnection attempts reached")
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.logger.info("🔌 Disconnecting from server...")
|
||||
this.connected = false
|
||||
this.stopKeepAlive()
|
||||
|
||||
if (this.socket) {
|
||||
this.socket.destroy()
|
||||
this.socket = null
|
||||
}
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return {
|
||||
connected: this.connected,
|
||||
sequenceNumber: this.sequenceNumber,
|
||||
keepAliveCounter: this.keepAliveCounter,
|
||||
reconnectAttempts: this.reconnectAttempts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BrawlClient
|
40
Config/config.js
Normal file
40
Config/config.js
Normal file
@ -0,0 +1,40 @@
|
||||
module.exports = {
|
||||
server: {
|
||||
host: "127.0.0.1",
|
||||
port: 9339,
|
||||
},
|
||||
|
||||
auth: {
|
||||
token: "",
|
||||
},
|
||||
|
||||
crypto: {
|
||||
rc4Key: "opahelpme",
|
||||
nonce: "DSFDSDECFJDJDKSK",
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
client: {
|
||||
version: {
|
||||
major: 52,
|
||||
minor: 0,
|
||||
build: 258,
|
||||
},
|
||||
device: "XIAOMI",
|
||||
language: "RU",
|
||||
},
|
||||
|
||||
network: {
|
||||
timeout: 30000,
|
||||
keepAliveInterval: 5000,
|
||||
reconnectAttempts: 3,
|
||||
reconnectDelay: 2000,
|
||||
},
|
||||
|
||||
logging: {
|
||||
level: "debug",
|
||||
saveToFile: true,
|
||||
maxFileSize: "10MB",
|
||||
maxFiles: 5,
|
||||
},
|
||||
}
|
98
Protocol.js
Normal file
98
Protocol.js
Normal file
@ -0,0 +1,98 @@
|
||||
const ByteStream = require("./ByteStream")
|
||||
|
||||
class Protocol {
|
||||
constructor() {
|
||||
this.version = "1.0.0"
|
||||
this.clientVersion = 52
|
||||
}
|
||||
|
||||
createPacketHeader(messageType, payload) {
|
||||
const stream = new ByteStream()
|
||||
stream.writeUInt16(messageType)
|
||||
stream.writeUInt24(payload.length)
|
||||
return Buffer.concat([stream.getBuffer(), payload])
|
||||
}
|
||||
|
||||
createClientHello() {
|
||||
const stream = new ByteStream()
|
||||
|
||||
// Protocol version
|
||||
stream.writeUInt32(1)
|
||||
|
||||
// Key version
|
||||
stream.writeUInt32(this.clientVersion)
|
||||
|
||||
// Major version
|
||||
stream.writeUInt32(52)
|
||||
|
||||
// Minor version
|
||||
stream.writeUInt32(0)
|
||||
|
||||
// Build version
|
||||
stream.writeUInt32(258)
|
||||
|
||||
// Hash
|
||||
stream.writeString("")
|
||||
|
||||
return this.createPacketHeader(10100, stream.getBuffer())
|
||||
}
|
||||
|
||||
createLoginMessage() {
|
||||
const stream = new ByteStream()
|
||||
|
||||
// High ID
|
||||
stream.writeUInt32(0)
|
||||
|
||||
// Low ID
|
||||
stream.writeUInt32(Math.floor(Math.random() * 1000000))
|
||||
|
||||
// Token
|
||||
stream.writeString("")
|
||||
|
||||
// Major version
|
||||
stream.writeUInt32(52)
|
||||
|
||||
// Minor version
|
||||
stream.writeUInt32(0)
|
||||
|
||||
// Build version
|
||||
stream.writeUInt32(258)
|
||||
|
||||
// Hash
|
||||
stream.writeString("")
|
||||
|
||||
// Device
|
||||
stream.writeString("OpenBrawlProject")
|
||||
|
||||
// Language
|
||||
stream.writeString("EN")
|
||||
|
||||
return this.createPacketHeader(10101, stream.getBuffer())
|
||||
}
|
||||
|
||||
createKeepAlive() {
|
||||
const stream = new ByteStream()
|
||||
// Keep alive doesn't need payload
|
||||
return this.createPacketHeader(20108, stream.getBuffer())
|
||||
}
|
||||
|
||||
createHeartbeat() {
|
||||
const stream = new ByteStream()
|
||||
// Heartbeat with timestamp
|
||||
stream.writeUInt32(Date.now())
|
||||
return this.createPacketHeader(0, stream.getBuffer())
|
||||
}
|
||||
|
||||
createCustomPacket() {
|
||||
const stream = new ByteStream()
|
||||
|
||||
// Custom data
|
||||
stream.writeString("OpenBrawlProject")
|
||||
stream.writeUInt32(Date.now())
|
||||
stream.writeUInt8(1)
|
||||
|
||||
return this.createPacketHeader(6076, stream.getBuffer())
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Protocol
|
69
Protocol/MessageFactory.js
Normal file
69
Protocol/MessageFactory.js
Normal file
@ -0,0 +1,69 @@
|
||||
const ClientHelloMessage = require("./Messages/ClientHelloMessage")
|
||||
const LoginMessage = require("./Messages/LoginMessage")
|
||||
const KeepAliveMessage = require("./Messages/KeepAliveMessage")
|
||||
const HeartbeatMessage = require("./Messages/HeartbeatMessage")
|
||||
|
||||
class MessageFactory {
|
||||
constructor(config, crypto, logger) {
|
||||
this.config = config
|
||||
this.crypto = crypto
|
||||
this.logger = logger
|
||||
|
||||
this.messageTypes = {
|
||||
10100: ClientHelloMessage,
|
||||
10101: LoginMessage,
|
||||
20108: KeepAliveMessage,
|
||||
0: HeartbeatMessage,
|
||||
// 6076: CustomMessage,
|
||||
}
|
||||
}
|
||||
|
||||
createMessage(type, data = null) {
|
||||
const MessageClass = this.messageTypes[type]
|
||||
|
||||
if (!MessageClass) {
|
||||
throw new Error(`Unknown message type: ${type}`)
|
||||
}
|
||||
|
||||
return new MessageClass(this.config, this.crypto, this.logger, data)
|
||||
}
|
||||
|
||||
parseMessage(buffer) {
|
||||
try {
|
||||
const ByteStream = require("../ByteStream/ByteStream")
|
||||
const stream = new ByteStream(buffer)
|
||||
|
||||
const messageType = stream.readUInt16()
|
||||
const length = stream.readUInt24()
|
||||
const payload = stream.readBytes(length)
|
||||
|
||||
this.logger.packet("in", messageType, length, { payload: payload.toString("hex") })
|
||||
|
||||
return {
|
||||
type: messageType,
|
||||
length: length,
|
||||
payload: payload,
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to parse message:", error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
createPacketHeader(messageType, payload) {
|
||||
const ByteStream = require("../ByteStream/ByteStream")
|
||||
const stream = new ByteStream()
|
||||
|
||||
stream.writeUInt16(messageType)
|
||||
stream.writeUInt24(payload.length)
|
||||
|
||||
const header = stream.getBuffer()
|
||||
const packet = Buffer.concat([header, payload])
|
||||
|
||||
this.logger.packet("out", messageType, packet.length, { payload: payload.toString("hex") })
|
||||
|
||||
return packet
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageFactory
|
61
Protocol/Messages/ClientHelloMessage.js
Normal file
61
Protocol/Messages/ClientHelloMessage.js
Normal file
@ -0,0 +1,61 @@
|
||||
const ByteStream = require("../../ByteStream/ByteStream")
|
||||
|
||||
class ClientHelloMessage {
|
||||
constructor(config, crypto, logger, data = null) {
|
||||
this.config = config
|
||||
this.crypto = crypto
|
||||
this.logger = logger
|
||||
this.type = 10100
|
||||
this.data = data
|
||||
}
|
||||
|
||||
encode() {
|
||||
const stream = new ByteStream()
|
||||
|
||||
// Protocol version
|
||||
stream.writeUInt32(1)
|
||||
|
||||
stream.writeUInt32(this.config.client.version.major)
|
||||
|
||||
stream.writeUInt32(this.config.client.version.major)
|
||||
|
||||
|
||||
stream.writeUInt32(this.config.client.version.minor)
|
||||
|
||||
stream.writeUInt32(this.config.client.version.build)
|
||||
|
||||
stream.writeString("")
|
||||
|
||||
const nonce = this.crypto.generateNonce()
|
||||
stream.writeBytes(nonce)
|
||||
|
||||
this.logger.debug(`Encoding ClientHelloMessage`, {
|
||||
version: this.config.client.version,
|
||||
nonceLength: nonce.length,
|
||||
})
|
||||
|
||||
return stream.getBuffer()
|
||||
}
|
||||
|
||||
decode(buffer) {
|
||||
const stream = new ByteStream(buffer)
|
||||
|
||||
const data = {
|
||||
protocolVersion: stream.readUInt32(),
|
||||
keyVersion: stream.readUInt32(),
|
||||
majorVersion: stream.readUInt32(),
|
||||
minorVersion: stream.readUInt32(),
|
||||
buildVersion: stream.readUInt32(),
|
||||
hash: stream.readString(),
|
||||
}
|
||||
|
||||
this.logger.debug(`Decoded ClientHelloMessage`, data)
|
||||
return data
|
||||
}
|
||||
|
||||
getType() {
|
||||
return this.type
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClientHelloMessage
|
49
Protocol/Messages/HeartbeatMessage.js
Normal file
49
Protocol/Messages/HeartbeatMessage.js
Normal file
@ -0,0 +1,49 @@
|
||||
const ByteStream = require("../../ByteStream/ByteStream")
|
||||
|
||||
class HeartbeatMessage {
|
||||
constructor(config, crypto, logger, data = null) {
|
||||
this.config = config
|
||||
this.crypto = crypto
|
||||
this.logger = logger
|
||||
this.type = 0
|
||||
this.data = data
|
||||
}
|
||||
|
||||
encode() {
|
||||
const stream = new ByteStream()
|
||||
|
||||
const timestamp = Math.floor(Date.now() / 1000)
|
||||
stream.writeUInt32(timestamp)
|
||||
|
||||
// Client status
|
||||
stream.writeUInt8(1) // 1 = alive
|
||||
|
||||
stream.writeUInt32(this.data?.sequence || 0)
|
||||
|
||||
this.logger.debug(`Encoding HeartbeatMessage`, {
|
||||
timestamp: timestamp,
|
||||
sequence: this.data?.sequence || 0,
|
||||
})
|
||||
|
||||
return stream.getBuffer()
|
||||
}
|
||||
|
||||
decode(buffer) {
|
||||
const stream = new ByteStream(buffer)
|
||||
|
||||
const data = {
|
||||
timestamp: stream.readUInt32(),
|
||||
status: stream.readUInt8(),
|
||||
sequence: stream.readUInt32(),
|
||||
}
|
||||
|
||||
this.logger.debug(`Decoded HeartbeatMessage`, data)
|
||||
return data
|
||||
}
|
||||
|
||||
getType() {
|
||||
return this.type
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HeartbeatMessage
|
44
Protocol/Messages/KeepAliveMessage.js
Normal file
44
Protocol/Messages/KeepAliveMessage.js
Normal file
@ -0,0 +1,44 @@
|
||||
const ByteStream = require("../../ByteStream/ByteStream")
|
||||
|
||||
class KeepAliveMessage {
|
||||
constructor(config, crypto, logger, data = null) {
|
||||
this.config = config
|
||||
this.crypto = crypto
|
||||
this.logger = logger
|
||||
this.type = 20108
|
||||
this.data = data
|
||||
}
|
||||
|
||||
encode() {
|
||||
const stream = new ByteStream()
|
||||
|
||||
stream.writeUInt32(Math.floor(Date.now() / 1000))
|
||||
|
||||
stream.writeUInt32(this.data?.counter || 0)
|
||||
|
||||
this.logger.debug(`Encoding KeepAliveMessage`, {
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
counter: this.data?.counter || 0,
|
||||
})
|
||||
|
||||
return stream.getBuffer()
|
||||
}
|
||||
|
||||
decode(buffer) {
|
||||
const stream = new ByteStream(buffer)
|
||||
|
||||
const data = {
|
||||
timestamp: stream.readUInt32(),
|
||||
counter: stream.readUInt32(),
|
||||
}
|
||||
|
||||
this.logger.debug(`Decoded KeepAliveMessage`, data)
|
||||
return data
|
||||
}
|
||||
|
||||
getType() {
|
||||
return this.type
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = KeepAliveMessage
|
71
Protocol/Messages/LoginMessage.js
Normal file
71
Protocol/Messages/LoginMessage.js
Normal file
@ -0,0 +1,71 @@
|
||||
const ByteStream = require("../../ByteStream/ByteStream")
|
||||
|
||||
class LoginMessage {
|
||||
constructor(config, crypto, logger, data = null) {
|
||||
this.config = config
|
||||
this.crypto = crypto
|
||||
this.logger = logger
|
||||
this.type = 10101
|
||||
this.data = data
|
||||
}
|
||||
|
||||
encode() {
|
||||
const stream = new ByteStream()
|
||||
|
||||
stream.writeUInt32(0)
|
||||
|
||||
|
||||
const playerId = Math.floor(Math.random() * 1000000)
|
||||
stream.writeUInt32(playerId)
|
||||
|
||||
const token = this.config.auth.token || ""
|
||||
stream.writeString(token)
|
||||
|
||||
stream.writeUInt32(this.config.client.version.major)
|
||||
|
||||
stream.writeUInt32(this.config.client.version.minor)
|
||||
|
||||
stream.writeUInt32(this.config.client.version.build)
|
||||
|
||||
stream.writeString("")
|
||||
|
||||
stream.writeString(this.config.client.device)
|
||||
|
||||
stream.writeString(this.config.client.language)
|
||||
|
||||
this.logger.debug(`Encoding LoginMessage`, {
|
||||
playerId: playerId,
|
||||
device: this.config.client.device,
|
||||
language: this.config.client.language,
|
||||
hasToken: token.length > 0,
|
||||
cryptoEnabled: this.config.crypto.enabled,
|
||||
})
|
||||
|
||||
return stream.getBuffer()
|
||||
}
|
||||
|
||||
decode(buffer) {
|
||||
const stream = new ByteStream(buffer)
|
||||
|
||||
const data = {
|
||||
highId: stream.readUInt32(),
|
||||
lowId: stream.readUInt32(),
|
||||
token: stream.readString(),
|
||||
majorVersion: stream.readUInt32(),
|
||||
minorVersion: stream.readUInt32(),
|
||||
buildVersion: stream.readUInt32(),
|
||||
hash: stream.readString(),
|
||||
device: stream.readString(),
|
||||
language: stream.readString(),
|
||||
}
|
||||
|
||||
this.logger.debug(`Decoded LoginMessage`, data)
|
||||
return data
|
||||
}
|
||||
|
||||
getType() {
|
||||
return this.type
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LoginMessage
|
101
Utils/Crypto.js
Normal file
101
Utils/Crypto.js
Normal file
@ -0,0 +1,101 @@
|
||||
class RC4 {
|
||||
constructor(key) {
|
||||
this.key = Buffer.isBuffer(key) ? key : Buffer.from(key, "utf8")
|
||||
this.s = new Array(256)
|
||||
this.init()
|
||||
}
|
||||
|
||||
init() {
|
||||
for (let i = 0; i < 256; i++) {
|
||||
this.s[i] = i
|
||||
}
|
||||
|
||||
let j = 0
|
||||
for (let i = 0; i < 256; i++) {
|
||||
j = (j + this.s[i] + this.key[i % this.key.length]) % 256
|
||||
this.swap(i, j)
|
||||
}
|
||||
}
|
||||
|
||||
swap(i, j) {
|
||||
const temp = this.s[i]
|
||||
this.s[i] = this.s[j]
|
||||
this.s[j] = temp
|
||||
}
|
||||
|
||||
encrypt(data) {
|
||||
const input = Buffer.isBuffer(data) ? data : Buffer.from(data, "utf8")
|
||||
const output = Buffer.alloc(input.length)
|
||||
|
||||
let i = 0,
|
||||
j = 0
|
||||
|
||||
for (let k = 0; k < input.length; k++) {
|
||||
i = (i + 1) % 256
|
||||
j = (j + this.s[i]) % 256
|
||||
this.swap(i, j)
|
||||
|
||||
const keystream = this.s[(this.s[i] + this.s[j]) % 256]
|
||||
output[k] = input[k] ^ keystream
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
decrypt(data) {
|
||||
return this.encrypt(data)
|
||||
}
|
||||
}
|
||||
|
||||
class Crypto {
|
||||
constructor(config) {
|
||||
this.config = config
|
||||
this.rc4 = new RC4(config.rc4Key)
|
||||
this.nonce = Buffer.from(config.nonce, "utf8")
|
||||
}
|
||||
|
||||
encrypt(data) {
|
||||
if (!this.config.enabled) return data
|
||||
|
||||
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data)
|
||||
return this.rc4.encrypt(buffer)
|
||||
}
|
||||
|
||||
decrypt(data) {
|
||||
if (!this.config.enabled) return data
|
||||
|
||||
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data)
|
||||
return this.rc4.decrypt(buffer)
|
||||
}
|
||||
|
||||
generateNonce() {
|
||||
const timestamp = Date.now()
|
||||
const random = Math.floor(Math.random() * 0xffffffff)
|
||||
|
||||
const nonceBuffer = Buffer.alloc(16)
|
||||
nonceBuffer.writeUInt32BE(timestamp >> 32, 0)
|
||||
nonceBuffer.writeUInt32BE(timestamp & 0xffffffff, 4)
|
||||
nonceBuffer.writeUInt32BE(random, 8)
|
||||
nonceBuffer.writeUInt32BE(random ^ timestamp, 12)
|
||||
|
||||
return nonceBuffer
|
||||
}
|
||||
|
||||
hash(data) {
|
||||
const crypto = require("crypto")
|
||||
return crypto.createHash("sha256").update(data).digest()
|
||||
}
|
||||
|
||||
xorWithNonce(data) {
|
||||
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data)
|
||||
const result = Buffer.alloc(buffer.length)
|
||||
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
result[i] = buffer[i] ^ this.nonce[i % this.nonce.length]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { RC4, Crypto }
|
160
Utils/Logger.js
Normal file
160
Utils/Logger.js
Normal file
@ -0,0 +1,160 @@
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
|
||||
class Logger {
|
||||
constructor(config = {}) {
|
||||
this.config = {
|
||||
level: config.level || "info",
|
||||
saveToFile: config.saveToFile || true,
|
||||
maxFileSize: config.maxFileSize || "10MB",
|
||||
maxFiles: config.maxFiles || 5,
|
||||
logDir: config.logDir || "./logs",
|
||||
}
|
||||
|
||||
this.levels = {
|
||||
debug: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3,
|
||||
success: 1,
|
||||
}
|
||||
|
||||
this.colors = {
|
||||
debug: "\x1b[36m", // Cyan
|
||||
info: "\x1b[34m", // Blue
|
||||
warn: "\x1b[33m", // Yellow
|
||||
error: "\x1b[31m", // Red
|
||||
success: "\x1b[32m", // Green
|
||||
reset: "\x1b[0m",
|
||||
}
|
||||
|
||||
this.initLogDirectory()
|
||||
}
|
||||
|
||||
initLogDirectory() {
|
||||
if (this.config.saveToFile && !fs.existsSync(this.config.logDir)) {
|
||||
fs.mkdirSync(this.config.logDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
formatMessage(level, message, data = null) {
|
||||
const timestamp = new Date().toISOString()
|
||||
const upperLevel = level.toUpperCase().padEnd(7)
|
||||
|
||||
let logMessage = `[${timestamp}] [${upperLevel}] ${message}`
|
||||
|
||||
if (data) {
|
||||
logMessage += ` ${JSON.stringify(data, null, 2)}`
|
||||
}
|
||||
|
||||
return logMessage
|
||||
}
|
||||
|
||||
shouldLog(level) {
|
||||
return this.levels[level] >= this.levels[this.config.level]
|
||||
}
|
||||
|
||||
writeToFile(message) {
|
||||
if (!this.config.saveToFile) return
|
||||
|
||||
const logFile = path.join(this.config.logDir, `openbrawl-${new Date().toISOString().split("T")[0]}.log`)
|
||||
|
||||
try {
|
||||
fs.appendFileSync(logFile, message + "\n")
|
||||
this.rotateLogsIfNeeded(logFile)
|
||||
} catch (error) {
|
||||
console.error("Failed to write to log file:", error.message)
|
||||
}
|
||||
}
|
||||
|
||||
rotateLogsIfNeeded(logFile) {
|
||||
try {
|
||||
const stats = fs.statSync(logFile)
|
||||
const maxSize = this.parseSize(this.config.maxFileSize)
|
||||
|
||||
if (stats.size > maxSize) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
|
||||
const rotatedFile = logFile.replace(".log", `-${timestamp}.log`)
|
||||
fs.renameSync(logFile, rotatedFile)
|
||||
|
||||
this.cleanOldLogs()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to rotate logs:", error.message)
|
||||
}
|
||||
}
|
||||
|
||||
cleanOldLogs() {
|
||||
try {
|
||||
const files = fs
|
||||
.readdirSync(this.config.logDir)
|
||||
.filter((file) => file.startsWith("openbrawl-") && file.endsWith(".log"))
|
||||
.map((file) => ({
|
||||
name: file,
|
||||
path: path.join(this.config.logDir, file),
|
||||
time: fs.statSync(path.join(this.config.logDir, file)).mtime,
|
||||
}))
|
||||
.sort((a, b) => b.time - a.time)
|
||||
|
||||
if (files.length > this.config.maxFiles) {
|
||||
files.slice(this.config.maxFiles).forEach((file) => {
|
||||
fs.unlinkSync(file.path)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to clean old logs:", error.message)
|
||||
}
|
||||
}
|
||||
|
||||
parseSize(sizeStr) {
|
||||
const units = { B: 1, KB: 1024, MB: 1024 * 1024, GB: 1024 * 1024 * 1024 }
|
||||
const match = sizeStr.match(/^(\d+)(B|KB|MB|GB)$/i)
|
||||
|
||||
if (!match) return 10 * 1024 * 1024 // Default 10MB
|
||||
|
||||
return Number.parseInt(match[1]) * units[match[2].toUpperCase()]
|
||||
}
|
||||
|
||||
log(level, message, data = null) {
|
||||
if (!this.shouldLog(level)) return
|
||||
|
||||
const formattedMessage = this.formatMessage(level, message, data)
|
||||
const coloredMessage = `${this.colors[level]}${formattedMessage}${this.colors.reset}`
|
||||
|
||||
console.log(coloredMessage)
|
||||
this.writeToFile(formattedMessage)
|
||||
}
|
||||
|
||||
debug(message, data = null) {
|
||||
this.log("debug", message, data)
|
||||
}
|
||||
|
||||
info(message, data = null) {
|
||||
this.log("info", message, data)
|
||||
}
|
||||
|
||||
warn(message, data = null) {
|
||||
this.log("warn", message, data)
|
||||
}
|
||||
|
||||
error(message, data = null) {
|
||||
this.log("error", message, data)
|
||||
}
|
||||
|
||||
success(message, data = null) {
|
||||
this.log("success", message, data)
|
||||
}
|
||||
|
||||
packet(direction, type, size, data = null) {
|
||||
const arrow = direction === "in" ? "📨" : "📤"
|
||||
const message = `${arrow} ${direction.toUpperCase()} | Type: ${type} | Size: ${size} bytes`
|
||||
this.debug(message, data)
|
||||
}
|
||||
|
||||
connection(status, details = null) {
|
||||
const emoji = status === "connected" ? "✅" : status === "disconnected" ? "❌" : "🔄"
|
||||
this.info(`${emoji} Connection ${status}`, details)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Logger
|
86
index.js
Normal file
86
index.js
Normal file
@ -0,0 +1,86 @@
|
||||
const BrawlClient = require("./Client/BrawlClient")
|
||||
const Logger = require("./Utils/Logger")
|
||||
const config = require("./Config/config")
|
||||
|
||||
const asciiArt = `
|
||||
╔═══════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ ██████╗ ██████╗ ███████╗███╗ ██╗██████╗ ██████╗ █████╗ ║
|
||||
║ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║██╔══██╗██╔══██╗██╔══██╗ ║
|
||||
║ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║██████╔╝██████╔╝███████║ ║
|
||||
║ ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║██╔══██╗██╔══██╗██╔══██║ ║
|
||||
║ ╚██████╔╝██║ ███████╗██║ ╚████║██████╔╝██║ ██║██║ ██║ ║
|
||||
║ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ║
|
||||
║ ║
|
||||
║ ██████╗ ██████╗ █████╗ ██╗ ██╗██╗ ██████╗ ║
|
||||
║ ██╔══██╗██╔══██╗██╔══██╗██║ ██║██║ ██╔══██╗ ║
|
||||
║ ██████╔╝██████╔╝███████║██║ █╗ ██║██║ ██████╔╝ ║
|
||||
║ ██╔══██╗██╔══██╗██╔══██║██║███╗██║██║ ██╔═══╝ ║
|
||||
║ ██████╔╝██║ ██║██║ ██║╚███╔███╔╝███████╗ ██║ ║
|
||||
║ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚══╝╚══╝ ╚══════╝ ╚═╝ ║
|
||||
║ ║
|
||||
║ ██████╗ ██████╗ ██████╗ ██╗███████╗ ██████╗████████╗ ║
|
||||
║ ██╔══██╗██╔══██╗██╔═══██╗ ██║██╔════╝██╔════╝╚══██╔══╝ ║
|
||||
║ ██████╔╝██████╔╝██║ ██║ ██║█████╗ ██║ ██║ ║
|
||||
║ ██╔═══╝ ██╔══██╗██║ ██║██ ██║██╔══╝ ██║ ██║ ║
|
||||
║ ██║ ██║ ██║╚██████╔╝╚█████╔╝███████╗╚██████╗ ██║ ║
|
||||
║ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚════╝ ╚══════╝ ╚═════╝ ╚═╝ ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
`
|
||||
|
||||
async function main() {
|
||||
console.log(asciiArt)
|
||||
|
||||
const logger = new Logger()
|
||||
|
||||
logger.info("🚀 Starting OpenBrawl Client...")
|
||||
await sleep(500)
|
||||
|
||||
logger.info("📋 Loading configuration...")
|
||||
await sleep(300)
|
||||
|
||||
logger.info("🔐 Initializing encryption...")
|
||||
await sleep(400)
|
||||
|
||||
logger.info("🌐 Preparing network connection...")
|
||||
await sleep(300)
|
||||
|
||||
logger.info("📦 Loading protocol handlers...")
|
||||
await sleep(400)
|
||||
|
||||
logger.success("✅ All systems ready!")
|
||||
await sleep(200)
|
||||
|
||||
const readline = require("readline")
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
})
|
||||
|
||||
rl.question("🔑 Enter password: ", (password) => {
|
||||
if (password === "openproject") {
|
||||
logger.success("🔓 Password correct! Starting client...")
|
||||
rl.close()
|
||||
|
||||
const client = new BrawlClient(config, logger)
|
||||
client.connect()
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
logger.info("\n👋 Shutting down client...")
|
||||
client.disconnect()
|
||||
process.exit(0)
|
||||
})
|
||||
} else {
|
||||
logger.error("❌ Invalid password!")
|
||||
rl.close()
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
23
package.json
Normal file
23
package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "openbrawl-client",
|
||||
"version": "1.0.0",
|
||||
"description": "Advanced Brawl Stars client for OpenBrawlProject with encryption and logging",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "node index.js",
|
||||
"test": "node test.js"
|
||||
},
|
||||
"keywords": ["brawl-stars", "client", "openbrawlproject", "nodejs", "rc4", "encryption", "logging"],
|
||||
"author": "OpenBrawlProject",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/OpenBrawlProject/openbrawl-client"
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user