├── .gitignore ├── _config.yml ├── Attain.png ├── version.ts ├── procedure.png ├── performance ├── oak.png ├── attain.png ├── express.png ├── oak-filesend.png ├── oak-middle.png ├── attain-middle.png ├── express-middle.png ├── attain-filesend.png ├── express-sendfile.png ├── oak-middlewithfive.png ├── attain-middlewithfive.png ├── express-middlewithfive.png └── performance.md ├── sample ├── localTest │ ├── public │ │ ├── test.png │ │ └── hello.html │ ├── index.ts │ ├── secondRouter.ts │ ├── router.ts │ └── cert │ │ ├── secret.crt │ │ └── secret.key ├── simple.ts ├── basic │ ├── api.ts │ ├── main.ts │ └── router.ts ├── postParsing │ └── plugin.ts └── ws │ ├── client.ts │ └── index.ts ├── defaultHandler ├── index.ts ├── defaultPageNotFound.ts └── defaultError.ts ├── test ├── static │ └── test.html ├── test_deps.ts ├── basicTest │ ├── router.ts │ ├── param.ts │ └── main_test.ts ├── database │ └── database_test.ts └── benchmarks │ ├── graph_test.ts │ └── linear_test.ts ├── howto ├── autorecovery.md └── websocket.md ├── helmat ├── hide-powered-by.ts ├── dont-sniff-mimetype.ts ├── dns-prefetch-control.ts ├── LICENSE ├── x-xxs-protection.ts └── frame-guard.ts ├── .vscode └── settings.json ├── goorm.manifest ├── mod.ts ├── start_test.cmd ├── core ├── database.ts ├── debug.ts ├── types.ts ├── application.ts ├── httpError.ts ├── process.ts ├── request.ts ├── utils.ts ├── router.ts └── response.ts ├── egg.yaml ├── start_test.sh ├── .github └── workflows │ └── attain-ci.yaml ├── plugins ├── logger.ts ├── parser.ts ├── staticServe.ts └── security.ts ├── LICENSE ├── recovery.ts ├── deps.ts └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /index.ts -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /Attain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronwlee/attain/HEAD/Attain.png -------------------------------------------------------------------------------- /version.ts: -------------------------------------------------------------------------------- 1 | const version = "1.1.2"; 2 | 3 | export default version; 4 | -------------------------------------------------------------------------------- /procedure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronwlee/attain/HEAD/procedure.png -------------------------------------------------------------------------------- /performance/oak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/oak.png -------------------------------------------------------------------------------- /performance/attain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/attain.png -------------------------------------------------------------------------------- /performance/express.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/express.png -------------------------------------------------------------------------------- /performance/oak-filesend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/oak-filesend.png -------------------------------------------------------------------------------- /performance/oak-middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/oak-middle.png -------------------------------------------------------------------------------- /performance/attain-middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/attain-middle.png -------------------------------------------------------------------------------- /performance/express-middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/express-middle.png -------------------------------------------------------------------------------- /performance/attain-filesend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/attain-filesend.png -------------------------------------------------------------------------------- /performance/express-sendfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/express-sendfile.png -------------------------------------------------------------------------------- /sample/localTest/public/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronwlee/attain/HEAD/sample/localTest/public/test.png -------------------------------------------------------------------------------- /performance/oak-middlewithfive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/oak-middlewithfive.png -------------------------------------------------------------------------------- /performance/attain-middlewithfive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/attain-middlewithfive.png -------------------------------------------------------------------------------- /performance/express-middlewithfive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/express-middlewithfive.png -------------------------------------------------------------------------------- /defaultHandler/index.ts: -------------------------------------------------------------------------------- 1 | export { defaultError } from "./defaultError.ts"; 2 | export { defaultPageNotFound } from "./defaultPageNotFound.ts"; 3 | -------------------------------------------------------------------------------- /test/static/test.html: -------------------------------------------------------------------------------- 1 |

Test this page is a simple html

-------------------------------------------------------------------------------- /howto/autorecovery.md: -------------------------------------------------------------------------------- 1 | # Auto Recovery 2 | 3 | ## how to recovery from general deno error? 4 | ``` 5 | deno run -A --unstable https://deno.land/x/attain/recovery.ts 6 | ``` -------------------------------------------------------------------------------- /helmat/hide-powered-by.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "../mod.ts"; 2 | 3 | export const hidePoweredBy = () => { 4 | return (req: Request, res: Response) => { 5 | res.removeHeader("X-Powered-By"); 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "[typescript]": { 4 | "editor.defaultFormatter": "denoland.vscode-deno", 5 | }, 6 | "[typescriptreact]": { 7 | "editor.defaultFormatter": "denoland.vscode-deno", 8 | }, 9 | } -------------------------------------------------------------------------------- /helmat/dont-sniff-mimetype.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "../mod.ts"; 2 | 3 | export const dontSniffMimetype = () => { 4 | return (_req: Request, res: Response) => { 5 | res.setHeader("X-Content-Type-Options", "nosniff"); 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /sample/simple.ts: -------------------------------------------------------------------------------- 1 | import { App, logger } from "../mod.ts"; 2 | const app = new App(); 3 | 4 | app.use(logger); 5 | 6 | app.use("/", (req, res) => { 7 | res.status(200).send("asd"); 8 | }); 9 | 10 | await app.listen(8000, { debug: true }); 11 | 12 | -------------------------------------------------------------------------------- /test/test_deps.ts: -------------------------------------------------------------------------------- 1 | export const test = Deno.test; 2 | 3 | export { 4 | assert, 5 | assertEquals, 6 | assertStrictEquals, 7 | assertThrows, 8 | assertThrowsAsync, 9 | } from "https://deno.land/std@0.117.0/testing/asserts.ts"; 10 | 11 | export { runBenchmarks, bench } from "https://deno.land/std@0.117.0/testing/bench.ts"; 12 | -------------------------------------------------------------------------------- /sample/localTest/public/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Hello World!

5 |

Attain 0.2 / Aaron Wooseok Lee / MIT / 2020

6 | 7 | 8 | go /asd 9 | 10 | 11 | 12 | back 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /sample/basic/api.ts: -------------------------------------------------------------------------------- 1 | import { App, Request, Response } from "https://deno.land/x/attain/mod.ts"; 2 | 3 | const second = new App(); 4 | second.get("/", async (req: Request, res: Response) => { 5 | res.status(200).send({ status: "/api" }); 6 | }); 7 | 8 | second.post("/ok", (req: Request, res: Response) => { 9 | console.log("params", req.params); 10 | console.log("here im ok"); 11 | res.status(200).send({ status: "/api/ok" }); 12 | }); 13 | 14 | export default second; 15 | -------------------------------------------------------------------------------- /goorm.manifest: -------------------------------------------------------------------------------- 1 | {"author":"34082543_buozq_naver","name":"attain","type":"blank","detailedtype":"empty","description":"","date":"2021-12-09T13:20:26.000Z","plugins":{"goorm.plugin.blank":[{"plugin.blank.build_command":"","plugin.blank.run_as":"console","plugin.blank.run_command":"","plugin.blank.stop_command":"","name":"blank"}]},"is_user_plugin":false,"storage":"container","project_domain":[{"id":"34082543_buozq_naver","url":"deno-gql.run.goorm.io","port":"8080"}],"author_email":"stoneage95xp@naver.com","author_name":"이우석","ignore_patterns":[],"visibility":2} -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export { App } from "./core/application.ts"; 2 | export { Router } from "./core/router.ts"; 3 | 4 | export { AttainRequest as Request } from "./core/request.ts"; 5 | export { AttainResponse as Response } from "./core/response.ts"; 6 | 7 | export { parser } from "./plugins/parser.ts"; 8 | export { logger } from "./plugins/logger.ts"; 9 | export { staticServe } from "./plugins/staticServe.ts"; 10 | export { security } from "./plugins/security.ts"; 11 | 12 | export { AttainDatabase } from "./core/database.ts" 13 | 14 | export * from "./core/types.ts"; -------------------------------------------------------------------------------- /start_test.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | deno -V 4 | 5 | echo "start basic benchmark" 6 | deno test -A --unstable test/basicTest/ 7 | IF ERRORLEVEL 1 ( 8 | EXIT 1 9 | ) 10 | 11 | echo "start graph benchmark" 12 | deno test -A --unstable test/benchmarks/graph_test.ts 13 | IF ERRORLEVEL 1 ( 14 | EXIT 1 15 | ) 16 | 17 | echo "start linear benchmark" 18 | deno test -A --unstable test/benchmarks/linear_test.ts 19 | IF ERRORLEVEL 1 ( 20 | EXIT 1 21 | ) 22 | 23 | echo "start database test" 24 | deno test -A --unstable test/database/ 25 | IF ERRORLEVEL 1 ( 26 | EXIT 1 27 | ) 28 | 29 | EXIT 0 -------------------------------------------------------------------------------- /core/database.ts: -------------------------------------------------------------------------------- 1 | export class DatabaseError extends Error { 2 | constructor(message: string) { 3 | super(`Database Error: ${message}`); 4 | } 5 | } 6 | 7 | // the class to expand 8 | export class AttainDatabase { 9 | static __type: string = "attain" 10 | async connect() { 11 | console.log('database connected') 12 | } 13 | } 14 | 15 | export interface NoParamConstructor { 16 | new(): I 17 | __type: string; 18 | } 19 | 20 | /* 21 | async function createDatabase(ctor: NoParamConstructor): Promise { 22 | const database = new ctor(); 23 | await database.connect(); 24 | return database; 25 | } 26 | */ -------------------------------------------------------------------------------- /helmat/dns-prefetch-control.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "../mod.ts"; 2 | 3 | export interface DnsPrefetchControlOptions { 4 | allow?: boolean; 5 | } 6 | 7 | function getHeaderValueFromOptions( 8 | options?: DnsPrefetchControlOptions, 9 | ): "on" | "off" { 10 | if (options && options.allow) { 11 | return "on"; 12 | } else { 13 | return "off"; 14 | } 15 | } 16 | 17 | export const dnsPrefetchControl = (options?: DnsPrefetchControlOptions) => { 18 | const headerValue = getHeaderValueFromOptions(options); 19 | 20 | return (req: Request, res: Response) => { 21 | res.setHeader("X-DNS-Prefetch-Control", headerValue); 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /egg.yaml: -------------------------------------------------------------------------------- 1 | name: attain 2 | description: A middleware web framework, inspired by express.js 3 | stable: true 4 | version: 1.1.0 5 | entry: ./mod.ts 6 | repository: https://github.com/aaronwlee/Attain 7 | files: 8 | - mod.ts 9 | - deps.ts 10 | - README.md 11 | - attain-cli.ts 12 | - Attain.png 13 | - _config.yml 14 | - version.ts 15 | - procedure.png 16 | - recovery.ts 17 | - .gitignore 18 | - LICENSE 19 | - .vscode/**/* 20 | - cli/**/* 21 | - core/**/* 22 | - defaultHandler/**/* 23 | - helmat/**/* 24 | - howto/**/* 25 | - performance/**/* 26 | - plugins/**/* 27 | - react/**/* 28 | - sample/**/* 29 | - test/**/* 30 | - viewEngine/**/* 31 | -------------------------------------------------------------------------------- /test/basicTest/router.ts: -------------------------------------------------------------------------------- 1 | import { Router, parser } from "../../mod.ts"; 2 | 3 | /** 4 | * Test purpose 5 | * 6 | * - check nested router 7 | */ 8 | const router = new Router(); 9 | 10 | router.get("/", (req, res) => { 11 | res.send("/router"); 12 | }); 13 | 14 | router.get("/second", (req, res) => { 15 | res.send("/router/second"); 16 | }); 17 | 18 | router.get("/second/:id", (req, res) => { 19 | res.send(`/router/second/${req.params.id}`); 20 | }); 21 | 22 | router.get("/search", parser, (req, res) => { 23 | res.send(req.query); 24 | }); 25 | 26 | router.post("/post", parser, async (req, res) => { 27 | res.send(req.params); 28 | }); 29 | 30 | export default router; 31 | -------------------------------------------------------------------------------- /sample/localTest/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | Request, 4 | Response, 5 | logger, 6 | parser, 7 | staticServe, 8 | } from "../../mod.ts"; 9 | import router from "./router.ts"; 10 | 11 | try { 12 | const app = new App(); 13 | 14 | app.use(logger); 15 | app.use(parser); 16 | 17 | app.use("/", (req, res) => { 18 | res.status(200).send({ text: "hello" }); 19 | }); 20 | 21 | app.get("/error", (req, res) => { 22 | throw new Error("here"); 23 | }); 24 | 25 | app.error((err, req, res) => { 26 | console.log("in error handler", err); 27 | }); 28 | console.log("http://localhost:3500"); 29 | 30 | await app.listen(3500, { debug: true }); 31 | } catch (error) { 32 | console.log("here i error, ", error); 33 | } 34 | -------------------------------------------------------------------------------- /defaultHandler/defaultPageNotFound.ts: -------------------------------------------------------------------------------- 1 | import type { CallBackType } from "../mod.ts"; 2 | 3 | export const defaultPageNotFound: CallBackType = (req, res) => { 4 | res.status(404).send(` 5 | 6 | 7 | 20 | 21 | 22 | 23 |
24 |

Sorry: '${req.url.pathname}' page not found

25 |
26 | 27 | `); 28 | }; 29 | -------------------------------------------------------------------------------- /start_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | deno -V 4 | 5 | echo "start basic benchmark" 6 | deno test -A --unstable test/basicTest/ 7 | status=$? 8 | if test $status -ne 0 9 | then 10 | echo "Error code - $status" 11 | exit $status 12 | fi 13 | 14 | echo "start graph benchmark" 15 | deno test -A --unstable test/benchmarks/graph_test.ts 16 | status=$? 17 | if test $status -ne 0 18 | then 19 | echo "Error code - $status" 20 | exit $status 21 | fi 22 | 23 | echo "start linear benchmark" 24 | deno test -A --unstable test/benchmarks/linear_test.ts 25 | status=$? 26 | if test $status -ne 0 27 | then 28 | echo "Error code - $status" 29 | exit $status 30 | fi 31 | 32 | echo "start database test" 33 | deno test -A --unstable test/database/ 34 | status=$? 35 | if test $status -ne 0 36 | then 37 | echo "Error code - $status" 38 | exit $status 39 | fi 40 | 41 | exit 0 -------------------------------------------------------------------------------- /.github/workflows/attain-ci.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: attain ci 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | build: 8 | name: Attain test 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: [macOS-latest, windows-latest, ubuntu-latest] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: denolib/setup-deno@master 17 | with: 18 | deno-version: 1.16.4 19 | 20 | - run: deno --version 21 | 22 | - name: run test (macOS) 23 | if: startsWith(matrix.os, 'mac') 24 | run: ./start_test.sh 25 | 26 | - name: run test (Linux) 27 | if: startsWith(matrix.os, 'ubuntu') 28 | run: ./start_test.sh 29 | 30 | - name: run test (windows) 31 | if: startsWith(matrix.os, 'windows') 32 | run: ./start_test.cmd 33 | 34 | 35 | -------------------------------------------------------------------------------- /plugins/logger.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "../mod.ts"; 2 | import { 3 | green, 4 | cyan, 5 | red, 6 | yellow, 7 | bold, 8 | magenta, 9 | } from "../deps.ts"; 10 | 11 | const colorize = (status: number) => { 12 | if (status >= 500) { 13 | return red(String(status)); 14 | } else if(status >= 400) { 15 | return magenta(String(status)); 16 | } else if (status >= 300) { 17 | return yellow(String(status)); 18 | } 19 | return cyan(String(status)); 20 | } 21 | 22 | export const logger = (_: Request, res: Response) => { 23 | res.pend( 24 | (pendReq, pendRes) => { 25 | const ms = Date.now() - pendReq.startDate; 26 | console.log( 27 | `${green(pendReq.method)} ${colorize(pendRes.getStatus!)} ${pendReq.url.pathname} - ${bold(String(ms))}ms`, 28 | ); 29 | }, 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /plugins/parser.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "../mod.ts"; 2 | 3 | const setQueryVariable = (req: Request) => { 4 | const queries = req.url.search && req.url.search.substring(1).split("&") || 5 | []; 6 | if (queries.length > 0 && queries[0] !== "") { 7 | if (!req.query) { 8 | req.query = {}; 9 | } 10 | queries.map((qs) => { 11 | const pair = qs.split("="); 12 | req.query[decodeURIComponent(pair[0])] = decodeURIComponent( 13 | pair[1] || "", 14 | ); 15 | }); 16 | } 17 | }; 18 | 19 | export const parser = async (req: Request, res: Response) => { 20 | if (req.hasBody) { 21 | const params = await req.body(); 22 | if (params.type === "json") { 23 | req.params = await params.value; 24 | } else { 25 | req.params = { value: await params.value }; 26 | } 27 | } 28 | setQueryVariable(req); 29 | }; 30 | -------------------------------------------------------------------------------- /sample/postParsing/plugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | logger, 4 | parser, 5 | staticServe, 6 | } from "https://deno.land/x/attain/mod.ts"; 7 | // import { App, logger, parser } from "https://deno.land/x/attain/mod.ts"; 8 | 9 | const app = new App(); 10 | 11 | // logging response method status path time 12 | app.use(logger); 13 | 14 | // parsing the request body and save it to request.params 15 | app.use(parser); 16 | 17 | // serve static files 18 | app.use(staticServe("./public")); 19 | 20 | app.use("/", (req, res) => { 21 | res.status(200).send("hello"); 22 | }); 23 | 24 | app.post("/submit", (req, res) => { 25 | console.log(req.params); 26 | res.status(200).send({ data: "has received" }); 27 | }); 28 | 29 | app.use((req, res) => { 30 | res.status(404).send("page not found"); 31 | }); 32 | 33 | app.listen(4000); 34 | console.log("Starting to listen at http://localhost:4000"); 35 | -------------------------------------------------------------------------------- /sample/basic/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | Router, 4 | Request, 5 | Response, 6 | logger, 7 | parser, 8 | } from "https://deno.land/x/attain/mod.ts"; 9 | import api from "./router.ts"; 10 | 11 | const app = new App(); 12 | 13 | const sampleMiddleware = (req: Request, res: Response) => { 14 | console.log("before send"); 15 | }; 16 | 17 | app.use(logger); 18 | app.use(parser); 19 | 20 | app.use("/:id", sampleMiddleware, (req: Request, res: Response) => { 21 | console.log(req.params); 22 | res.status(200).send(`id: ${req.params.id}`); 23 | }); 24 | 25 | app.use("/api", api); 26 | 27 | app.use(sampleMiddleware, (req: Request, res: Response) => { 28 | res.status(404).send(` 29 | 30 | 31 | 32 |

Page not found

33 | 34 | 35 | `); 36 | }); 37 | 38 | app.listen(3500, { debug: true }); 39 | 40 | console.log("http://localhost:3500"); 41 | -------------------------------------------------------------------------------- /defaultHandler/defaultError.ts: -------------------------------------------------------------------------------- 1 | import type { ErrorCallBackType } from "../mod.ts"; 2 | 3 | export const defaultError: ErrorCallBackType = (error: Error, req, res) => { 4 | res.status(500).send(` 5 | 6 | 7 | 20 | 21 | 22 | 23 |
24 |

${error.name ? error.name : "Danger"}: ${ 25 | error.message ? error.message : error 26 | }

27 |

${error.stack ? error.stack : ""}

28 |
29 | 30 | `); 31 | }; 32 | -------------------------------------------------------------------------------- /sample/localTest/secondRouter.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "../../mod.ts"; 2 | const api = new Router(); 3 | 4 | const sleep = (time: number) => 5 | new Promise((resolve) => setTimeout(() => resolve(null), time)); 6 | 7 | // this will block the current request 8 | api.get("/test1", async (req, res) => { 9 | console.log("here '/test1'"); 10 | await sleep(1000); 11 | res.status(200).send(` 12 | 13 | 14 | 15 |

test1

16 | 17 | 18 | `); 19 | }); 20 | 21 | // this will not work 22 | api.get("/error", async (req, res) => { 23 | console.log("here '/error'"); 24 | throw new Error("Here i made a error"); 25 | }); 26 | 27 | api.get("/test3*", async (req, res) => { 28 | console.log(req.query, "here test3"); 29 | res.status(200).send(` 30 | 31 | 32 | 33 |

test3 done

34 | 35 | 36 | `); 37 | }); 38 | 39 | export default api; 40 | -------------------------------------------------------------------------------- /sample/basic/router.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response } from "https://deno.land/x/attain/mod.ts"; 2 | import second from "./api.ts"; 3 | const api = new Router(); 4 | 5 | api.use("/", second); 6 | 7 | const sleep = (time: number) => 8 | new Promise((resolve) => setTimeout(() => resolve(), time)); 9 | 10 | // this will block the current request 11 | api.get("/hello", async (req: Request, res: Response) => { 12 | console.log("here '/hello'"); 13 | await sleep(1000); 14 | res.status(200).send(` 15 | 16 | 17 | 18 |

Hello

19 | 20 | 21 | `); 22 | }); 23 | 24 | // this will not work 25 | api.get("/hello", async (req: Request, res: Response) => { 26 | console.log("here '/second hello'"); 27 | res.status(200).send(` 28 | 29 | 30 | 31 |

Second hello

32 | 33 | 34 | `); 35 | }); 36 | 37 | api.use("/post", second); 38 | 39 | export default api; 40 | -------------------------------------------------------------------------------- /plugins/staticServe.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "../mod.ts"; 2 | import { posix } from "../deps.ts" 3 | 4 | 5 | export const staticServe = ( 6 | path: string, 7 | options?: { maxAge?: number }, 8 | ) => 9 | async (req: Request, res: Response) => { 10 | if (req.method !== "GET" && req.method !== "HEAD") { 11 | return; 12 | } 13 | const maxAge = options && options.maxAge || 0; 14 | const fullPath = posix.join(path, req.url.pathname); 15 | try { 16 | let fileInfo = await Deno.stat(fullPath); 17 | if (fileInfo.isFile) { 18 | res.setHeader("Cache-Control", `public`); 19 | res.getHeaders.append("Cache-Control", `max-age=${maxAge / 1000 | 0}`); 20 | var t = new Date(); 21 | t.setSeconds(t.getSeconds() + maxAge); 22 | res.setHeader("Expires", t.toUTCString()); 23 | await res.sendFile(fullPath); 24 | } 25 | } catch (e) { 26 | if (e instanceof Deno.errors.PermissionDenied) { 27 | console.error(e); 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2022] [Aaron Wooseok Lee](https://github.com/aaronwlee) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /sample/localTest/router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "../../mod.ts"; 2 | import secondRouter from "./secondRouter.ts"; 3 | const api = new Router(); 4 | 5 | const sleep = (time: number) => 6 | new Promise((resolve) => setTimeout(() => resolve(null), time)); 7 | 8 | // this will block the current request 9 | api.get("/hello", async (req, res) => { 10 | console.log("here '/hello'"); 11 | await sleep(1000); 12 | res.status(200).send(` 13 | 14 | 15 | 16 |

Hello

17 | 18 | 19 | `); 20 | }); 21 | 22 | // this will not work 23 | api.get("/second", async (req, res) => { 24 | console.log("here '/second hello'"); 25 | res.status(200).send(` 26 | 27 | 28 | 29 |

Second hello

30 | 31 | 32 | `); 33 | }); 34 | 35 | api.get("/test(.*)", async (req, res) => { 36 | console.log(req.query, "here test"); 37 | res.status(200).send(` 38 | 39 | 40 | 41 |

test done

42 | 43 | 44 | `); 45 | }); 46 | 47 | api.use("/second", secondRouter); 48 | 49 | export default api; 50 | -------------------------------------------------------------------------------- /helmat/LICENSE: -------------------------------------------------------------------------------- 1 | https://github.com/helmetjs/helmet/blob/master/LICENSE 2 | 3 | (The MIT License) 4 | 5 | Copyright (c) 2012-2020 Evan Hahn, Adam Baldwin 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | 'Software'), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /sample/localTest/cert/secret.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDajCCAlKgAwIBAgIJAOPyQVdy/UpPMA0GCSqGSIb3DQEBCwUAMCcxCzAJBgNV 3 | BAYTAlVTMRgwFgYDVQQDDA9FeGFtcGxlLVJvb3QtQ0EwIBcNMTkxMDIxMTYyODU4 4 | WhgPMjExODA5MjcxNjI4NThaMG0xCzAJBgNVBAYTAlVTMRIwEAYDVQQIDAlZb3Vy 5 | U3RhdGUxETAPBgNVBAcMCFlvdXJDaXR5MR0wGwYDVQQKDBRFeGFtcGxlLUNlcnRp 6 | ZmljYXRlczEYMBYGA1UEAwwPbG9jYWxob3N0LmxvY2FsMIIBIjANBgkqhkiG9w0B 7 | AQEFAAOCAQ8AMIIBCgKCAQEAz9svjVdf5jihUBtofd84XKdb8dEHQRJfDNKaJ4Ar 8 | baqMHAdnqi/fWtlqEEMn8gweZ7+4hshECY5mnx4Hhy7IAbePDsTTbSm01dChhlxF 9 | uvd9QuvzvrqSjSq+v4Jlau+pQIhUzzV12dF5bFvrIrGWxCZp+W7lLDZI6Pd6Su+y 10 | ZIeiwrUaPMzdUePNf2hZI/IvWCUMCIyoqrrKHdHoPuvQCW17IyxsnFQJNbmN+Rtp 11 | BQilhtwvBbggCBWhHxEdiqBaZHDw6Zl+bU7ejx1mu9A95wpQ9SCL2cRkAlz2LDOy 12 | wznrTAwGcvqvFKxlV+3HsaD7rba4kCA1Ihp5mm/dS2k94QIDAQABo1EwTzAfBgNV 13 | HSMEGDAWgBTzut+pwwDfqmMYcI9KNWRDhxcIpTAJBgNVHRMEAjAAMAsGA1UdDwQE 14 | AwIE8DAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEBAKVu 15 | vVpu5nPGAGn1SX4FQUcbn9Z5wgBkjnZxfJHJQX4sYIRlcirZviPHCZGPWex4VHC+ 16 | lFMm+70YEN2uoe5jGrdgcugzx2Amc7/mLrsvvpMsaS0PlxNMcqhdM1WHbGjjdNln 17 | XICVITSKnB1fSGH6uo9CMCWw5kgPS9o4QWrLLkxnds3hoz7gVEUyi/6V65mcfFNA 18 | lof9iKcK9JsSHdBs35vpv7UKLX+96RM7Nm2Mu0yue5JiS79/zuMA/Kryxot4jv5z 19 | ecdWFl0eIyQBZmBzMw2zPUqkxEnXLiKjV8jutEg/4qovTOB6YiA41qbARXdzNA2V 20 | FYuchcTcWmnmVVRFyyU= 21 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /helmat/x-xxs-protection.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "../mod.ts"; 2 | 3 | export interface XXssProtectionOptions { 4 | mode?: "block" | null; 5 | reportUri?: string; 6 | } 7 | 8 | const doesUserAgentMatchOldInternetExplorer = ( 9 | userAgent: string | null | undefined, 10 | ): boolean => { 11 | if (!userAgent) { 12 | return false; 13 | } 14 | 15 | const matches = /msie\s*(\d{1,2})/i.exec(userAgent); 16 | return matches ? parseFloat(matches[1]) < 9 : false; 17 | }; 18 | 19 | const getHeaderValueFromOptions = (options: XXssProtectionOptions): string => { 20 | const directives: string[] = ["1"]; 21 | 22 | let isBlockMode: boolean; 23 | if ("mode" in options) { 24 | if (options.mode === "block") { 25 | isBlockMode = true; 26 | } else if (options.mode === null) { 27 | isBlockMode = false; 28 | } else { 29 | throw new Error('The `mode` option must be set to "block" or null.'); 30 | } 31 | } else { 32 | isBlockMode = true; 33 | } 34 | 35 | if (isBlockMode) { 36 | directives.push("mode=block"); 37 | } 38 | 39 | if (options.reportUri) { 40 | directives.push(`report=${options.reportUri}`); 41 | } 42 | 43 | return directives.join("; "); 44 | }; 45 | 46 | export const xXssProtection = (options: XXssProtectionOptions = {}) => { 47 | const headerValue = getHeaderValueFromOptions(options); 48 | 49 | return (req: Request, res: Response) => { 50 | const value = 51 | doesUserAgentMatchOldInternetExplorer(req.headers.get("user-agent")) 52 | ? "0" 53 | : headerValue; 54 | res.setHeader("X-XSS-Protection", value); 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /test/basicTest/param.ts: -------------------------------------------------------------------------------- 1 | import { Router, parser } from "../../mod.ts"; 2 | 3 | const sleep = (time: number = 100) => 4 | new Promise((resolve) => { 5 | setTimeout(() => resolve(null), time); 6 | }); 7 | 8 | /** 9 | * Test purpose 10 | * 11 | * - check param method 12 | */ 13 | const paramTest = new Router(); 14 | 15 | paramTest.param("username", async (req, res, username) => { 16 | await sleep(); 17 | req.username = username; 18 | }); 19 | 20 | paramTest.param("password", async (req, res, password) => { 21 | await sleep(500); 22 | if(password !== "123") { 23 | return res.status(404).send("password is not matched"); 24 | } 25 | req.password = password; 26 | }); 27 | 28 | paramTest.get("/:username", (req, res) => { 29 | res.send(req.username); 30 | }); 31 | 32 | paramTest.get("/:username/:password", (req, res) => { 33 | res.send({ name: req.username, password: req.password }); 34 | }); 35 | 36 | paramTest.post("/:username/password", parser, (req, res) => { 37 | res.send({ name: req.username, password: req.params.password }); 38 | }); 39 | 40 | paramTest.post("/:username/post", async (req, res) => { 41 | res.send(req.username); 42 | }); 43 | 44 | paramTest.patch("/:username/patch", async (req, res) => { 45 | res.send(req.username); 46 | }); 47 | 48 | paramTest.put("/:username/put", async (req, res) => { 49 | res.send(req.username); 50 | }); 51 | 52 | paramTest.delete("/:username/delete", async (req, res) => { 53 | res.send(req.username); 54 | }); 55 | 56 | paramTest.options("/:username/options", async (req, res) => { 57 | res.send(req.username); 58 | }); 59 | 60 | 61 | 62 | export default paramTest; 63 | -------------------------------------------------------------------------------- /recovery.ts: -------------------------------------------------------------------------------- 1 | const fileName = Deno.args; 2 | 3 | // const decoder = new TextDecoder("utf-8"); 4 | console.log("Auto Recovery mode with Attain"); 5 | // let memoryInspect = null; 6 | const exec = async () => { 7 | const p = Deno.run({ 8 | cmd: [ 9 | "deno", 10 | "run", 11 | "-A", 12 | "--unstable", 13 | fileName[0], 14 | ], 15 | }); 16 | 17 | // memoryInspect = setInterval(() => checkMem(p), 2000); 18 | const { code } = await p.status(); 19 | console.log("code: ", code); 20 | console.warn("auto recovered!"); 21 | p.close(); 22 | // clearInterval(memoryInspect); 23 | await exec(); 24 | }; 25 | 26 | // const checkMem = async (p: any) => { 27 | // const p2 = Deno.run({ 28 | // cmd: [ 29 | // "wmic", 30 | // "process", 31 | // "where", 32 | // `processid=${p.pid}`, 33 | // "get", 34 | // "WorkingSetSize", 35 | // ], 36 | // stdout: "piped" 37 | // }) 38 | // // "get-process chrome | select-object @{l='Private Memory (MB)'; e={$_.privatememorysize64 / 1mb}}" 39 | 40 | // const string = await p2.output(); 41 | // console.log(formatBytes(parseInt(decoder.decode(string).split("\n")[1]))); 42 | // } 43 | 44 | // function formatBytes(bytes: number, decimals = 4) { 45 | // if (bytes === 0) return '0 Bytes'; 46 | 47 | // const k = 1024; 48 | // const dm = decimals < 0 ? 0 : decimals; 49 | // const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 50 | 51 | // const i = Math.floor(Math.log(bytes) / Math.log(k)); 52 | 53 | // return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; 54 | // } 55 | 56 | // await exec(); 57 | -------------------------------------------------------------------------------- /plugins/security.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "../mod.ts"; 2 | import { 3 | XXssProtectionOptions, 4 | xXssProtection, 5 | } from "../helmat/x-xxs-protection.ts"; 6 | import { hidePoweredBy } from "../helmat/hide-powered-by.ts"; 7 | import { 8 | dnsPrefetchControl, 9 | DnsPrefetchControlOptions, 10 | } from "../helmat/dns-prefetch-control.ts"; 11 | import { dontSniffMimetype } from "../helmat/dont-sniff-mimetype.ts"; 12 | import { frameGuard, FrameguardOptions } from "../helmat/frame-guard.ts"; 13 | 14 | interface SecurityProps { 15 | xss?: XXssProtectionOptions | boolean; 16 | removePoweredBy?: boolean; 17 | DNSPrefetchControl?: DnsPrefetchControlOptions | boolean; 18 | noSniff?: boolean; 19 | frameguard?: FrameguardOptions | boolean; 20 | } 21 | 22 | export const security = (options?: SecurityProps) => { 23 | const { 24 | xss = true, 25 | removePoweredBy = true, 26 | DNSPrefetchControl = true, 27 | noSniff = true, 28 | frameguard = true, 29 | } = options || {}; 30 | return function security(req: Request, res: Response) { 31 | if (xss) { 32 | typeof xss === "boolean" 33 | ? xXssProtection()(req, res) 34 | : xXssProtection(xss)(req, res); 35 | } 36 | 37 | if (removePoweredBy) { 38 | hidePoweredBy()(req, res); 39 | } 40 | 41 | if (noSniff) { 42 | dontSniffMimetype()(req, res); 43 | } 44 | 45 | if (DNSPrefetchControl) { 46 | typeof DNSPrefetchControl === "boolean" 47 | ? dnsPrefetchControl()(req, res) 48 | : dnsPrefetchControl(DNSPrefetchControl)(req, res); 49 | } 50 | 51 | if (frameguard) { 52 | typeof frameguard === "boolean" 53 | ? frameGuard()(req, res) 54 | : frameGuard(frameguard)(req, res); 55 | } 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /sample/localTest/cert/secret.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDP2y+NV1/mOKFQ 3 | G2h93zhcp1vx0QdBEl8M0pongCttqowcB2eqL99a2WoQQyfyDB5nv7iGyEQJjmaf 4 | HgeHLsgBt48OxNNtKbTV0KGGXEW6931C6/O+upKNKr6/gmVq76lAiFTPNXXZ0Xls 5 | W+sisZbEJmn5buUsNkjo93pK77Jkh6LCtRo8zN1R481/aFkj8i9YJQwIjKiqusod 6 | 0eg+69AJbXsjLGycVAk1uY35G2kFCKWG3C8FuCAIFaEfER2KoFpkcPDpmX5tTt6P 7 | HWa70D3nClD1IIvZxGQCXPYsM7LDOetMDAZy+q8UrGVX7cexoPuttriQIDUiGnma 8 | b91LaT3hAgMBAAECggEBAJABfn+BQorBP1m9s3ZJmcXvmW7+7/SwYrQCkRS+4te2 9 | 6h1dMAAj7K4HpUkhDeLPbJ1aoeCXjTPFuemRp4uL6Lvvzahgy059L7FXOyFYemMf 10 | pmQgDx5cKr6tF7yc/eDJrExuZ7urgTvouiRNxqmhuh+psZBDuXkZHwhwtQSH7uNg 11 | KBDKu0qWO73vFLcLckdGEU3+H9oIWs5xcvvOkWzyvHbRGFJSihgcRpPPHodF5xB9 12 | T/gZIoJHMmCbUMlWaSasUyNXTuvCnkvBDol8vXrMJCVzKZj9GpPDcIFdc08GSn4I 13 | pTdSNwzUcHbdERzdVU28Xt+t6W5rvp/4FWrssi4IzkUCgYEA//ZcEcBguRD4OFrx 14 | 6wbSjzCcUW1NWhzA8uTOORZi4SvndcH1cU4S2wznuHNubU1XlrGwJX6PUGebmY/l 15 | 53B5PJvStbVtZCVIxllR+ZVzRuL8wLodRHzlYH8GOzHwoa4ivSupkzl72ij1u/tI 16 | NMLGfYEKVdNd8zXIESUY88NszvsCgYEAz+MDp3xOhFaCe+CPv80A592cJcfzc8Al 17 | +rahEOu+VdN2QBZf86PIf2Bfv/t0QvnRvs1z648TuH6h83YSggOAbmfHyd789jkq 18 | UWlktIaXbVn+VaHmPTcBWTg3ZTlvG+fiFCbZXiYhm+UUf1MDqZHdiifAoyVIjV/Z 19 | YhCNJo3q39MCgYEAknrpK5t9fstwUcfyA/9OhnVaL9suVjB4V0iLn+3ovlXCywgp 20 | ryLv9X3IKi2c9144jtu3I23vFCOGz3WjKzSZnQ7LogNmy9XudNxu5jcZ1mpWHPEl 21 | iKk1F2j6Juwoek5OQRX4oHFYKHwiTOa75r3Em9Q6Fu20KVgQ24bwZafj3/sCgYAy 22 | k0AoVw2jFIjaKl/Ogclen4OFjYek+XJD9Hpq62964d866Dafx5DXrFKfGkXGpZBp 23 | owI4pK5fjC9KU8dc6g0szwLEEgPowy+QbtuZL8VXTTWbD7A75E3nrs2LStXFLDzM 24 | OkdXqF801h6Oe1vAvUPwgItVJZTpEBCK0wwD/TLPEQKBgQDRkhlTtAoHW7W6STd0 25 | A/OWc0dxhzMurpxg0bLgCqUjw1ESGrSCGhffFn0IWa8sv19VWsZuBhTgjNatZsYB 26 | AhDs/6OosT/3nJoh2/t0hYDj1FBI0lPXWYD4pesuZ5yIMrmSaAOtIzp4BGY7ui8N 27 | wOqcq/jdiHj/MKEdqOXy3YAJrA== 28 | -----END PRIVATE KEY----- -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | Server, 3 | } from "https://deno.land/std@0.117.0/http/server.ts"; 4 | 5 | 6 | export { 7 | Status, 8 | STATUS_TEXT, 9 | } from "https://deno.land/std@0.117.0/http/http_status.ts"; 10 | 11 | export { 12 | contentType, 13 | extension, 14 | lookup, 15 | } from "https://deno.land/x/media_types/mod.ts"; 16 | 17 | export { 18 | deferred, 19 | } from "https://deno.land/std@0.117.0/async/mod.ts"; 20 | 21 | export type { Deferred } from "https://deno.land/std@0.117.0/async/mod.ts"; 22 | 23 | export { 24 | Sha1, 25 | } from "https://deno.land/std@0.117.0/hash/sha1.ts"; 26 | 27 | export { 28 | pathToRegexp, 29 | match, 30 | } from "https://raw.githubusercontent.com/pillarjs/path-to-regexp/master/src/index.ts"; 31 | 32 | export { 33 | red, 34 | yellow, 35 | green, 36 | cyan, 37 | bold, 38 | blue, 39 | magenta, 40 | } from "https://deno.land/std@0.117.0/fmt/colors.ts"; 41 | 42 | export { default as isEmpty } from "https://raw.githubusercontent.com/lodash/lodash/master/isEmpty.js"; 43 | 44 | export { extname } from "https://deno.land/std@0.117.0/path/mod.ts"; 45 | 46 | export { parse } from "https://deno.land/std@0.117.0/flags/mod.ts"; 47 | export { ensureDir } from "https://deno.land/std@0.117.0/fs/mod.ts"; 48 | export { EventEmitter } from "https://deno.land/std@0.117.0/node/events.ts"; 49 | export { listenAndServe } from "https://deno.land/std@0.117.0/http/server.ts"; 50 | export { 51 | acceptWebSocket, 52 | acceptable, 53 | isWebSocketCloseEvent, 54 | } from "https://deno.land/std@0.117.0/ws/mod.ts"; 55 | export { 56 | parse as yamlParse, 57 | stringify as yamlStringify, 58 | } from "https://deno.land/std@0.117.0/encoding/yaml.ts"; 59 | 60 | export { assert } from "https://deno.land/std@0.117.0/testing/asserts.ts"; 61 | 62 | export { equals } from "https://deno.land/std@0.117.0/bytes/mod.ts"; 63 | export { 64 | posix, 65 | } from "https://deno.land/std@0.117.0/path/mod.ts"; 66 | -------------------------------------------------------------------------------- /sample/ws/client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | connectWebSocket, 3 | isWebSocketCloseEvent, 4 | isWebSocketPingEvent, 5 | isWebSocketPongEvent, 6 | } from "https://deno.land/std/ws/mod.ts"; 7 | import { encode } from "https://deno.land/std/encoding/utf8.ts"; 8 | import { BufReader } from "https://deno.land/std/io/bufio.ts"; 9 | import { TextProtoReader } from "https://deno.land/std/textproto/mod.ts"; 10 | import { blue, green, red, yellow } from "https://deno.land/std/fmt/colors.ts"; 11 | 12 | const endpoint = Deno.args[0] || "ws://127.0.0.1:8080/socket"; 13 | /** simple websocket cli */ 14 | try { 15 | const sock = await connectWebSocket(endpoint); 16 | console.log(green("ws connected! (type 'close' to quit)")); 17 | 18 | const messages = async (): Promise => { 19 | for await (const msg of sock) { 20 | if (typeof msg === "string") { 21 | console.log(yellow(`< ${msg}`)); 22 | } else if (isWebSocketPingEvent(msg)) { 23 | console.log(blue("< ping")); 24 | } else if (isWebSocketPongEvent(msg)) { 25 | console.log(blue("< pong")); 26 | } else if (isWebSocketCloseEvent(msg)) { 27 | console.log(red(`closed: code=${msg.code}, reason=${msg.reason}`)); 28 | } 29 | } 30 | }; 31 | 32 | const cli = async (): Promise => { 33 | const tpr = new TextProtoReader(new BufReader(Deno.stdin)); 34 | while (true) { 35 | await Deno.stdout.write(encode("> ")); 36 | const line = await tpr.readLine(); 37 | if (line === null || line === "close") { 38 | break; 39 | } else if (line === "ping") { 40 | await sock.ping(); 41 | } else { 42 | await sock.send(line); 43 | } 44 | } 45 | }; 46 | 47 | await Promise.race([messages(), cli()]).catch(console.error); 48 | 49 | if (!sock.isClosed) { 50 | await sock.close(1000).catch(console.error); 51 | } 52 | } catch (err) { 53 | console.error(red(`Could not connect to WebSocket: '${err}'`)); 54 | } 55 | 56 | Deno.exit(0); 57 | -------------------------------------------------------------------------------- /sample/ws/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | logger, 4 | security, 5 | } from "../../mod.ts"; 6 | import { 7 | acceptWebSocket, 8 | isWebSocketCloseEvent, 9 | isWebSocketPingEvent, 10 | } from "https://deno.land/std/ws/mod.ts"; 11 | 12 | const app = new App(); 13 | 14 | app.use(logger); 15 | app.use(security()); 16 | 17 | app.use((req, res) => { 18 | console.log("first start"); 19 | }); 20 | 21 | app.get("/hello", async (req, res) => { 22 | await res.status(200).send("hello"); 23 | }); 24 | 25 | app.use("/socket", async (req, res) => { 26 | console.log(req.method); 27 | const { conn, r: bufReader, w: bufWriter, headers } = req.serverRequest; 28 | 29 | try { 30 | const sock = await acceptWebSocket({ 31 | conn, 32 | bufReader, 33 | bufWriter, 34 | headers, 35 | }); 36 | 37 | console.log("socket connected!"); 38 | 39 | try { 40 | for await (const ev of sock) { 41 | if (typeof ev === "string") { 42 | // text message 43 | console.log("ws:Text", ev); 44 | await sock.send(ev); 45 | } else if (ev instanceof Uint8Array) { 46 | // binary message 47 | console.log("ws:Binary", ev); 48 | } else if (isWebSocketPingEvent(ev)) { 49 | const [, body] = ev; 50 | // ping 51 | console.log("ws:Ping", body); 52 | } else if (isWebSocketCloseEvent(ev)) { 53 | // close 54 | const { code, reason } = ev; 55 | console.log("ws:Close", code, reason); 56 | } 57 | } 58 | } catch (err) { 59 | console.error(`failed to receive frame: ${err}`); 60 | 61 | if (!sock.isClosed) { 62 | await sock.close(1000).catch(console.error); 63 | } 64 | } 65 | 66 | console.log("socket connection end"); 67 | res.end(); 68 | } catch (err) { 69 | console.error(`failed to accept websocket: ${err}`); 70 | await res.status(400).send("error"); 71 | } 72 | }); 73 | 74 | console.log("Server at http://localhost:8080"); 75 | await app.listen(8080); 76 | -------------------------------------------------------------------------------- /helmat/frame-guard.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "../mod.ts"; 2 | 3 | export interface FrameguardOptions { 4 | action?: string; 5 | domain?: string; 6 | } 7 | 8 | function parseActionOption(actionOption: unknown): string { 9 | const invalidActionErr = new Error( 10 | 'action must be undefined, "DENY", "ALLOW-FROM", or "SAMEORIGIN".', 11 | ); 12 | 13 | if (actionOption === undefined) { 14 | actionOption = "SAMEORIGIN"; 15 | } else if (actionOption instanceof String) { 16 | actionOption = actionOption.valueOf(); 17 | } 18 | 19 | let result: string; 20 | if (typeof actionOption === "string") { 21 | result = actionOption.toUpperCase(); 22 | } else { 23 | throw invalidActionErr; 24 | } 25 | 26 | if (result === "ALLOWFROM") { 27 | result = "ALLOW-FROM"; 28 | } else if (result === "SAME-ORIGIN") { 29 | result = "SAMEORIGIN"; 30 | } 31 | 32 | if (["DENY", "ALLOW-FROM", "SAMEORIGIN"].indexOf(result) === -1) { 33 | throw invalidActionErr; 34 | } 35 | 36 | return result; 37 | } 38 | 39 | function parseDomainOption(domainOption: unknown): string { 40 | if (domainOption instanceof String) { 41 | domainOption = domainOption.valueOf(); 42 | } 43 | 44 | if (typeof domainOption !== "string") { 45 | throw new Error("ALLOW-FROM action requires a string domain parameter."); 46 | } else if (!domainOption.length) { 47 | throw new Error("domain parameter must not be empty."); 48 | } 49 | 50 | return domainOption; 51 | } 52 | 53 | function getHeaderValueFromOptions(options?: FrameguardOptions): string { 54 | options = options || {}; 55 | 56 | const action = parseActionOption(options.action); 57 | 58 | if (action === "ALLOW-FROM") { 59 | const domain = parseDomainOption(options.domain); 60 | return `${action} ${domain}`; 61 | } else { 62 | return action; 63 | } 64 | } 65 | 66 | export const frameGuard = (options?: FrameguardOptions) => { 67 | const headerValue = getHeaderValueFromOptions(options); 68 | 69 | return (_req: Request, res: Response) => { 70 | res.setHeader("X-Frame-Options", headerValue); 71 | }; 72 | }; 73 | -------------------------------------------------------------------------------- /test/database/database_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | bench, 4 | runBenchmarks, 5 | } from "../test_deps.ts"; 6 | import { App, Router, AttainDatabase } from "../../mod.ts"; 7 | 8 | const port = 8564; 9 | 10 | class SampleDatabaseClass extends AttainDatabase { 11 | #connection: boolean = false; 12 | async connect() { 13 | console.log("fake DB connected"); 14 | this.#connection = true; 15 | } 16 | 17 | get connection() { 18 | return this.#connection 19 | } 20 | } 21 | 22 | async function SampleDatabaseConnector() { 23 | const instance = new SampleDatabaseClass() 24 | await instance.connect() 25 | return instance.connection 26 | } 27 | 28 | const app1 = new App(); 29 | app1.database(SampleDatabaseClass) 30 | app1.use((req, res, db) => { 31 | Deno.test("'database' method with a class instance", () => { 32 | assertEquals(db.connection, true) 33 | }) 34 | 35 | res.send("Ok") 36 | }) 37 | app1.listen(port); 38 | 39 | await fetch( 40 | `http://localhost:${port}`, 41 | ); 42 | 43 | 44 | 45 | const app2 = new App(); 46 | app2.database(SampleDatabaseConnector) 47 | app2.use((req, res, db) => { 48 | Deno.test("'database' method with a function", () => { 49 | assertEquals(db, true) 50 | }) 51 | 52 | res.send("Ok") 53 | }) 54 | app2.listen(port + 1); 55 | 56 | await fetch( 57 | `http://localhost:${port + 1}`, 58 | ); 59 | 60 | 61 | const app3 = App.startWith(SampleDatabaseClass); 62 | app3.use((req, res, db) => { 63 | Deno.test("'startWith' method with a class instance", () => { 64 | assertEquals(db.connection, true) 65 | }) 66 | 67 | res.send("Ok") 68 | }) 69 | app3.listen(port + 2); 70 | 71 | await fetch( 72 | `http://localhost:${port + 2}`, 73 | ); 74 | 75 | 76 | const app4 = App.startWith(SampleDatabaseConnector); 77 | app4.use((req, res, db) => { 78 | Deno.test("'startWith' method with a function", () => { 79 | assertEquals(db, true) 80 | }) 81 | 82 | res.send("Ok") 83 | }) 84 | app4.listen(port + 3); 85 | 86 | await fetch( 87 | `http://localhost:${port + 3}`, 88 | ); 89 | 90 | 91 | await app1.close(); 92 | await app2.close(); 93 | await app3.close(); 94 | await app4.close(); -------------------------------------------------------------------------------- /test/benchmarks/graph_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | bench, 4 | runBenchmarks, 5 | } from "../test_deps.ts"; 6 | import { App, Router } from "../../mod.ts"; 7 | 8 | const app = new App(); 9 | 10 | const createLinearGraph = (current: number, max: number) => { 11 | const newRouter = new Router(); 12 | newRouter.get(`/${current}`, (req, res) => { 13 | res.send(current.toString()); 14 | }); 15 | if (current !== max) { 16 | newRouter.use(createLinearGraph(current + 1, max)); 17 | } 18 | return newRouter; 19 | }; 20 | 21 | const createNestedGraph = (current: number, max: number) => { 22 | const newRouter = new Router(); 23 | newRouter.get(`/${current}`, (req, res) => { 24 | res.send(current.toString()); 25 | }); 26 | if (current !== max) { 27 | newRouter.use(`/${current}`, createNestedGraph(current + 1, max)); 28 | } 29 | return newRouter; 30 | }; 31 | 32 | app.use(createLinearGraph(0, 100)); 33 | 34 | app.use("/nested", createNestedGraph(0, 100)); 35 | 36 | app.listen(9550); 37 | 38 | /** 39 | * prepare section 40 | */ 41 | const temp = []; 42 | for (let i = 0; i < 101; i++) { 43 | temp.push(i); 44 | } 45 | const urlForNested = temp.join("/"); 46 | 47 | bench({ 48 | name: "warming up", 49 | runs: 1, 50 | async func(b: any): Promise { 51 | b.start(); 52 | const response = await fetch(`http://localhost:9550/0`); 53 | const data = await response.text(); 54 | assertEquals(data, "0"); 55 | b.stop(); 56 | }, 57 | }); 58 | 59 | bench({ 60 | name: "GET: linear/10", 61 | runs: 1, 62 | async func(b: any): Promise { 63 | b.start(); 64 | const response = await fetch(`http://localhost:9550/${100}`); 65 | const data = await response.text(); 66 | assertEquals(data, "100"); 67 | b.stop(); 68 | }, 69 | }); 70 | 71 | bench({ 72 | name: "GET: nested/0...100 ", 73 | runs: 1, 74 | async func(b: any): Promise { 75 | b.start(); 76 | const response = await fetch( 77 | `http://localhost:9550/nested/${urlForNested}`, 78 | ); 79 | const data = await response.text(); 80 | assertEquals(data, "100"); 81 | b.stop(); 82 | }, 83 | }); 84 | 85 | await runBenchmarks(); 86 | 87 | await app.close(); 88 | -------------------------------------------------------------------------------- /test/benchmarks/linear_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | bench, 4 | runBenchmarks, 5 | } from "../test_deps.ts"; 6 | import { App, staticServe } from "../../mod.ts"; 7 | 8 | const app = new App(); 9 | 10 | for (let i = 0; i < 10001; i++) { 11 | app.use(`/${i}`, (req, res) => { 12 | res.send(i.toString()); 13 | }); 14 | } 15 | 16 | app.listen(9555); 17 | 18 | bench({ 19 | name: "warming up", 20 | runs: 1, 21 | async func(b: any): Promise { 22 | b.start(); 23 | const response = await fetch(`http://localhost:9555/${0}`); 24 | const data = await response.text(); 25 | assertEquals(data, "0"); 26 | b.stop(); 27 | }, 28 | }); 29 | 30 | bench({ 31 | name: "GET: /0", 32 | runs: 1, 33 | async func(b: any): Promise { 34 | b.start(); 35 | const response = await fetch(`http://localhost:9555/${0}`); 36 | const data = await response.text(); 37 | assertEquals(data, "0"); 38 | b.stop(); 39 | }, 40 | }); 41 | 42 | bench({ 43 | name: "GET: /10", 44 | runs: 1, 45 | async func(b: any): Promise { 46 | b.start(); 47 | const response = await fetch(`http://localhost:9555/${10}`); 48 | const data = await response.text(); 49 | assertEquals(data, "10"); 50 | b.stop(); 51 | }, 52 | }); 53 | 54 | bench({ 55 | name: "GET: /100", 56 | runs: 1, 57 | async func(b: any): Promise { 58 | b.start(); 59 | const response = await fetch(`http://localhost:9555/${100}`); 60 | const data = await response.text(); 61 | assertEquals(data, "100"); 62 | b.stop(); 63 | }, 64 | }); 65 | 66 | bench({ 67 | name: "GET: /1000", 68 | runs: 1, 69 | async func(b: any): Promise { 70 | b.start(); 71 | const response = await fetch(`http://localhost:9555/${1000}`); 72 | const data = await response.text(); 73 | assertEquals(data, "1000"); 74 | b.stop(); 75 | }, 76 | }); 77 | 78 | bench({ 79 | name: "GET: /5000", 80 | runs: 1, 81 | async func(b: any): Promise { 82 | b.start(); 83 | const response = await fetch(`http://localhost:9555/${5000}`); 84 | const data = await response.text(); 85 | assertEquals(data, "5000"); 86 | b.stop(); 87 | }, 88 | }); 89 | 90 | bench({ 91 | name: "GET: /10000", 92 | runs: 1, 93 | async func(b: any): Promise { 94 | b.start(); 95 | const response = await fetch(`http://localhost:9555/${10000}`); 96 | const data = await response.text(); 97 | assertEquals(data, "10000"); 98 | b.stop(); 99 | }, 100 | }); 101 | 102 | await runBenchmarks(); 103 | 104 | await app.close(); 105 | -------------------------------------------------------------------------------- /core/debug.ts: -------------------------------------------------------------------------------- 1 | import type { MiddlewareProps, ErrorMiddlewareProps } from "./types.ts"; 2 | import { 3 | yellow, 4 | cyan, 5 | green, 6 | red 7 | } from "../deps.ts"; 8 | 9 | 10 | export async function circulateMiddlewares( 11 | currentMiddlewares: MiddlewareProps[], 12 | step: number = 0, 13 | ) { 14 | for (const current of currentMiddlewares) { 15 | if (current.next) { 16 | const currentIndent = step + 1; 17 | const nextIndent = step + 2; 18 | console.log(" ".repeat(step) + "{"); 19 | current.method && 20 | console.log( 21 | " ".repeat(currentIndent) + `method: ${cyan(current.method)},`, 22 | ); 23 | current.url && 24 | console.log( 25 | " ".repeat(currentIndent) + `url: ${green(current.url)},`, 26 | ); 27 | console.log(" ".repeat(currentIndent) + `next: [`); 28 | circulateMiddlewares(current.next, nextIndent); 29 | console.log(" ".repeat(currentIndent) + `]`); 30 | console.log(" ".repeat(step) + "}"); 31 | } else { 32 | console.log(`${" ".repeat(step)}{`); 33 | current.method && 34 | console.log( 35 | `${" ".repeat(step + 1)}method: ${cyan(current.method)}`, 36 | ); 37 | current.url && 38 | console.log(`${" ".repeat(step + 1)}url: ${green(current.url)}`); 39 | current.paramHandlers && 40 | console.log( 41 | `${" ".repeat(step + 1)}paramHandlers: [${ 42 | red((current.paramHandlers.map((e) => e.paramName)).join(", ")) 43 | }]`, 44 | ); 45 | current.callBack && 46 | console.log( 47 | `${" ".repeat(step + 1)}callBack: ${ 48 | yellow(current.callBack.name || "Anonymous") 49 | }`, 50 | ); 51 | console.log(`${" ".repeat(step)}},`); 52 | } 53 | } 54 | }; 55 | 56 | export async function circulateErrorMiddlewares( 57 | currentErrorMiddlewares: ErrorMiddlewareProps[], 58 | step: number = 0, 59 | ) { 60 | for (const current of currentErrorMiddlewares) { 61 | if (current.next) { 62 | const currentIndent = step + 1; 63 | const nextIndent = step + 2; 64 | console.log(" ".repeat(step) + "{"); 65 | current.url && 66 | console.log( 67 | " ".repeat(currentIndent) + `url: ${green(current.url)},`, 68 | ); 69 | console.log(" ".repeat(currentIndent) + `next: [`); 70 | circulateErrorMiddlewares(current.next, nextIndent); 71 | console.log(" ".repeat(currentIndent) + `]`); 72 | console.log(" ".repeat(step) + "}"); 73 | } else { 74 | console.log(`${" ".repeat(step)}{`); 75 | current.url && 76 | console.log(`${" ".repeat(step + 1)}url: ${green(current.url)}`); 77 | current.callBack && 78 | console.log( 79 | `${" ".repeat(step + 1)}callBack: ${ 80 | yellow(current.callBack.name || "Anonymous") 81 | }`, 82 | ); 83 | console.log(`${" ".repeat(step)}},`); 84 | } 85 | } 86 | }; 87 | 88 | -------------------------------------------------------------------------------- /core/types.ts: -------------------------------------------------------------------------------- 1 | import type { Status } from "../deps.ts"; 2 | import type { AttainRequest } from "./request.ts"; 3 | import type { AttainResponse } from "./response.ts"; 4 | 5 | export type ThenArg = T extends PromiseLike ? U : T; 6 | 7 | export interface AttainListenProps { 8 | debug?: boolean; 9 | secure?: boolean; 10 | hostname?: string; 11 | certFile?: string; 12 | keyFile?: string; 13 | 14 | transport?: "tcp"; 15 | } 16 | 17 | export type SupportMethodType = 18 | | "HEAD" 19 | | "OPTIONS" 20 | | "ALL" 21 | | "GET" 22 | | "POST" 23 | | "PUT" 24 | | "PATCH" 25 | | "DELETE"; 26 | 27 | export type CallBackType = ( 28 | request: AttainRequest, 29 | response: AttainResponse, 30 | db: T, 31 | ) => Promise | void; 32 | 33 | export type ParamCallBackType = ( 34 | request: AttainRequest, 35 | response: AttainResponse, 36 | param: any, 37 | db: T, 38 | ) => Promise | void; 39 | 40 | export type ErrorCallBackType = ( 41 | error: any, 42 | request: AttainRequest, 43 | response: AttainResponse, 44 | db: T, 45 | ) => Promise | void; 46 | 47 | export interface MiddlewareProps { 48 | url?: string; 49 | paramHandlers?: ParamStackProps[]; 50 | callBack?: CallBackType; 51 | method?: SupportMethodType; 52 | next?: MiddlewareProps[]; 53 | } 54 | 55 | export interface ParamStackProps { 56 | paramName: string; 57 | callBack: ParamCallBackType; 58 | } 59 | 60 | export interface ErrorMiddlewareProps { 61 | url?: string; 62 | callBack?: ErrorCallBackType; 63 | next?: ErrorMiddlewareProps[]; 64 | } 65 | 66 | // Copyright 2018-2020 the oak authors. All rights reserved. MIT license. 67 | /** A HTTP status that is an error (4XX and 5XX). */ 68 | export type ErrorStatus = 69 | | Status.BadRequest 70 | | Status.Unauthorized 71 | | Status.PaymentRequired 72 | | Status.Forbidden 73 | | Status.NotFound 74 | | Status.MethodNotAllowed 75 | | Status.NotAcceptable 76 | | Status.ProxyAuthRequired 77 | | Status.RequestTimeout 78 | | Status.Conflict 79 | | Status.Gone 80 | | Status.LengthRequired 81 | | Status.PreconditionFailed 82 | | Status.RequestEntityTooLarge 83 | | Status.RequestURITooLong 84 | | Status.UnsupportedMediaType 85 | | Status.RequestedRangeNotSatisfiable 86 | | Status.ExpectationFailed 87 | | Status.Teapot 88 | | Status.MisdirectedRequest 89 | | Status.UnprocessableEntity 90 | | Status.Locked 91 | | Status.FailedDependency 92 | | Status.UpgradeRequired 93 | | Status.PreconditionRequired 94 | | Status.TooManyRequests 95 | | Status.RequestHeaderFieldsTooLarge 96 | | Status.UnavailableForLegalReasons 97 | | Status.InternalServerError 98 | | Status.NotImplemented 99 | | Status.BadGateway 100 | | Status.ServiceUnavailable 101 | | Status.GatewayTimeout 102 | | Status.HTTPVersionNotSupported 103 | | Status.VariantAlsoNegotiates 104 | | Status.InsufficientStorage 105 | | Status.LoopDetected 106 | | Status.NotExtended 107 | | Status.NetworkAuthenticationRequired; 108 | -------------------------------------------------------------------------------- /howto/websocket.md: -------------------------------------------------------------------------------- 1 | # WEB Socket 2 | This example is combined the WebSocket with Attain 3 | 4 | ## Attain Server 5 | 6 | ```ts 7 | import { 8 | App, 9 | logger, 10 | security 11 | } from "../../mod.ts"; 12 | import { 13 | acceptWebSocket, 14 | isWebSocketCloseEvent, 15 | isWebSocketPingEvent, 16 | } from "https://deno.land/std/ws/mod.ts"; 17 | 18 | const app = new App(); 19 | 20 | app.use(logger); 21 | app.use(security()); 22 | 23 | app.use((req, res) => { 24 | console.log("first start"); 25 | }) 26 | 27 | app.get("/hello", async (req, res) => { 28 | await res.status(200).send("hello"); 29 | }); 30 | 31 | app.use("/socket", async (req, res) => { 32 | console.log(req.method) 33 | const { conn, r: bufReader, w: bufWriter, headers } = req.serverRequest; 34 | 35 | try { 36 | const sock = await acceptWebSocket({ 37 | conn, 38 | bufReader, 39 | bufWriter, 40 | headers, 41 | }); 42 | 43 | console.log("socket connected!"); 44 | 45 | try { 46 | for await (const ev of sock) { 47 | if (typeof ev === "string") { 48 | // text message 49 | console.log("ws:Text", ev); 50 | await sock.send(ev); 51 | } else if (ev instanceof Uint8Array) { 52 | // binary message 53 | console.log("ws:Binary", ev); 54 | } else if (isWebSocketPingEvent(ev)) { 55 | const [, body] = ev; 56 | // ping 57 | console.log("ws:Ping", body); 58 | } else if (isWebSocketCloseEvent(ev)) { 59 | // close 60 | const { code, reason } = ev; 61 | console.log("ws:Close", code, reason); 62 | } 63 | } 64 | } catch (err) { 65 | console.error(`failed to receive frame: ${err}`); 66 | 67 | if (!sock.isClosed) { 68 | await sock.close(1000).catch(console.error); 69 | } 70 | } 71 | 72 | // make it sure it's end; 73 | res.end(); 74 | } catch (err) { 75 | console.error(`failed to accept websocket: ${err}`); 76 | await res.status(400).send("error"); 77 | } 78 | }) 79 | 80 | ``` 81 | 82 | ## Simple Client 83 | 84 | ```ts 85 | import { 86 | connectWebSocket, 87 | isWebSocketCloseEvent, 88 | isWebSocketPingEvent, 89 | isWebSocketPongEvent, 90 | } from "https://deno.land/std/ws/mod.ts"; 91 | import { encode } from "https://deno.land/std/encoding/utf8.ts"; 92 | import { BufReader } from "https://deno.land/std/io/bufio.ts"; 93 | import { TextProtoReader } from "https://deno.land/std/textproto/mod.ts"; 94 | import { blue, green, red, yellow } from "https://deno.land/std/fmt/colors.ts"; 95 | 96 | const endpoint = Deno.args[0] || "ws://127.0.0.1:8080/socket"; 97 | /** simple websocket cli */ 98 | try { 99 | const sock = await connectWebSocket(endpoint); 100 | console.log(green("ws connected! (type 'close' to quit)")); 101 | 102 | const messages = async (): Promise => { 103 | for await (const msg of sock) { 104 | if (typeof msg === "string") { 105 | console.log(yellow(`< ${msg}`)); 106 | } else if (isWebSocketPingEvent(msg)) { 107 | console.log(blue("< ping")); 108 | } else if (isWebSocketPongEvent(msg)) { 109 | console.log(blue("< pong")); 110 | } else if (isWebSocketCloseEvent(msg)) { 111 | console.log(red(`closed: code=${msg.code}, reason=${msg.reason}`)); 112 | } 113 | } 114 | }; 115 | 116 | const cli = async (): Promise => { 117 | const tpr = new TextProtoReader(new BufReader(Deno.stdin)); 118 | while (true) { 119 | await Deno.stdout.write(encode("> ")); 120 | const line = await tpr.readLine(); 121 | if (line === null || line === "close") { 122 | break; 123 | } else if (line === "ping") { 124 | await sock.ping(); 125 | } else { 126 | await sock.send(line); 127 | } 128 | } 129 | }; 130 | 131 | await Promise.race([messages(), cli()]).catch(console.error); 132 | 133 | if (!sock.isClosed) { 134 | await sock.close(1000).catch(console.error); 135 | } 136 | } catch (err) { 137 | console.error(red(`Could not connect to WebSocket: '${err}'`)); 138 | } 139 | 140 | Deno.exit(0); 141 | ``` 142 | 143 | For more information [WS](https://deno.land/std/ws) 144 | -------------------------------------------------------------------------------- /core/application.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "./router.ts"; 2 | import { blue, cyan, green, red } from "../deps.ts"; 3 | import type { AttainListenProps, ThenArg } from "./types.ts"; 4 | import version from "../version.ts"; 5 | import { defaultError, defaultPageNotFound } from "../defaultHandler/index.ts"; 6 | import { AttainHandler } from "./process.ts"; 7 | import { circulateErrorMiddlewares, circulateMiddlewares } from "./debug.ts"; 8 | import type { AttainDatabase, NoParamConstructor } from "./database.ts"; 9 | 10 | export class App extends Router { 11 | #serve?: Deno.Listener; 12 | #serveTLS?: Deno.Listener; 13 | #process?: Promise; 14 | #processTLS?: Promise; 15 | #database?: T; 16 | #databaseInitializer?: () => Promise; 17 | #handler?: AttainHandler; 18 | 19 | #debug = async () => { 20 | console.log(red("------- Debug Middlewares -----------------")); 21 | circulateMiddlewares(this.middlewares); 22 | console.log(red("------- End Debug Middlewares -------------\n")); 23 | 24 | console.log(red("------- Debug Error Middlewares -----------")); 25 | circulateErrorMiddlewares(this.errorMiddlewares); 26 | console.log(red("------- End Debug Error Middlewares -------\n")); 27 | }; 28 | 29 | close = async () => { 30 | if (this.#serve) { 31 | this.#serve.close(); 32 | await this.#process; 33 | } 34 | 35 | if (this.#serveTLS) { 36 | this.#serveTLS.close(); 37 | await this.#processTLS; 38 | } 39 | }; 40 | 41 | /** 42 | * Start to listen 43 | */ 44 | listen = ( 45 | port: number, 46 | linstenProps?: AttainListenProps, 47 | ) => { 48 | const options = { hostname: "0.0.0.0", debug: false, ...linstenProps }; 49 | const mode = Deno.env.get("PRODUCTION") 50 | ? "PRODUCTION" 51 | : (Deno.env.get("DEVELOPMENT") ? "DEVELOPMENT" : "GENERAL"); 52 | this.use(defaultPageNotFound); 53 | this.error(defaultError); 54 | 55 | if (options.debug) { 56 | this.#debug(); 57 | } 58 | 59 | if (options.secure) { 60 | if (!options.keyFile || !options.certFile) { 61 | throw "TLS mode require keyFile and certFile options."; 62 | } 63 | } 64 | 65 | console.log( 66 | `[${blue(mode)}] ${cyan("Attain FrameWork")} ${ 67 | blue("v" + version.toString()) 68 | } - ${green("Ready!")}`, 69 | ); 70 | 71 | if (options.secure && options.keyFile && options.certFile) { 72 | this.#serveTLS = Deno.listenTls({ 73 | hostname: options.hostname, 74 | port, 75 | keyFile: options.keyFile, 76 | certFile: options.certFile, 77 | }); 78 | this.#processTLS = this.#start(this.#serveTLS, options.secure); 79 | } else { 80 | this.#serve = Deno.listen({ hostname: options.hostname, port }); 81 | this.#process = this.#start(this.#serve); 82 | } 83 | console.log( 84 | `Server running at ${ 85 | options.secure ? "https:" : "http:" 86 | }//localhost:${port}`, 87 | ); 88 | }; 89 | 90 | #start = async (server: Deno.Listener, secure: boolean = false) => { 91 | if (this.#database) { 92 | await (this.#database as any).connect(); 93 | this.#handler = new AttainHandler(this.#database, secure); 94 | } else if (this.#databaseInitializer) { 95 | this.#database = await this.#databaseInitializer(); 96 | this.#handler = new AttainHandler(this.#database, secure); 97 | } else { 98 | this.#handler = new AttainHandler(undefined!, secure); 99 | } 100 | 101 | for await (const conn of server) { 102 | (async () => { 103 | const httpConn = Deno.serveHttp(conn); 104 | for await (const requestEvent of httpConn) { 105 | this.#handler!.execute( 106 | requestEvent!, 107 | this.middlewares as any, 108 | this.errorMiddlewares as any, 109 | ); 110 | } 111 | })(); 112 | } 113 | }; 114 | 115 | constructor(db?: T, dbinit?: () => Promise) { 116 | super(); 117 | if (db) { 118 | this.#database = db; 119 | } 120 | 121 | if (dbinit) { 122 | this.#databaseInitializer = dbinit; 123 | } 124 | } 125 | 126 | static startWith( 127 | this: T, 128 | func: () => Promise, 129 | ): App>; 130 | static startWith( 131 | this: T, 132 | dbClass: NoParamConstructor, 133 | ): App; 134 | static startWith(this: T, arg: U) { 135 | if ((arg as any).__type) { 136 | return new this(new (arg as any)()); 137 | } else { 138 | return new this>(undefined, arg as any); 139 | } 140 | } 141 | 142 | public database(func: () => Promise): void; 143 | public database( 144 | dbClass: NoParamConstructor, 145 | ): void; 146 | public database(arg: any) { 147 | if (arg.__type) { 148 | this.#database = new (arg as any)(); 149 | } else { 150 | this.#databaseInitializer = arg; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /core/httpError.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Adapted directly from http-errors at https://github.com/jshttp/http-errors 3 | * which is licensed as follows: 4 | * 5 | * The MIT License (MIT) 6 | * 7 | * Copyright (c) 2014 Jonathan Ong me@jongleberry.com 8 | * Copyright (c) 2016 Douglas Christopher Wilson doug@somethingdoug.com 9 | * 10 | * Permission is hereby granted, free of charge, to any person obtaining a copy 11 | * of this software and associated documentation files (the "Software"), to deal 12 | * in the Software without restriction, including without limitation the rights 13 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | * copies of the Software, and to permit persons to whom the Software is 15 | * furnished to do so, subject to the following conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be included in 18 | * all copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | * THE SOFTWARE. 27 | */ 28 | 29 | import { Status, STATUS_TEXT } from "../deps.ts"; 30 | import type { ErrorStatus } from "./types.ts"; 31 | 32 | const errorStatusMap = { 33 | "BadRequest": 400, 34 | "Unauthorized": 401, 35 | "PaymentRequired": 402, 36 | "Forbidden": 403, 37 | "NotFound": 404, 38 | "MethodNotAllowed": 405, 39 | "NotAcceptable": 406, 40 | "ProxyAuthRequired": 407, 41 | "RequestTimeout": 408, 42 | "Conflict": 409, 43 | "Gone": 410, 44 | "LengthRequired": 411, 45 | "PreconditionFailed": 412, 46 | "RequestEntityTooLarge": 413, 47 | "RequestURITooLong": 414, 48 | "UnsupportedMediaType": 415, 49 | "RequestedRangeNotSatisfiable": 416, 50 | "ExpectationFailed": 417, 51 | "Teapot": 418, 52 | "MisdirectedRequest": 421, 53 | "UnprocessableEntity": 422, 54 | "Locked": 423, 55 | "FailedDependency": 424, 56 | "UpgradeRequired": 426, 57 | "PreconditionRequired": 428, 58 | "TooManyRequests": 429, 59 | "RequestHeaderFieldsTooLarge": 431, 60 | "UnavailableForLegalReasons": 451, 61 | "InternalServerError": 500, 62 | "NotImplemented": 501, 63 | "BadGateway": 502, 64 | "ServiceUnavailable": 503, 65 | "GatewayTimeout": 504, 66 | "HTTPVersionNotSupported": 505, 67 | "VariantAlsoNegotiates": 506, 68 | "InsufficientStorage": 507, 69 | "LoopDetected": 508, 70 | "NotExtended": 510, 71 | "NetworkAuthenticationRequired": 511, 72 | }; 73 | 74 | /** A base class for individual classes of HTTP errors. */ 75 | export class HttpError extends Error { 76 | /** Determines if details about the error should be automatically exposed 77 | * in a response. This is automatically set to `true` for 4XX errors, as 78 | * they represent errors in the request, while 5XX errors are set to `false` 79 | * as they are internal server errors and exposing details could leak 80 | * important server security information. */ 81 | expose = false; 82 | 83 | /** The HTTP error status associated with this class of error. */ 84 | status = Status.InternalServerError; 85 | } 86 | 87 | function createHttpErrorConstructor( 88 | status: ErrorStatus, 89 | ): E { 90 | const name = `${Status[status]}Error`; 91 | const Ctor = class extends HttpError { 92 | constructor(message?: string) { 93 | super(); 94 | this.message = message || STATUS_TEXT.get(status as any)!; 95 | this.status = status; 96 | this.expose = status >= 400 && status < 500 ? true : false; 97 | Object.defineProperty(this, "name", { 98 | configurable: true, 99 | enumerable: false, 100 | value: name, 101 | writable: true, 102 | }); 103 | } 104 | }; 105 | return Ctor as E; 106 | } 107 | 108 | /** An object which contains an individual HTTP Error for each HTTP status 109 | * error code (4XX and 5XX). When errors are raised related to a particular 110 | * HTTP status code, they will be of the appropriate instance located on this 111 | * object. Also, context's `.throw()` will throw errors based on the passed 112 | * status code. */ 113 | export const httpErrors: Record = 114 | {} as any; 115 | 116 | for (const [key, value] of Object.entries(errorStatusMap)) { 117 | httpErrors[key as keyof typeof errorStatusMap] = createHttpErrorConstructor( 118 | value, 119 | ); 120 | } 121 | 122 | /** Create a specific class of `HttpError` based on the status, which defaults 123 | * to _500 Internal Server Error_. 124 | */ 125 | export function createHttpError( 126 | status: ErrorStatus = 500, 127 | message?: string, 128 | ): HttpError { 129 | return new httpErrors[Status[status] as keyof typeof errorStatusMap](message); 130 | } 131 | 132 | export function isHttpError(value: any): value is HttpError { 133 | return value instanceof HttpError; 134 | } -------------------------------------------------------------------------------- /core/process.ts: -------------------------------------------------------------------------------- 1 | import { AttainRequest } from "./request.ts"; 2 | import { AttainResponse } from "./response.ts"; 3 | import { checkPathAndParseURLParams } from "./utils.ts"; 4 | import type { 5 | MiddlewareProps, 6 | ErrorMiddlewareProps, 7 | ParamStackProps, 8 | } from "./types.ts"; 9 | 10 | /* an alternative, clearer code of the process file */ 11 | export class AttainHandler { 12 | #database: DB 13 | #secure: boolean 14 | 15 | constructor(database: DB, secure: boolean) { 16 | this.#database = database; 17 | this.#secure = secure; 18 | } 19 | 20 | // main function 21 | async execute( 22 | srq: Deno.RequestEvent, 23 | middlewares: MiddlewareProps[], 24 | errorMiddlewares: ErrorMiddlewareProps[] 25 | ) { 26 | const res = new AttainResponse(srq.request); 27 | const req = new AttainRequest(srq.request, this.#secure); 28 | 29 | // execute middlewares. if any errors, execute error middlewares. 30 | try { 31 | await this.handleMiddlewares(req, res, middlewares); 32 | } catch (error) { 33 | await this.handleErrorMiddlewares(error, req, res, errorMiddlewares); 34 | } 35 | 36 | // execute pending jobs (aka - res.pend()); 37 | await res.executePendingJobs(req); 38 | 39 | // if the response was created, send it. if not, close connection. 40 | if (res.getBody) { 41 | await srq.respondWith(res.getResponse); 42 | } 43 | 44 | // delete response from memory. 45 | res.destroy(); 46 | } 47 | 48 | // handle regular middlewares 49 | private async handleMiddlewares( 50 | req: AttainRequest, 51 | res: AttainResponse, 52 | current: MiddlewareProps[] 53 | ) { 54 | try { 55 | const currentMethod = req.method; 56 | const currentUrl = req.url.pathname; 57 | for (const middleware of current) { 58 | // check if the middleware method matches the method in the request 59 | if ( 60 | middleware.method === currentMethod || middleware.method === "ALL" 61 | ) { 62 | // if the middleware doesn't have a url, execute it. if it does, check match and execute. 63 | if (!middleware.url) { 64 | await this.#run(req, res, middleware); 65 | } else if ( 66 | checkPathAndParseURLParams(req, middleware.url, currentUrl) 67 | ) { 68 | // if any params handlers in the request (aka, Router.param()), handle them. 69 | if (middleware.paramHandlers && req.params) { 70 | await this.#runWithParams(middleware.paramHandlers, req, res); 71 | // if the middleare has a res.send() method, finish the request. 72 | if (res.processDone) { 73 | break; 74 | } 75 | } 76 | // fallback middleware execution 77 | await this.#run(req, res, middleware); 78 | } 79 | } 80 | // if the middleare has a res.send() method, finish the request. 81 | if (res.processDone) { 82 | break; 83 | } 84 | } 85 | } catch (error) { // throw error to the error middlewares 86 | if (error instanceof Error) { 87 | throw error; 88 | } else { 89 | throw new Error(error); 90 | } 91 | } 92 | } 93 | 94 | // handle error middlewares 95 | private async handleErrorMiddlewares( 96 | error: Error | any, 97 | req: AttainRequest, 98 | res: AttainResponse, 99 | current: ErrorMiddlewareProps[] 100 | ) { 101 | try { 102 | const currentUrl = req.url.pathname; 103 | for (const middleware of current) { 104 | // execute all middlewares without a url or middlewares that match the current url. 105 | if (!middleware.url) { 106 | await this.#runWithError(error, req, res, middleware); 107 | } else if ( 108 | checkPathAndParseURLParams(req, middleware.url, currentUrl) 109 | ) { 110 | await this.#runWithError(error, req, res, middleware); 111 | } 112 | if (res.processDone) { 113 | break; 114 | } 115 | } 116 | } catch (error) { 117 | console.error( 118 | "Attain Error: Can't handle it due to Error middlewares can't afford it.", 119 | ); 120 | console.error(error); 121 | } 122 | }; 123 | 124 | // execute a middleware callback if exists 125 | #run = async ( 126 | req: AttainRequest, 127 | res: AttainResponse, 128 | middleware: MiddlewareProps 129 | ) => { 130 | try { 131 | middleware.callBack 132 | ? await middleware.callBack(req, res, this.#database!) 133 | : await this.handleMiddlewares(req, res, middleware.next!); /* No idea why I need this '!' */ 134 | } catch (err) { 135 | throw err; 136 | } 137 | } 138 | 139 | // execute an error middleware callback if exists. 140 | #runWithError = async ( 141 | err: any, 142 | req: AttainRequest, 143 | res: AttainResponse, 144 | middleware: ErrorMiddlewareProps 145 | ) => { 146 | try { 147 | middleware.callBack 148 | ? await middleware.callBack(err, req, res, this.#database!) 149 | : await this.handleErrorMiddlewares(err, req, res, middleware.next!); 150 | } catch (err) { 151 | throw err; 152 | } 153 | } 154 | 155 | // execute a paramHandler callback 156 | #runWithParams = async ( 157 | paramHandlers: ParamStackProps[], 158 | req: AttainRequest, 159 | res: AttainResponse, 160 | ) => { 161 | for await (const paramHandler of paramHandlers) { 162 | await paramHandler.callBack(req, res, req.params[paramHandler.paramName], this.#database!) 163 | if (res.processDone) { 164 | break; 165 | } 166 | } 167 | }; 168 | } -------------------------------------------------------------------------------- /performance/performance.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | 3 | Tested in Windows 10 system with 4 |
deno 1.0.3 5 |
v8 8.4.300 6 |
typescript 3.9.2 7 | 8 | Powered by Apache JMeter (v5.3) 9 | Each test performed with 10 threads during the 10 seconds 10 | 11 | ## Contents 12 | - [Attain](#attain) 13 | - [Minimal test](#minimal-test) 14 | - [With logger and router](#with-logger-and-router) 15 | - [Multi middlewares](#multi-middlewares) 16 | - [Send File](#send-file) 17 | - [Express](#express) 18 | - [Minimal test](#minimal-test-1) 19 | - [With logger and router](#with-logger-and-router-1) 20 | - [Multi middlewares](#multi-middlewares-1) 21 | - [Send File](#send-file-1) 22 | - [Oak](#oak) 23 | - [Minimal test](#minimal-test-2) 24 | - [With logger and router](#with-logger-and-router-2) 25 | - [Multi middlewares](#multi-middlewares-2) 26 | - [Send File](#send-file-2) 27 | 28 | ## Attain 29 | 30 | ### minimal test 31 | 32 | ```ts 33 | import { App } from "https://deno.land/x/attain/mod.ts"; 34 | 35 | const app = new App(); 36 | 37 | app.use((req, res) => { 38 | res.status(200).send("Hello world!"); 39 | }); 40 | 41 | app.listen({ port: 3500 }); 42 | ``` 43 | ![alt text](https://github.com/aaronwlee/Attain/blob/master/performance/attain.png?raw=true "Attain performance") 44 | 45 | ### with logger and router 46 | 47 | ```ts 48 | import { App, logger } from "https://deno.land/x/attain/mod.ts"; 49 | 50 | const app = new App(); 51 | 52 | app.use(logger); 53 | 54 | app.use("/", (req, res) => { 55 | res.status(200).send("Hello world!"); 56 | }); 57 | 58 | app.listen({ port: 3500 }); 59 | ``` 60 | 61 | ![alt text](https://github.com/aaronwlee/Attain/blob/master/performance/attain-middle.png?raw=true "Attain performance") 62 | 63 | ### multi middlewares 64 | five simple middlewares with logger and router 65 | 66 | ```ts 67 | import { App, Request, Response } from "https://deno.land/x/attain/mod.ts"; 68 | 69 | const app = new App(); 70 | 71 | const hello = (req: Request, res: Response) => { 72 | console.log("hello") 73 | }; 74 | 75 | app.use(logger); 76 | app.use(hello, hello, hello, hello, hello); 77 | app.use("/", (req, res) => { 78 | res.status(200).send("Hello world!"); 79 | }); 80 | 81 | app.listen({ port: 3500 }); 82 | ``` 83 | 84 | ![alt text](https://github.com/aaronwlee/Attain/blob/master/performance/attain-middlewithfive.png?raw=true "Attain performance") 85 | 86 | ### Send file 87 | 88 | ![alt text](https://github.com/aaronwlee/Attain/blob/master/performance/attain-filesend.png?raw=true "Attain performance") 89 | 90 | 91 | ## Express 92 | 93 | ### minimal test 94 | 95 | ```ts 96 | const express = require('express') 97 | const app = express() 98 | const port = 5000 99 | 100 | app.get((req, res) => { 101 | res.send('Hello World!') 102 | }) 103 | 104 | app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`)) 105 | ``` 106 | 107 | ![alt text](https://github.com/aaronwlee/Attain/blob/master/performance/express.png?raw=true "Express performance") 108 | 109 | ### with logger and router 110 | 111 | ```ts 112 | const express = require('express') 113 | const app = express() 114 | const morgan = require('morgan') 115 | const port = 5000 116 | 117 | app.use(morgan('tiny')); 118 | 119 | app.get('/', (req, res) => { 120 | res.send('Hello World!') 121 | }) 122 | 123 | app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`)) 124 | ``` 125 | 126 | ![alt text](https://github.com/aaronwlee/Attain/blob/master/performance/express-middle.png?raw=true "Express performance") 127 | 128 | 129 | ### multi middlewares 130 | five simple middlewares with logger and router 131 | 132 | ```ts 133 | const express = require('express') 134 | const app = express() 135 | const morgan = require('morgan') 136 | const port = 5000 137 | 138 | const hello = (req, res, next) => { 139 | console.log("hello") 140 | next(); 141 | } 142 | app.use(morgan('tiny')); 143 | app.use(hello, hello, hello, hello, hello) 144 | app.get('/', (req, res) => { 145 | res.send('Hello World!') 146 | }) 147 | 148 | app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`)) 149 | ``` 150 | 151 | ![alt text](https://github.com/aaronwlee/Attain/blob/master/performance/express-middlewithfive.png?raw=true "Express performance") 152 | 153 | ### Send file 154 | 155 | ![alt text](https://github.com/aaronwlee/Attain/blob/master/performance/express-sendfile.png?raw=true "Express performance") 156 | 157 | 158 | ## Oak 159 | 160 | 161 | ### minimal test 162 | 163 | ```ts 164 | import { Application } from "https://deno.land/x/oak/mod.ts"; 165 | 166 | const app = new Application(); 167 | 168 | app.use((ctx) => { 169 | ctx.response.body = "Hello World!"; 170 | }); 171 | 172 | await app.listen({ port: 8000 }); 173 | ``` 174 | 175 | ![alt text](https://github.com/aaronwlee/Attain/blob/master/performance/oak.png?raw=true "Oak performance") 176 | 177 | ### with logger and router 178 | 179 | ```ts 180 | import { Application, Router } from "https://deno.land/x/oak/mod.ts"; 181 | 182 | const app = new Application(); 183 | const router = new Router(); 184 | router 185 | .get("/", (context) => { 186 | context.response.body = "Hello world!"; 187 | }) 188 | 189 | app.use(async (ctx, next) => { 190 | await next(); 191 | const rt = ctx.response.headers.get("X-Response-Time"); 192 | console.log(`${ctx.request.method} ${ctx.request.url} - ${rt}`); 193 | }); 194 | app.use(router.routes()); 195 | app.use(router.allowedMethods()); 196 | 197 | 198 | await app.listen({ port: 8000 }); 199 | ``` 200 | 201 | ![alt text](https://github.com/aaronwlee/Attain/blob/master/performance/oak-middle.png?raw=true "Oak performance") 202 | 203 | ### multi middlewares 204 | five simple middlewares with logger and router 205 | 206 | ```ts 207 | import { Application, Router } from "https://deno.land/x/oak/mod.ts"; 208 | 209 | const app = new Application(); 210 | const router = new Router(); 211 | 212 | const hello = (ctx: any, next: any) => { 213 | console.log("hello1"); 214 | next() 215 | } 216 | 217 | router 218 | .get("/", (context) => { 219 | context.response.body = "Hello world!"; 220 | }) 221 | 222 | app.use(hello, hello, hello, hello, hello) 223 | app.use(async (ctx, next) => { 224 | await next(); 225 | const rt = ctx.response.headers.get("X-Response-Time"); 226 | console.log(`${ctx.request.method} ${ctx.request.url} - ${rt}`); 227 | }); 228 | app.use(router.routes()); 229 | app.use(router.allowedMethods()); 230 | 231 | 232 | await app.listen({ port: 8000 }); 233 | ``` 234 | 235 | ![alt text](https://github.com/aaronwlee/Attain/blob/master/performance/oak-middlewithfive.png?raw=true "Oak performance") 236 | 237 | ### Send file 238 | 239 | ![alt text](https://github.com/aaronwlee/Attain/blob/master/performance/oak-filesend.png?raw=true "Oak performance") 240 | -------------------------------------------------------------------------------- /core/request.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Adapted from oak/request at https://github.com/oakserver/oak/blob/master/request.ts 3 | * which is licensed as: 4 | * 5 | * The MIT License (MIT) 6 | * 7 | * Copyright (c) 2018-2020 the oak authors 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | */ 27 | 28 | import type { 29 | Body, 30 | BodyBytes, 31 | BodyForm, 32 | BodyFormData, 33 | BodyJson, 34 | BodyOptions, 35 | BodyReader, 36 | BodyStream, 37 | BodyText, 38 | } from "https://deno.land/x/oak@v9.0.1/body.ts"; 39 | import { RequestBody } from "https://deno.land/x/oak@v9.0.1/body.ts"; 40 | import type { SupportMethodType } from "./types.ts"; 41 | import { preferredCharsets } from "https://deno.land/x/oak@v9.0.1/negotiation/charset.ts"; 42 | import { preferredEncodings } from "https://deno.land/x/oak@v9.0.1/negotiation/encoding.ts"; 43 | import { preferredLanguages } from "https://deno.land/x/oak@v9.0.1/negotiation/language.ts"; 44 | import { preferredMediaTypes } from "https://deno.land/x/oak@v9.0.1/negotiation/mediaType.ts"; 45 | 46 | const decoder = new TextDecoder(); 47 | 48 | /** An interface which provides information about the current request. */ 49 | export class AttainRequest { 50 | #body: RequestBody; 51 | #serverRequest: Request; 52 | #url?: URL; 53 | #secure: boolean; 54 | #startDate: number; 55 | 56 | [dynamicProperty: string]: any 57 | 58 | /** Is `true` if the request has a body, otherwise `false`. */ 59 | get hasBody(): boolean { 60 | return this.#body.has(); 61 | } 62 | 63 | /** The `Headers` supplied in the request. */ 64 | get headers(): Headers { 65 | return this.#serverRequest.headers; 66 | } 67 | 68 | /** The HTTP Method used by the request. */ 69 | get method(): SupportMethodType { 70 | return this.#serverRequest.method as SupportMethodType; 71 | } 72 | 73 | /** Set to the value of the _original_ Deno server request. */ 74 | get serverRequest(): Request { 75 | return this.#serverRequest; 76 | } 77 | 78 | get startDate(): number { 79 | return this.#startDate; 80 | } 81 | 82 | /** A parsed URL for the request which complies with the browser standards. 83 | * When the application's `.proxy` is `true`, this value will be based off of 84 | * the `X-Forwarded-Proto` and `X-Forwarded-Host` header values if present in 85 | * the request. */ 86 | get url(): URL { 87 | return new URL(this.#serverRequest.url); 88 | } 89 | 90 | constructor(serverRequest: Request, secure = false) { 91 | this.#serverRequest = serverRequest; 92 | this.#secure = secure; 93 | this.#body = new RequestBody(serverRequest); 94 | this.#startDate = Date.now(); 95 | } 96 | 97 | /** Returns an array of media types, accepted by the requestor, in order of 98 | * preference. If there are no encodings supplied by the requestor, 99 | * `undefined` is returned. 100 | */ 101 | accepts(): string[] | undefined; 102 | /** For a given set of media types, return the best match accepted by the 103 | * requestor. If there are no encoding that match, then the method returns 104 | * `undefined`. 105 | */ 106 | accepts(...types: string[]): string | undefined; 107 | accepts(...types: string[]): string | string[] | undefined { 108 | const acceptValue = this.#serverRequest.headers.get("Accept"); 109 | if (!acceptValue) { 110 | return; 111 | } 112 | if (types.length) { 113 | return preferredMediaTypes(acceptValue, types)[0]; 114 | } 115 | return preferredMediaTypes(acceptValue); 116 | } 117 | 118 | /** Returns an array of charsets, accepted by the requestor, in order of 119 | * preference. If there are no charsets supplied by the requestor, 120 | * `undefined` is returned. 121 | */ 122 | acceptsCharsets(): string[] | undefined; 123 | /** For a given set of charsets, return the best match accepted by the 124 | * requestor. If there are no charsets that match, then the method returns 125 | * `undefined`. */ 126 | acceptsCharsets(...charsets: string[]): string | undefined; 127 | acceptsCharsets(...charsets: string[]): string[] | string | undefined { 128 | const acceptCharsetValue = this.#serverRequest.headers.get( 129 | "Accept-Charset", 130 | ); 131 | if (!acceptCharsetValue) { 132 | return; 133 | } 134 | if (charsets.length) { 135 | return preferredCharsets(acceptCharsetValue, charsets)[0]; 136 | } 137 | return preferredCharsets(acceptCharsetValue); 138 | } 139 | 140 | /** Returns an array of encodings, accepted by the requestor, in order of 141 | * preference. If there are no encodings supplied by the requestor, 142 | * `undefined` is returned. 143 | */ 144 | acceptsEncodings(): string[] | undefined; 145 | /** For a given set of encodings, return the best match accepted by the 146 | * requestor. If there are no encodings that match, then the method returns 147 | * `undefined`. 148 | * 149 | * **NOTE:** You should always supply `identity` as one of the encodings 150 | * to ensure that there is a match when the `Accept-Encoding` header is part 151 | * of the request. 152 | */ 153 | acceptsEncodings(...encodings: string[]): string | undefined; 154 | acceptsEncodings(...encodings: string[]): string[] | string | undefined { 155 | const acceptEncodingValue = this.#serverRequest.headers.get( 156 | "Accept-Encoding", 157 | ); 158 | if (!acceptEncodingValue) { 159 | return; 160 | } 161 | if (encodings.length) { 162 | return preferredEncodings(acceptEncodingValue, encodings)[0]; 163 | } 164 | return preferredEncodings(acceptEncodingValue); 165 | } 166 | 167 | /** Returns an array of languages, accepted by the requestor, in order of 168 | * preference. If there are no languages supplied by the requestor, 169 | * `undefined` is returned. 170 | */ 171 | acceptsLanguages(): string[] | undefined; 172 | /** For a given set of languages, return the best match accepted by the 173 | * requestor. If there are no languages that match, then the method returns 174 | * `undefined`. */ 175 | acceptsLanguages(...langs: string[]): string | undefined; 176 | acceptsLanguages(...langs: string[]): string[] | string | undefined { 177 | const acceptLanguageValue = this.#serverRequest.headers.get( 178 | "Accept-Language", 179 | ); 180 | if (!acceptLanguageValue) { 181 | return; 182 | } 183 | if (langs.length) { 184 | return preferredLanguages(acceptLanguageValue, langs)[0]; 185 | } 186 | return preferredLanguages(acceptLanguageValue); 187 | } 188 | 189 | body(options: BodyOptions<"bytes">): BodyBytes; 190 | body(options: BodyOptions<"form">): BodyForm; 191 | body(options: BodyOptions<"form-data">): BodyFormData; 192 | body(options: BodyOptions<"json">): BodyJson; 193 | body(options: BodyOptions<"reader">): BodyReader; 194 | body(options: BodyOptions<"stream">): BodyStream; 195 | body(options: BodyOptions<"text">): BodyText; 196 | body(options?: BodyOptions): Body; 197 | body(options: BodyOptions = {}): Body | BodyReader | BodyStream { 198 | return this.#body.get(options); 199 | } 200 | } -------------------------------------------------------------------------------- /test/basicTest/main_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | bench, 4 | runBenchmarks, 5 | } from "../test_deps.ts"; 6 | import { App, staticServe } from "../../mod.ts"; 7 | import router from "./router.ts"; 8 | import paramTest from "./param.ts"; 9 | 10 | /** 11 | * Main part 12 | */ 13 | const app = new App(); 14 | 15 | app.use(staticServe("test/static")); 16 | app.use("/router", router); 17 | app.use("/param", paramTest); 18 | 19 | app.get("/", (req, res) => { 20 | res.send("/"); 21 | }); 22 | 23 | app.get("/sendFile", async (req, res) => { 24 | await res.sendFile("test/static/test.html"); 25 | }); 26 | 27 | app.get("/error", (req, res) => { 28 | throw new Error("test error"); 29 | }); 30 | 31 | app.error("/error", (error, req, res) => { 32 | res.send(error.message); 33 | }); 34 | 35 | app.listen(8080); 36 | 37 | /** 38 | * basic route test 39 | */ 40 | bench({ 41 | name: "GET: / (50 times)", 42 | runs: 5, 43 | async func(b: any): Promise { 44 | b.start(); 45 | const conns = []; 46 | for (let i = 0; i < 10; ++i) { 47 | conns.push(fetch("http://localhost:8080/").then((resp) => resp.text())); 48 | } 49 | for await (const i of conns) { 50 | assertEquals(await i, "/"); 51 | } 52 | b.stop(); 53 | }, 54 | }); 55 | 56 | /** 57 | * error test 58 | */ 59 | bench({ 60 | name: "GET: /error", 61 | runs: 1, 62 | async func(b: any): Promise { 63 | b.start(); 64 | const errorTest = await fetch("http://localhost:8080/error"); 65 | const data = await errorTest.text(); 66 | assertEquals(data, "test error"); 67 | b.stop(); 68 | }, 69 | }); 70 | 71 | /** 72 | * static serve 73 | */ 74 | bench({ 75 | name: "GET: /test.html", 76 | runs: 1, 77 | async func(b: any): Promise { 78 | b.start(); 79 | const staticTest = await fetch("http://localhost:8080/test.html"); 80 | const staticData = await staticTest.text(); 81 | assertEquals( 82 | staticData, 83 | `

Test this page is a simple html

`, 84 | ); 85 | b.stop(); 86 | }, 87 | }); 88 | bench({ 89 | name: "GET: /sendFile", 90 | runs: 1, 91 | async func(b: any): Promise { 92 | b.start(); 93 | const sendFileTest = await fetch("http://localhost:8080/sendFile"); 94 | const sendFileData = await sendFileTest.text(); 95 | assertEquals( 96 | sendFileData, 97 | `

Test this page is a simple html

`, 98 | ); 99 | b.stop(); 100 | }, 101 | }); 102 | 103 | /** 104 | * nested 105 | */ 106 | bench({ 107 | name: "GET: /router and /router/second", 108 | runs: 1, 109 | async func(b: any): Promise { 110 | b.start(); 111 | const routerPath = await fetch("http://localhost:8080/router"); 112 | const routerData = await routerPath.text(); 113 | assertEquals(routerData, "/router"); 114 | 115 | const secondPath = await fetch("http://localhost:8080/router/second"); 116 | const secondData = await secondPath.text(); 117 | assertEquals(secondData, "/router/second"); 118 | b.stop(); 119 | }, 120 | }); 121 | 122 | bench({ 123 | name: "GET: /router/second/:id (5 times)", 124 | runs: 1, 125 | async func(b: any): Promise { 126 | b.start(); 127 | const conns = []; 128 | for (let i = 0; i < 5; ++i) { 129 | conns.push( 130 | fetch(`http://localhost:8080/router/second/${i}`).then((resp) => 131 | resp.text() 132 | ), 133 | ); 134 | } 135 | await Promise.all(conns); 136 | for (const key in conns) { 137 | const data = await conns[key]; 138 | assertEquals(data, `/router/second/${key}`); 139 | } 140 | b.stop(); 141 | }, 142 | }); 143 | 144 | bench({ 145 | name: "GET: /router/seach?name=aaron", 146 | runs: 1, 147 | async func(b: any): Promise { 148 | b.start(); 149 | const test = await fetch("http://localhost:8080/router/search?name=aaron"); 150 | const data = await test.json(); 151 | assertEquals(data, { name: "aaron" }); 152 | b.stop(); 153 | }, 154 | }); 155 | 156 | bench({ 157 | name: "POST: /router/post", 158 | runs: 1, 159 | async func(b: any): Promise { 160 | b.start(); 161 | const test = await fetch("http://localhost:8080/router/post", { 162 | method: "POST", 163 | headers: { 164 | "Content-Type": "application/json", 165 | }, 166 | body: JSON.stringify({ name: "aaron" }), 167 | }); 168 | const data = await test.json(); 169 | assertEquals(data, { name: "aaron" }); 170 | b.stop(); 171 | }, 172 | }); 173 | 174 | /** 175 | * param bench mark 176 | */ 177 | bench({ 178 | name: "GET: /param/:username", 179 | runs: 1, 180 | async func(b: any): Promise { 181 | b.start(); 182 | const test = await fetch("http://localhost:8080/param/aaron"); 183 | const data = await test.text(); 184 | assertEquals(data, "aaron"); 185 | b.stop(); 186 | }, 187 | }); 188 | 189 | bench({ 190 | name: "POST: /param/:username/post", 191 | runs: 1, 192 | async func(b: any): Promise { 193 | b.start(); 194 | const test = await fetch( 195 | "http://localhost:8080/param/aaron/post", 196 | { method: "POST" }, 197 | ); 198 | const data = await test.text(); 199 | assertEquals(data, "aaron"); 200 | b.stop(); 201 | }, 202 | }); 203 | 204 | bench({ 205 | name: "PATCH: /param/:username/patch", 206 | runs: 1, 207 | async func(b: any): Promise { 208 | b.start(); 209 | const test = await fetch( 210 | "http://localhost:8080/param/aaron/patch", 211 | { method: "PATCH" }, 212 | ); 213 | const data = await test.text(); 214 | assertEquals(data, "aaron"); 215 | b.stop(); 216 | }, 217 | }); 218 | 219 | bench({ 220 | name: "PUT: /param/:username/put", 221 | runs: 1, 222 | async func(b: any): Promise { 223 | b.start(); 224 | const test = await fetch( 225 | "http://localhost:8080/param/aaron/put", 226 | { method: "PUT" }, 227 | ); 228 | const data = await test.text(); 229 | assertEquals(data, "aaron"); 230 | b.stop(); 231 | }, 232 | }); 233 | 234 | bench({ 235 | name: "DELETE: /param/:username/delete", 236 | runs: 1, 237 | async func(b: any): Promise { 238 | b.start(); 239 | const test = await fetch( 240 | "http://localhost:8080/param/aaron/delete", 241 | { method: "DELETE" }, 242 | ); 243 | const data = await test.text(); 244 | assertEquals(data, "aaron"); 245 | b.stop(); 246 | }, 247 | }); 248 | 249 | bench({ 250 | name: "OPTIONS: /param/:username/options", 251 | runs: 1, 252 | async func(b: any): Promise { 253 | b.start(); 254 | const test = await fetch( 255 | "http://localhost:8080/param/aaron/options", 256 | { method: "OPTIONS" }, 257 | ); 258 | const data = await test.text(); 259 | assertEquals(data, "aaron"); 260 | b.stop(); 261 | }, 262 | }); 263 | 264 | bench({ 265 | name: "POST: /param/:username/password", 266 | runs: 1, 267 | async func(b: any): Promise { 268 | b.start() 269 | const test = await fetch("http://localhost:8080/param/aaron/password", { 270 | method: "POST", 271 | headers: { 272 | "Content-Type": "application/json", 273 | }, 274 | body: JSON.stringify({ password: "123" }), 275 | }); 276 | const data = await test.json(); 277 | assertEquals(data, { name: "aaron", password: "123" }); 278 | b.stop() 279 | }, 280 | }); 281 | 282 | bench({ 283 | name: "GET: /param/:username/:password", 284 | runs: 1, 285 | async func(b: any): Promise { 286 | b.start() 287 | const test = await fetch("http://localhost:8080/param/aaron/123") 288 | const data = await test.json(); 289 | assertEquals(data, { name: "aaron", password: "123" }); 290 | b.stop() 291 | }, 292 | }); 293 | 294 | bench({ 295 | name: "GET: /param/:username/:password error", 296 | runs: 1, 297 | async func(b: any): Promise { 298 | b.start() 299 | const test = await fetch("http://localhost:8080/param/aaron/555") 300 | const data = await test.text(); 301 | assertEquals(test.status, 404); 302 | assertEquals(data, "password is not matched"); 303 | b.stop() 304 | }, 305 | }); 306 | 307 | await runBenchmarks(); 308 | 309 | await app.close(); 310 | -------------------------------------------------------------------------------- /core/utils.ts: -------------------------------------------------------------------------------- 1 | import type { AttainRequest } from "./request.ts"; 2 | 3 | import { Sha1, lookup, match, extname, parse } from "../deps.ts"; 4 | import { AttainResponse } from "./response.ts"; 5 | 6 | const CR = "\r".charCodeAt(0); 7 | const LF = "\n".charCodeAt(0); 8 | const HTAB = "\t".charCodeAt(0); 9 | const SPACE = " ".charCodeAt(0); 10 | 11 | function isCloser(value: unknown): value is Deno.Closer { 12 | return typeof value === "object" && value != null && "close" in value && 13 | // deno-lint-ignore no-explicit-any 14 | typeof (value as Record)["close"] === "function"; 15 | } 16 | 17 | export const DEFAULT_CHUNK_SIZE = 16_640; // 17 Kib 18 | 19 | export interface ReadableStreamFromReaderOptions { 20 | /** If the `reader` is also a `Deno.Closer`, automatically close the `reader` 21 | * when `EOF` is encountered, or a read error occurs. 22 | * 23 | * Defaults to `true`. */ 24 | autoClose?: boolean; 25 | 26 | /** The size of chunks to allocate to read, the default is ~16KiB, which is 27 | * the maximum size that Deno operations can currently support. */ 28 | chunkSize?: number; 29 | 30 | /** The queuing strategy to create the `ReadableStream` with. */ 31 | strategy?: { highWaterMark?: number | undefined; size?: undefined }; 32 | } 33 | 34 | export function readableStreamFromReader( 35 | reader: Deno.Reader | (Deno.Reader & Deno.Closer), 36 | options: ReadableStreamFromReaderOptions = {}, 37 | ): ReadableStream { 38 | const { 39 | autoClose = true, 40 | chunkSize = DEFAULT_CHUNK_SIZE, 41 | strategy, 42 | } = options; 43 | 44 | return new ReadableStream({ 45 | async pull(controller) { 46 | const chunk = new Uint8Array(chunkSize); 47 | try { 48 | const read = await reader.read(chunk); 49 | if (read === null) { 50 | if (isCloser(reader) && autoClose) { 51 | reader.close(); 52 | } 53 | controller.close(); 54 | return; 55 | } 56 | controller.enqueue(chunk.subarray(0, read)); 57 | } catch (e) { 58 | controller.error(e); 59 | if (isCloser(reader)) { 60 | reader.close(); 61 | } 62 | } 63 | }, 64 | cancel() { 65 | if (isCloser(reader) && autoClose) { 66 | reader.close(); 67 | } 68 | }, 69 | }, strategy); 70 | } 71 | 72 | /** Returns the content-type based on the extension of a path. */ 73 | function contentType(path: string): string | undefined { 74 | const result = lookup(extname(path)); 75 | return result ? result : undefined; 76 | } 77 | 78 | export function stripEol(value: Uint8Array): Uint8Array { 79 | if (value[value.byteLength - 1] == LF) { 80 | let drop = 1; 81 | if (value.byteLength > 1 && value[value.byteLength - 2] === CR) { 82 | drop = 2; 83 | } 84 | return value.subarray(0, value.byteLength - drop); 85 | } 86 | return value; 87 | } 88 | 89 | export const checkPathAndParseURLParams = ( 90 | req: AttainRequest, 91 | middlewareURL: string, 92 | currentURL: string, 93 | ) => { 94 | const matcher = match(middlewareURL, { decode: decodeURIComponent }); 95 | const isMatch: any = matcher(currentURL); 96 | if (isMatch.params) { 97 | const { 0: extra, ...result } = isMatch.params; 98 | req.params = { ...req.params, ...result }; 99 | } 100 | return isMatch; 101 | }; 102 | 103 | export function getRandomFilename(prefix = "", extension = ""): string { 104 | return `${prefix}${ 105 | new Sha1().update(crypto.getRandomValues(new Uint8Array(256))).hex() 106 | }${extension ? `.${extension}` : ""}`; 107 | } 108 | 109 | export const etag = (entity: Uint8Array, len: number) => { 110 | if (!entity) { 111 | // fast-path empty 112 | return `W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"`; 113 | } 114 | 115 | // compute hash of entity 116 | const sha1 = new Sha1(); 117 | sha1.update(entity); 118 | sha1.digest(); 119 | const hash = sha1.toString().substring(0, 27); 120 | 121 | return `W/"${len.toString(16)}-${hash}"`; 122 | }; 123 | 124 | export function skipLWSPChar(u8: Uint8Array): Uint8Array { 125 | const result = new Uint8Array(u8.length); 126 | let j = 0; 127 | for (let i = 0; i < u8.length; i++) { 128 | if (u8[i] === SPACE || u8[i] === HTAB) continue; 129 | result[j++] = u8[i]; 130 | } 131 | return result.slice(0, j); 132 | } 133 | 134 | export const fresh = (req: AttainRequest, res: AttainResponse) => { 135 | const modifiedSince = req.headers.get("if-modified-since"); 136 | const noneMatch = req.headers.get("if-none-match"); 137 | 138 | if (!modifiedSince && !noneMatch) { 139 | return false; 140 | } 141 | 142 | const CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/; 143 | const cacheControl = req.headers.get("cache-control"); 144 | if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) { 145 | return false; 146 | } 147 | 148 | if (noneMatch && noneMatch !== "*") { 149 | const etag = res.getHeader("etag"); 150 | 151 | if (!etag) { 152 | return false; 153 | } 154 | 155 | let etagStale = true; 156 | const matches = parseTokenList(noneMatch); 157 | for (let i = 0; i < matches.length; i++) { 158 | const match = matches[i]; 159 | if (match === etag || match === "W/" + etag || "W/" + match === etag) { 160 | etagStale = false; 161 | break; 162 | } 163 | } 164 | 165 | if (etagStale) { 166 | return false; 167 | } 168 | } 169 | 170 | // if-modified-since 171 | if (modifiedSince) { 172 | const lastModified = res.getHeader("last-modified"); 173 | const modifiedStale = !lastModified || 174 | !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince)); 175 | 176 | if (modifiedStale) { 177 | return false; 178 | } 179 | } 180 | 181 | return true; 182 | }; 183 | 184 | const parseHttpDate = (date: any) => { 185 | const timestamp = date && Date.parse(date); 186 | 187 | // istanbul ignore next: guard against date.js Date.parse patching 188 | return typeof timestamp === "number" ? timestamp : NaN; 189 | }; 190 | 191 | const parseTokenList = (str: string) => { 192 | let end = 0; 193 | const list = []; 194 | let start = 0; 195 | 196 | // gather tokens 197 | for (let i = 0, len = str.length; i < len; i++) { 198 | switch (str.charCodeAt(i)) { 199 | case 0x20:/* */ 200 | if (start === end) { 201 | start = end = i + 1; 202 | } 203 | break; 204 | case 0x2c:/* , */ 205 | list.push(str.substring(start, end)); 206 | start = end = i + 1; 207 | break; 208 | default: 209 | end = i + 1; 210 | break; 211 | } 212 | } 213 | 214 | // final token 215 | list.push(str.substring(start, end)); 216 | 217 | return list; 218 | }; 219 | 220 | export const acceptParams = (str: string, index: number = 0) => { 221 | var parts = str.split(/ *; */); 222 | var ret: any = { 223 | value: parts[0], 224 | quality: 1, 225 | params: {}, 226 | originalIndex: index, 227 | }; 228 | 229 | for (var i = 1; i < parts.length; ++i) { 230 | var pms = parts[i].split(/ *= */); 231 | if ("q" === pms[0]) { 232 | ret.quality = parseFloat(pms[1]); 233 | } else { 234 | ret.params[pms[0]] = pms[1]; 235 | } 236 | } 237 | 238 | return ret; 239 | }; 240 | 241 | export const normalizeType = (type: string) => { 242 | return ~type.indexOf("/") 243 | ? acceptParams(type) 244 | : { value: lookup(type), params: {} }; 245 | }; 246 | 247 | export const fileStream = async ( 248 | res: AttainResponse, 249 | filePath: string, 250 | ): Promise => { 251 | const [file, fileInfo] = await Promise.all( 252 | [Deno.open(filePath), Deno.stat(filePath)], 253 | ); 254 | 255 | res.setHeader("content-length", fileInfo.size.toString()); 256 | const contentTypeValue = contentType(filePath); 257 | if (contentTypeValue) { 258 | res.setHeader("content-type", contentTypeValue); 259 | } 260 | fileInfo.mtime && 261 | res.setHeader("Last-Modified", fileInfo.mtime.toUTCString()); 262 | 263 | return file; 264 | }; 265 | 266 | export const last = (array: any[]) => { 267 | var length = array == null ? 0 : array.length; 268 | return length ? array[length - 1] : undefined; 269 | }; 270 | 271 | export const getEnvFlags = () => { 272 | const options = parse(Deno.args); 273 | const container: any = {} 274 | 275 | if (options["mode"]) { 276 | container.mode = options["mode"] 277 | } 278 | 279 | if (options["env"]) { 280 | container.env = options["env"] 281 | } 282 | 283 | return container; 284 | } 285 | -------------------------------------------------------------------------------- /core/router.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | MiddlewareProps, 3 | CallBackType, 4 | SupportMethodType, 5 | ErrorCallBackType, 6 | ErrorMiddlewareProps, 7 | ParamCallBackType, 8 | ParamStackProps, 9 | } from "./types.ts"; 10 | import { App } from "./application.ts"; 11 | import { isEmpty } from "../deps.ts"; 12 | 13 | export class Router { 14 | #middlewares: MiddlewareProps[] = []; 15 | #errorMiddlewares: ErrorMiddlewareProps[] = []; 16 | #paramHandlerStacks: ParamStackProps[] = []; 17 | 18 | get middlewares() { 19 | return this.#middlewares; 20 | } 21 | 22 | get errorMiddlewares() { 23 | return this.#errorMiddlewares; 24 | } 25 | 26 | private isString(arg: any): boolean { 27 | return typeof arg === "string"; 28 | } 29 | 30 | private appendParentsPaths(currentPath: string): string { 31 | let newPath: string = currentPath; 32 | if (currentPath !== "/") { 33 | newPath += "(.*)"; 34 | } 35 | return newPath; 36 | } 37 | 38 | private appendNextPaths( 39 | parentsPath: string, 40 | middlewares: MiddlewareProps[] | ErrorMiddlewareProps[], 41 | ): MiddlewareProps[] | ErrorMiddlewareProps[] { 42 | const newMiddlewares: MiddlewareProps[] = []; 43 | middlewares.forEach((middleware: any) => { 44 | if (middleware.url && parentsPath !== "/") { 45 | const combinedUrl = `${parentsPath}${ 46 | middleware.url === "/" ? "" : middleware.url 47 | }`; 48 | newMiddlewares.push({ 49 | ...middleware, 50 | url: combinedUrl.includes("(.*)") 51 | ? combinedUrl 52 | : combinedUrl.replace(/\*/g, "(.*)"), 53 | next: middleware.next 54 | ? this.appendNextPaths(parentsPath, middleware.next) 55 | : middleware.next, 56 | }); 57 | } else { 58 | newMiddlewares.push(middleware); 59 | } 60 | }); 61 | return newMiddlewares; 62 | } 63 | 64 | /** 65 | * @false Callback 66 | * @true Instance 67 | */ 68 | private isInstance(arg: any): boolean { 69 | return arg instanceof App || arg instanceof Router; 70 | } 71 | 72 | private saveMiddlewares( 73 | type: SupportMethodType, 74 | args: any[], 75 | ) { 76 | let temp: MiddlewareProps = { method: type }; 77 | args.forEach((arg) => { 78 | if (this.isString(arg)) { 79 | temp.url = arg; 80 | } else { 81 | if (this.isInstance(arg)) { 82 | if (temp.url) { 83 | if (temp.url.includes("*")) { 84 | throw "If middleware has a next, the parent's middleware can't have a wildcard. : " + 85 | temp.url; 86 | } 87 | temp.next = 88 | (this.appendNextPaths( 89 | temp.url, 90 | arg.middlewares, 91 | ) as MiddlewareProps[]); 92 | temp.url = this.appendParentsPaths(temp.url); 93 | } else { 94 | temp.next = arg.middlewares; 95 | } 96 | } else { 97 | if (temp.url) { 98 | temp.url = temp.url.includes("(.*)") 99 | ? temp.url 100 | : temp.url.replace(/\*/g, "(.*)"); 101 | 102 | const matchedParamHandler = this.#paramHandlerStacks.filter(paramHandler => temp.url?.includes(`:${paramHandler.paramName}`)); 103 | 104 | if (!isEmpty(matchedParamHandler)) { 105 | temp.paramHandlers = matchedParamHandler; 106 | } 107 | } 108 | temp.callBack = arg as CallBackType; 109 | } 110 | } 111 | 112 | if (temp.callBack || temp.next) { 113 | this.middlewares.push(temp); 114 | temp = temp.url ? { method: type, url: temp.url } : { method: type }; 115 | } 116 | }); 117 | } 118 | 119 | private saveParamStacks( 120 | paramName: string, 121 | args: ParamCallBackType[], 122 | ) { 123 | args.forEach((arg) => { 124 | this.#paramHandlerStacks.push({ paramName, callBack: arg }); 125 | }); 126 | } 127 | 128 | private saveErrorMiddlewares( 129 | args: any[], 130 | ) { 131 | let temp: ErrorMiddlewareProps = {}; 132 | args.forEach((arg) => { 133 | if (this.isString(arg)) { 134 | temp.url = arg; 135 | } else { 136 | if (this.isInstance(arg)) { 137 | if (temp.url) { 138 | if (temp.url.includes("*")) { 139 | throw new Error( 140 | `If middleware has a next, the parent's middleware can't have a wildcard. ${temp.url}`, 141 | ); 142 | } 143 | temp.next = 144 | (this.appendNextPaths( 145 | temp.url, 146 | arg.errorMiddlewares, 147 | ) as ErrorMiddlewareProps[]); 148 | temp.url = this.appendParentsPaths(temp.url); 149 | } else { 150 | temp.next = arg.errorMiddlewares; 151 | } 152 | } else { 153 | if (temp.url) { 154 | temp.url = temp.url.includes("(.*)") 155 | ? temp.url 156 | : temp.url.replace(/\*/g, "(.*)"); 157 | } 158 | temp.callBack = arg as ErrorCallBackType; 159 | } 160 | } 161 | 162 | if (temp.callBack || temp.next) { 163 | this.errorMiddlewares.push(temp); 164 | temp = temp.url ? { url: temp.url } : {}; 165 | } 166 | }); 167 | } 168 | 169 | public use(app: App | Router): void; 170 | public use(callBack: CallBackType): void; 171 | public use(...callBack: CallBackType[]): void; 172 | public use(url: string, callBack: CallBackType): void; 173 | public use(url: string, ...callBack: CallBackType[]): void; 174 | public use(url: string, app: App | Router): void; 175 | public use( 176 | ...args: any 177 | ) { 178 | this.saveMiddlewares("ALL", args); 179 | } 180 | 181 | public get(app: App | Router): void; 182 | public get(callBack: CallBackType): void; 183 | public get(...callBack: CallBackType[]): void; 184 | public get(url: string, callBack: CallBackType): void; 185 | public get(url: string, ...callBack: CallBackType[]): void; 186 | public get(url: string, app: App | Router): void; 187 | public get( 188 | ...args: any 189 | ) { 190 | this.saveMiddlewares("GET", args); 191 | } 192 | 193 | public post(app: App | Router): void; 194 | public post(callBack: CallBackType): void; 195 | public post(...callBack: CallBackType[]): void; 196 | public post(url: string, callBack: CallBackType): void; 197 | public post(url: string, ...callBack: CallBackType[]): void; 198 | public post(url: string, app: App | Router): void; 199 | public post( 200 | ...args: any 201 | ) { 202 | this.saveMiddlewares("POST", args); 203 | } 204 | 205 | public put(app: App | Router): void; 206 | public put(callBack: CallBackType): void; 207 | public put(...callBack: CallBackType[]): void; 208 | public put(url: string, callBack: CallBackType): void; 209 | public put(url: string, ...callBack: CallBackType[]): void; 210 | public put(url: string, app: App | Router): void; 211 | public put( 212 | ...args: any 213 | ) { 214 | this.saveMiddlewares("PUT", args); 215 | } 216 | 217 | public patch(app: App | Router): void; 218 | public patch(callBack: CallBackType): void; 219 | public patch(...callBack: CallBackType[]): void; 220 | public patch(url: string, callBack: CallBackType): void; 221 | public patch(url: string, ...callBack: CallBackType[]): void; 222 | public patch(url: string, app: App | Router): void; 223 | public patch( 224 | ...args: any 225 | ) { 226 | this.saveMiddlewares("PATCH", args); 227 | } 228 | 229 | public delete(app: App | Router): void; 230 | public delete(callBack: CallBackType): void; 231 | public delete(...callBack: CallBackType[]): void; 232 | public delete(url: string, callBack: CallBackType): void; 233 | public delete(url: string, ...callBack: CallBackType[]): void; 234 | public delete(url: string, app: App | Router): void; 235 | public delete( 236 | ...args: any 237 | ) { 238 | this.saveMiddlewares("DELETE", args); 239 | } 240 | 241 | public options(app: App | Router): void; 242 | public options(callBack: CallBackType): void; 243 | public options(...callBack: CallBackType[]): void; 244 | public options(url: string, callBack: CallBackType): void; 245 | public options(url: string, ...callBack: CallBackType[]): void; 246 | public options(url: string, app: App | Router): void; 247 | public options( 248 | ...args: any 249 | ) { 250 | this.saveMiddlewares("OPTIONS", args); 251 | } 252 | 253 | public error(app: App | Router): void; 254 | public error(callBack: ErrorCallBackType): void; 255 | public error(...callBack: ErrorCallBackType[]): void; 256 | public error(url: string, callBack: ErrorCallBackType): void; 257 | public error(url: string, ...callBack: ErrorCallBackType[]): void; 258 | public error(url: string, app: App | Router): void; 259 | public error( 260 | ...args: any 261 | ) { 262 | this.saveErrorMiddlewares(args); 263 | } 264 | 265 | /** 266 | * Param handler 267 | * @param {string} paramName param name ex) `username` 268 | * @param {ParamCallBackType} callBack param callback type 269 | */ 270 | public param(paramName: string, ...callBack: ParamCallBackType[]) { 271 | this.saveParamStacks(paramName, callBack); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /core/response.ts: -------------------------------------------------------------------------------- 1 | import type { CallBackType } from "./types.ts"; 2 | import version from "../version.ts"; 3 | import { etag, fileStream, last, normalizeType, readableStreamFromReader } from "./utils.ts"; 4 | import { AttainRequest } from "./request.ts"; 5 | 6 | type ContentsType = Uint8Array | Deno.Reader | string | object | boolean; 7 | function instanceOfReader(object: any): object is Deno.Reader { 8 | return "read" in object; 9 | } 10 | 11 | const generalBody = ["string", "number", "bigint", "boolean", "symbol"]; 12 | const encoder = new TextEncoder(); 13 | 14 | const isHtml = (value: string): boolean => { 15 | return /^\s*<(?:!DOCTYPE|html|body)/i.test(value); 16 | }; 17 | 18 | export class AttainResponse { 19 | #serverRequest?: Request; 20 | #headers: Headers; 21 | #status: number; 22 | #statusText?: string; 23 | #body?: BodyInit; 24 | #pending: Function[]; 25 | #resources: number[] = []; 26 | #processDone: boolean; 27 | 28 | constructor(_serverRequest: Request) { 29 | this.#serverRequest = _serverRequest; 30 | this.#headers = new Headers(); 31 | this.#body = undefined; 32 | this.#status = 200; 33 | this.#pending = []; 34 | this.#processDone = false; 35 | this.setHeader("X-Powered-By", `Deno, Attain v${version}`); 36 | } 37 | 38 | destroy(): void { 39 | this.#serverRequest = undefined; 40 | for (const rid of this.#resources) { 41 | try { 42 | Deno.close(rid); 43 | } catch(e) {} 44 | } 45 | this.#resources = []; 46 | } 47 | 48 | get processDone(): boolean { 49 | return this.#processDone; 50 | } 51 | 52 | /** 53 | * Return the original ServerRequest class object. 54 | */ 55 | get serverRequest(): Request { 56 | if (!this.#serverRequest) { 57 | throw new Error("already responded"); 58 | } 59 | return this.#serverRequest; 60 | } 61 | 62 | /** 63 | * Return the current response object which will be used for responding 64 | */ 65 | get getResponse(): Response { 66 | return new Response(this.getBody, { 67 | headers: this.getHeaders, 68 | status: this.getStatus, 69 | statusText: this.getStatusText, 70 | }); 71 | } 72 | 73 | /** 74 | * Return the current header class object. 75 | */ 76 | get getHeaders(): Headers { 77 | return this.#headers; 78 | } 79 | 80 | /** 81 | * Return the current status number 82 | */ 83 | get getStatus(): number | undefined { 84 | return this.#status; 85 | } 86 | 87 | get getStatusText(): string | undefined { 88 | return this.#statusText; 89 | } 90 | 91 | /** 92 | * Return the current body data 93 | */ 94 | get getBody() { 95 | return this.#body; 96 | } 97 | 98 | /** 99 | * Execute pend jobs, It's automatically executed after calling the `end()` or `send()`. 100 | * @param request - latest Request class object 101 | */ 102 | public async executePendingJobs(request: AttainRequest): Promise { 103 | if (this.#pending.length === 0) { 104 | return; 105 | } 106 | for await (const job of this.#pending) { 107 | await job(request, this); 108 | } 109 | } 110 | 111 | /** 112 | * Pend the jobs which will execute right before responding. 113 | * @param fn - An array of callback types 114 | * 115 | * pend((afterReq, afterRes) => {...jobs}) 116 | */ 117 | public pend(...fn: CallBackType[]): void { 118 | this.#pending.push(...fn); 119 | } 120 | 121 | /** 122 | * Set the status 123 | * @param status - number of the http code. 124 | */ 125 | public status(status: number, statusText?: string) { 126 | this.#status = status; 127 | this.#statusText = statusText; 128 | return this; 129 | } 130 | 131 | /** 132 | * Set the body without response 133 | * @param body - contents 134 | */ 135 | public body(body: ContentsType) { 136 | if (generalBody.includes(typeof body)) { 137 | this.#body = encoder.encode(String(body)); 138 | this.setContentType( 139 | isHtml(String(body)) 140 | ? "text/html; charset=utf-8" 141 | : "text/plain; charset=utf-8", 142 | ); 143 | } else if (body instanceof Uint8Array) { 144 | this.#body = body; 145 | } else if (body && instanceOfReader(body)) { 146 | this.#body = readableStreamFromReader(body); 147 | } else if (body && typeof body === "object") { 148 | this.#body = encoder.encode(JSON.stringify(body)); 149 | this.setContentType("application/json; charset=utf-8"); 150 | } 151 | return this; 152 | } 153 | 154 | /** 155 | * Replace the current entire header object with new Headers 156 | * @param headers - Headers(deno) class object 157 | */ 158 | public setHeaders(headers: Headers) { 159 | this.#headers = headers; 160 | return this; 161 | } 162 | 163 | /** 164 | * Get the header data by a key 165 | * @param name - key 166 | */ 167 | public getHeader(name: string) { 168 | return this.#headers.get(name); 169 | } 170 | 171 | /** 172 | * Set the header data 173 | * @param name - key 174 | * @param value - header data 175 | * 176 | * setHeader("Content-Type", "application/json"); 177 | */ 178 | public setHeader(name: string, value: string) { 179 | this.#headers.set(name, value); 180 | return this; 181 | } 182 | 183 | /** 184 | * Remove data from the header by a key. 185 | * @param name - header key 186 | */ 187 | public removeHeader(name: string) { 188 | this.#headers.delete(name); 189 | return this; 190 | } 191 | 192 | /** 193 | * Set the Content-Type header 194 | * It'll append the data 195 | * @param type - content type like "application/json" 196 | */ 197 | public setContentType(type: string) { 198 | if (this.getHeaders.has("Content-Type")) { 199 | const contentType = this.getHeaders.get("Content-Type"); 200 | if (contentType && contentType.includes(type)) { 201 | return this; 202 | } 203 | this.#headers.append("Content-Type", type); 204 | } else { 205 | this.setHeader("Content-Type", type); 206 | } 207 | return this; 208 | } 209 | 210 | private format(obj: any) { 211 | const defaultFn = obj.default; 212 | if (defaultFn) delete obj.default; 213 | const keys: any = Object.keys(obj); 214 | 215 | const tempRequest = new AttainRequest(this.serverRequest); 216 | const key: any = keys.length > 0 ? tempRequest.accepts(keys) : false; 217 | 218 | if (key) { 219 | this.setHeader("Content-type", normalizeType(key).value); 220 | this.body(key()); 221 | } else if (defaultFn) { 222 | this.body(defaultFn()); 223 | } else { 224 | this.status(406); 225 | } 226 | } 227 | 228 | /** 229 | * Set the body and respond with response object. 230 | * @param contents - the body contents 231 | */ 232 | public async send(contents: ContentsType): Promise { 233 | try { 234 | this.body(contents); 235 | this.end(); 236 | } catch (error) { 237 | throw error; 238 | } 239 | } 240 | 241 | /** 242 | * Serve the static files 243 | * @param filePath - path of the static file 244 | * 245 | * Required `await` 246 | */ 247 | public async sendFile(filePath: string): Promise { 248 | let fileInfo = await Deno.stat(filePath); 249 | if (fileInfo.isFile) { 250 | const stream = await fileStream(this, filePath); 251 | this.#resources.push(stream.rid); 252 | this.status(200).body(stream).end(); 253 | } else { 254 | throw new Error(`${filePath} can't find.`); 255 | } 256 | } 257 | 258 | /** 259 | * Serve the static file and force the browser to download it. 260 | * @param filePath - path of the static file 261 | * @param name - save as the `name` 262 | * 263 | * Required `await` 264 | */ 265 | public async download(filePath: string, name?: string): Promise { 266 | try { 267 | let fileName = filePath; 268 | if (!name) { 269 | const splited = filePath.split("/"); 270 | fileName = last(splited); 271 | } else { 272 | const hasFileType = name.split(".").length > 1 ? true : false; 273 | if (hasFileType) { 274 | fileName = name; 275 | } else { 276 | throw `${name} dosen't have filetype`; 277 | } 278 | } 279 | 280 | const hasFileType = fileName.split(".").length > 1 ? true : false; 281 | if (hasFileType) { 282 | this.setHeader( 283 | "Content-Disposition", 284 | `attachment; filename="${fileName}"`, 285 | ); 286 | } else { 287 | throw `${fileName} dosen't have filetype`; 288 | } 289 | 290 | await this.sendFile(filePath); 291 | } catch (error) { 292 | console.error(error); 293 | } 294 | } 295 | 296 | /** 297 | * Redirection 298 | * @param url 299 | */ 300 | public redirect(url: string | "back") { 301 | let loc = url; 302 | if (url === "back") { 303 | loc = this.serverRequest.referrer || "/"; 304 | } 305 | this.setHeader("Location", encodeURI(loc)); 306 | 307 | this.format({ 308 | text: function () { 309 | return 302 + ". Redirecting to " + loc; 310 | }, 311 | html: function () { 312 | var u = escape(loc); 313 | return "

" + 302 + '. Redirecting to ' + u + 314 | "

"; 315 | }, 316 | default: function () { 317 | return ""; 318 | }, 319 | }); 320 | 321 | this.status(302).end(); 322 | } 323 | 324 | /** 325 | * End the current process and respond 326 | */ 327 | public async end(): Promise { 328 | try { 329 | this.setHeader("Date", new Date().toUTCString()); 330 | const currentETag = this.getHeader("etag"); 331 | const len = this.getHeader("content-length") || 332 | (this.getBody as Uint8Array).length.toString(); 333 | const newETag = etag( 334 | (this.getBody as Uint8Array), 335 | parseInt(len, 10), 336 | ); 337 | if (currentETag && currentETag === newETag) { 338 | this.status(304); 339 | } else { 340 | this.setHeader("etag", newETag); 341 | } 342 | this.#processDone = true; 343 | } catch (error) { 344 | if (error instanceof Deno.errors.BadResource) { 345 | console.log("Connection Lost"); 346 | } else { 347 | console.error(error); 348 | } 349 | } 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | attain 3 |

4 | 5 | # Attain - v1.1.2 - [Website](https://aaronwlee.github.io/attain/) 6 | 7 | ![attain ci](https://github.com/aaronwlee/attain/workflows/attain%20ci/badge.svg) 8 | ![license](https://img.shields.io/github/license/aaronwlee/attain) 9 | 10 | [![nest badge](https://nest.land/badge.svg)](https://nest.land/package/attain) 11 | 12 | A middleware web framework for Deno which is using [http](https://github.com/denoland/deno_std/tree/master/http#http) standard library inspired by [express](https://github.com/expressjs/express) and [Oak](https://github.com/oakserver/oak). 13 | 14 | Attain is blazingly fast due to handled the multi-structured middleware and routes effectively. It also strictly manage memory consumption. 15 | 16 | Only for [Deno](https://deno.land/) - __Require Deno version up to: v1.16.4__ 17 | 18 | Any contributions to the code would be appreciated. :) 19 | 20 | 21 |
22 | 23 | ### Download and use 24 | 25 | ```js 26 | import { App, Router } from "https://deno.land/x/attain/mod.ts"; 27 | // or 28 | import { App, Router } from "https://deno.land/x/attain@1.1.2/mod.ts"; 29 | // or 30 | import { App, Router, Request, Response } from "https://raw.githubusercontent.com/aaronwlee/attain/1.1.2/mod.ts"; 31 | ``` 32 | 33 | 34 | ``` 35 | # deno run --allow-net --unstable main.ts 36 | ``` 37 | 38 | ## Contents 39 | 40 | - [Getting Start](#getting-start) 41 | - [Procedure explain](#procedure-explain) 42 | - [How To](#how-to) 43 | - [Boilerplate](#boilerplate) 44 | - [Methods and Properies](#methods-and-properies) 45 | - [Response](#response) 46 | - [Request](#request) 47 | - [Router](#router) 48 | - [App](#app) 49 | - [Nested Routing](#nested-routing) 50 | - [Extra plugins](#extra-plugins) 51 | - [More Features](#more-features) 52 | - [Performance](https://github.com/aaronwlee/attain/blob/master/performance/performance.md) 53 | 54 | ## Getting Start 55 | 56 | ```ts 57 | import { App } from "https://deno.land/x/attain/mod.ts"; 58 | import type { Request, Response } from "https://deno.land/x/attain/mod.ts"; 59 | 60 | const app = new App(); 61 | 62 | const sampleMiddleware = (req: Request, res: Response) => { 63 | console.log("before send"); 64 | }; 65 | 66 | app.get("/:id", (req, res) => { 67 | console.log(req.params); 68 | res.status(200).send(`id: ${req.params.id}`); 69 | }); 70 | 71 | app.use(sampleMiddleware, (req, res) => { 72 | res.status(200).send({ status: "Good" }); 73 | }); 74 | 75 | app.listen({ port: 3500 }); 76 | ``` 77 | 78 | ### Procedure explain 79 | 80 | The middleware process the function step by step based on registered order. 81 | 82 | ![alt text](https://github.com/aaronwlee/attain/blob/master/procedure.png?raw=true "procedure") 83 | 84 | ```ts 85 | import { App } from "https://deno.land/x/attain/mod.ts"; 86 | 87 | const app = new App(); 88 | 89 | const sleep = (time: number) => { 90 | return new Promise(resolve => setTimeout(() => resolve(), time) 91 | }; 92 | 93 | app.use((req, res) => { 94 | console.log("First step"); 95 | }, async (req, res) => { 96 | await sleep(2000); // the current request procedure will stop here for two seconds. 97 | console.log("Second step"); 98 | }); 99 | 100 | app.use((req, res) => { 101 | // pend a job 102 | res.pend((afterReq, afterRes) => { 103 | console.log("Fourth step"); 104 | console.log("Fifth step with error"); 105 | console.log("You can finalize your procedure right before respond.") 106 | console.log("For instance, add a header or caching.") 107 | }) 108 | }) 109 | 110 | // last step 111 | app.use("/", (req, res) => { 112 | console.log("Third step with GET '/'"); 113 | // this is the end point 114 | res.status(200).send({status: "Good"}); 115 | }); 116 | 117 | app.use("/", (req, res) => { 118 | console.log("Will not executed"); 119 | }); 120 | 121 | app.get("/error", (req, res) => { 122 | console.log("Third step with GET '/error'"); 123 | throw new Error("I have error!") 124 | }) 125 | 126 | app.error((err, req, res) => { 127 | console.log("Fourth step with error"); 128 | console.log("A sequence of error handling.", err) 129 | res.status(500).send("Critical error."); 130 | }) 131 | 132 | app.listen({ port: 3500 }); 133 | ``` 134 | 135 | ## How To 136 | 137 | [Web Socket Example](https://github.com/aaronwlee/Attain/tree/master/howto/websocket.md) 138 | 139 | [Auto Recovery](https://github.com/aaronwlee/Attain/tree/master/howto/autorecovery.md) 140 | 141 | ## Boilerplate 142 | 143 | [A Deno web boilerplate](https://github.com/burhanahmeed/Denamo) by [burhanahmeed](https://github.com/burhanahmeed) 144 | 145 | ## Methods and Properies 146 | 147 | ### Response 148 | 149 | Methods 150 | Getter 151 | 152 | - `getResponse(): AttainResponse` 153 |
Get current response object, It will contain the body, status and headers. 154 | 155 | - `headers(): Headers` 156 |
Get current header map 157 | 158 | - `getStatus(): number | undefined` 159 |
Get current status 160 | 161 | - `getBody(): Uint8Array` 162 |
Get current body contents 163 | 164 | Functions 165 | 166 | - `pend(...fn: CallBackType[]): void` 167 |
Pend the jobs. It'll start right before responding. 168 | 169 | - `status(status: number)` 170 |
Set status number 171 | 172 | - `body(body: ContentsType)` 173 |
Set body. Allows setting `Uint8Array, Deno.Reader, string, object, boolean`. This will not respond. 174 | 175 | - `setHeaders(headers: Headers)` 176 |
You can overwrite the response header. 177 | 178 | - `getHeader(name: string)` 179 |
Get a header from the response by key name. 180 | 181 | - `setHeader(name: string, value: string)` 182 |
Set a header. 183 | 184 | - `setContentType(type: string)` 185 |
This is a shortcut for the "Content-Type" in the header. It will try to find "Content-Type" from the header then set or append the values. 186 | 187 | - `send(contents: ContentsType): Promise` 188 |
Setting the body then executing the end() method. 189 | 190 | - `await sendFile(filePath: string): Promise` 191 |
Transfers the file at the given path. Sets the Content-Type response HTTP header field based on the filename's extension. 192 |
_Required to be await_ 193 |
These response headers might be needed to set for fully functioning 194 | 195 | | Property | Description | 196 | | ------------ | ---------------------------------------------------------------------------------------------- | 197 | | maxAge | Sets the max-age property of the Cache-Control header in milliseconds or a string in ms format | 198 | | root | Root directory for relative filenames. | 199 | | cacheControl | Enable or disable setting Cache-Control response header. | 200 | 201 | - `await download(filePath: string, name?: string): Promise` 202 |
Transfers the file at the path as an "attachment". Typically, browsers will prompt the user to download and save it as a name if provided. 203 |
_Required to be await_ 204 | 205 | - `redirect(url: string | "back")` 206 |
Redirecting the current response. 207 | 208 | - `end(): Promise` 209 |
Executing the pended job then respond back to the current request. It'll end the current procedure. 210 | 211 | ### Request 212 | 213 | > [Oak](https://github.com/oakserver/oak/tree/master#request) for deno 214 | 215 | This class used Oak's request library. Check this. 216 |
Note: to access Oak's `Context.params` use `Request.params`. but require to use a `app.use(parser)` plugin. 217 | 218 | ### Router 219 | 220 | Methods 221 | 222 | - `use(app: App | Router): void` 223 | - `use(callBack: CallBackType): void` 224 | - `use(...callBack: CallBackType[]): void` 225 | - `use(url: string, callBack: CallBackType): void` 226 | - `use(url: string, ...callBack: CallBackType[]): void` 227 | - `use(url: string, app: App | Router): void` 228 | - `get...` 229 | - `post...` 230 | - `put...` 231 | - `patch...` 232 | - `delete...` 233 | - `error(app: App | Router): void;` 234 | - `error(callBack: ErrorCallBackType): void;` 235 | - `error(...callBack: ErrorCallBackType[]): void;` 236 | - `error(url: string, callBack: ErrorCallBackType): void;` 237 | - `error(url: string, ...callBack: ErrorCallBackType[]): void;` 238 | - `error(url: string, app: App | Router): void;` 239 |
It'll handle the error If thrown from one of the above procedures. 240 | 241 | Example 242 | 243 | ```ts 244 | app.use((req, res) => { 245 | throw new Error("Something wrong!"); 246 | }); 247 | 248 | app.error((error, req, res) => { 249 | console.error("I handle the Error!", error); 250 | res.status(500).send("It's critical!"); 251 | }); 252 | ``` 253 | 254 | - `param(paramName: string, ...callback: ParamCallBackType[]): void;` 255 |
Parameter handler [router.param](https://expressjs.com/en/api.html#router.param) 256 | 257 | Example 258 | 259 | ```ts 260 | const userController = new Router(); 261 | 262 | userController.param("username", (req, res, username) => { 263 | const user = await User.findOne({ username: username }); 264 | if (!user) { 265 | throw new Error("user not found"); 266 | } 267 | req.profile = user; 268 | }); 269 | 270 | userController.get("/:username", (req, res) => { 271 | res.status(200).send({ profile: req.profile }); 272 | }); 273 | 274 | userController.post("/:username/follow", (req, res) => { 275 | const user = await User.findById(req.payload.id); 276 | if (user.following.indexOf(req.profile._id) === -1) { 277 | user.following.push(req.profile._id); 278 | } 279 | const profile = await user.save(); 280 | return res.status(200).send({ profile: profile }); 281 | }); 282 | 283 | export default userController; 284 | ``` 285 | 286 | These are middleware methods and it's like express.js. 287 | 288 | ### App 289 | 290 | _App extends Router_ 291 | Methods 292 | 293 | - `This has all router's methods` 294 | 295 | Properties 296 | 297 | - `listen(options)` 298 |
Start the Attain server. 299 | 300 | ```ts 301 | options: { 302 | port: number; // required 303 | debug?: boolean; // debug mode 304 | hostname?: string; // hostname default as 0.0.0.0 305 | secure?: boolean; // https use 306 | certFile?: string; // if secure is true, it's required 307 | keyFile?: string; // if secure is true, it's required 308 | } 309 | ``` 310 | 311 | - `database(dbCls)` **NEW FEATURE!** 312 |
Register a database to use in all of your middleware functions. 313 |
Example: 314 | 315 | ```ts 316 | /* ExampleDatabase.ts */ 317 | class ExampleDatabase extends AttainDatabase { 318 | async connect() { 319 | console.log('database connected'); 320 | } 321 | async getAllUsers() { 322 | return [{ name: 'Shaun' }, { name: 'Mike' }]; 323 | } 324 | } 325 | 326 | /* router.ts */ 327 | const router = new Router(); 328 | 329 | router.get('/', async (req: Request, res: Response, db: ExampleDatabase) => { 330 | const users = await db.getAllUsers(); 331 | res.status(200).send(users); 332 | }) 333 | 334 | /* index.ts */ 335 | const app = new App(); 336 | 337 | await app.database(ExampleDatabase); 338 | 339 | app.use('/api/users', router); 340 | 341 | ``` 342 | 343 | **NOTE:** for this feature to work as expected, you must: 344 | - provide a `connect()` method to your database class 345 | - extend the `AttainDatabase` class 346 | 347 |
348 | This feature is brand new and any contributins and ideas will be welcomed 349 | 350 | - `static startWith(connectFunc)` 351 | Automatically initialize the app and connect to the database with a connect function. 352 | 353 | - `static startWith(dbClass)` 354 | Automatically initialize the app create a database instance. 355 | 356 | ## Nested Routing 357 | 358 | > **Path** - router.ts 359 | 360 | **warn**: async await will block your procedures. 361 | 362 | ```ts 363 | import { Router } from "https://deno.land/x/attain/mod.ts"; 364 | 365 | const api = new Router(); 366 | // or 367 | // const api = new App(); 368 | 369 | const sleep = (time: number) => { 370 | new Promise((resolve) => setTimeout(() => resolve(), time)); 371 | }; 372 | 373 | // It will stop here for 1 second. 374 | api.get("/block", async (req, res) => { 375 | console.log("here '/block'"); 376 | await sleep(1000); 377 | res.status(200).send(` 378 | 379 | 380 | 381 |

Hello

382 | 383 | 384 | `); 385 | }); 386 | 387 | // It will not stop here 388 | api.get("/nonblock", (req, res) => { 389 | console.log("here '/nonblock'"); 390 | sleep(1000).then((_) => { 391 | res.status(200).send(` 392 | 393 | 394 | 395 |

Hello

396 | 397 | 398 | `); 399 | }); 400 | }); 401 | 402 | export default api; 403 | ``` 404 | 405 | > **Path** - main.ts 406 | 407 | ```ts 408 | import { App } from "https://deno.land/x/attain/mod.ts"; 409 | import api from "./router.ts"; 410 | 411 | const app = new App(); 412 | 413 | // nested router applied 414 | app.use("/api", api); 415 | 416 | app.use((req, res) => { 417 | res.status(404).send("page not found"); 418 | }); 419 | 420 | app.listen({ port: 3500 }); 421 | ``` 422 | 423 | ``` 424 | # start with: deno run -A ./main.ts 425 | ``` 426 | 427 | ## Extra plugins 428 | 429 | - **logger** : `Logging response "response - method - status - path - time"` 430 | - **parser** : `Parsing the request body and save it to request.params` 431 | - **security**: `Helping you make secure application by setting various HTTP headers` [Helmet](https://helmetjs.github.io/) 432 | 433 | ### Security options 434 | 435 | | Options | Default? | 436 | | ------------------------------------------------------- | -------- | 437 | | `xss` (adds some small XSS protections) | yes | 438 | | `removePoweredBy` (remove the X-Powered-By header) | yes | 439 | | `DNSPrefetchControl` (controls browser DNS prefetching) | yes | 440 | | `noSniff` (to keep clients from sniffing the MIME type) | yes | 441 | | `frameguard` (prevent clickjacking) | yes | 442 | 443 | - **staticServe** : `It'll serve the static files from a provided path by joining the request path.` 444 | 445 | > Out of box 446 | 447 | - [**Attain-GraphQL**](https://deno.land/x/attain_graphql#attain-graphql) : `GraphQL middleware` 448 | - [**deno_graphql**](https://deno.land/x/deno_graphql#setup-with-attain): `GraphQL middleware` 449 | - [**session**](https://deno.land/x/session): `cookie session` 450 | - [**cors**](https://deno.land/x/cors/#examples): `CORS` 451 | 452 | ```ts 453 | import { 454 | App, 455 | logger, 456 | parser, 457 | security, 458 | staticServe, 459 | } from "https://deno.land/x/Attain/mod.ts"; 460 | 461 | const app = new App(); 462 | 463 | // Set Extra Security setting 464 | app.use(security()); 465 | 466 | // Logging response method status path time 467 | app.use(logger); 468 | 469 | // Parsing the request body and save it to request.params 470 | // Also, updated to parse the queries from search params 471 | app.use(parser); 472 | 473 | // Serve static files 474 | // This path must be started from your command line path. 475 | app.use(staticServe("./public", { maxAge: 1000 })); 476 | 477 | app.use("/", (req, res) => { 478 | res.status(200).send("hello"); 479 | }); 480 | 481 | app.use("/google", (req, res) => { 482 | res.redirect("https://www.google.ca"); 483 | }); 484 | 485 | app.use("/:id", (req, res) => { 486 | // This data has parsed by the embedded URL parser. 487 | console.log(req.params); 488 | res.status(200).send(`id: ${req.params.id}`); 489 | }); 490 | 491 | app.post("/submit", (req, res) => { 492 | // By the parser middleware, the body and search query get parsed and saved. 493 | console.log(req.params); 494 | console.log(req.query); 495 | res.status(200).send({ data: "has received" }); 496 | }); 497 | 498 | app.listen({ port: 4000 }); 499 | ``` 500 | 501 | ## More Features 502 | 503 | ### Switch your database with just one line of code 504 | Using the `app.database()` option, you can switch your database with just one line of code! To use this feature, create a database class that extends the `AttainDatabase` class: 505 | 506 | ```ts 507 | class PostgresDatabase extends AttainDatabase { 508 | #client: Client 509 | async connect() { 510 | const client = new Client({ 511 | user: Deno.env.get('USER'), 512 | database: Deno.env.get('DB'), 513 | hostname: Deno.env.get('HOST'), 514 | password: Deno.env.get('PASSWORD')!, 515 | port: parseInt(Deno.env.get('PORT')), 516 | }); 517 | await client.connect(); 518 | this.#client = client; 519 | } 520 | async getAllProducts() { 521 | const data = await this.#client.query('SELECT * FROM products'); 522 | /* map data */ 523 | return products; 524 | } 525 | } 526 | 527 | /* OR */ 528 | class MongoDatabase extends AttainDatabase { 529 | #Product: Collection 530 | async connect() { 531 | const client = new MongoClient(); 532 | await client.connectWithUri(Deno.env.get('DB_URL')); 533 | const database = client.database(Deno.env.get('DB_NAME')); 534 | this.#Product = database.collection('Product'); 535 | } 536 | async getAllProducts() { 537 | return await this.#Product.findAll() 538 | } 539 | } 540 | ``` 541 | 542 | Then pick one of the databases to use in your app: 543 | ```ts 544 | await app.database(MongoDatabase); 545 | /* OR */ 546 | await app.database(PostgresDatabase); 547 | /* OR */ 548 | const app = App.startWith(MongoDatabase); 549 | 550 | app.get('/products', (req, res, db) => { 551 | const products = await db.getAllProducts(); 552 | res.status(200).send(products); /* will work the same! */ 553 | }) 554 | 555 | ``` 556 | 557 | You can also provide a function that returns a database connection 558 | ```ts 559 | import { App, Router, Request, Response, AttainDatabase } from "./mod.ts"; 560 | import { MongoClient, Database } from "https://deno.land/x/mongo@v0.9.2/mod.ts"; 561 | 562 | async function DB() { 563 | const client = new MongoClient() 564 | await client.connectWithUri("mongodb://localhost:27017") 565 | const database = client.database("test") 566 | return database; 567 | } 568 | 569 | // allow auto inherit mode (auto inherit the types to the middleware) 570 | 571 | const app = App.startWith(DB); 572 | // or 573 | const app = new App() 574 | app.database(DB) 575 | 576 | // this db params will have automatically inherited types from the app<> or startWith method. 577 | app.use((req, res, db) => { 578 | 579 | }) 580 | ``` 581 | 582 | --- 583 | 584 | There are several modules that are directly adapted from other modules. 585 | They have preserved their individual licenses and copyrights. All of the modules, 586 | including those directly adapted are licensed under the MIT License. 587 | 588 | All additional work is copyright 2021 the Attain authors. All rights reserved. 589 | --------------------------------------------------------------------------------