-
-
Save terence410/235d13497e6d0c7202db39391c924a3c to your computer and use it in GitHub Desktop.
Anit Client Cheat API (Server Side Logic) - Written in NestJs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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