Basic KCP

This commit is contained in:
memetrollsXD 2022-07-28 22:02:12 +02:00
parent 8779554ed2
commit 8f45d17319
No known key found for this signature in database
GPG Key ID: 105C2F3417AC32CD
23 changed files with 446 additions and 22 deletions

1
.gitignore vendored
View File

@ -40,6 +40,7 @@ build/Release
# Dependency directories
node_modules/
jspm_packages/
package-lock.json
# TypeScript v1 declaration files
typings/

39
package-lock.json generated
View File

@ -7,8 +7,10 @@
"dependencies": {
"@types/express": "^4.17.13",
"colorts": "^0.1.63",
"dgram": "^1.0.1",
"express": "^4.18.1",
"mongodb": "^4.8.0",
"node-kcp-token": "github:memetrollsxd/node-kcp",
"protobufjs": "^7.0.0"
},
"devDependencies": {
@ -536,6 +538,12 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dgram": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dgram/-/dgram-1.0.1.tgz",
"integrity": "sha512-zJVFL1EWfKtE0z2VN6qfpn/a+qG1viEzcwJA0EjtzS76ONSE3sEyWBwEbo32hS4IFw/EWVuWN+8b89aPW6It2A==",
"deprecated": "npm is holding this package for security reasons. As it's a core Node module, we will not transfer it over to other users. You may safely remove the package from your dependencies."
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@ -1014,6 +1022,11 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/nan": {
"version": "2.16.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz",
"integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA=="
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@ -1022,6 +1035,15 @@
"node": ">= 0.6"
}
},
"node_modules/node-kcp-token": {
"version": "1.0.12",
"resolved": "git+ssh://git@github.com/memetrollsxd/node-kcp.git#c92fdc77cfa3b62aed09976ac774102107b7452f",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"nan": "^2.15.0"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@ -2055,6 +2077,11 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
},
"dgram": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dgram/-/dgram-1.0.1.tgz",
"integrity": "sha512-zJVFL1EWfKtE0z2VN6qfpn/a+qG1viEzcwJA0EjtzS76ONSE3sEyWBwEbo32hS4IFw/EWVuWN+8b89aPW6It2A=="
},
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@ -2411,11 +2438,23 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"nan": {
"version": "2.16.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz",
"integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA=="
},
"negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
},
"node-kcp-token": {
"version": "git+ssh://git@github.com/memetrollsxd/node-kcp.git#c92fdc77cfa3b62aed09976ac774102107b7452f",
"from": "node-kcp-token@github:memetrollsxd/node-kcp",
"requires": {
"nan": "^2.15.0"
}
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",

View File

@ -8,8 +8,10 @@
"dependencies": {
"@types/express": "^4.17.13",
"colorts": "^0.1.63",
"dgram": "^1.0.1",
"express": "^4.18.1",
"mongodb": "^4.8.0",
"node-kcp-token": "github:memetrollsxd/node-kcp",
"protobufjs": "^7.0.0"
}
}

6
src/data/packetIds.json Normal file
View File

@ -0,0 +1,6 @@
{
"101": "DebugNotify",
"5": "PlayerGetTokenCsReq",
"49": "PlayerGetTokenScRsp",
"22": "PlayerKeepAliveNotify"
}

View File

@ -0,0 +1,7 @@
syntax = "proto3";
message PlayerGetTokenCsReq {
uint32 account_type = 1;
string account_uid = 2; // Related to v2/login HTTP endpoint
string account_token = 3;
}

View File

@ -0,0 +1,28 @@
syntax = "proto3";
message PlayerGetTokenScRsp {
uint32 retcode = 1;
string msg = 2;
uint32 uid = 3;
string token = 4;
uint32 black_uid_end_time = 5;
uint32 account_type = 6;
string account_uid = 7;
bool is_proficient_player = 8;
string secret_key = 9;
uint32 gm_uid = 10;
uint64 secret_key_seed = 11;
bytes security_cmd_buffer = 12;
uint32 platform_type = 13;
bytes extra_bin_data = 14;
bool is_guest = 15;
uint32 channel_id = 16;
uint32 sub_channel_id = 17;
uint32 tag = 18;
string country_code = 19;
bool is_login_white_list = 20;
string psn_id = 21;
string client_version_random_key = 22;
uint32 reg_platform = 23;
string client_ip_str = 24;
}

View File

@ -4,7 +4,7 @@ import Logger from "../util/Logger";
const c = new Logger("Database");
export default class Database {
public static instance: Database;
private static instance: Database;
public static client: MongoClient;
private constructor() {
Database.client = new MongoClient(Config.MONGO_URI);

View File

@ -4,7 +4,7 @@ import fs from 'fs';
import { resolve } from 'path';
import Config from '../util/Config';
import Logger, { VerboseLevel } from '../util/Logger';
const c = new Logger("HTTP");
const c = new Logger("HTTP", "cyan");
function r(...args: string[]) {
return fs.readFileSync(resolve(__dirname, ...args)).toString();
@ -17,11 +17,11 @@ const HTTPS_CONFIG = {
export default class HttpServer {
private readonly server;
public static instance: HttpServer;
private static instance: HttpServer;
private constructor() {
this.server = express();
this.server.use(express.json());
this.server.use(express.json());
this.server.route('/*').all((req, res) => {
if (Logger.VERBOSE_LEVEL > VerboseLevel.WARNS) c.log(`${req.method} ${req.url}`);
import(`./routes${req.url.split('?')[0]}`).then(async r => {
@ -34,7 +34,9 @@ export default class HttpServer {
c.error(err);
});
});
}
public start(): void {
https.createServer(HTTPS_CONFIG, this.server).listen(Config.HTTP.HTTP_PORT, Config.HTTP.HTTP_PORT);
this.server.listen(80, Config.HTTP.HTTP_HOST, () => {
c.log(`Listening on ${Config.HTTP.HTTP_HOST}:${Config.HTTP.HTTP_PORT}`);

View File

@ -1,14 +1,18 @@
import { Request, Response } from "express";
export default function handle(req: Request, res: Response) {
const data = JSON.parse(req.body.data)
res.send({
retcode: 0,
message: "OK",
data: {
combo_id: "0",
open_id: "",
combo_token: "",
data: '{"guest":false}',
combo_id: 1,
open_id: data.uid,
combo_token: data.token,
data: {
guest: data.guest
},
heartbeat: false,
account_type: 1,
fatigue_remind: null

View File

@ -1,6 +1,7 @@
import { Request, Response } from "express";
import Account from "../../../../../../db/Account";
import Logger from "../../../../../../util/Logger";
const c = new Logger("Dispatch");
// Example request:
// {
@ -10,7 +11,6 @@ import Logger from "../../../../../../util/Logger";
// }
export default async function handle(req: Request, res: Response) {
const c = new Logger(req.ip);
const acc = await Account.getAccountByUsername(req.body.account);
const dataObj: any = {
retcode: 0,
@ -22,11 +22,11 @@ export default async function handle(req: Request, res: Response) {
if (!acc) {
dataObj.retcode = -202;
dataObj.message = "Account not found";
c.warn(`[DISPATCH] Player ${req.body.account} not found`);
c.warn(`Player ${req.body.account} not found (${req.ip})`);
res.send(dataObj);
} else {
dataObj.data.account = acc;
c.log(`[DISPATCH] Player ${req.body.account} logged in`);
c.log(`Player ${req.body.account} logged in (${req.ip})`);
res.send(dataObj);
}
}

View File

@ -1,12 +1,11 @@
import { Request, Response } from "express";
import Account from "../../../../../../db/Account";
import Logger from "../../../../../../util/Logger";
const c = new Logger("Dispatch");
// Example request:
// {"uid":"63884253","token":"ZQmgMdXA1StL9A3aPBUedr8yoiuoLrmV"}
export default async function handle(req: Request, res: Response) {
const c = new Logger(req.ip);
const acc = await Account.getAccountByUID(req.body.uid);
const dataObj: any = {
retcode: 0,
@ -19,11 +18,11 @@ export default async function handle(req: Request, res: Response) {
dataObj.retcode = -202;
dataObj.message = "Account not found";
res.send(dataObj);
c.warn(`[DISPATCH] Player ${req.body.uid} not found`);
c.warn(`Player ${req.body.uid} not found (${req.ip})`);
} else {
if (acc.token === req.body.token) {
dataObj.data.account = acc;
c.log(`[DISPATCH] Player ${req.body.uid} logged in`);
c.log(`Player ${req.body.uid} logged in (${req.ip})`);
res.send(dataObj);
} else {
dataObj.retcode = -202;

View File

@ -3,7 +3,7 @@ import protobuf from 'protobufjs';
import { resolve } from 'path';
import Config from "../../util/Config";
const proto = protobuf.loadSync(resolve(__dirname, '../../proto/QueryCurrRegionHttpRsp.proto')).lookup('QueryCurrRegionHttpRsp') as any;
const proto = protobuf.loadSync(resolve(__dirname, '../../data/proto/QueryCurrRegionHttpRsp.proto')).lookup('QueryCurrRegionHttpRsp') as any;
export default function handle(req: Request, res: Response) {
const dataObj = {

View File

@ -5,9 +5,11 @@
*/
import Interface from "./commands/Interface";
import HttpServer from "./http/HttpServer";
import SRServer from "./server/kcp/SRServer";
import Logger from "./util/Logger";
const c = new Logger("CrepeSR");
c.log(`Starting CrepeSR...`);
Interface.start();
HttpServer.getInstance();
HttpServer.getInstance().start();
SRServer.getInstance().start();

View File

@ -0,0 +1,57 @@
export enum HandshakeType {
CONNECT = 1,
DISCONNECT = 2,
SEND_BACK_CONV = 3,
UNKNOWN = 4
}
export default class Handshake {
private static readonly CONNECT: number[] = [0xff, 0xFFFFFFFF]
private static readonly SEND_BACK_CONV: number[] = [0x145, 0x14514545]
private static readonly DISCONNECT: number[] = [0x194, 0x19419494]
public readonly conv: number;
public readonly type: number[];
public readonly handshakeType!: HandshakeType;
public readonly token: number;
public readonly data: number;
public constructor(public readonly bytes: Buffer | HandshakeType) {
if (Buffer.isBuffer(bytes)) {
this.conv = bytes.readUInt32BE(4);
this.token = bytes.readUInt32BE(8);
this.data = bytes.readUInt32BE(12);
this.type = [bytes.readUInt32BE(0), bytes.readUInt32BE(16)];
this.handshakeType = this.decodeType();
} else {
this.conv = 0x69;
this.token = 0x420;
this.data = 0;
this.type = Handshake.SEND_BACK_CONV;
this.handshakeType = HandshakeType.SEND_BACK_CONV;
}
}
public encode(): Buffer {
const buf = Buffer.alloc(20);
buf.writeUInt32BE(this.type[0]);
buf.writeUInt32BE(this.conv, 4);
buf.writeUInt32BE(this.token, 8);
buf.writeUInt32BE(this.data, 12);
buf.writeUInt32BE(this.type[1], 16);
return buf;
}
public decodeType(): HandshakeType {
if (this.type[0] == Handshake.CONNECT[0] && this.type[1] == Handshake.CONNECT[1]) {
return HandshakeType.CONNECT;
}
if (this.type[0] == Handshake.SEND_BACK_CONV[0] && this.type[1] == Handshake.SEND_BACK_CONV[1]) {
return HandshakeType.SEND_BACK_CONV
}
if (this.type[0] == Handshake.DISCONNECT[0] && this.type[1] == Handshake.DISCONNECT[1]) {
return HandshakeType.DISCONNECT;
}
return HandshakeType.UNKNOWN;
}
}

73
src/server/kcp/Packet.ts Normal file
View File

@ -0,0 +1,73 @@
import Logger, { VerboseLevel } from "../../util/Logger";
import protobuf from 'protobufjs';
import { resolve } from 'path';
import _packetIds from '../../data/packetIds.json';
const packetIds = _packetIds as { [key: string]: string };
const switchedPacketIds: { [key: string]: number } = (function () {
const obj: { [key: string]: number } = {};
Object.keys(packetIds).forEach((key) => {
obj[packetIds[key]] = Number(key);
});
return obj;
})();
const c = new Logger("Packet")
export default class Packet {
public readonly cmdid: number;
public readonly data: Buffer;
public body: {} = {};
public constructor(public readonly rawData: Buffer, public readonly protoName = "") {
// Remove the header and metadata
const metadataLength = rawData.readUInt16BE(6);
this.data = rawData.subarray(12 + metadataLength, 12 + metadataLength + rawData.readUInt32BE(8));
this.cmdid = this.rawData.readUInt16BE(4);
this.protoName = packetIds[this.cmdid.toString()];
if (this.protoName) {
try {
const root = protobuf.loadSync(resolve(__dirname, `../../data/proto/${this.protoName}.proto`));
const Message = root.lookupTypeOrEnum(this.protoName);
this.body = Message.decode(this.data);
} catch (e) {
c.warn(`Failed to decode ${this.protoName}`);
if (Logger.VERBOSE_LEVEL >= VerboseLevel.ALL) {
c.error(e as Error, false);
}
c.debug(`Data: ${this.data.toString("hex")}`);
}
} else {
c.error(`Unknown packet id ${this.cmdid}`);
}
}
public static isValid(data: Buffer): boolean {
// Buffer acting fucky so i'll just use good ol' string manipulation
const str = data.toString('hex');
return str.startsWith("01234567") && str.endsWith("89abcdef");
}
public static encode(name: string, body: {}): Packet | null {
try {
const cmdid = switchedPacketIds[name];
const root = protobuf.loadSync(resolve(__dirname, `../../data/proto/${name}.proto`));
const Message = root.lookupTypeOrEnum(name);
const data = Buffer.from(Message.encode(body).finish());
const packet = Buffer.allocUnsafe(16 + data.length);
packet.writeUInt32BE(0x1234567);
packet.writeUint16BE(cmdid, 4);
packet.writeUint16BE(0, 6);
packet.writeUint32BE(data.length, 8);
data.copy(packet, 12);
packet.writeUint32BE(0x89abcdef, 12 + data.length);
return new Packet(packet);
} catch (e) {
c.error(e as Error);
return null;
}
}
}

View File

@ -0,0 +1,83 @@
import _KCP from 'node-kcp-token';
import Logger from "../../util/Logger";
import { Socket, createSocket, RemoteInfo } from "dgram";
import Session from "./Session";
import Config from "../../util/Config";
import Handshake, { HandshakeType } from "./Handshake";
const KCP = _KCP.KCP;
const c = new Logger("KCP", "yellow");
export default class SRServer {
private static instance: SRServer;
public readonly udpSocket: Socket;
public readonly sessions: Map<string, Session> = new Map();
private constructor() {
this.udpSocket = createSocket("udp4");
}
public static getInstance(): SRServer {
if (!SRServer.instance) {
SRServer.instance = new SRServer();
}
return SRServer.instance;
}
public start() {
this.udpSocket.bind(Config.GAMESERVER.SERVER_PORT, "0.0.0.0");
this.udpSocket.on('listening', () => this.onListening());
this.udpSocket.on('message', (d, i) => this.onMessage(d, i));
this.udpSocket.on('error', (e) => this.onError(e));
}
private async onMessage(data: Buffer, rinfo: RemoteInfo) {
const client = `${rinfo.address}:${rinfo.port}`;
if (data.byteLength == 20) {
// Hamdshanke
const handshake = new Handshake(data);
c.debug(data.toString("hex"));
switch (handshake.handshakeType) {
case HandshakeType.CONNECT:
c.log(`${client} connected`);
const rsp = new Handshake(HandshakeType.SEND_BACK_CONV).encode();
this.udpSocket.send(rsp, 0, rsp.byteLength, rinfo.port, rinfo.address);
const kcpobj = new KCP(0x69, 0x420, {
address: rinfo.address,
port: rinfo.port,
family: rinfo.family
});
kcpobj.nodelay(1, 5, 2, 0);
kcpobj.output((d, s, u) => this.output(d, s, u));
kcpobj.wndsize(256, 256);
this.sessions.set(client, new Session(kcpobj, rinfo));
break;
case HandshakeType.DISCONNECT:
c.log(`${client} disconnected`);
this.sessions.delete(client);
break;
default:
c.error(`${client} unknown Handshake: ${data.readUint32BE(0)}`);
}
return;
}
const session = this.sessions.get(client);
if (!session) return;
session.inputRaw(data);
}
private output(buf: Buffer, size: number, ctx: { address: string, port: number, family: string }) {
if (!buf) return;
this.udpSocket.send(buf, 0, size, ctx.port, ctx.address);
}
private async onError(err: Error) {
c.error(err);
}
private async onListening() {
c.log(`Listening on 0.0.0.0:${Config.GAMESERVER.SERVER_PORT}`);
}
}

81
src/server/kcp/Session.ts Normal file
View File

@ -0,0 +1,81 @@
import _KCP from 'node-kcp-token';
import { RemoteInfo } from 'dgram';
import { resolve } from 'path';
import fs from 'fs';
import KCP from 'node-kcp-token';
import Packet from './Packet';
import Logger, { VerboseLevel } from '../../util/Logger';
import defaultHandler from '../packets/PacketHandler';
function r(...args: string[]) {
return fs.readFileSync(resolve(__dirname, ...args));
}
function xor(data: Buffer, key: Buffer) {
const ret: Buffer = Buffer.from(data);
for (let i = 0; i < data.length; i++) ret.writeUInt8(data.readUInt8(i) ^ key.readUInt8(i % key.length), i);
return ret;
}
export default class Session {
public key: Buffer = r('./initial.key');
public c: Logger;
public constructor(private readonly kcpobj: KCP.KCP, public readonly ctx: RemoteInfo) {
this.kcpobj = kcpobj;
this.ctx = ctx;
this.c = new Logger(`${this.ctx.address}:${this.ctx.port}`, 'yellow');
this.update();
}
public inputRaw(data: Buffer) {
this.kcpobj.input(data);
}
public async update() {
if (!this.kcpobj) {
console.error("wtf kcpobj is undefined");
console.debug(this)
return;
}
const hr = process.hrtime();
const timestamp = hr[0] * 1000000 + hr[1] / 1000;
this.kcpobj.update(timestamp);
let recv;
do {
recv = this.kcpobj.recv();
if (!recv) break;
this.c.debug(`recv ${recv.toString("hex")}`);
if (Packet.isValid(recv)) {
this.handlePacket(new Packet(recv));
}
} while (recv)
setTimeout(() => this.update(), 1);
}
public async handlePacket(packet: Packet) {
if (Logger.VERBOSE_LEVEL >= VerboseLevel.WARNS) this.c.log(packet.protoName)
import(`../packets/${packet.protoName}`).then(mod => {
mod.default(this, packet);
}).catch(e => {
if (e.code === 'MODULE_NOT_FOUND') this.c.warn(`Unhandled packet: ${packet.protoName}`);
else this.c.error(e);
defaultHandler(this, packet);
});
}
public send(name: string, body: {}) {
const packet = Packet.encode(name, body);
if (!packet) return;
if (Logger.VERBOSE_LEVEL >= VerboseLevel.WARNS) this.c.log(packet.protoName);
this.c.debug(`send ${packet.rawData.toString('hex')}`);
this.kcpobj.send(packet.rawData);
}
}

BIN
src/server/kcp/initial.key Normal file

Binary file not shown.

View File

@ -0,0 +1,6 @@
import Packet from "../kcp/Packet";
import Session from "../kcp/Session";
export default async function handle(session: Session, packet: Packet) {
session.c.debug(packet.body);
}

View File

@ -0,0 +1,34 @@
import Logger from "../../util/Logger";
import Account from "../../db/Account";
import Packet from "../kcp/Packet";
import Session from "../kcp/Session";
const c = new Logger("Dispatch");
interface PlayerGetTokenCsReq {
accountToken?: string;
accountUid?: string;
accountType?: number;
}
export default async function handle(session: Session, packet: Packet) {
const body = packet.body as PlayerGetTokenCsReq;
const account = await Account.getAccountByUID(body.accountUid || 0);
if (!account) {
c.error(`Account not found: ${body.accountUid}`);
return;
}
const isTokenValid = account.token === body.accountToken;
if (!isTokenValid) {
c.error(`Token invalid (${session.ctx.address}:${session.ctx.port})`);
return;
}
session.send('PlayerGetTokenScRsp', {
uid: account.uid,
token: body.accountToken,
secretKey: BigInt(0).toString(),
accountUid: account.uid.toString(),
accountType: body.accountType,
});
}

View File

@ -23,7 +23,7 @@ const DEFAULT_CONFIG = {
// GameServer
GAMESERVER: {
SERVER_IP: "0.0.0.0",
SERVER_IP: "127.0.0.1",
SERVER_PORT: 22102,
MAINTENANCE: false,
MAINTENANCE_MSG: "Server is in maintenance mode."

View File

@ -12,7 +12,7 @@ type Color = 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white'
export default class Logger {
public static VERBOSE_LEVEL: VerboseLevel = Config.VERBOSE_LEVEL || 1;
constructor(public name: string, public color: Color = 'cyan') {
constructor(public name: string, public color: Color = 'blue') {
this.name = name;
this.color = color;
}
@ -34,10 +34,10 @@ export default class Logger {
console.log(`\t↳ ${args.join(' ').gray}`);
}
public error(e: Error | string) {
public error(e: Error | string, stack: boolean = true) {
if (typeof e === 'string') e = new Error(e);
console.log(`[${this.getDate().white.bold}] ${`ERROR<${this.name}>`.bgRed.bold}`, e.message);
if (e.stack) this.trail(e.stack);
if (e.stack && stack) this.trail(e.stack);
}
public warn(...args: string[]) {