Skip to content

Instantly share code, notes, and snippets.

@terence410
Last active July 9, 2019 03:50
Show Gist options
  • Save terence410/235d13497e6d0c7202db39391c924a3c to your computer and use it in GitHub Desktop.
Save terence410/235d13497e6d0c7202db39391c924a3c to your computer and use it in GitHub Desktop.
Anit Client Cheat API (Server Side Logic) - Written in NestJs
import {ArgumentsHost, Catch, ExceptionFilter, Injectable} from '@nestjs/common';
import {Response, Request} from 'express';
import {ApiError} from '../errors/ApiError';
import {RequestService} from '../providers/request.service';
import {ErrorCode} from '../states/ErrorCode';
import {IRequestBody} from '../entities/IRequestBody';
import {EncryptionService} from '../providers/encryption.service';
@Injectable()
@Catch(ApiError)
export class ApiException implements ExceptionFilter {
constructor(private readonly requestService: RequestService, private readonly encryptionService: EncryptionService) {
}
async catch(exception: ApiError, host: ArgumentsHost) {
console.log('catch api error new', exception.errorCode);
// prepare objects
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const requestBody = request.body as IRequestBody;
// log
this.requestService.addLog('error', request.originalUrl, {error: exception.errorCode, errorMessage: exception.errorMessage});
if (exception.errorCode === ErrorCode.HasCache) {
exception.respondData.isCache = true;
const payload = this.requestService.sendEncryptData(response, exception.respondData, requestBody.nonce);
} else {
this.requestService.sendRawError(response, exception.errorCode);
}
}
}
import {Injectable, NestMiddleware} from '@nestjs/common';
import {Request, Response} from 'express';
import {IRequestBody} from '../entities/IRequestBody';
import {UserService} from '../providers/user.service';
import {RequestService} from '../providers/request.service';
import {ApiError} from '../errors/ApiError';
import {ErrorCode} from '../states/ErrorCode';
@Injectable()
export class ApiMiddleware implements NestMiddleware {
constructor(private readonly userService: UserService,
private readonly requestService: RequestService,
) {
}
async use(request: Request, response: Response, next: () => any) {
const requestBody = request.body as IRequestBody;
this.requestService.addLog('request', request.originalUrl, {requestBody});
const query = request.query;
const signedBase64 = query.signedBase64;
const token = query.token;
// verify client send enough things
this.requestService.verifyRequest(request);
// we validate the user identify first
const user = this.requestService.verifyToken(token);
requestBody.user = user;
// get the nonce and verify sign
const nonce = this.requestService.openSigned(requestBody.payloadBase64, signedBase64);
requestBody.nonce = nonce;
// we update the nonce after verify
this.requestService.verifyNonce(user, nonce);
this.userService.updateNonce(user, nonce);
// decrypt
const requestData = this.requestService.decryptData(requestBody.payloadBase64, nonce);
requestBody.requestData = requestData;
// verify data
this.requestService.verifyVersion(requestData.version, requestData.versionKey);
this.requestService.verifySession(user, requestData.session);
this.requestService.verifyTimestamp(requestData.timestamp);
this.requestService.checkHasCache(requestData.cacheKey);
// lock for atomic request
if (this.requestService.canLock(user)) {
this.requestService.lock(user, signedBase64);
await this.requestService.testingRemoteTimeout(query, requestBody);
next();
this.requestService.unlock(user, signedBase64);
} else {
throw new ApiError(ErrorCode.Locked);
}
}
}
import {MiddlewareConsumer, Module, RequestMethod} from '@nestjs/common';
import {AppController} from './controllers/app.controller';
import {AppService} from './providers/app.service';
import {CardsController} from './controllers/cards.controller';
import {UserService} from './providers/user.service';
import {UsersController} from './controllers/users.controller';
import {LoginMiddleware} from './middlewares/login.middleware';
import {ApiMiddleware} from './middlewares/api.middleware';
import {EncryptionService} from './providers/encryption.service';
import {RequestService} from './providers/request.service';
@Module({
imports: [],
controllers: [AppController, CardsController, UsersController],
providers: [AppService, UserService, EncryptionService, RequestService],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoginMiddleware)
.forRoutes(UsersController);
consumer
.apply(ApiMiddleware)
.forRoutes({ path: 'cards/*', method: RequestMethod.ALL });
}
}
import {Body, Controller, Get, Post, Req, Res} from '@nestjs/common';
import {Request, Response} from 'express';
import {UserService} from '../providers/user.service';
import {ApiError} from '../errors/ApiError';
import {IRequestBody} from '../entities/IRequestBody';
import {RequestService} from '../providers/request.service';
import {EncryptionService} from '../providers/encryption.service';
import {IRespondData} from '../entities/IRespondData';
@Controller('cards')
export class CardsController {
constructor(private readonly userService: UserService,
private readonly requestService: RequestService,
private readonly encryptionService: EncryptionService,
) {
}
@Get('/')
index(@Req() request: Request): string {
return '/cards';
}
@Post('list')
async list(@Req() request: Request, @Res() response: Response, @Body() requestBody: IRequestBody) {
const user = requestBody.user;
const cards = this.userService.listCards(user.deviceId);
const respondData: IRespondData = {
body: {
cards,
},
};
return await this.requestService.sendData(response, respondData);
}
@Post('create')
async create(@Req() request: Request, @Res() response: Response, @Body() requestBody: IRequestBody) {
const user = requestBody.user;
const requestData = requestBody.requestData;
const [cards, card] = this.userService.createCard(user.deviceId, requestData.monsterId);
const respondData: IRespondData = {
body: {
cards,
card,
},
};
return await this.requestService.sendData(response, respondData);
}
@Post('update')
async update(@Req() request: Request, @Res() response: Response, @Body() requestBody: IRequestBody) {
const user = requestBody.user;
const requestData = requestBody.requestData;
const [cards, card] = this.userService.updateCard(user.deviceId, requestData.cardId, requestData.exp);
const respondData: IRespondData = {
body: {
cards,
card,
},
};
return await this.requestService.sendData(response, respondData);
}
@Post('delete')
async delete(@Req() request: Request, @Res() response: Response, @Body() requestBody: IRequestBody) {
const user = requestBody.user;
const requestData = requestBody.requestData;
const [cards, card] = this.userService.deleteCard(user.deviceId, requestData.cardId);
const respondData: IRespondData = {
body: {
cards,
card,
},
};
return await this.requestService.sendData(response, respondData);
}
}
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
import {UserService} from '../providers/user.service';
import {RequestService} from '../providers/request.service';
import {IRequestBody} from '../entities/IRequestBody';
@Injectable()
export class LoginMiddleware implements NestMiddleware {
constructor(private readonly userService: UserService,
private readonly requestService: RequestService,
) {
}
async use(request: Request, response: Response, next: () => any) {
const requestBody = request.body as IRequestBody;
this.requestService.addLog('request', request.originalUrl, {requestBody});
const query = request.query;
const signedBase64 = query.signedBase64;
// verify client send enough things
this.requestService.verifyRequest(request);
// verify sign and get the nonce
const nonce = this.requestService.openSigned(requestBody.payloadBase64, signedBase64);
requestBody.nonce = nonce;
// decrypt
const requestData = this.requestService.decryptData(requestBody.payloadBase64, nonce);
requestBody.requestData = requestData;
// verify
this.requestService.verifyVersion(requestData.version, requestData.versionKey);
// testing
await this.requestService.testingRemoteTimeout(query, requestBody);
next();
}
}
import {Injectable} from '@nestjs/common';
import {Response, Request} from 'express';
import {IRequestData} from '../entities/IRequestData';
import {EncryptionService} from './encryption.service';
import {ErrorCode} from '../states/ErrorCode';
import {IRespondData} from '../entities/IRespondData';
import {IUser} from '../entities/IUser';
import {UserService} from './user.service';
import {IRequestBody} from '../entities/IRequestBody';
import {ApiError} from '../errors/ApiError';
import * as crypto from 'crypto';
import * as zlib from 'zlib';
@Injectable()
export class RequestService {
public logs = [];
constructor(private readonly encryptionService: EncryptionService,
private readonly userService: UserService) {
}
public addLog(type: string, url: string, data: any) {
this.logs.unshift({
type,
dateTime: new Date(),
baseUrl: url,
data,
});
const max = 100;
if (this.logs.length > max) {
this.logs.slice(0, max);
}
}
public async timeout(ms: number) {
await new Promise(resolve => setTimeout(resolve, ms));
}
public canLock(user: IUser): boolean {
if (user.lockTimestamp === 0) {
return true;
}
// check if lock expired
const currentTimestamp = +new Date() / 1000 | 0;
const maxLockSeconds = 60;
if (currentTimestamp > user.lockTimestamp + maxLockSeconds) {
return true;
}
return false;
}
public lock(user: IUser, signedBase64: string) {
if (this.canLock(user)) {
user.lockTimestamp = +new Date() / 1000 | 0;
user.lockSignature = signedBase64;
}
}
public unlock(user: IUser, signedBase64: string) {
if (user.lockSignature === signedBase64) {
user.lockTimestamp = 0;
}
}
public verifyRequest(request: Request) {
if (!request.query.signedBase64 || !request.body.payloadBase64) {
throw new ApiError(ErrorCode.InvalidRequest);
}
}
public verifyToken(token: string): IUser {
const deviceId = this.encryptionService.verifyJwt(token);
return this.userService.get(deviceId);
}
public verifyNonce(user: IUser, nonce: number): void {
if (!nonce || nonce <= user.nonce) {
throw new ApiError(ErrorCode.InvalidNonce);
}
}
public async testingRemoteTimeout(query: any, message: IRequestBody) {
if (query.remoteTimeout && query.remoteTimeout.toLocaleLowerCase() === 'true') {
await this.timeout(5000);
}
}
public openSigned(payloadBase64: string, signedBase64: string): number {
let opened: Buffer;
let digest: Buffer;
let payload: Buffer;
let signed: Buffer;
try {
payload = Buffer.from(payloadBase64, 'base64');
signed = Buffer.from(signedBase64, 'base64');
digest = crypto.createHash('md5').update(payload).digest();
opened = this.encryptionService.signOpenMessage(signed);
} catch (e) {
throw new ApiError(ErrorCode.InvalidSign, e.message);
}
const nonce = opened.readInt32LE(0);
const signedDigest = opened.slice(8);
if (!digest.equals(signedDigest)) {
throw new ApiError(ErrorCode.InvalidSign, `invalid digest. yours: ${signedDigest.toString('base64')}, expected: ${digest.toString('base64')}`);
}
return nonce;
}
public checkHasCache(cacheKey: string) {
if (cacheKey) {
const respondData = this.userService.getCache(cacheKey);
if (respondData) {
throw new ApiError(ErrorCode.HasCache, '', respondData);
}
}
}
public verifyVersion(version: string, versionKey: string) {
if (version !== '1.0') {
throw new ApiError(ErrorCode.InvalidVersion);
}
}
public verifySession(user: IUser, session: string) {
if (user.session !== session) {
throw new ApiError(ErrorCode.InvalidSession);
}
}
public verifyTimestamp(timestamp: number) {
const currentTimestamp = (+new Date() / 1000 | 0);
if (isNaN(timestamp)) {
throw new ApiError(ErrorCode.InvalidTimestamp);
} else {
const diffTimestamp = Math.abs(timestamp - currentTimestamp);
if (diffTimestamp > 3600) {
throw new ApiError(ErrorCode.InvalidTimestamp);
}
}
}
public decryptData(payloadBase64: string, nonce: number): IRequestData {
try {
const input = new Buffer(payloadBase64, 'base64');
const output = this.encryptionService.decrypt(input, nonce) as IRequestData;
return JSON.parse(output.toString());
} catch (err) {
throw new ApiError(ErrorCode.FailedToDecryptClientPayload);
}
}
public async sendData(response: Response, respondData: IRespondData) {
const requestBody = response.req.body as IRequestBody;
const cacheKey = requestBody.requestData.cacheKey;
respondData.errorCode = 0;
respondData.timestamp = +new Date() / 1000 | 0;
respondData.user = requestBody.user;
// save the cache
if (cacheKey) {
this.userService.setCache(cacheKey, respondData);
}
this.sendEncryptData(response, respondData, requestBody.nonce);
}
public sendEncryptData(response: Response, respondData: IRespondData, nonce: number) {
const message = Buffer.from(JSON.stringify(respondData));
const payload = this.encryptionService.encrypt(message, nonce);
let gzippedPayload = zlib.gzipSync(payload);
if (response.req.query.remoteSendInvalidPayload && response.req.query.remoteSendInvalidPayload.toLowerCase() === 'true') {
gzippedPayload = Buffer.alloc(10);
}
response.setHeader('Content-Encoding', 'encrypted');
response.send(gzippedPayload);
}
public sendRawError(response: Response, errorCode: ErrorCode) {
const data = {
errorCode: errorCode as number,
timestamp: +new Date() / 1000 | 0,
};
response.send(data);
}
}
import {Body, Controller, Get, Post, Req, Res} from '@nestjs/common';
import {Request, Response} from 'express';
import {UserService} from '../providers/user.service';
import {EncryptionService} from '../providers/encryption.service';
import {IRequestBody} from '../entities/IRequestBody';
import {IRespondData} from '../entities/IRespondData';
import {ErrorCode} from '../states/ErrorCode';
import {RequestService} from '../providers/request.service';
import {ApiError} from '../errors/ApiError';
@Controller('users')
export class UsersController {
constructor(private readonly userService: UserService,
private readonly requestService: RequestService,
private readonly encryptionService: EncryptionService,
) {
}
@Get('/')
getLogin(): string {
return '/users';
}
@Post('login')
async index(@Req() request: Request, @Res() response: Response, @Body() requestBody: IRequestBody) {
const query = request.query;
const requestData = requestBody.requestData;
// check is valid device id
if (!requestData.deviceId || requestData.deviceId.length !== 32) {
throw new ApiError(ErrorCode.InvalidDeviceId);
}
// login user
const user = this.userService.login(requestData.deviceId);
requestBody.user = user;
this.userService.updateNonce(user, requestBody.nonce);
this.userService.createSession(user);
const respondData: IRespondData = {
token: this.encryptionService.generateJwt(user.deviceId),
};
return await this.requestService.sendData(response, respondData);
}
}
import {Injectable} from '@nestjs/common';
import {IUser} from '../entities/IUser';
import {ICard} from '../entities/ICard';
import {ApiError} from '../errors/ApiError';
import {ErrorCode} from '../states/ErrorCode';
import {IRespondData} from '../entities/IRespondData';
@Injectable()
export class UserService {
private _users: Map<string, IUser> = new Map();
private _cards: Map<string, ICard[]> = new Map();
private _caches: Map<string, IRespondData> = new Map();
private _totalUser: number = 0;
constructor() {
//
}
login(deviceId: string): IUser {
let user = this._users.get(deviceId);
if (!user) {
this._totalUser++;
user = {userId: this._totalUser, deviceId, nonce: 0, lockTimestamp: 0, session: ''};
this._users.set(deviceId, user);
this._cards.set(deviceId, []);
}
return user;
}
updateNonce(user: IUser, nonce: number) {
user.nonce = nonce;
}
createSession(user: IUser) {
user.session = Math.random().toString();
}
setCache(hash: string, respondData: IRespondData) {
this._caches.set(hash, respondData);
}
getCache(hash: string): IRespondData | undefined {
return this._caches.get(hash);
}
get(deviceId: string): IUser | undefined {
return this._users.get(deviceId);
}
listCards(deviceId: string): ICard[] {
return this._cards.get(deviceId);
}
createCard(deviceId: string, monsterId: number): [ICard[], ICard] {
if (isNaN(monsterId) || monsterId <= 0) {
throw new ApiError(ErrorCode.InvalidMonsterId);
}
const cards = this.listCards(deviceId);
const id = Math.max(0, ...cards.map(x => x.id)) + 1;
const card: ICard = {
id,
monsterId,
exp: 0,
};
cards.push(card);
return [cards, card];
}
updateCard(deviceId: string, cardId: number, exp: number): [ICard[], ICard] {
const cards = this.listCards(deviceId);
const index = cards.findIndex(x => x.id === cardId);
if (index >= 0) {
const card = cards[index];
card.exp = exp;
return [cards, card];
}
throw new ApiError(ErrorCode.InvalidCardId);
}
deleteCard(deviceId: string, cardId: number): [ICard[], ICard] {
const cards = this.listCards(deviceId);
const index = cards.findIndex(x => x.id === cardId);
if (index >= 0) {
const card = cards[index];
cards.splice(index, 1);
return [cards, card];
}
throw new ApiError(ErrorCode.InvalidCardId);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment