mirror of
https://github.com/laochan-eacnet/backend.git
synced 2026-10-14 09:09:02 +03:00
initial commit
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import pug from 'pug';
|
||||
import { to_bin } from "@kamyu/kbinxml";
|
||||
|
||||
export function kxml(templatePath: string): MethodDecorator {
|
||||
const template = pug.compileFile('templates/' + templatePath + '.pug');
|
||||
|
||||
return function (_target, _propertyKey, descriptor) {
|
||||
const orig = descriptor.value as Function;
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const obj = await orig.apply(this, args);
|
||||
const xml = '<?xml version="1.0" encoding="SHIFT_JIS"?>' + template(obj);
|
||||
console.log(xml);
|
||||
const kxml = to_bin(xml);
|
||||
|
||||
return Buffer.from(kxml.data);
|
||||
} as unknown as any;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { singleton } from "tsyringe";
|
||||
import { Context, ILaochanService } from "../types.js";
|
||||
import dedent from 'dedent-js'
|
||||
import { to_bin } from "@kamyu/kbinxml";
|
||||
|
||||
@singleton()
|
||||
export class DefaultService implements ILaochanService {
|
||||
async process(ctx: Context) {
|
||||
const tmp = ctx.service.split('.');
|
||||
const module = tmp[0];
|
||||
const method = tmp[1];
|
||||
|
||||
const xml = dedent`
|
||||
<?xml version="1.0" encoding="SHIFT_JIS"?>
|
||||
<response>
|
||||
<${module} method="${method}" status="0"/>
|
||||
</response>
|
||||
`;
|
||||
|
||||
return Buffer.from(to_bin(xml).data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { kxml } from "../../decorators/render-kxml.js";
|
||||
import { ILaochanService } from "../../types.js";
|
||||
import { singleton } from "tsyringe";
|
||||
|
||||
@singleton()
|
||||
export default class implements ILaochanService {
|
||||
@kxml('facility/get')
|
||||
async process() {
|
||||
return {
|
||||
status: 0,
|
||||
location: {
|
||||
id: 'EA000000',
|
||||
type: 0,
|
||||
country: 'JP',
|
||||
region: 'JP-0',
|
||||
name: 'LaoChan Test',
|
||||
countryName: 'LaoChan',
|
||||
countryJName: '老禅',
|
||||
regionName: 'BianYu',
|
||||
regionJName: '變魚',
|
||||
customerCode: 'LaoChan',
|
||||
companyCode: 'LaoChan',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
accuracy: 0,
|
||||
},
|
||||
line: {
|
||||
id: '0',
|
||||
class: 8,
|
||||
upClass: 8,
|
||||
rtt: 22,
|
||||
},
|
||||
portfw: {
|
||||
globalIp: '127.0.0.1',
|
||||
globalport: 5700,
|
||||
privateport: 5700,
|
||||
},
|
||||
public: {
|
||||
flag: 0,
|
||||
name: 'LaoChan Test',
|
||||
latitude: '0',
|
||||
longitude: '0',
|
||||
},
|
||||
share: {
|
||||
url: '老禅 LaoChan LaoChan 老禅',
|
||||
eapass: 365,
|
||||
eacoin: {
|
||||
notchAmount: 0,
|
||||
notchCount: 0,
|
||||
supplyLimit: 100000,
|
||||
}
|
||||
},
|
||||
calendar: {
|
||||
year: 2024,
|
||||
holiday: [0, 1, 8, 41, 53, 79, 118, 122, 123, 124, 197, 222, 260, 265, 281, 306, 326, 365, 372, 406, 407, 418, 444, 484, 488, 489, 490, 491, 561, 588, 589, 624, 630, 631, 652, 672, 673, 692],
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { kxml } from "../../decorators/render-kxml.js";
|
||||
import { ILaochanService } from "../../types.js";
|
||||
import { singleton } from "tsyringe";
|
||||
|
||||
@singleton()
|
||||
export default class implements ILaochanService {
|
||||
@kxml('message/get')
|
||||
async process() {
|
||||
return {
|
||||
status: 0,
|
||||
expire: 60,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { kxml } from "../../decorators/render-kxml.js";
|
||||
import { ILaochanService } from "../../types.js";
|
||||
import { singleton } from "tsyringe";
|
||||
import { DefaultService } from "../default.js";
|
||||
|
||||
@singleton()
|
||||
export default class extends DefaultService {}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { kxml } from "../../decorators/render-kxml.js";
|
||||
import { ILaochanService } from "../../types.js";
|
||||
import { singleton } from "tsyringe";
|
||||
|
||||
@singleton()
|
||||
export default class implements ILaochanService {
|
||||
@kxml('pcbtracker/alive')
|
||||
async process() {
|
||||
return {
|
||||
status: 0,
|
||||
expire: 3600,
|
||||
time: Math.floor(new Date().valueOf() / 1000),
|
||||
ecEnable: 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { kxml } from "../../decorators/render-kxml.js";
|
||||
import { ILaochanService } from "../../types.js";
|
||||
import { singleton } from "tsyringe";
|
||||
|
||||
@singleton()
|
||||
export default class implements ILaochanService {
|
||||
@kxml('services/get')
|
||||
async process() {
|
||||
return {
|
||||
keepaliveUrl: 'ping://127.0.0.1/?pa=127.0.0.1&ia=127.0.0.1&ga=127.0.0.1ma=127.0.0.1&t1=2&t2=15&rt=2',
|
||||
ntpUrl: 'ntp://pool.ntp.org',
|
||||
selfUrl: 'http://localhost:10573',
|
||||
};
|
||||
}
|
||||
}
|
||||
+82
-28
@@ -1,34 +1,88 @@
|
||||
/**
|
||||
* Some predefined delay values (in milliseconds).
|
||||
*/
|
||||
export enum Delays {
|
||||
Short = 500,
|
||||
Medium = 2000,
|
||||
Long = 5000,
|
||||
import "reflect-metadata";
|
||||
|
||||
import Koa, { DefaultState } from 'koa';
|
||||
import { InjectionToken, Provider, container } from "tsyringe";
|
||||
|
||||
import { ILaochanContext, ILaochanService } from './types.js';
|
||||
import { DefaultService } from "./functions/default.js";
|
||||
import { Logger } from "@cordisjs/logger";
|
||||
import { crypto } from "./middlewares/crypto.js";
|
||||
import { binaryBody } from "./middlewares/binarybody.js";
|
||||
import { parseKxml } from "./middlewares/parseKxml.js";
|
||||
|
||||
function tryResolve<T>(token: InjectionToken<T>): T | undefined {
|
||||
if (!container.isRegistered(token))
|
||||
return undefined;
|
||||
|
||||
return container.resolve(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Promise<string> that resolves after a given time.
|
||||
*
|
||||
* @param {string} name - A name.
|
||||
* @param {number=} [delay=Delays.Medium] - A number of milliseconds to delay resolution of the Promise.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function delayedHello(
|
||||
name: string,
|
||||
delay: number = Delays.Medium,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve: (value?: string) => void) =>
|
||||
setTimeout(() => resolve(`Hello, ${name}`), delay),
|
||||
);
|
||||
function register<T>(token: InjectionToken<T>, provider: Provider<T> | { new(...args: any[]): T }): T {
|
||||
if (container.isRegistered(token))
|
||||
return container.resolve(token);
|
||||
|
||||
// @ts-ignore
|
||||
return container.register(token, provider)
|
||||
.resolve(token);
|
||||
}
|
||||
|
||||
// Please see the comment in the .eslintrc.json file about the suppressed rule!
|
||||
// Below is an example of how to use ESLint errors suppression. You can read more
|
||||
// at https://eslint.org/docs/latest/user-guide/configuring/rules#disabling-rules
|
||||
async function main() {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export async function greeter(name: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
// The name parameter should be of type string. Any is used only to trigger the rule.
|
||||
return await delayedHello(name, Delays.Long);
|
||||
const logger = new Logger('main');
|
||||
|
||||
const app = new Koa<DefaultState, ILaochanContext>()
|
||||
.use(binaryBody)
|
||||
.use(crypto)
|
||||
.use(parseKxml)
|
||||
.use(async (ctx, next) => {
|
||||
if (typeof ctx.query.f !== 'string') {
|
||||
throw new Error('invaild service');
|
||||
}
|
||||
|
||||
if (typeof ctx.query.model !== 'string') {
|
||||
throw new Error('invaild model');
|
||||
}
|
||||
|
||||
ctx.model = ctx.query.model;
|
||||
ctx.service = ctx.query.f;
|
||||
ctx.logger = ctx.service ? register(`logger:${ctx.service}`, {
|
||||
useValue: new Logger(ctx.service),
|
||||
}) : logger;
|
||||
|
||||
let service: ILaochanService | undefined = ctx.service ?
|
||||
tryResolve(ctx.service) : container.resolve(DefaultService);
|
||||
|
||||
if (service === undefined) {
|
||||
try {
|
||||
const module = await import(`./functions/${ctx.service.replace('.', '/')}.js`);
|
||||
service = register(ctx.service, {
|
||||
useClass: module.default,
|
||||
});
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (service === undefined) {
|
||||
logger.warn(`unimplemented service ${ctx.service}`);
|
||||
service = container.resolve(DefaultService);
|
||||
}
|
||||
|
||||
logger.info('%s [%s]: request = %s', ctx.service, ctx.pcbid, JSON.stringify(ctx.request.body));
|
||||
|
||||
try {
|
||||
ctx.body = await service.process(ctx);
|
||||
ctx.status = 200;
|
||||
} catch (e) {
|
||||
logger.error('unexcepted error: %s', JSON.stringify(e, Object.getOwnPropertyNames(e), 2));
|
||||
|
||||
ctx.status = e.status ?? 500;
|
||||
ctx.body = `${e.stack ?? 'Internal Error'}`;
|
||||
}
|
||||
|
||||
logger.info('%s [server]: status = %d', ctx.service, ctx.status);
|
||||
return await next();
|
||||
});
|
||||
|
||||
app.listen(10573);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Next } from "koa";
|
||||
import { Context } from "../types.js";
|
||||
|
||||
export async function binaryBody(ctx: Context, next: Next) {
|
||||
if (typeof ctx.headers["content-length"] !== 'string') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const length = parseInt(ctx.headers["content-length"]);
|
||||
if (length === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
ctx.request.body = await new Promise((resolve, reject) => {
|
||||
const buffer = Buffer.allocUnsafe(length);
|
||||
let len = 0
|
||||
|
||||
ctx.req.on('data', (chunk: Buffer) => {
|
||||
chunk.copy(buffer, len);
|
||||
len += chunk.length;
|
||||
|
||||
if (len == length) {
|
||||
resolve(buffer);
|
||||
}
|
||||
});
|
||||
|
||||
ctx.req.on('close', () => {
|
||||
if (len < length) {
|
||||
reject(new Error('socket hanged up unexceptly.'));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return next();
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Next } from "koa";
|
||||
import { Context } from "../types.js";
|
||||
import { rc4Transfrom } from "../utils/rc4.js";
|
||||
|
||||
export async function crypto(ctx: Context, next: Next) {
|
||||
if (typeof ctx.req.headers['x-eamuse-info'] !== 'string') {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!(ctx.request.body instanceof Buffer)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
{
|
||||
const eamuseInfo: string[] = ctx.req.headers['x-eamuse-info'].split('-');
|
||||
const version = eamuseInfo[0];
|
||||
const time = eamuseInfo[1];
|
||||
const salt = eamuseInfo[2];
|
||||
|
||||
if (version !== '1') {
|
||||
throw new Error(`unsupported crypto version ${version}`);
|
||||
}
|
||||
|
||||
const decrypted = rc4Transfrom(time + salt, ctx.request.body);
|
||||
ctx.request.body = decrypted;
|
||||
}
|
||||
|
||||
await next();
|
||||
|
||||
if (!(ctx.body instanceof Buffer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const time = Math.floor(new Date().valueOf() / 1000).toString(16).padStart(8, '0');
|
||||
const salt = Math.floor(Math.random() * 0xFFFF).toString(16).padStart(4, '0');
|
||||
|
||||
const encrypted = rc4Transfrom(time + salt, ctx.body);
|
||||
ctx.body = encrypted;
|
||||
|
||||
ctx.set('X-Eamuse-Info', `1-${time}-${salt}`);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Next } from "koa";
|
||||
import { Context } from "../types.js";
|
||||
import { to_xml } from "@kamyu/kbinxml";
|
||||
import { XMLParser } from 'fast-xml-parser';
|
||||
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
parseAttributeValue: true,
|
||||
attributeNamePrefix: '$',
|
||||
removeNSPrefix: true,
|
||||
allowBooleanAttributes: true,
|
||||
});
|
||||
|
||||
function parseValue(node: any) {
|
||||
const isArray = '$__count' in node;
|
||||
|
||||
if (['s8', 'u8', 's16', 'u16', 's32', 'u32', 's64', 'u64', 'float', 'double'].includes(node.$__type)) {
|
||||
if (isArray) {
|
||||
return node['#text'].split(' ').map(v => new Number(v));
|
||||
}
|
||||
|
||||
return new Number(node['#text']);
|
||||
}
|
||||
|
||||
if(['b', 'bool'].includes(node.$__type)) {
|
||||
return !!parseInt(node['#text']);
|
||||
}
|
||||
|
||||
if (['bin', 'binary'].includes(node.$__type)) {
|
||||
return Buffer.from(node['#text'], 'hex')
|
||||
}
|
||||
|
||||
if(['ip4', 'str', 'string'].includes(node.$__type)) {
|
||||
return node['#text'];
|
||||
}
|
||||
|
||||
if(node.$__type == 'time') {
|
||||
return new Date(parseInt(node['#text']) * 1000);
|
||||
}
|
||||
|
||||
return {
|
||||
type: node.$__type,
|
||||
value: node['#text'],
|
||||
}
|
||||
}
|
||||
|
||||
function kxmlToObject(node: any) {
|
||||
if (node instanceof Array) {
|
||||
return node.map(v => kxmlToObject(v));
|
||||
}
|
||||
|
||||
if ('$__type' in node) {
|
||||
return parseValue(node);
|
||||
}
|
||||
|
||||
const obj: object = {};
|
||||
|
||||
for (const key in node) {
|
||||
if (key == '?xml') continue;
|
||||
const value = node[key];
|
||||
|
||||
if (key.startsWith('$')) {
|
||||
obj[key.substring(1)] = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
obj[key] = kxmlToObject(node[key]);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
export async function parseKxml(ctx: Context, next: Next) {
|
||||
if (!(ctx.request.body instanceof Buffer)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if ('x-dev' in ctx.request.headers) {
|
||||
ctx.request.body = JSON.parse(ctx.request.body.toString('utf-8'));
|
||||
} else {
|
||||
const xmlResult = to_xml(ctx.request.body);
|
||||
ctx.request.body = kxmlToObject(parser.parse(xmlResult.data));
|
||||
}
|
||||
|
||||
const body = ctx.request.body as any;
|
||||
|
||||
if ('call' in body) {
|
||||
ctx.pcbid = body.call.srcid;
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Logger } from "@cordisjs/logger";
|
||||
import { DefaultState, ParameterizedContext } from "koa";
|
||||
|
||||
export interface ILaochanContext {
|
||||
model?: string;
|
||||
service?: string;
|
||||
pcbid?: string;
|
||||
logger: Logger;
|
||||
|
||||
request: {
|
||||
body: Buffer | object | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export type Context = ParameterizedContext<DefaultState, ILaochanContext, any>;
|
||||
|
||||
export interface ILaochanService {
|
||||
process(ctx: Context): Promise<any>;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createHash } from "crypto";
|
||||
import rc4 from 'arc4';
|
||||
|
||||
const EAMUSE_RC4_KEY = '69D74627D985EE2187161570D08D93B12455035B6DF0D8205DF5';
|
||||
|
||||
export function rc4Transfrom(keyHex: string, data: Buffer) {
|
||||
const key = createHash('md5')
|
||||
.update(keyHex + EAMUSE_RC4_KEY, 'hex')
|
||||
.digest();
|
||||
|
||||
return rc4('arc4', key)
|
||||
.encodeBuffer(data)
|
||||
}
|
||||
Reference in New Issue
Block a user