diff --git a/src/JWT.ts b/src/JWT.ts new file mode 100644 index 0000000..3591236 --- /dev/null +++ b/src/JWT.ts @@ -0,0 +1,75 @@ +import { hmac } from "https://deno.land/x/hmac@v2.0.1/mod.ts"; + +function trimBase64(val: string | Uint8Array) { + if (val instanceof Uint8Array) { + val = new TextDecoder().decode(val); + } + + while (val.endsWith('=')) { + val = val.substring(0, val.length - 1); + } + + return val.replaceAll('+', '-').replaceAll('/', '_'); +} +function toBase64(val: string) { + val = btoa(val); + + while (val.endsWith('=')) { + val = val.substring(0, val.length - 1); + } + + return val.replaceAll('+', '-').replaceAll('/', '_'); +} +function fromBase64(val: string) { + return atob(val.replaceAll('-', '+').replaceAll('_', '/')); +} + +export interface JWTPayload { + iss?: string; + sub?: string; + aud?: string; + exp?: number; + nbf?: number; + iat?: number; + jti?: string; + // deno-lint-ignore no-explicit-any + [prop: string]: any; +} + +export default { + encode(payload: JWTPayload | string, key: string) { + if (typeof payload === 'string') return payload; + const rawHeader = JSON.stringify({ alg: "HS256", typ: "JWT" }); + const rawPayload = JSON.stringify(payload); + const data = toBase64(rawHeader) + '.' + toBase64(rawPayload); + const rawSignature = trimBase64(hmac('sha256', key, data, 'utf8', 'base64')); + return data + '.' + rawSignature; + }, + decode(jwt: string | JWTPayload, key?: string) { + if (typeof jwt === 'object') return jwt; + const segments = jwt.split('.'); + if (segments.length != 3) throw new Error("Expected jwt to have exactly 2 dots."); + const [ rawHeader, rawPayload, givenSig ] = segments; + const data = rawHeader + '.' + rawPayload; + + if (key != undefined) { + if (JSON.parse(fromBase64(rawHeader))?.alg != "HS256") return undefined; + const actualSig = trimBase64(hmac('sha256', key, data, 'utf8', 'base64')) as string; + if (givenSig != actualSig) return undefined; + } + + return JSON.parse(fromBase64(rawPayload)) as JWTPayload; + }, + validate(j: string | JWTPayload, key: string) { + if (typeof j === 'object') { + j = this.encode(j, key); + } + const segments = j.split('.'); + if (segments.length != 3) throw new Error("Expected jwt to have exactly 2 dots."); + const [ header, payload, givenSig ] = segments; + const data = header + '.' + payload; + const actualSig = trimBase64(hmac('sha256', key, data, 'utf8', 'base64')) as string; + + return givenSig != actualSig; + } +} diff --git a/src/index.ts b/src/index.ts index d9bf42e..d966344 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,5 @@ -import { Collection, MongoClient } from "https://deno.land/x/mongo@v0.31.2/mod.ts"; -import UserRouter from "./routers/UserRouter.ts"; -import User from "./models/User.ts"; -import Response from "./server/RestResponse.ts"; -import Router, { rest, route } from "./server/Router.ts"; - -class RootRouter extends Router { - @route('users/*') users: UserRouter; - - @rest('*', '*') - default() { - return new Response().body(new Blob(['Page not found :/'])).status(404); - } - - constructor(salt: string, users: Collection) { - super(); - this.users = new UserRouter(salt, users); - } -} +import { MongoClient } from "https://deno.land/x/mongo@v0.31.2/mod.ts"; +import { RootRouter } from "./routers/RootRouter.ts"; export default async function clonegur() { const salt = new TextDecoder().decode(await Deno.readFile('keys/salt.txt')); diff --git a/src/routers/AppRouter.ts b/src/routers/AppRouter.ts index 235144b..d6852aa 100644 --- a/src/routers/AppRouter.ts +++ b/src/routers/AppRouter.ts @@ -1,4 +1,4 @@ -import { HttpError } from "../server/HttpError.ts"; +import HttpError from "../server/HttpError.ts"; import RestRequest from "../server/RestRequest.ts"; import RestResponse from "../server/RestResponse.ts"; import Router from "../server/Router.ts"; diff --git a/src/routers/RootRouter.ts b/src/routers/RootRouter.ts new file mode 100644 index 0000000..d48a63d --- /dev/null +++ b/src/routers/RootRouter.ts @@ -0,0 +1,20 @@ +import { Collection } from "https://deno.land/x/mongo@v0.31.2/mod.ts"; +import UserRouter from "../routers/UserRouter.ts"; +import User from "../models/User.ts"; +import Response from "../server/RestResponse.ts"; +import Router from "../server/Router.ts"; +import { rest, route } from "../server/decorators.ts"; + +export class RootRouter extends Router { + @route('users/*') users; + + @rest('*', '*') + default() { + return new Response().body(new Blob(['Page not found :/'])).status(404); + } + + constructor(salt: string, users: Collection) { + super(); + this.users = new UserRouter(salt, users); + } +} diff --git a/src/routers/UserRouter.ts b/src/routers/UserRouter.ts index 0cd9c4d..e698b6a 100644 --- a/src/routers/UserRouter.ts +++ b/src/routers/UserRouter.ts @@ -1,9 +1,10 @@ import { Collection } from "https://deno.land/x/mongo@v0.31.2/mod.ts"; +import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts"; + +import { body, rest, schema } from "../server/decorators.ts"; import HttpError from "../server/HttpError.ts"; import User from "../models/User.ts"; -import { body, rest, schema } from "../server/decorators.ts"; import AppRouter from "./AppRouter.ts"; -import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts"; interface SignupRequest { username: string; @@ -11,7 +12,7 @@ interface SignupRequest { } export default class UserRouter extends AppRouter { - @rest('GET', '/') + @rest('GET', '/:username') async get(@schema('string') username: string) { const res = await this.users.findOne({ username }); diff --git a/src/server/Router.ts b/src/server/Router.ts index 6fb786d..abd9c34 100644 --- a/src/server/Router.ts +++ b/src/server/Router.ts @@ -13,7 +13,7 @@ export interface RestOptions { route?: string; } -export type ProcessFunc = (req: RestRequest, arg: unknown, name: string) => Promise | unknown; +export type ProcessFunc = (this: T, req: RestRequest, arg: unknown, name: string) => Promise | unknown; export interface RouterHandler { path: string; @@ -26,8 +26,8 @@ export function addMetaQuery(target: T, ...handlers: ((r: T) = props.push(...handlers); } -export function makeParameterModifier(func: ProcessFunc) { - return (target: Router, key: string, index: number) => { +export function makeParameterModifier(func: ProcessFunc) { + return (target: T, key: string & keyof T, index: number) => { let res = Reflect.getOwnMetadata('router:params', target, key); if (res === undefined) Reflect.defineMetadata('router:params', res = [], target, key); diff --git a/src/server/decorators/auth.ts b/src/server/decorators/auth.ts index 71d610a..a5c82ed 100644 --- a/src/server/decorators/auth.ts +++ b/src/server/decorators/auth.ts @@ -1,14 +1,15 @@ -import { makeParameterModifier } from "../Router.ts"; +import JWT from "../../JWT.ts"; +import Router, { makeParameterModifier } from "../Router.ts"; export type AuthType = 'raw' | 'jwt'; -export function auth(type: AuthType = 'jwt') { - return makeParameterModifier(req => { +export function auth(salt: (self: T) => string, type: AuthType = 'jwt') { + return makeParameterModifier(function (req) { let res = req.headers.authorization; if (typeof res !== 'string') return undefined; if (res.startsWith('Bearer')) res = res.substring(6).trimStart(); - if (type === 'jwt') throw new Error('JWT is not supported.'); + if (type === 'jwt') return JWT.decode(res, salt(this)); return res; }); diff --git a/src/server/decorators/rest.ts b/src/server/decorators/rest.ts index 190af43..703661e 100644 --- a/src/server/decorators/rest.ts +++ b/src/server/decorators/rest.ts @@ -8,7 +8,7 @@ import serialize from "../serialize.ts"; export type HttpMethod = '*' | 'GET' | 'POST' | 'CHANGE' | 'DELETE' | 'PUT' | 'UPDATE'; type Base = Router & { [X in KeyT]: Function; }; -type ModArray = ([ProcessFunc, ...ProcessFunc[]] | undefined)[]; +type ModArray = ([ProcessFunc, ...ProcessFunc[]] | undefined)[]; export default function rest>(method: HttpMethod, route?: string) { return (target: T, key: KeyT) => { @@ -21,14 +21,14 @@ export default function rest const params: string[] = []; const args: unknown[] = []; - const allMods: ModArray = []; + const allMods: ModArray = []; let signature = r[key].toString(); signature = signature.substring(signature.indexOf('(') + 1, signature.indexOf(')')); params.push(...signature.split(',').map(v => v.trim()).filter(v => v !== '')); for (let proto = r; proto instanceof Router; proto = Object.getPrototypeOf(proto)) { - const data = Reflect.getOwnMetadata('router:params', proto, key) as ModArray; + const data = Reflect.getOwnMetadata('router:params', proto, key) as ModArray; if (!data) continue; for (let i = 0; i < data.length; i++) { if (data[i] === undefined) continue; @@ -44,7 +44,7 @@ export default function rest let arg: unknown = req.params[param]; for (const mod of allMods[i] ?? []) { - arg = await mod(req, arg, params[i]); + arg = await mod.call(r, req, arg, params[i]); } args.push(arg); diff --git a/src/server/serialize.ts b/src/server/serialize.ts index c54ba02..a12bc2e 100644 --- a/src/server/serialize.ts +++ b/src/server/serialize.ts @@ -23,5 +23,5 @@ export default async function serialize(val: unknown, depth = 16): Promise val = val.toString(); } - return new Blob([new TextEncoder().encode(JSON.stringify(val))]); + return new Blob([JSON.stringify(val)]); }