-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #30 from rikilele/develop
v0.5.0-rc
- Loading branch information
Showing
12 changed files
with
310 additions
and
187 deletions.
There are no files selected for viewing
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
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
// Copyright 2021 Riki Singh Khorana. All rights reserved. MIT license. | ||
|
||
import { Status } from "../src/deps.ts"; | ||
import { KyukoMiddleware, KyukoRequest, KyukoResponse } from "../mod.ts"; | ||
|
||
/** | ||
* An extension of `KyukoRequest` that can be used with the `basicAuth` middleware. | ||
* Adds authentication information onto the request object. | ||
* | ||
* ```ts | ||
* app.use(basicAuth(authenticator)); | ||
* | ||
* app.get("/secret", (req, res) => { | ||
* const { authenticated } = (req as WithBasicAuth).basicAuth; | ||
* if (authenticated) { | ||
* res.send("a secret message"); | ||
* } | ||
* | ||
* // ... | ||
* }); | ||
* ``` | ||
*/ | ||
export interface WithBasicAuth extends KyukoRequest { | ||
basicAuth: { | ||
realm: string; | ||
authenticated: boolean; | ||
|
||
/** | ||
* The username of the authenticated user. | ||
*/ | ||
user: string | undefined; | ||
}; | ||
} | ||
|
||
/** | ||
* A function that returns `true` if the username and password are valid. | ||
*/ | ||
export type Authenticator = ( | ||
| ((username: string, password: string) => boolean) | ||
| ((username: string, password: string) => Promise<boolean>) | ||
); | ||
|
||
/** | ||
* Returns a `KyukoMiddleware` that handles basic authentication. | ||
* The result of authentication is stored in `req.basicAuth`. | ||
* See [RFC7617](https://datatracker.ietf.org/doc/html/rfc7617) for more information. | ||
* | ||
* @param authenticator Authenticates the username and password supplied by the middleware. | ||
* @param realm Defines a "protection space" that can be informed to clients. | ||
* @param sendResponse Whether to automatically send a `401 Unauthorized` response on failed authentication. | ||
*/ | ||
export function basicAuth( | ||
authenticator: Authenticator, | ||
realm = "Access to app", | ||
sendResponse = false, | ||
): KyukoMiddleware { | ||
return async function basicAuth(req: KyukoRequest, res: KyukoResponse) { | ||
const _req = req as WithBasicAuth; | ||
_req.basicAuth = { | ||
realm, | ||
authenticated: false, | ||
user: undefined, | ||
}; | ||
|
||
const h = _req.headers.get("authorization"); | ||
if (!h?.startsWith("Basic ")) { | ||
return sendResponse && unauthenticated(_req, res); | ||
} | ||
|
||
const [username, password] = (h as string).substr(6).split(":").map(atob); | ||
if (!await authenticator(username, password)) { | ||
return sendResponse && unauthenticated(_req, res); | ||
} | ||
|
||
_req.basicAuth.authenticated = true; | ||
_req.basicAuth.user = username; | ||
}; | ||
} | ||
|
||
function unauthenticated(req: WithBasicAuth, res: KyukoResponse) { | ||
if (!res.wasSent()) { | ||
const { realm } = req.basicAuth; | ||
res.headers.append("WWW-Authenticate", `Basic: realm="${realm}"`); | ||
res.headers.append("WWW-Authenticate", 'charset="UTF-8"'); | ||
res.status(Status.Unauthorized).send(); | ||
} | ||
} |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,43 @@ | ||
import { KyukoMiddleware, KyukoRequest } from "../mod.ts"; | ||
// Copyright 2021 Riki Singh Khorana. All rights reserved. MIT license. | ||
|
||
export interface KyukoRequestWithJson extends KyukoRequest { | ||
// deno-lint-ignore no-explicit-any | ||
requestBody: any; | ||
} | ||
import { KyukoMiddleware, KyukoRequest } from "../mod.ts"; | ||
|
||
/** | ||
* Returns a `KyukoMiddleware` that attempts to parse the request body as JSON. | ||
* The parsed body is stored into `req.requestBody`. | ||
* Note that `req.body` will be stay unused (hence `req.bodyUsed === false`). | ||
* | ||
* example: | ||
* An extension of `KyukoRequest` that can be used with the `json` middleware. | ||
* The generic `T` can be supplied to assist with request body type checking. | ||
* | ||
* ```ts | ||
* interface UserSchema { | ||
* firstName: string; | ||
* middleName: string; | ||
* lastName: string; | ||
* age: number; | ||
* } | ||
* | ||
* app.use(json()); | ||
* | ||
* app.post("/", (req, res) => { | ||
* const { requestBody } = req as KyukoRequestWithJson; | ||
* const { requestBody } = req as WithBody<UserSchema>; | ||
* // use req.firstName,... | ||
* }); | ||
* ``` | ||
*/ | ||
export interface WithBody<T = unknown> extends KyukoRequest { | ||
requestBody: T; | ||
} | ||
|
||
/** | ||
* Returns a `KyukoMiddleware` that attempts to parse the request body as JSON. | ||
* The parsed body is stored into `req.requestBody`. | ||
* Note that `req.body` will be stay unused (hence `req.bodyUsed === false`). | ||
*/ | ||
export function json(): KyukoMiddleware { | ||
return async function (req: KyukoRequest) { | ||
return async function json(req: KyukoRequest) { | ||
const contentType = req.headers.get("content-type"); | ||
if (contentType?.includes("application/json")) { | ||
const requestClone = req.clone(); | ||
const json = await requestClone.json(); | ||
(req as KyukoRequestWithJson).requestBody = json; | ||
(req as WithBody).requestBody = json; | ||
} | ||
}; | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ | |
/// <reference path="https://deno.land/x/[email protected]/types/deploy.ns.d.ts" /> | ||
/// <reference path="https://deno.land/x/[email protected]/types/deploy.window.d.ts" /> | ||
|
||
import { brightRed } from "./deps.ts"; | ||
import { brightRed, Status } from "./deps.ts"; | ||
import { KyukoRequest, KyukoRequestImpl } from "./KyukoRequest.ts"; | ||
import { KyukoResponse, KyukoResponseImpl } from "./KyukoResponse.ts"; | ||
import { RoutePathHandler } from "./RoutePathHandler.ts"; | ||
|
@@ -14,28 +14,29 @@ import { RoutePathHandler } from "./RoutePathHandler.ts"; | |
* Runs after all middleware functions have been called. | ||
*/ | ||
export type KyukoRouteHandler = ( | ||
| ((req: KyukoRequest, res: KyukoResponse) => void) | ||
| ((req: KyukoRequest, res: KyukoResponse) => Promise<void>) | ||
); | ||
req: KyukoRequest, | ||
res: KyukoResponse, | ||
) => Promise<unknown> | unknown; | ||
|
||
/** | ||
* A function that is invoked before the route handler is called. | ||
* Hands over execution to the next middleware / route handler on return. | ||
*/ | ||
export type KyukoMiddleware = ( | ||
| ((req: KyukoRequest, res: KyukoResponse) => void) | ||
| ((req: KyukoRequest, res: KyukoResponse) => Promise<void>) | ||
); | ||
req: KyukoRequest, | ||
res: KyukoResponse, | ||
) => Promise<unknown> | unknown; | ||
|
||
/** | ||
* A function that is invoked when errors are thrown within the Kyuko app. | ||
* Has access to the `err` object as well as the `req` and `res` objects. | ||
* Hands over execution to the next error handler on return. | ||
*/ | ||
export type KyukoErrorHandler = ( | ||
| ((err: Error, req: KyukoRequest, res: KyukoResponse) => void) | ||
| ((err: Error, req: KyukoRequest, res: KyukoResponse) => Promise<void>) | ||
); | ||
err: Error, | ||
req: KyukoRequest, | ||
res: KyukoResponse, | ||
) => Promise<unknown> | unknown; | ||
|
||
/** | ||
* An ultra-light framework for http servers hosted on [Deno Deploy](https://deno.com/deploy). | ||
|
@@ -55,7 +56,7 @@ export class Kyuko { | |
this.#routes = new RoutePathHandler(); | ||
this.#middleware = []; | ||
this.#errorHandlers = []; | ||
this.#defaultHandler = (_, res) => res.status(404).send(); | ||
this.#defaultHandler = (_, res) => res.status(Status.NotFound).send(); | ||
this.#customHandlers = new Map(); | ||
this.#customHandlers.set("GET", new Map()); | ||
this.#customHandlers.set("POST", new Map()); | ||
|
@@ -203,12 +204,12 @@ export class Kyuko { | |
const { pathname, searchParams } = new URL(req.url); | ||
|
||
// Handle routing | ||
let handler: KyukoRouteHandler = this.#defaultHandler; | ||
let routeHandler: KyukoRouteHandler = this.#defaultHandler; | ||
const routePath = this.#routes.findMatch(pathname); | ||
if (routePath !== undefined) { | ||
const customHandlers = this.#customHandlers.get(req.method); | ||
if (customHandlers?.has(routePath)) { | ||
handler = customHandlers.get(routePath) as KyukoRouteHandler; | ||
routeHandler = customHandlers.get(routePath) as KyukoRouteHandler; | ||
} | ||
|
||
// Fill req.params | ||
|
@@ -220,44 +221,49 @@ export class Kyuko { | |
req.query.append(key, value); | ||
}); | ||
|
||
this.invokeHandlers(req, res, handler); | ||
this.invokeHandlers(req, res, routeHandler); | ||
} | ||
|
||
private async invokeHandlers( | ||
req: KyukoRequest, | ||
res: KyukoResponse, | ||
handler: KyukoRouteHandler, | ||
routeHandler: KyukoRouteHandler, | ||
) { | ||
// Run middleware and route handler | ||
// Run middleware | ||
try { | ||
for (const middleware of this.#middleware) { | ||
await middleware(req, res); | ||
} | ||
} catch (err) { | ||
console.error(brightRed("Error in KyukoMiddleware:")); | ||
console.error(err); | ||
this.handleError(err, req, res); | ||
} | ||
|
||
// Run route handler | ||
try { | ||
if (!res.wasSent()) { | ||
handler(req, res); | ||
await routeHandler(req, res); | ||
} | ||
} catch (err) { | ||
console.error(brightRed("Error in KyukoRouteHandler:")); | ||
console.error(err); | ||
this.handleError(err, req, res); | ||
} | ||
} | ||
|
||
// Catch error from middleware OR route handler | ||
} catch (err1) { | ||
console.error(brightRed("Error in KyukoMiddleware / KyukoRouteHandler:")); | ||
console.error(err1); | ||
|
||
// Run error handlers | ||
try { | ||
for (const errorHandler of this.#errorHandlers) { | ||
await errorHandler(err1, req, res); | ||
} | ||
|
||
// Catch error from error handler | ||
} catch (err2) { | ||
console.error(brightRed("Error in KyukoErrorHandler:")); | ||
console.error(err2); | ||
private async handleError(err: Error, req: KyukoRequest, res: KyukoResponse) { | ||
try { | ||
for (const errorHandler of this.#errorHandlers) { | ||
await errorHandler(err, req, res); | ||
} | ||
} catch (ohShit) { | ||
console.error(brightRed("Error in KyukoErrorHandler:")); | ||
console.error(ohShit); | ||
} | ||
|
||
if (!res.wasSent()) { | ||
res.status(500).send(); | ||
} | ||
if (!res.wasSent()) { | ||
res.status(Status.InternalServerError).send(); | ||
} | ||
} | ||
} |
Oops, something went wrong.