Skip to content

Commit

Permalink
Merge pull request #30 from rikilele/develop
Browse files Browse the repository at this point in the history
v0.5.0-rc
  • Loading branch information
rikilele authored Jul 20, 2021
2 parents 0938072 + f59a6f2 commit 3d19a59
Show file tree
Hide file tree
Showing 12 changed files with 310 additions and 187 deletions.
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ jobs:

strategy:
matrix:
deno: ["v1.x", "canary"]
deno:
- "v1.x"
- "canary"
os:
- macOS-latest
# - macOS-latest
# - windows-latest
- ubuntu-latest

Expand Down Expand Up @@ -55,4 +57,4 @@ jobs:
run: deno cache src/dev_deps.ts

- name: Run tests
run: deno test -A --unstable
run: deno test
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Where:
- `app` is an instance of
[`Kyuko`](https://doc.deno.land/https/deno.land/x/kyuko/mod.ts#Kyuko)
- `METHOD` is an http request method in lowercase
- `PATH` is a valid [route paths](#route-path)
- `PATH` is a valid [route path](#route-paths)
- `HANDLER` is the
[`KyukoRouteHandler`](https://doc.deno.land/https/deno.land/x/kyuko/mod.ts#KyukoRouteHandler)
executed when the route is matched
Expand Down Expand Up @@ -263,17 +263,16 @@ responded to within a Kyuko app:
completion, unless an error is thrown.

A middleware can choose to respond early to an event by calling `res.send()`,
`res.redirect()`, etc. In that case, the `[ROUTE HANDLING]` and
`[ERROR HANDLING]` steps will not be taken, and the event lifecycle ends in
this step.
`res.redirect()`, etc. In that case, the `[ROUTE HANDLING]` step will not be
taken, and the event lifecycle ends in this step.

1. **`[ROUTE HANDLING]` Running the route handler**

The **one** route handler that was chosen in the `[ROUTING]` step will be
executed in this step. The route handler will not run however, if

- A middleware threw an error
- A middleware chose to respond early
- A middleware threw an error AND the error handler responded early

1. **`[ERROR HANDLING]` Handling errors**

Expand Down
4 changes: 2 additions & 2 deletions examples/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { Kyuko } from "../mod.ts";
import { decodeParams } from "../middleware/decodeParams.ts";
import { json, KyukoRequestWithJson } from "../middleware/json.ts";
import { json, WithBody } from "../middleware/json.ts";

const app = new Kyuko();

Expand All @@ -20,7 +20,7 @@ app.get("/:name", (req, res) => {
* Responds with a pretty version of the JSON request body.
*/
app.post("/", (req, res) => {
const { requestBody } = req as KyukoRequestWithJson;
const { requestBody } = req as WithBody;
res.send(JSON.stringify(requestBody, null, 2));
});

Expand Down
87 changes: 87 additions & 0 deletions middleware/basicAuth.ts
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();
}
}
4 changes: 3 additions & 1 deletion middleware/decodeParams.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Copyright 2021 Riki Singh Khorana. All rights reserved. MIT license.

import { KyukoMiddleware, KyukoRequest } from "../mod.ts";

/**
* Returns a `KyukoMiddleware` that decodes the values of `req.params`.
*/
export function decodeParams(): KyukoMiddleware {
return function (req: KyukoRequest) {
return function decodeParams(req: KyukoRequest) {
Object.keys(req.params).forEach((param) => {
const encoded = req.params[param];
req.params[param] = decodeURIComponent(encoded);
Expand Down
37 changes: 24 additions & 13 deletions middleware/json.ts
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;
}
};
}
76 changes: 41 additions & 35 deletions src/Kyuko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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).
Expand All @@ -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());
Expand Down Expand Up @@ -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
Expand All @@ -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();
}
}
}
Loading

0 comments on commit 3d19a59

Please sign in to comment.