Embed base images into binary
Include resource directory Ran `deno fmt` with 4 space indent, that changed every file (!!!!!) various changes
This commit is contained in:
158
src/apiutils.ts
158
src/apiutils.ts
@@ -4,27 +4,29 @@ import Logging from "@proxnet/undead-logging";
|
||||
import { decode } from "@gz/jwt";
|
||||
import { Config } from "./config.ts";
|
||||
import { AuthType, User, UserTokenFormat } from "./data/users.ts";
|
||||
import Profile, { ProfileTokenFormat } from "./data/profiles.ts";
|
||||
|
||||
const config = Config.getConfig();
|
||||
|
||||
const log = new Logging('APIUtils');
|
||||
const log = new Logging("APIUtils");
|
||||
|
||||
type AppRouter = {
|
||||
path: string,
|
||||
router: express.Router
|
||||
}
|
||||
path: string;
|
||||
router: express.Router;
|
||||
};
|
||||
|
||||
export function createRouter(path: string) {
|
||||
const router: AppRouter = {
|
||||
path: path,
|
||||
router: express.Router()
|
||||
}
|
||||
router: express.Router(),
|
||||
};
|
||||
return router;
|
||||
}
|
||||
|
||||
export function generateRandomString(length: number) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let randomString = '';
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let randomString = "";
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * characters.length);
|
||||
@@ -35,11 +37,20 @@ export function generateRandomString(length: number) {
|
||||
}
|
||||
|
||||
export function checkQueryTypes<T>(typeDef: T) {
|
||||
return (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
|
||||
return (
|
||||
rq: express.Request,
|
||||
rs: express.Response,
|
||||
nxt: express.NextFunction,
|
||||
) => {
|
||||
for (const key in typeDef) {
|
||||
if (typeof rq.query[key] !== typeof (typeDef)[key]) {
|
||||
if (typeof rq.query[key] !== typeof typeDef[key]) {
|
||||
rs.statusCode = 400;
|
||||
rs.json(genericResponseFormat(true, "One or more query parameters were invalid or not found."));
|
||||
rs.json(
|
||||
genericResponseFormat(
|
||||
true,
|
||||
"One or more query parameters were invalid or not found.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -47,12 +58,21 @@ export function checkQueryTypes<T>(typeDef: T) {
|
||||
};
|
||||
}
|
||||
export function checkBodyTypes<T>(typeDef: T) {
|
||||
return (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
|
||||
for (const key in typeDef) {
|
||||
if (typeof rq.body[key] !== typeof (typeDef)[key]) {
|
||||
return (
|
||||
rq: express.Request,
|
||||
rs: express.Response,
|
||||
nxt: express.NextFunction,
|
||||
) => {
|
||||
for (const key in typeDef) {
|
||||
if (typeof rq.body[key] !== typeof typeDef[key]) {
|
||||
log.e(`Body check for key '${key}' failed.`);
|
||||
rs.statusCode = 400;
|
||||
rs.json(genericResponseFormat(true, "One or more body values were invalid or not found."));
|
||||
rs.json(
|
||||
genericResponseFormat(
|
||||
true,
|
||||
"One or more body values were invalid or not found.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -60,28 +80,40 @@ export function checkBodyTypes<T>(typeDef: T) {
|
||||
};
|
||||
}
|
||||
|
||||
export function genericResponseFormat(failure: boolean, msg: string | null = null, data: object | null = null) {
|
||||
export function genericResponseFormat(
|
||||
failure: boolean,
|
||||
msg: string | null = null,
|
||||
data: object | null = null,
|
||||
) {
|
||||
return { failed: failure, message: msg, data: data };
|
||||
}
|
||||
export function genericResponse(failure: boolean, msg: string | null = null, data: object | null = null) {
|
||||
export function genericResponse(
|
||||
failure: boolean,
|
||||
msg: string | null = null,
|
||||
data: object | null = null,
|
||||
) {
|
||||
return (_rq: express.Request, rs: express.Response) => {
|
||||
rs.json({ failed: failure, message: msg, data: data });
|
||||
};
|
||||
}
|
||||
type RecNetResponse = {
|
||||
Success: boolean,
|
||||
Message: string
|
||||
Success: boolean;
|
||||
Message: string;
|
||||
};
|
||||
export function RecNetResponse(success: boolean, message: string) {
|
||||
const msg: RecNetResponse = { Success: success, Message: message };
|
||||
return (_rq: express.Request, rs: express.Response) => {
|
||||
rs.json(msg);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function logBody(rq: express.Request, _rs: express.Response, nxt: express.NextFunction) {
|
||||
nxt();
|
||||
export function logBody(
|
||||
rq: express.Request,
|
||||
_rs: express.Response,
|
||||
nxt: express.NextFunction,
|
||||
) {
|
||||
log.d(`Request body: ${JSON.stringify(rq.body)}`);
|
||||
nxt();
|
||||
}
|
||||
|
||||
export function emptyArrayResponse(_rq: express.Request, rs: express.Response) {
|
||||
@@ -89,27 +121,26 @@ export function emptyArrayResponse(_rq: express.Request, rs: express.Response) {
|
||||
}
|
||||
|
||||
export function getSrcIpDefault(rq: express.Request) {
|
||||
const cfIp = rq.header('cf-connecting-ip');
|
||||
const cfIp = rq.header("cf-connecting-ip");
|
||||
if (cfIp !== undefined) return cfIp;
|
||||
|
||||
const xrIp = rq.header('x-real-ip');
|
||||
const xrIp = rq.header("x-real-ip");
|
||||
if (xrIp !== undefined) return xrIp;
|
||||
|
||||
const ip = typeof rq.ip === 'undefined' ? '(unknown source)' : rq.ip;
|
||||
const ip = typeof rq.ip === "undefined" ? "(unknown source)" : rq.ip;
|
||||
return ip;
|
||||
}
|
||||
|
||||
export function statusResponse(code: number) {
|
||||
return (_rq: express.Request, rs: express.Response) => {
|
||||
rs.sendStatus(code);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class RateLimiter {
|
||||
#intervalId: number;
|
||||
|
||||
#intervalId: number
|
||||
|
||||
#hitLimit: number
|
||||
#hitLimit: number;
|
||||
|
||||
#addressHits: Map<string, number> = new Map();
|
||||
|
||||
@@ -118,17 +149,15 @@ export class RateLimiter {
|
||||
* @param limit Number of hits (inclusive) before requests are blocked
|
||||
*/
|
||||
constructor(interval: number, limit: number) {
|
||||
|
||||
this.#hitLimit = limit;
|
||||
|
||||
this.#intervalId = setInterval(() => {
|
||||
this.#addressHits.clear();
|
||||
}, interval * 1000);
|
||||
|
||||
Deno.addSignalListener('SIGINT', () => {
|
||||
Deno.addSignalListener("SIGINT", () => {
|
||||
this.#close();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
#addressIncrement(address: string) {
|
||||
@@ -147,66 +176,89 @@ export class RateLimiter {
|
||||
}
|
||||
|
||||
middle() {
|
||||
|
||||
return (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
|
||||
return (
|
||||
rq: express.Request,
|
||||
rs: express.Response,
|
||||
nxt: express.NextFunction,
|
||||
) => {
|
||||
const address = getSrcIpDefault(rq);
|
||||
this.#addressIncrement(address);
|
||||
|
||||
const hits = this.#getAddressHits(address);
|
||||
if (hits && hits > this.#hitLimit) {
|
||||
rs.statusCode = 429;
|
||||
rs.json(genericResponseFormat(true, `Rate limit for address ${address} reached. Try again in a moment.`));
|
||||
rs.json(
|
||||
genericResponseFormat(
|
||||
true,
|
||||
`Rate limit for address ${address} reached. Try again in a moment.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
} else nxt();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
#close() {
|
||||
clearInterval(this.#intervalId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function UserAuthentication(rq: express.Request, rs: express.Response, nxt: express.NextFunction) {
|
||||
|
||||
export interface TokenBaseFormat {
|
||||
typ: AuthType;
|
||||
iss: string;
|
||||
nbf: number;
|
||||
exp: number;
|
||||
iat: number;
|
||||
}
|
||||
export type TokenFormat = UserTokenFormat | ProfileTokenFormat;
|
||||
|
||||
export async function Authentication(
|
||||
rq: express.Request,
|
||||
rs: express.Response,
|
||||
nxt: express.NextFunction,
|
||||
) {
|
||||
function returnUnauthorized() {
|
||||
rs.statusCode = 401;
|
||||
rs.json(genericResponseFormat(true, 'Authorization required.'));
|
||||
rs.json(genericResponseFormat(true, "Authorization required."));
|
||||
}
|
||||
|
||||
const token: string | undefined = rq.header('GalvanicAuth');
|
||||
if (typeof token == 'undefined') {
|
||||
const token: string | undefined = rq.header("GalvanicAuth");
|
||||
if (typeof token == "undefined") {
|
||||
returnUnauthorized();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const decodedToken = await decode<UserTokenFormat>(token, config.auth.secret, { algorithm: "HS512" });
|
||||
const decodedToken = await decode<TokenFormat>(
|
||||
token,
|
||||
config.auth.secret,
|
||||
{
|
||||
algorithm: "HS512",
|
||||
},
|
||||
);
|
||||
|
||||
const valid = ![
|
||||
decodedToken.iss == config.web.publichost,
|
||||
decodedToken.nbf < Math.round(Date.now() / 1000),
|
||||
decodedToken.exp > Math.round(Date.now() / 1000),
|
||||
decodedToken.typ == AuthType.Web
|
||||
].includes(false);
|
||||
if (valid) {
|
||||
rs.locals.user = new User(decodedToken.sub);
|
||||
if (decodedToken.typ == AuthType.Web) {
|
||||
rs.locals.user = new User(decodedToken.sub);
|
||||
} else if (decodedToken.typ == AuthType.Game) {
|
||||
rs.locals.profile = new Profile(decodedToken.sub);
|
||||
}
|
||||
nxt();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
returnUnauthorized();
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
returnUnauthorized();
|
||||
log.w(`User Authentication failed: ${err}`);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export type NoBody = Record<string | number | symbol, never>
|
||||
export type NoBody = Record<string | number | symbol, never>;
|
||||
|
||||
export * as APIUtils from "./apiutils.ts"
|
||||
export * as APIUtils from "./apiutils.ts";
|
||||
|
||||
Reference in New Issue
Block a user