diff --git a/README.md b/README.md index 0a6c63e..b32b3ca 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,81 @@ -# ๐Ÿš€ **Faster** - -> [!IMPORTANT]\ -> **Please give a star!** โญ - ---- - -## ๐ŸŒŸ Introduction - -**Faster** is a **fast and optimized middleware server** with an incredibly -small codebase (~300 lines), built on top of native HTTP APIs **with no -dependencies**. It includes a collection of useful middlewares (Some are -specific to Deno): - -- ๐Ÿ“„ **Log file** -- ๐Ÿ—‚๏ธ **Serve static** -- ๐ŸŒ **CORS** -- ๐Ÿ” **Session** -- โฑ๏ธ **Rate limit** -- ๐Ÿ›ก๏ธ **Token** -- ๐Ÿ“ฅ **Body parsers** -- ๐Ÿ”€ **Redirect** -- ๐Ÿ”Œ **Proxy** -- ๐Ÿ“ค **Handle upload** - -Fully compatible with **Deno Deploy** and other environments. Examples of all -resources are available in this README. Faster's ideology is simple: all you -need is an optimized middleware manager; all other functionality is middleware. - ---- - -## ๐Ÿ“š **Contents** - -- [โšก Benchmarks](#-benchmarks) -- [๐Ÿš€ Example](#-example) - - [๐Ÿ›ฃ๏ธ Defining Routes](#%EF%B8%8F-defining-routes) - - [๐Ÿ“จ POST: Read and Return JSON](#-post-read-and-return-json) - - [๐ŸŒ GET: Return HTML](#-get-return-html) - - [๐Ÿ” Get URL Params](#-get-url-params) - - [๐Ÿช Cookies](#-cookies) - - [โ†ฉ๏ธ Redirect](#%EF%B8%8F-redirect) - - [๐Ÿ’ฌ WebSockets](#-websockets) -- [๐Ÿ› ๏ธ Middlewares](#%EF%B8%8F-middlewares) - - [๐Ÿ“ฆ Set Deno KV and Deno KV FS](#-set-deno-kv-and-deno-kv-fs) - - [๐Ÿ“ Logger](#-logger) - - [๐Ÿ“ฅ Body Parsers (`res` and `req`)](#-body-parsers-res-and-req) - - [โฑ๏ธ Rate Limit](#%EF%B8%8F-rate-limit) - - [๐Ÿ—‚๏ธ Serve Static](#%EF%B8%8F-serve-static) - - [๐ŸŒ Set CORS](#-set-cors) - - [๐Ÿ”‘ Token](#-token) - - [โ†ฉ๏ธ Redirect Middleware](#%EF%B8%8F-redirect-middleware) - - [๐Ÿ” Session](#-session) - - [๐Ÿ”Œ Proxy](#-proxy) - - [๐Ÿ“ค Upload](#-upload) - - [๐Ÿš€ Upload Usage](#-upload-usage) - - [๐Ÿ’ป Upload Examples in Frontend and Backend](#-upload-examples-in-frontend-and-backend) -- [๐Ÿ“ Organizing Routes in Files](#-organizing-routes-in-files) -- [๐Ÿ“ฆ All Imports](#-all-imports) -- [๐ŸŒ Example Deploy in Ubuntu](#-example-deploy-in-ubuntu) - - [๐Ÿ› ๏ธ Create Service](#%EF%B8%8F-create-service) - - [๐Ÿ”’ Configure HTTPS](#-configure-https) -- [๐Ÿ’ก See Also: Faster with React](#-see-also-faster-with-react) -- [๐Ÿ‘จโ€๐Ÿ’ป About](#-about) - ---- - -## โšก **Benchmarks** - -The middleware is built on top of Deno's native HTTP APIs. See the benchmarks -(for a 'Hello World' server): - -**Machine**: 8 GiB RAM, Intelยฎ Coreโ„ข i5-10210U CPU @ 2.11GHz ร— 4\ -**Method**: `autocannon -c 100 -d 40 -p 10 localhost:80`\ -**Environment**: Deno v1.46.3, Ubuntu 24.04 LTS - -| Framework | Version | Router? | Results | -| ---------- | :------: | :-----: | ----------------------------------------- | -| Express | 4.19.2 | โœ“ | 167k requests in 40.11s, **29 MB** read | -| Fastify | 4.28.1 | โœ“ | 1105k requests in 40.07s, **193 MB** read | -| Oak | 17.0.0 | โœ“ | 260k requests in 40.09s, **45 MB** read | -| **Faster** | **12.1** | **โœ“** | **1432k requests in 40.17s, 250 MB read** | - -> **Note:** In addition to its performance, Faster is a very complete framework -> considering its middleware collection. - ---- - -## ๐Ÿš€ **Example** - -### ๐Ÿ›ฃ๏ธ **Defining Routes** - -- **Static Routes**: `/foo`, `/foo/bar` -- **Parameterized Routes**: - - Simple: `/:title`, `/books/:title`, `/books/:genre/:title` - - With Suffix: `/movies/:title.mp4`, `/movies/:title.(mp4|mov)` - - Optional Parameters: `/:title?`, `/books/:title?`, `/books/:genre/:title?` -- **Wildcards**: `*`, `/books/*`, `/books/:genre/*` - ---- - -### ๐Ÿ“จ **POST: Read and Return JSON** +# Faster + +A fast and optimized middleware server with an absurdly small amount of code +(300 lines) built on top of Deno's native HTTP APIs with no dependencies. It +also has a collection of useful middlewares: log file, serve static, CORS, +session, rate limit, token, body parsers, redirect, proxy and handle upload. In +"README" there are examples of all the resources. Faster's ideology is: all you +need is an optimized middleware manager, all other functionality is middleware. + +## Contents + +- [Benchmarks](#benchmarks) + +- [Example](#example) + - [Defining routes](#defining-routes) + - [POST read and return JSON](#post-read-and-return-json) + - [GET return HTML](#get-return-html) + - [Get URL params](#get-url-params) + - [Cookies](#cookies) + - [Redirect](#redirect) +- [Middleares](#middleares) + - [Logger](#logger) + - [Body Parsers res and req](#body-Parsers-res-and-req) + - [Rate Limit](#rate-limit) + - [Serve Static](#serve-static) + - [Set Cors](#set-cors) + - [Token](#token) + - [Redirect](#redirect) + - [Session](#session) + - [Proxy](#proxy) + - [Upload](#upload) + - [Upload usage](#upload-usage) + - [Upload examples in frontend and backend](#upload-examples-in-frontend-and-backend) +- [All imports](#all-imports) +- [Example Deploy](#example-deploy) + - [Create service](#create-service) + - [Configure HTTPS](#configure-https) +- [About](#about) + +## Benchmarks + +The middleware is built on top of Deno's native HTTP APIs, see the benchmarks +('hello word' server): + +**Machine**: 8 GiB, Intelยฎ Coreโ„ข i5-10210U CPU @ 2.11GHz ร— 4 + +**method**: `autocannon -c 100 -d 40 -p 10 localhost:80`. Deno v1.19.3, Ubuntu +20.04 LTS. + +| Framework | Version | Router? | Results | +| ---------- | :-----: | :-----: | ----------------------------------------: | +| Express | 4.17.3 | โœ“ | 167k requests in 40.11s, 29 MB read | +| Fastify | 3.27.4 | โœ“ | 1105k requests in 40.07s ,193 MB read | +| Oak | 10.4.0 | โœ“ | 260k requests in 40.09s, 45 MB read | +| **Faster** | **5.7** | **โœ“** | **1432k requests in 40.17s, 250 MB read** | + +Note that in addition to performance, Faster is a very complete framework +considering its middleware collection. + +## Example + +### Defining routes + +Static (/foo, /foo/bar) + +Parameter (/:title, /books/:title, /books/:genre/:title) + +Parameter w/ Suffix (/movies/:title.mp4, /movies/:title.(mp4|mov)) + +Optional Parameters (/:title?, /books/:title?, /books/:genre/:title?) + +Wildcards (\*, /books/\*, /books/:genre/\*) + +### POST read and return JSON ```typescript import { req, res, Server } from "https://deno.land/x/faster/mod.ts"; - const server = new Server(); - server.post( "/example_json", res("json"), @@ -115,16 +86,10 @@ server.post( await next(); }, ); - await server.listen({ port: 80 }); - -//or with the portable command "serve": -export default { fetch: server.fetch }; ``` ---- - -### ๐ŸŒ **GET: Return HTML** +### GET return HTML ```typescript server.get( @@ -136,11 +101,11 @@ server.get( - Title Example + title example - - HTML body example + HTML body example + `; await next(); @@ -148,39 +113,33 @@ server.get( ); ``` ---- - -### ๐Ÿ” **Get URL Params** +### Get URL params ```typescript server.get( "/example_params/:ex1?foo=bar", async (ctx: any, next: any) => { console.log(ctx.params.ex1); - console.log(ctx.url.searchParams.get("foo")); // Explore the URL (ctx.url) object + console.log(ctx.url.searchParams.get("foo")); //you can explore the URL (ctx.url) object await next(); }, ); ``` ---- - -### ๐Ÿช **Cookies** +### Cookies ```typescript import { Cookie, deleteCookie, getCookies, - getSetCookies, Server, setCookie, -} from "https://deno.land/x/faster/mod.ts"; // Alias to Deno std - +} from "https://deno.land/x/faster/mod.ts"; //alias to deno std server.get( "/cookies", async (ctx: any, next: any) => { - setCookie(ctx.res.headers, { name: "user_name", value: "San" }); // Explore interface 'Cookie' for more options + setCookie(ctx.res.headers, { name: "user_name", value: "San" }); //explore interface 'Cookie' for more options deleteCookie(ctx.res.headers, "last_order"); console.log(getCookies(ctx.req.headers)); await next(); @@ -188,24 +147,11 @@ server.get( ); ``` ---- - -### โ†ฉ๏ธ **Redirect** - -Use: `ctx.redirect([status,] "/my_custom_url_or_path")`. The default status is -`302`. +### Redirect ```typescript server.get( "/redirect_example", - async (ctx: any, next: any) => { - ctx.redirect(303, "/my_custom_url_or_path"); - await next(); - }, -); - -server.get( - "/redirect_example2", async (ctx: any, next: any) => { ctx.redirect("/my_custom_url_or_path"); await next(); @@ -213,164 +159,86 @@ server.get( ); ``` ---- - -### ๐Ÿ’ฌ **WebSockets** - -By default, the server will reject WebSocket connections to prevent -vulnerabilities. To accept connections, use the `acceptOrRejectSocketConn` -function, which should return an ID to retrieve the WebSocket later. If the -function returns `undefined`, `""`, `null`, `0`, etc., the connection will be -rejected. - -**Example:** - -```typescript -server.acceptOrRejectSocketConn = async (ctx: Context) => { - // Returning undefined, "", null, or 0 will reject the connection. - return ctx.req.headers.get("Host")!; // Return ID -}; -``` - -**Retrieving the Socket by ID:** - -```typescript -server.openedSockets.get(yourId); // As in the example, ctx.req.headers.get("Host")! -``` - -**Receiving WebSocket Events:** - -```typescript -server.onSocketMessage = async (id: string, socket: WebSocket, event: any) => { - console.log(id); - console.log(socket); - console.log(event); -}; - -server.onSocketClosed = async (id: string, socket: WebSocket) => { - console.log(id); - console.log(socket); -}; -//... server.onSocketError, server.onSocketOpen -``` - ---- - -## ๐Ÿ› ๏ธ **Middlewares** - -This project has a standard set of middlewares useful for most cases. - -### ๐Ÿ“ฆ **Set Deno KV and Deno KV FS** - -You need to launch Deno KV and Deno KV FS as several middlewares depend on it. - -```typescript -const kv = await Deno.openKv(); // Use your parameters here to launch a custom Deno.Kv -Server.setKv(kv); -``` - -Now, you can globally access instances in `Server.kv` and `Server.kvFs`. - -- **Deno KV File System (`Server.kvFs`):** Compatible with Deno Deploy. Saves - files in 64KB chunks. You can organize files into directories, control the - KB/s rate for saving and reading files, impose rate limits, set user space - limits, and limit concurrent operationsโ€”useful for controlling - uploads/downloads. Utilizes the Web Streams API. - -See more at: [deno_kv_fs](https://github.com/hviana/deno_kv_fs) - ---- +## Middleares -### ๐Ÿ“ **Logger** +This project has a standard set of middleware useful for most cases. -```typescript -logger(save: boolean = true, print: boolean = true) -``` +### Logger -**Initialize Deno KV (if not already done):** +Example: ```typescript -const kv = await Deno.openKv(); -Server.setKv(kv); +server.use(logger()); ``` -**Usage:** +You can pass custom log file: ```typescript -// You can also use useAtBeginning -server.use(logger()); // With default options: save and print are true +logger("./my_dir/my_custom_log.txt"); ``` -**Access Log Data:** - -- **Retrieve Logs:** `await FasterLog.get(startMillis, endMillis)` -- **Delete Logs:** `await FasterLog.delete(startMillis, endMillis)` +### Body Parsers res and req ---- - -### ๐Ÿ“ฅ **Body Parsers (`res` and `req`)** - -**Example:** +Example: ```typescript server.post( "/example_parsers", - res("json"), // Response parser - req("json"), // Request parser + res("json"), //Response parser + req("json"), //Request parser async (ctx: any, next: any) => { - console.log(ctx.body); // The original (unparsed) body is in ctx.req.body + console.log(ctx.body); //the original (no parser) body is in ctx.req.body ctx.res.body = { msg: "json response example" }; await next(); }, ); ``` -**Supported Options:** +The current supported options for "req" are: "arrayBuffer", "blob", "formData", +"json", "text". -- **`req` Parsers:** `"arrayBuffer"`, `"blob"`, `"formData"`, `"json"`, `"text"` -- **`res` Parsers:** `"json"`, `"html"`, `"javascript"` +The current supported options for "res" are: "json", "html", "javascript". -**Custom Parsing Example:** +If there are no parsers for your data, don't worry, you can handle the data +manually, Ex: ```typescript server.post( - "/custom_parse", + "/upload", async (ctx: any, next: any) => { - ctx.res.headers.set("Content-Type", "application/json"); - const data = await customParseBody(ctx.req.body); // Handle ctx.req.body manually - ctx.res.body = JSON.stringify({ msg: "ok" }); + ctx.res.headers.set( + "Content-Type", + "application/json", + ); + const data = await exCustomParseBody(ctx.req.body); //do what you want with ctx.req.body + ctx.res.body = JSON.stringify({ msg: "ok" }); // //ctx.res.body can also be other data types such as streams, bytes and etc. await next(); }, ); ``` ---- - -### โฑ๏ธ **Rate Limit** +### Rate Limit -**Usage:** +Example: ```typescript -// You can also use useAtBeginning server.use(rateLimit()); ``` -**Options (with default values):** +OPTIONS (with default values): ```typescript rateLimit({ attempts: 30, interval: 10, maxTableSize: 100000, - id: (ctx: Context) => ctx.req.headers.get("Host")!, + id: (ctx: Context) => JSON.stringify(ctx.conn.remoteAddr), }); ``` ---- +### Serve Static -### ๐Ÿ—‚๏ธ **Serve Static** - -**Example (route must end with `/*`):** +Example (must end with "/*"): ```typescript server.get( @@ -379,15 +247,12 @@ server.get( ); ``` ---- - -### ๐ŸŒ **Set CORS** +### Set Cors -**Example:** +Example: ```typescript -server.options("/example_cors", setCORS()); // Enable pre-flight request - +server.options("/example_cors", setCORS()); //enable pre-fligh request server.get( "/example_cors", setCORS(), @@ -397,25 +262,21 @@ server.get( ); ``` -**Specify Allowed Hosts:** +You can pass valid hosts to cors function: ```typescript setCORS("http://my.custom.url:8080"); ``` ---- - -### ๐Ÿ”‘ **Token** +### Token This middleware is encapsulated in an entire static class. It uses Bearer Token -and default options with the "HS256" algorithm, generating a random secret when -starting the application (you can also set a secret manually). - -**Usage:** +and default options with the "HS256" algorithm, and generates a random secret +when starting the application (you can also set a secret manually). Ex: ```typescript server.get( - "/example_verify_token", // Send token to server in Header => Authorization: Bearer TOKEN + "/example_verify_token", //send token to server in Header => Authorization: Bearer TOKEN Token.middleware, async (ctx, next) => { console.log(ctx.extra.tokenPayload); @@ -425,151 +286,104 @@ server.get( ); ``` -**Generate Token:** - -```typescript -await Token.generate({ user_id: "172746" }, null); // Null for never expire; defaults to "1h" -``` - -**Set Secret:** +Generate Token ex: ```typescript -Token.setSecret("a3d2r366wgb3dh6yrwzw99kzx2"); // Do this at the beginning of your application +await Token.generate({ user_id: "172746" }, null); //null to never expire, this parameter defaults to "1h" ``` -**Get Token Payload Outside Middleware:** +Set secret ex: ```typescript -await Token.getPayload("YOUR_TOKEN_STRING"); // For example, to get token data from token string in URL parameter +Token.setSecret("a3d2r366wgb3dh6yrwzw99kzx2"); //Do this at the beginning of your application ``` -**Set Configurations:** +Get token payload out of middleware: ```typescript -Token.setConfigs(/* your configurations */); +await Token.getPayload("YOUR_TOKEN_STRING"); //Ex: use for get token data from token string in URL parameter. ``` ---- +You can also use the static method `Token.setConfigs`. -### โ†ฉ๏ธ **Redirect Middleware** +### Redirect -**Usage:** `redirect([status,] "/my_custom_url_or_path")`. The default status is -`302`. - -**Example:** +Ex: ```typescript server.get( "/my_url_1", - redirect(303, "/my_url_2"), // Or the full URL -); - -server.get( - "/my_url_2", - redirect("/my_url_3"), // Or the full URL + redirect("/my_url_2"), //or the full url ); ``` ---- - -### ๐Ÿ” **Session** - -**Initialize Deno KV (if not already done):** - -```typescript -const kv = await Deno.openKv(); -Server.setKv(kv); -``` +### Session -#### **Example** +Ex: ```typescript -// You can also use useAtBeginning server.use(session()); - -// In routes: +//in routes: server.get( "/session_example", async (ctx, next) => { - console.log(ctx.extra.session); // Get session data - ctx.extra.session.value.foo = "bar"; // Set session data (foo => "bar") + console.log(ctx.extra.session); //get session data + ctx.extra.session.foo = "bar"; //set session data await next(); }, ); ``` -- The default engine uses Deno KV and is optimized. - -#### **Expiration Policies** - -- **Absolute Expiration:** The object in the cache will expire after a certain - time from when it was inserted, regardless of its usage. A value of `0` - disables this expiration. -- **Sliding Expiration:** The object expires after a configured time from the - last request (`get` or `set`). A value of `0` disables this expiration. - -**Note:** If both `slidingExpiration` and `absoluteExpiration` are `0`, -expiration is disabled. If both are greater than `0`, `absoluteExpiration` -cannot be less than `slidingExpiration`. - -**Session Storage Engine Interface:** +OPTIONS (with default values): -```typescript -constructor( - slidingExpiration: number = 0, - absoluteExpiration: number = 0 -) ``` - -**Default Values:** - -```typescript -session(engine: SessionStorageEngine = new KVStorageEngine()) // Default is 60 min slidingExpiration +session(engine: SessionStorageEngine = new SQLiteStorageEngine(60)) //60 is 60 minutes to expire session ``` ---- - -### ๐Ÿ”Œ **Proxy** +### Proxy -**Usage:** +Ex: ```typescript -// You can also use useAtBeginning server.use(proxy({ url: "https://my-url-example.com" })); -server.use(proxy({ url: async (ctx) => "https://my-url-example.com" })); ``` -**In Routes:** +In routes: ```typescript server.get( "/proxy_example", async (ctx, next) => { - console.log(ctx.req); // Request points to the proxy - console.log(ctx.res); // Response contains the proxy answer + console.log(ctx.req); //req has changed as it now points to the proxy + console.log(ctx.res); //res has changed because now it has the proxy answer + + //OR if replaceReqAndRes = false + console.log(ctx.extra.proxyReq); + console.log(ctx.extra.proxyRes); + await next(); }, ); ``` -**Specific Proxy Route:** +Or proxy in specific route: ```typescript server.get( "/proxy_example", proxy({ url: "https://my-url-example.com/proxy_ex2", - replaceProxyPath: false, // Specific proxy route for "/proxy_example" + replaceProxyPath: false, //specific proxy route for the route "/proxy_example" }), async (ctx, next) => { - console.log(ctx.req); - console.log(ctx.res); + console.log(ctx.req); //req has changed as it now points to the proxy + console.log(ctx.res); //res has changed because now it has the proxy answer await next(); }, ); ``` -**Conditional Proxy:** +Conditional proxy: ```typescript server.get( @@ -577,314 +391,227 @@ server.get( proxy({ url: "https://my-url-example.com/proxy_ex3", condition: (ctx) => { - return ctx.url.searchParams.get("foo") ? true : false; + if (ctx.url.searchParams.get("foo")) { + return true; + } else { + return false; + } }, }), async (ctx, next) => { - console.log(ctx.extra.proxied); // True if proxy condition is true - console.log(ctx.req); - console.log(ctx.res); + console.log(ctx.extra.proxied); //will be true if proxy condition is true + console.log(ctx.req); //req has changed as it now points to the proxy + console.log(ctx.res); //res has changed because now it has the proxy answer await next(); }, ); ``` -**Options (with default values):** +OPTIONS (with default values): -```typescript -proxy({ - url: string, - replaceReqAndRes: true, - replaceProxyPath: true, - condition: (ctx: Context) => true, -}); +``` +proxy(url: string, replaceReqAndRes: true, replaceProxyPath: true, condition: : (ctx: Context) => true ) ``` -> **Warning:** Do not use "res body parsers" with `replaceReqAndRes: true` -> (default)!\ -> **Note:** If you don't use Request body information before the proxy or in -> your condition, avoid using "req body parsers" to reduce processing cost. +**Do not use "res body parsers" with 'replaceReqAndRes: true' (default) !!!** ---- +**If you don't use Request body information before the proxy or in your +condition, don't use "req body parsers" as this will increase the processing +cost !!!** -### ๐Ÿ“ค **Upload** +### Upload -**Initialize Deno KV (if not already done):** +This middleware automatically organizes uploads to avoid file system problems +and create dirs if not exists, perform validations and optimizes ram usage when +uploading large files using Deno standard libraries! -```typescript -const kv = await Deno.openKv(); -Server.setKv(kv); -``` +#### Upload usage -This middleware uses Deno KV File System -([deno_kv_fs](https://github.com/hviana/deno_kv_fs)). +Ex: -#### ๐Ÿš€ **Upload Usage** +```typescript +.post("/upload", upload(), async (ctx: any, next: any) => { ... +``` -**Example:** +Ex (with custom options): ```typescript -// The route must end with * -server.post("/files/*", upload(), async (ctx: any, next: any) => {/* ... */}); -server.get("/files/*", download(), async (ctx: any, next: any) => {/* ... */}); +.post("/upload", upload({ path: 'uploads_custom_dir' , extensions: ['jpg', 'png'], maxSizeBytes: 20000000, maxFileSizeBytes: 10000000, saveFile: true, readFile: false, useCurrentDir: true }), async (ctx: any, next: any) => { ... ``` -**With Custom Options:** +Request must contains a body with form type "multipart/form-data", and inputs +with type="file". -- **Download:** +Ex (pre validation): -```typescript -server.post( - "/files/*", - upload({ - allowedExtensions: async (ctx: Context) => ["jpg"], - maxSizeBytes: async (ctx: Context) => - (ctx.extra.user.isPremium() ? 1 : 0.1) * 1024 * 1024 * 1024, // 1GB or 100MB - maxFileSizeBytes: async (ctx: Context) => - (ctx.extra.user.isPremium() ? 1 : 0.1) * 1024 * 1024 * 1024, // 1GB or 100MB - chunksPerSecond: async (ctx: Context) => - (ctx.extra.user.isPremium() ? 10 : 1) / - kvFs.getClientReqs(ctx.extra.user.id), - maxClientIdConcurrentReqs: async ( - ctx: Context, - ) => (ctx.extra.user.isPremium() ? 10 : 1), - clientId: async (ctx: Context) => ctx.extra.user.id, - validateAccess: async (ctx: Context, path: string[]) => - ctx.extra.user.hasDirAccess(path), - }), - async (ctx: any, next: any) => {/* ... */}, -); +```javascript +.post("/pre_upload", preUploadValidate(["jpg", "png"], 20000000, 10000000), async (ctx: any, next: any) => { ... ``` -- **Upload:** +Pre validation options: -```typescript -server.get( - "/files/*", - download({ - chunksPerSecond: async (ctx: Context) => - (ctx.extra.user.isPremium() ? 10 : 1) / - kvFs.getClientReqs(ctx.extra.user.id), - maxClientIdConcurrentReqs: async ( - ctx: Context, - ) => (ctx.extra.user.isPremium() ? 10 : 1), - clientId: async (ctx: Context) => ctx.extra.user.id, - validateAccess: async (ctx: Context, path: string[]) => - ctx.extra.user.hasDirAccess(path), - maxDirEntriesPerSecond: async ( - ctx: Context, - ) => (ctx.extra.user.isPremium() ? 1000 : 100), - pagination: async (ctx: Context) => true, - cursor: async (ctx: Context) => ctx.url.searchParams.get("cursor"), - }), -); +``` +preUploadValidate( + extensions: Array = [], + maxSizeBytes: number = Number.MAX_SAFE_INTEGER, + maxFileSizeBytes: number = Number.MAX_SAFE_INTEGER, +) ``` -#### ๐Ÿ’ป **Upload Examples in Frontend and Backend** +#### Upload examples in frontend and backend -**Frontend (AJAX with multiple files):** +Below an frontend example to work with AJAX, also accepting type="file" +multiple: ```javascript -const files = document.querySelector("#yourFormId input[type=file]").files; -const name = document.querySelector("#yourFormId input[type=file]") - .getAttribute("name"); +var files = document.querySelector("#yourFormId input[type=file]").files; +var name = document.querySelector("#yourFormId input[type=file]").getAttribute( + "name", +); -const form = new FormData(); -for (let i = 0; i < files.length; i++) { +var form = new FormData(); +for (var i = 0; i < files.length; i++) { form.append(`${name}_${i}`, files[i]); } -const userId = 1; // Example -const res = await fetch(`/files/${userId}`, { +var res = await fetch("/upload", { //Fetch API automatically puts the form in the format "multipart/form-data". method: "POST", body: form, }).then((response) => response.json()); - console.log(res); + +//VALIDATIONS -------------- + +var validationData = {}; +for (var i = 0; i < files.length; i++) { + var newObj = { //newObj is needed, JSON.stringify(files[i]) not work + "name": files[i].name, + "size": files[i].size, + }; + validationData[`${name}_${i}`] = newObj; +} +var validations = await fetch("/pre_upload", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(validationData), +}).then((response) => response.json()); +console.log(validations); ``` -**Backend (Deno):** +In Deno (backend): ```typescript import { - download, + preUploadValidate, res, Server, upload, } from "https://deno.land/x/faster/mod.ts"; - const server = new Server(); - server.post( - "/files/*", // For example: /files/general/myFile.xlsx + "/upload", res("json"), - upload(), // Using default options. No controls. + upload({ + path: "my_uploads", + extensions: ["jpg", "png"], + maxSizeBytes: 20000000, + maxFileSizeBytes: 10000000, + }), async (ctx: any, next: any) => { ctx.res.body = ctx.extra.uploadedFiles; await next(); }, ); - -server.get( - "/files/*", - download(), // Using default options. No controls. +server.post( + "/pre_upload", + res("json"), + preUploadValidate(["jpg", "png"], 20000000, 10000000), + async (ctx: any, next: any) => { + ctx.res.body = { msg: "Passed upload validations." }; + await next(); + }, ); - server.get("/", res("html"), async (ctx: any, next: any) => { ctx.res.body = ` -
-
- -
- `; +
+
+ +
+ `; await next(); }); - -await server.listen({ port: 80 }); - -//or with the portable command "serve": -export default { fetch: server.fetch }; -``` - ---- - -## ๐Ÿ“ **Organizing Routes in Files** - -It's possible to organize routes into files using native JavaScript resources. - -**Main File:** - -```typescript -import { Server } from "https://deno.land/x/faster/mod.ts"; -import exampleRoutes from "./example_routes.ts"; - -const server = new Server(); -exampleRoutes("example", server); - await server.listen({ port: 80 }); - -//or with the portable command "serve": -export default { fetch: server.fetch }; -``` - -**Secondary Route File (`example_routes.ts`):** - -```typescript -import { req, res, Server } from "https://deno.land/x/faster/mod.ts"; - -export default function exampleRoutes(namespace: string, server: Server) { - server.post( - `${namespace}/json`, - res("json"), - req("json"), - async (ctx: any, next: any) => { - console.log(ctx.body); - ctx.res.body = { msg: "json response example" }; - await next(); - }, - ); - - server.get( - `${namespace}/html`, - res("html"), - async (ctx: any, next: any) => { - ctx.res.body = ` - - - - - Title Example - - - HTML body example - - - `; - await next(); - }, - ); -} ``` ---- - -## ๐Ÿ“ฆ **All Imports** +## All imports ```typescript import { Context, - ContextResponse, // Type - Cookie, // Type, alias to Deno std - deleteCookie, // Alias to Deno std - download, - FasterLog, - getCookies, // Alias to Deno std - getSetCookies, // Alias to Deno std - KVStorageEngine, + ContextResponse, //type + Cookie, //type, alias to deno std + deleteCookie, //alias to deno std + getCookies, //alias to deno std logger, - NextFunc, // Type - Params, // Type + NextFunc, //type + Params, //type parse, - ProcessorFunc, // Type + preUploadValidate, + ProcessorFunc, //type proxy, rateLimit, redirect, req, res, - Route, // Type - RouteFn, // Type + Route, //type + RouteFn, //type Server, serveStatic, - Session, // Type + Session, //type session, SessionStorageEngine, - setCookie, // Alias to Deno std + setCookie, //alias to deno std setCORS, + SQLiteStorageEngine, Token, upload, -} from "jsr:@hviana/faster"; -import * as jose from "jsr:@hviana/faster/jose"; // jsr port of deno panva/jose (v6.0.8) -import * as deno_kv_fs from "jsr:@hviana/faster/deno-kv-fs"; // Alias to jsr @hviana/deno-kv-fs (v1.0.1) +} from "https://deno.land/x/faster/mod.ts"; ``` ---- - -## ๐ŸŒ **Example Deploy in Ubuntu** +## Example Deploy -Example of deploying an application named "my-deno-app" in a Ubuntu environment. -Change "my-deno-app" and directories to yours. +Example of depoly application "my-deno-app" in ubuntu environment. Change the +"my-deno-app" and the directories to yours. -### ๐Ÿ› ๏ธ **Create Service** +### Create service -**Create Run Script ("run-server.sh") in Your Application Folder:** +Create run script ("run-server.sh") in your application folder with the content: -```bash +``` #!/bin/bash -/home/ubuntu/.deno/bin/deno run --allow-all --unstable-kv /home/ubuntu/my-deno-app/app.ts +/home/ubuntu/.deno/bin/deno run --allow-net --allow-read --allow-write /home/ubuntu/my-deno-app/app.ts ``` -**Give Execution Permission to the Script:** +Give permission to the script: -```bash +```console chmod +x run-server.sh ``` -**Create Service Files:** +Create service files: -```bash +```console sudo touch /etc/systemd/system/my-deno-app.service sudo nano /etc/systemd/system/my-deno-app.service ``` -**In "my-deno-app.service" (change "Description", "WorkingDirectory", and -"ExecStart" to yours):** +In "my-deno-app".service (change the "Description", "WorkingDirectory" and +"ExecStart" to yours): -```ini +``` [Unit] Description=My Deno App [Service] -Type=simple -User=ubuntu WorkingDirectory=/home/ubuntu/my-deno-app ExecStart=/home/ubuntu/my-deno-app/run-server.sh TimeoutSec=30 @@ -895,120 +622,94 @@ RestartSec=1 WantedBy=multi-user.target ``` -**If Your Application Depends on Another Service (e.g., MongoDB):** +If your application needs to wait for another service to start, such as the +mongodb database, you can use the ยด[Unit]ยด section like this: -```ini +``` [Unit] Description=My Deno App After=mongod.service ``` -**Enable the "my-deno-app" Service:** +Enable the "my-deno-app" service: -```bash +```console sudo systemctl enable my-deno-app.service ``` -**Start and Stop the "my-deno-app" Service:** +To start and stop the "my-deno-app" service: -```bash +```console sudo service my-deno-app stop sudo service my-deno-app start ``` -**View Logs:** +See log: -```bash +```console journalctl -u my-deno-app.service --since=today -e ``` ---- - -### ๐Ÿ”’ **Configure HTTPS** +### Configure HTTPS -**Install Certbot:** +Install certbot: -```bash +```console sudo apt install certbot ``` -**Generate Certificates (Port 80 Must Be Free):** +Generate certificates: -```bash -sudo certbot certonly --standalone +```console +sudo certbot certonly --manual ``` -**During Setup:** +In your application, to verify the domain you will need something like: -When prompted: - -``` -Please enter the domain name(s) you would like on your certificate (comma and/or space separated) (Enter 'c' to cancel): +```typescript +import { Server, serveStatic } from "https://deno.land/x/faster/mod.ts"; +const server = new Server(); +server.get( //verify http:///.well-known/acme-challenge/ + "/.well-known/*", + serveStatic("./.well-known"), // ex: create .well-known folder in yor app folder +); +await server.listen({ port: 80 }); ``` -Enter your domains and subdomains, e.g.: `yourdomain.link www.yourdomain.link` - -**Run Your Application on HTTPS (Change "yourdomain.link" to Your Domain):** +To run your application on https (Change "yourdomain.link" to your domain): ```typescript await server.listen({ port: 443, - cert: await Deno.readTextFile( - "/etc/letsencrypt/live/yourdomain.link/fullchain.pem", - ), - key: await Deno.readTextFile( - "/etc/letsencrypt/live/yourdomain.link/privkey.pem", - ), + certFile: "/etc/letsencrypt/live/yourdomain.link/fullchain.pem", + keyFile: "/etc/letsencrypt/live/yourdomain.link/privkey.pem", }); - -//or with the portable command "serve": -//in this case you need to pass arguments such as port and certificate in the command. -export default { fetch: server.fetch }; ``` -**Set Up Automatic Certificate Renewal:** - -The certificate is valid for a short period. Set up a cron job to renew -automatically. +The certificate is valid for a short period. Set crontab to update +automatically. The command 'sudo crontab' opens roots crontab, all commands are +executed as sudo. Do like this: -**Edit Root's Crontab:** - -```bash +```console sudo crontab -e ``` -**Add to the End of the File (to Check and Renew Every 12 Hours):** +Add to the end of the file (to check and renew if necessary every 12 hours): ``` 0 */12 * * * certbot -q renew --standalone --preferred-challenges=http ``` -**Alternatively, Check Every 7 Days:** +Or also to check every 7 days: ``` 0 0 * * 0 certbot -q renew --standalone --preferred-challenges=http ``` ---- - -## ๐Ÿ’ก **See Also: Faster with React** - -Check out the complete framework with Faster and React: - -๐Ÿ‘‰ -[https://github.com/hviana/faster_react](https://github.com/hviana/faster_react) - ---- - -## ๐Ÿ‘จโ€๐Ÿ’ป **About** - -**Author:** Henrique Emanoel Viana, a Brazilian computer scientist and web -technology enthusiast. - -- ๐Ÿ“ž **Phone:** +55 (41) 99999-4664 -- ๐ŸŒ **Website:** - [https://sites.google.com/view/henriqueviana](https://sites.google.com/view/henriqueviana) +## About -> **Improvements and suggestions are welcome!** +Author: Henrique Emanoel Viana, a Brazilian computer scientist, enthusiast of +web technologies, cel: +55 (41) 99999-4664. URL: +https://sites.google.com/site/henriqueemanoelviana ---- +Improvements and suggestions are welcome! diff --git a/deno.json b/deno.json deleted file mode 100644 index aee479b..0000000 --- a/deno.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "@hviana/faster", - "version": "1.1.2", - "exports": { - ".": "./mod.ts", - "./jose": "./vendor/jose/index.ts", - "./deno-kv-fs": "./vendor/deno_kv_fs/index.ts" - } -} diff --git a/deps.ts b/deps.ts index 0052891..120de13 100644 --- a/deps.ts +++ b/deps.ts @@ -1,31 +1,26 @@ -export { join } from "jsr:@std/path@^1.0.9"; - -export { - DenoKvFs, - type DirList, - type File, - type FileStatus, - type ReadOptions, - type SaveOptions, -} from "jsr:@hviana/deno-kv-fs@^1.0.2"; -import * as deno_kv_fs from "jsr:@hviana/deno-kv-fs@^1.0.2"; -export { deno_kv_fs }; +export { join, SEP } from "https://deno.land/std@0.142.0/path/mod.ts"; export { ensureDir, ensureDirSync, - ensureFile, ensureFileSync, move, -} from "jsr:@std/fs@^1.0.19"; -export { crypto } from "jsr:@std/crypto@^1.0.5"; -export { toReadableStream, toWritableStream } from "jsr:@std/io@^0.225.2"; +} from "https://deno.land/std@0.142.0/fs/mod.ts"; +export { crypto } from "https://deno.land/std@0.142.0/crypto/mod.ts"; +export { MultipartReader } from "https://deno.land/std@0.142.0/mime/multipart.ts"; +export { readableStreamFromReader } from "https://deno.land/std@0.142.0/io/mod.ts"; +export { readerFromStreamReader } from "https://deno.land/std@0.142.0/streams/conversion.ts"; +export { + generateSecret, + jwtVerify, + SignJWT, +} from "https://deno.land/x/jose@v4.8.1/index.ts"; + +export { storage } from "https://deno.land/x/fast_storage/mod.ts"; -export type { Cookie } from "jsr:@std/http@^1.0.20/cookie"; +export type { Cookie } from "https://deno.land/std@0.142.0/http/cookie.ts"; export { deleteCookie, getCookies, - getSetCookies, setCookie, -} from "jsr:@std/http@^1.0.20/cookie"; -export * as jose from "jsr:@panva/jose@^6.0.12"; +} from "https://deno.land/std@0.142.0/http/cookie.ts"; diff --git a/middlewares/logger.ts b/middlewares/logger.ts index b24faad..df27617 100644 --- a/middlewares/logger.ts +++ b/middlewares/logger.ts @@ -1,67 +1,26 @@ /* Created by: Henrique Emanoel Viana Githu: https://github.com/hviana -Page: https://sites.google.com/view/henriqueviana +Page: https://sites.google.com/site/henriqueemanoelviana cel: +55 (41) 99999-4664 */ -import { Context, NextFunc, RouteFn, Server } from "../server.ts"; -import { DenoKvFs } from "../deps.ts"; - -export class FasterLog { - static async write(data: string) { - const date = Date.now(); - const id = crypto.randomUUID(); - await Server.kv!.set(["faster", "log", date, id], data); - } - static async get( - startMillis: number = 0, - endMillis: number = Date.now(), - ): Promise { - const res: any[] = []; - const listParamsSaving = [{ - start: ["faster", "log", startMillis], - end: ["faster", "log", endMillis], - }, { - limit: 1000, - }]; - for await ( - const f of DenoKvFs.pagedListIterator(listParamsSaving, Server.kv!) - ) { - res.push({ time: f.key[f.key.length - 2], log: f.value }); - } - return res; - } - static async delete( - startMillis: number = 0, - endMillis: number = Date.now(), - ): Promise { - const listParamsSaving = [{ - start: ["faster", "log", startMillis], - end: ["faster", "log", endMillis], - }, { - limit: 1000, - }]; - for await ( - const f of DenoKvFs.pagedListIterator(listParamsSaving, Server.kv!) - ) { - await Server.kv!.delete(f.key); - } - } -} - -export function logger(salve: boolean = true, print: boolean = true): RouteFn { +import { Context, NextFunc } from "../server.ts"; +import { ensureFileSync } from "../deps.ts"; +export function logger( + file: string = "./log.txt", +) { + ensureFileSync(file); + const denoFile = Deno.openSync(file, { write: true, append: true }); return async function (ctx: Context, next: NextFunc) { - const entry = `${ctx.req.method} ${ctx.url.toString()} ${ctx.req.headers - .get("Host")!} ${new Date().toISOString()}`; - if (print) { - console.log(entry); - } - if (salve) { - await FasterLog.write( - entry, - ); - } + await Deno.write( + denoFile.rid, + new TextEncoder().encode( + `${ctx.req.method} ${ctx.url.toString()} ${ + JSON.stringify(ctx.conn.remoteAddr) + } ${new Date().toISOString()}\n`, + ), + ); await next(); }; } diff --git a/middlewares/parser.ts b/middlewares/parser.ts index 7faf0b2..c910e42 100644 --- a/middlewares/parser.ts +++ b/middlewares/parser.ts @@ -1,11 +1,11 @@ /* Created by: Henrique Emanoel Viana Githu: https://github.com/hviana -Page: https://sites.google.com/view/henriqueviana +Page: https://sites.google.com/site/henriqueemanoelviana cel: +55 (41) 99999-4664 */ -import { Context, NextFunc, ProcessorFunc, RouteFn } from "../server.ts"; +import { Context, NextFunc, ProcessorFunc } from "../server.ts"; const reqParsers: { [key: string]: ProcessorFunc } = {}; const resParsers: { [key: string]: ProcessorFunc } = { "json": (ctx: Context) => { @@ -22,7 +22,7 @@ const resParsers: { [key: string]: ProcessorFunc } = { }, }; -export function res(type: string): RouteFn { +export function res(type: string) { return async (ctx: Context, next: NextFunc) => { if (!resParsers[type]) { throw new Error(`Response body parser: '${type}' not supported.`); @@ -31,7 +31,7 @@ export function res(type: string): RouteFn { await next(); }; } -export function req(type: string): RouteFn { +export function req(type: string) { return async (ctx: Context, next: NextFunc) => { if (ctx.req.bodyUsed) { return; diff --git a/middlewares/proxy.ts b/middlewares/proxy.ts index c467601..819a982 100644 --- a/middlewares/proxy.ts +++ b/middlewares/proxy.ts @@ -1,14 +1,14 @@ /* Created by: Henrique Emanoel Viana Githu: https://github.com/hviana -Page: https://sites.google.com/view/henriqueviana +Page: https://sites.google.com/site/henriqueemanoelviana cel: +55 (41) 99999-4664 */ -import { Context, NextFunc, RouteFn } from "../server.ts"; +import { Context, NextFunc } from "../server.ts"; interface ProxyOptions { - url: string | ((ctx: Context) => string | Promise); + url: string; replaceReqAndRes?: boolean; replaceProxyPath?: boolean; condition?: (context: Context) => Promise | boolean; @@ -23,18 +23,12 @@ const defaultProxyOptions: ProxyOptions = { export function proxy( options: ProxyOptions = defaultProxyOptions, -): RouteFn { +) { const mergedOptions = { ...defaultProxyOptions, ...options }; return async (ctx: Context, next: NextFunc) => { if (await mergedOptions.condition!(ctx)) { - let url = ""; - if (typeof mergedOptions.url === "function") { - url = await mergedOptions.url(ctx); - } else { - url = mergedOptions.url; - } ctx.extra.proxied = true; - const proxyURL = new URL(url); + const proxyURL = new URL(mergedOptions.url); if (mergedOptions.replaceProxyPath) { proxyURL.pathname = ctx.url.pathname; proxyURL.search = ctx.url.search; diff --git a/middlewares/rate_limit.ts b/middlewares/rate_limit.ts index 7d3ed67..763a8a0 100644 --- a/middlewares/rate_limit.ts +++ b/middlewares/rate_limit.ts @@ -1,11 +1,11 @@ /* Created by: Henrique Emanoel Viana Githu: https://github.com/hviana -Page: https://sites.google.com/view/henriqueviana +Page: https://sites.google.com/site/henriqueemanoelviana cel: +55 (41) 99999-4664 */ -import { Context, NextFunc, RouteFn, Server } from "../server.ts"; +import { Context, NextFunc } from "../server.ts"; interface RateLimitOptions { attempts?: number; @@ -18,7 +18,7 @@ const defaultRateLimitOptions: RateLimitOptions = { attempts: 30, interval: 10, maxTableSize: 100000, //be careful, table uses a lot of memory - id: (ctx: Context) => Server.getClientIp(ctx.req), + id: (ctx: Context) => JSON.stringify(ctx.conn.remoteAddr), }; function clearMap( @@ -37,7 +37,7 @@ function clearMap( } export function rateLimit( options: RateLimitOptions = defaultRateLimitOptions, -): RouteFn { +) { const mergedOptions = { ...defaultRateLimitOptions, ...options }; const { attempts, @@ -64,7 +64,7 @@ export function rateLimit( data.attempts++; } await next(); - } catch (e: any) { + } catch (e) { ctx.res.headers.set("Content-Type", "application/json"); ctx.res.status = 429; ctx.res.body = JSON.stringify({ diff --git a/middlewares/redirect.ts b/middlewares/redirect.ts index a8de69f..54c4bc8 100644 --- a/middlewares/redirect.ts +++ b/middlewares/redirect.ts @@ -1,14 +1,14 @@ /* Created by: Henrique Emanoel Viana Githu: https://github.com/hviana -Page: https://sites.google.com/view/henriqueviana +Page: https://sites.google.com/site/henriqueemanoelviana cel: +55 (41) 99999-4664 */ -import { Context, NextFunc, RouteFn } from "../server.ts"; -export function redirect(...params: any[]): RouteFn { +import { Context, NextFunc } from "../server.ts"; +export function redirect(url: string) { return async (ctx: Context, next: NextFunc) => { - ctx.redirect(...params); + ctx.redirect(url); await next(); }; } diff --git a/middlewares/serve_static.ts b/middlewares/serve_static.ts index f3fb149..3de1b6c 100644 --- a/middlewares/serve_static.ts +++ b/middlewares/serve_static.ts @@ -1,20 +1,20 @@ /* Created by: Henrique Emanoel Viana Githu: https://github.com/hviana -Page: https://sites.google.com/view/henriqueviana +Page: https://sites.google.com/site/henriqueemanoelviana cel: +55 (41) 99999-4664 */ -import { Context, NextFunc, RouteFn } from "../server.ts"; -import { join, toReadableStream } from "../deps.ts"; -export function serveStatic(root: string): RouteFn { +import { Context, NextFunc } from "../server.ts"; +import { join, readableStreamFromReader } from "../deps.ts"; +export function serveStatic(root: string) { return async (ctx: Context, next: NextFunc) => { try { const file = await Deno.open( - join(root, ctx.params.wild), + join(root, ctx.params[Object.keys(ctx.params)[0]]), { read: true }, ); - ctx.res.body = toReadableStream(file); + ctx.res.body = readableStreamFromReader(file); await next(); } catch (e) { ctx.res.status = 404; diff --git a/middlewares/session.ts b/middlewares/session.ts index 4474aca..23b7798 100644 --- a/middlewares/session.ts +++ b/middlewares/session.ts @@ -1,122 +1,105 @@ /* Created by: Henrique Emanoel Viana Githu: https://github.com/hviana -Page: https://sites.google.com/view/henriqueviana +Page: https://sites.google.com/site/henriqueemanoelviana cel: +55 (41) 99999-4664 */ -import { Context, NextFunc, RouteFn, Server } from "../server.ts"; -import { crypto, deleteCookie, getCookies, setCookie } from "../deps.ts"; +import { Context, NextFunc, Params, ProcessorFunc } from "../server.ts"; +import { + crypto, + deleteCookie, + getCookies, + setCookie, + storage, +} from "../deps.ts"; export type Session = { key: string; - value: any; - created?: number; + value: Params; + lastAccessTime: number; }; export class SessionStorageEngine { - slidingExpiration: number = 0; - absoluteExpiration: number = 0; - initialized: boolean = false; - constructor( - slidingExpiration: number = 0, - absoluteExpiration: number = 0, - ) { - if (absoluteExpiration > 0 && slidingExpiration > 0) { - if (absoluteExpiration < slidingExpiration) { - throw new Error( - "Absolute Expiration cannot be less than Sliding Expiration.", - ); - } - } - this.slidingExpiration = slidingExpiration; - this.absoluteExpiration = absoluteExpiration; + expiresInMinutes: number; + checkInterval: number = 0; //checkInterval may not be necessary depending on the engine + constructor(expiresInMinutes: number) { + this.expiresInMinutes = expiresInMinutes; } async init(): Promise { throw new Error(`"init" not implemented in: ${this.constructor.name}`); } + async delete(key: string): Promise { + throw new Error(`"delete" not implemented in: ${this.constructor.name}`); + } async set(session: Session): Promise { throw new Error(`"set" not implemented in: ${this.constructor.name}`); } async get(key: string): Promise { throw new Error(`"get" not implemented in: ${this.constructor.name}`); } + async getAll(): Promise { + throw new Error(`"getAll" not implemented in: ${this.constructor.name}`); + } } -export class KVStorageEngine extends SessionStorageEngine { - constructor( - slidingExpiration: number = 60, - absoluteExpiration: number = 0, - ) { - super(slidingExpiration, absoluteExpiration); +export class SQLiteStorageEngine extends SessionStorageEngine { + constructor(expiresInMinutes: number) { + super(expiresInMinutes); } - override async init() { - } - override async set(session: Session): Promise { - const key = ["faster_sessions", session.key]; - var newEntry = false; - if (session.created == undefined) { - session.created = Date.now(); - newEntry = true; - } else { - if (this.slidingExpiration > 0 && this.absoluteExpiration > 0) { - if ( - ((Date.now() - session.created) / 1000 / 60) >= - this.absoluteExpiration - ) { - await Server.kv.delete(key); + async init(): Promise { + const expiresInMS = this.expiresInMinutes * 60 * 1000; + clearInterval(this.checkInterval); + this.checkInterval = setInterval(async () => { + const sessions = await this.getAll(); + const currentTime = Date.now(); + for (const s of sessions) { + if ((currentTime - s.lastAccessTime) > expiresInMS) { + await this.delete(s.key); } } - } - if (this.slidingExpiration > 0) { - await Server.kv.set(key, session, { - expireIn: this.slidingExpiration * 1000 * 60, - }); - } else if (this.absoluteExpiration > 0 && newEntry) { - await Server.kv.set(key, session, { - expireIn: this.absoluteExpiration * 1000 * 60, - }); - } else { - await Server.kv.set(key, session); - } + }, expiresInMS); } - override async get(key: string): Promise { - const session: Session = (await Server.kv.get(["faster_sessions", key])) - .value as Session; - if (this.slidingExpiration > 0) { - if (session) { - await this.set(session); - } - } - return session; + async delete(key: string): Promise { + await storage.delete(`faster_sessions.${key}`); + } + async set(session: Session): Promise { + return await storage.set(`faster_sessions.${session.key}`, session); + } + async get(key: string): Promise { + return await storage.get(`faster_sessions.${key}`); + } + async getAll(): Promise { + return await storage.getList("faster_sessions."); } } export function session( - engine: SessionStorageEngine = new KVStorageEngine(60), -): RouteFn { + engine: SessionStorageEngine = new SQLiteStorageEngine(60), +) { + engine.init(); //no await, beware return async (ctx: Context, next: NextFunc) => { - if (!engine.initialized) { - await engine.init(); - } var key = getCookies(ctx.req.headers).faster_session_id; - ctx.extra.session = { value: {} }; + ctx.extra.session = {}; var hasSession = false; if (key) { const session_data = await engine.get(key); if (session_data) { hasSession = true; - ctx.extra.session = session_data; + ctx.extra.session = session_data.value; } } ctx.postProcessors.add(async (ctx: Context) => { - if ((Object.keys(ctx.extra.session.value).length > 0) || hasSession) { + if ((Object.keys(ctx.extra.session).length > 0) || hasSession) { if (!key) { key = crypto.randomUUID(); setCookie(ctx.res.headers, { name: "faster_session_id", value: key }); } - ctx.extra.session["key"] = key; - await engine.set(ctx.extra.session); + await engine.set({ + key: key, + value: ctx.extra.session, + lastAccessTime: Date.now(), + }); } else { if (key) { deleteCookie(ctx.res.headers, "faster_session_id"); diff --git a/middlewares/set_cors.ts b/middlewares/set_cors.ts index 149e535..b509445 100644 --- a/middlewares/set_cors.ts +++ b/middlewares/set_cors.ts @@ -1,14 +1,14 @@ /* Created by: Henrique Emanoel Viana Githu: https://github.com/hviana -Page: https://sites.google.com/view/henriqueviana +Page: https://sites.google.com/site/henriqueemanoelviana cel: +55 (41) 99999-4664 */ -import { Context, NextFunc, RouteFn } from "../server.ts"; +import { Context, NextFunc } from "../server.ts"; //server.options("/path",setCORS()); // necessary enable pre-fligh request on "/path" -export function setCORS(origin: string = "*"): RouteFn { +export function setCORS(origin: string = "*") { return async (ctx: Context, next: NextFunc) => { ctx.res.headers.set( "Access-Control-Expose-Headers", @@ -18,7 +18,7 @@ export function setCORS(origin: string = "*"): RouteFn { ctx.res.headers.set("Access-Control-Allow-Origin", origin); ctx.res.headers.set( "Access-Control-Allow-Methods", - "GET,PUT,POST,DELETE,PATCH,HEAD,OPTIONS", + "GET,PUT,POST,DELETE,PATCH,OPTIONS", ); ctx.res.headers.set("Access-Control-Allow-Headers", "*, Authorization"); await next(); diff --git a/middlewares/token.ts b/middlewares/token.ts index 1cabb5e..186d3df 100644 --- a/middlewares/token.ts +++ b/middlewares/token.ts @@ -1,31 +1,31 @@ /* Created by: Henrique Emanoel Viana Githu: https://github.com/hviana -Page: https://sites.google.com/view/henriqueviana +Page: https://sites.google.com/site/henriqueemanoelviana cel: +55 (41) 99999-4664 */ import { Context, NextFunc } from "../server.ts"; -import { jose } from "../deps.ts"; -const randomKey = await jose.generateSecret("HS256"); +import { generateSecret, jwtVerify, SignJWT } from "../deps.ts"; +const randomKey = await generateSecret("HS256"); export class Token { - static _configs: any = { + static #configs: any = { key: randomKey, issuer: "urn:faster:issuer", audience: "urn:faster:audience", alg: "HS256", oneHour: "1h", }; - static setConfigs(configs: any): void { - Token._configs = { ...Token._configs, ...configs }; + static setConfigs(configs: any) { + Token.#configs = { ...Token.#configs, ...configs }; } - static setSecret(secret: string): void { - Token._configs.key = (new TextEncoder()).encode(secret); + static setSecret(secret: string) { + Token.#configs.key = (new TextEncoder()).encode(secret); } - static async getPayload(token: string): Promise { - const { payload, protectedHeader } = await jose.jwtVerify( + static async getPayload(token: string) { + const { payload, protectedHeader } = await jwtVerify( token, - Token._configs.key, + Token.#configs.key, ); return payload; } @@ -39,7 +39,7 @@ export class Token { ctx.extra.tokenPayload = await Token.getPayload(token); ctx.extra.token = token; await next(); - } catch (e: any) { + } catch (e) { ctx.res.headers.set("Content-Type", "application/json"); ctx.res.status = 403; ctx.res.body = JSON.stringify({ @@ -47,18 +47,15 @@ export class Token { }); } } - static async generate( - data: any = {}, - exp = Token._configs.oneHour, - ): Promise { - const jwt = await new jose.SignJWT(data) - .setProtectedHeader({ alg: Token._configs.alg }) + static async generate(data: any = {}, exp = Token.#configs.oneHour) { + const jwt = await new SignJWT(data) + .setProtectedHeader({ alg: Token.#configs.alg }) .setIssuedAt() - .setIssuer(Token._configs.issuer) - .setAudience(Token._configs.audience); + .setIssuer(Token.#configs.issuer) + .setAudience(Token.#configs.audience); if (exp) { jwt.setExpirationTime(exp); } - return jwt.sign(Token._configs.key); + return jwt.sign(Token.#configs.key); } } diff --git a/middlewares/upload.ts b/middlewares/upload.ts index 31d6b8e..90cbcc5 100644 --- a/middlewares/upload.ts +++ b/middlewares/upload.ts @@ -1,160 +1,208 @@ /* Created by: Henrique Emanoel Viana Githu: https://github.com/hviana -Page: https://sites.google.com/view/henriqueviana +Page: https://sites.google.com/site/henriqueemanoelviana cel: +55 (41) 99999-4664 */ -import { Context, NextFunc, RouteFn, Server } from "../server.ts"; - +import { + crypto, + ensureDir, + ensureDirSync, + join, + move, + MultipartReader, + readerFromStreamReader, + SEP, +} from "../deps.ts"; +import { Context, NextFunc } from "../server.ts"; interface UploadOptions { - allowedExtensions?: (ctx: Context) => Promise> | Array; - maxSizeBytes?: (ctx: Context) => Promise | number; - maxFileSizeBytes?: (ctx: Context) => Promise | number; - chunksPerSecond?: (ctx: Context) => Promise | number; - maxClientIdConcurrentReqs?: (ctx: Context) => Promise | number; - clientId?: ( - ctx: Context, - ) => Promise | string | number | undefined; - validateAccess?: (ctx: Context, path: string[]) => Promise | boolean; -} -interface DownloadOptions { - chunksPerSecond?: (ctx: Context) => Promise | number; - clientId?: ( - ctx: Context, - ) => Promise | string | number | undefined; - validateAccess?: (ctx: Context, path: string[]) => Promise | boolean; - maxClientIdConcurrentReqs?: (ctx: Context) => Promise | number; - maxDirEntriesPerSecond?: (ctx: Context) => Promise | number; - pagination?: (ctx: Context) => Promise | boolean; - cursor?: (ctx: Context) => Promise | string | undefined; //for readDir, If there is a next page. + path?: string; + extensions?: Array; + maxSizeBytes?: number; + maxFileSizeBytes?: number; + saveFile?: boolean; + readFile?: boolean; + useCurrentDir?: boolean; } + const defaultUploadOptions: UploadOptions = { - allowedExtensions: (ctx: Context) => [], - maxSizeBytes: (ctx: Context) => Number.MAX_SAFE_INTEGER, - maxFileSizeBytes: (ctx: Context) => Number.MAX_SAFE_INTEGER, - chunksPerSecond: (ctx: Context) => Number.MAX_SAFE_INTEGER, - maxClientIdConcurrentReqs: (ctx: Context) => Number.MAX_SAFE_INTEGER, - clientId: (ctx: Context) => undefined, - validateAccess: (ctx: Context, path: string[]) => true, -}; -const defaultDownloadOptions: DownloadOptions = { - chunksPerSecond: (ctx: Context) => Number.MAX_SAFE_INTEGER, - clientId: (ctx: Context) => undefined, - validateAccess: (ctx: Context, path: string[]) => true, - maxClientIdConcurrentReqs: (ctx: Context) => Number.MAX_SAFE_INTEGER, - maxDirEntriesPerSecond: (ctx: Context) => Number.MAX_SAFE_INTEGER, - pagination: (ctx: Context) => false, - cursor: (ctx: Context) => undefined, + path: "uploads", + extensions: [], + maxSizeBytes: Number.MAX_SAFE_INTEGER, + maxFileSizeBytes: Number.MAX_SAFE_INTEGER, + saveFile: true, + readFile: false, + useCurrentDir: true, }; -function upload( +const enc = new TextEncoder(); + +export function upload( options: UploadOptions = defaultUploadOptions, -): RouteFn { +) { const mergedOptions = { ...defaultUploadOptions, ...options }; const { - allowedExtensions, + path, + extensions, maxSizeBytes, maxFileSizeBytes, - chunksPerSecond, - maxClientIdConcurrentReqs, - clientId, - validateAccess, + saveFile, + readFile, + useCurrentDir, } = mergedOptions; + ensureDirSync(join(Deno.cwd(), "temp_uploads")); return async (ctx: Context, next: NextFunc) => { - const reqBody = await ctx.req.formData(); - const existingFileNamesInTheUpload: { [key: string]: number } = {}; - const res: any = {}; if ( - parseInt(ctx.req.headers.get("content-length")!) > - await maxSizeBytes!(ctx) + parseInt(ctx.req.headers.get("content-length")!) > maxSizeBytes! ) { throw new Error( `Maximum total upload size exceeded, size: ${ ctx.req.headers.get("content-length") - } bytes, maximum: ${await maxSizeBytes!(ctx)} bytes. `, + } bytes, maximum: ${maxSizeBytes} bytes. `, ); } - for (const item of reqBody.entries()) { - if (item[1] instanceof File) { - const formField: any = item[0]; - const fileData: any = item[1]; - if (!existingFileNamesInTheUpload[fileData.name]) { - existingFileNamesInTheUpload[fileData.name] = 1; - } else { - existingFileNamesInTheUpload[fileData.name]++; - } - let prepend = ""; - if (existingFileNamesInTheUpload[fileData.name] > 1) { - prepend += existingFileNamesInTheUpload[fileData.name].toString(); - } - var sep = ""; - if (!ctx.params.wild.endsWith("/")) { - sep = "/"; + const boundaryRegex = /^multipart\/form-data;\sboundary=(?.*)$/; + let match: RegExpMatchArray | null; + if ( + ctx.req.headers.get("content-type") && + (match = ctx.req.headers.get("content-type")!.match( + boundaryRegex, + )) + ) { + const formBoundary: string = match.groups!.boundary; + const mr = new MultipartReader( + readerFromStreamReader(ctx.req.body!.getReader()), + formBoundary, + ); + const form = await mr.readForm(0); + const res: any = {}; + const entries: any = Array.from(form.entries()); + let validations = ""; + for (const item of entries) { + const values: any = [].concat(item[1]); + for (const val of values) { + if (val.filename !== undefined) { + if (extensions!.length > 0) { + const ext = val.filename.split(".").pop(); + if (!extensions!.includes(ext)) { + validations += + `The file extension is not allowed (${ext} in ${val.filename}), allowed extensions: ${extensions}. `; + } + } + if (val.size > maxFileSizeBytes!) { + validations += + `Maximum file upload size exceeded, file: ${val.filename}, size: ${val.size} bytes, maximum: ${maxFileSizeBytes} bytes. `; + } + } } - const path = Server.kvFs!.URIComponentToPath( - ctx.params.wild + sep + prepend + fileData.name, - ); - let resData = await Server.kvFs!.save({ - path: path, - content: fileData.stream(), - allowedExtensions: await allowedExtensions!(ctx), - maxFileSizeBytes: await maxFileSizeBytes!(ctx), - chunksPerSecond: await chunksPerSecond!(ctx), - maxClientIdConcurrentReqs: await maxClientIdConcurrentReqs!(ctx), - clientId: await clientId!(ctx), - validateAccess: async (path: string[]) => - await validateAccess!(ctx, path), - }); - if (res[formField] !== undefined) { - if (Array.isArray(res[formField])) { - res[formField].push(resData); - } else { - res[formField] = [res[formField], resData]; + } + if (validations != "") { + await form.removeAll(); + throw new Error(validations); + } + for (const item of entries) { + const formField: any = item[0]; + const filesData: any = [].concat(item[1]); + for (const fileData of filesData) { + if (fileData.tempfile !== undefined) { + const resData = fileData; + if (readFile) { + resData["data"] = await Deno.readFile(resData["tempfile"]); + } + if (saveFile) { + let uploadPath = path; + const d = new Date(); + const uuid = join( + d.getFullYear().toString(), + (d.getMonth() + 1).toString(), + d.getDate().toString(), + d.getHours().toString(), + d.getMinutes().toString(), + d.getSeconds().toString(), + crypto.randomUUID(), + ); + uploadPath = join(path!, uuid); + let fullPath = uploadPath; + if (useCurrentDir) { + fullPath = join(Deno.cwd(), fullPath!); + } + await ensureDir(fullPath!); + await move( + fileData.tempfile, + join(fullPath!, fileData.filename), + ); + delete resData["tempfile"]; + resData["id"] = uuid.replace(/\\/g, "/"); + resData["url"] = encodeURI( + join(uploadPath!, fileData.filename).replace(/\\/g, "/"), + ); + resData["uri"] = join(fullPath!, fileData.filename); + } else { + const tempFileName = resData.tempfile.split(SEP).pop(); + const pathTempFile = join( + Deno.cwd(), + "temp_uploads", + tempFileName, + ); + await move( + resData.tempfile, + pathTempFile, + ); + resData.tempfile = pathTempFile; + } + if (res[formField] !== undefined) { + if (Array.isArray(res[formField])) { + res[formField].push(resData); + } else { + res[formField] = [res[formField], resData]; + } + } else { + res[formField] = resData; + } } - } else { - res[formField] = resData; } } + ctx.extra.uploadedFiles = res; + } else { + throw new Error( + 'Invalid upload data, request must contains a body with form "multipart/form-data", and inputs with type="file". ', + ); } - ctx.extra.uploadedFiles = res; await next(); }; } - -function download( - options: DownloadOptions = defaultDownloadOptions, -): RouteFn { - const mergedOptions = { ...defaultDownloadOptions, ...options }; - const { - chunksPerSecond, - clientId, - validateAccess, - maxClientIdConcurrentReqs, - maxDirEntriesPerSecond, - pagination, - cursor, - } = mergedOptions; +export function preUploadValidate( + extensions: Array = [], + maxSizeBytes: number = Number.MAX_SAFE_INTEGER, + maxFileSizeBytes: number = Number.MAX_SAFE_INTEGER, +) { return async (ctx: Context, next: NextFunc) => { - const path = Server.kvFs!.URIComponentToPath(ctx.params.wild); - const file = await Server.kvFs!.read({ - path: path, - chunksPerSecond: await chunksPerSecond!(ctx), - clientId: await clientId!(ctx), - validateAccess: async (path: string[]) => - await validateAccess!(ctx, path), - maxClientIdConcurrentReqs: await maxClientIdConcurrentReqs!(ctx), - maxDirEntriesPerSecond: await maxDirEntriesPerSecond!(ctx), - pagination: await pagination!(ctx), - cursor: await cursor!(ctx), - }); - if (file) { - ctx.res.body = (file as any).content; - await next(); - } else { - ctx.res.status = 404; + const jsonData = (await ctx.req.json())["value"]; + let totalBytes = 0; + let validations = ""; + for (const iName in jsonData) { + const files: any = [].concat(jsonData[iName]); + for (const file of files) { + totalBytes += jsonData[iName].size; + if (file.size > maxFileSizeBytes) { + validations += + `Maximum file upload size exceeded, file: ${file.name}, size: ${file.size} bytes, maximum: ${maxFileSizeBytes} bytes. `; + } + if (!extensions.includes(file.name.split(".").pop())) { + validations += `The file extension is not allowed (${ + file.name.split(".").pop() + } in ${file.name}), allowed extensions: ${extensions}. `; + } + } + } + if (totalBytes > maxSizeBytes) { + validations += + `Maximum total upload size exceeded, size: ${totalBytes} bytes, maximum: ${maxSizeBytes} bytes. `; } + if (validations != "") { + throw new Error(validations); + } + await next(); }; } - -export { download, upload }; diff --git a/mod.ts b/mod.ts index 39f3e61..e5592e6 100644 --- a/mod.ts +++ b/mod.ts @@ -1,4 +1,4 @@ -export { FasterLog, logger } from "./middlewares/logger.ts"; +export { logger } from "./middlewares/logger.ts"; export { proxy } from "./middlewares/proxy.ts"; export { req, res } from "./middlewares/parser.ts"; export { rateLimit } from "./middlewares/rate_limit.ts"; @@ -6,11 +6,11 @@ export { redirect } from "./middlewares/redirect.ts"; export { serveStatic } from "./middlewares/serve_static.ts"; export { setCORS } from "./middlewares/set_cors.ts"; export { Token } from "./middlewares/token.ts"; -export { download, upload } from "./middlewares/upload.ts"; +export { preUploadValidate, upload } from "./middlewares/upload.ts"; export { - KVStorageEngine, session, SessionStorageEngine, + SQLiteStorageEngine, } from "./middlewares/session.ts"; export type { Session } from "./middlewares/session.ts"; @@ -27,13 +27,4 @@ export type { export type { Cookie } from "./deps.ts"; -export { - DenoKvFs, - type DirList, - type File, - type FileStatus, - type ReadOptions, - type SaveOptions, -} from "./deps.ts"; - -export { deleteCookie, getCookies, getSetCookies, setCookie } from "./deps.ts"; +export { deleteCookie, getCookies, setCookie } from "./deps.ts"; diff --git a/server.ts b/server.ts index ffbb2f0..1276bc5 100644 --- a/server.ts +++ b/server.ts @@ -1,10 +1,10 @@ /* Created by: Henrique Emanoel Viana Githu: https://github.com/hviana -Page: https://sites.google.com/view/henriqueviana +Page: https://sites.google.com/site/henriqueemanoelviana cel: +55 (41) 99999-4664 */ -import { DenoKvFs } from "./deps.ts"; + export type Params = { [key: string]: string; }; @@ -19,6 +19,9 @@ export type ContextResponse = { }; export class Context { + #conn: Deno.Conn; + #httpConn: Deno.HttpConn; + #requestEvent: Deno.RequestEvent; #params: Params; #url: URL; req: Request; @@ -27,11 +30,17 @@ export class Context { error: Error | undefined = undefined; #postProcessors: Set = new Set(); constructor( + conn: Deno.Conn, + httpConn: Deno.HttpConn, + requestEvent: Deno.RequestEvent, params: Params, url: URL, req: Request, hasRoute: boolean, ) { + this.#conn = conn; + this.#httpConn = httpConn; + this.#requestEvent = requestEvent; this.#params = params; this.#url = url; this.req = req; @@ -43,25 +52,31 @@ export class Context { status: 200, statusText: "", }; - get params(): Params { + get conn() { + return this.#conn; + } + get httpConn() { + return this.#httpConn; + } + get requestEvent() { + return this.#requestEvent; + } + get params() { return this.#params; } - get url(): URL { + get url() { return this.#url; } - get extra(): any { + get extra() { return this.#extra; } - get postProcessors(): Set { + get postProcessors() { return this.#postProcessors; } - redirect(...params: any[]): void { + redirect(url: string) { this.postProcessors.add((ctx: Context) => { - if (params.length < 2) { - params.unshift(302); - } - ctx.res.headers.set("Location", params[1]); - ctx.res.status = params[0]; + ctx.res.headers.set("Location", url); + ctx.res.status = 301; }); } } @@ -74,7 +89,7 @@ export function parse( loose?: boolean, ): { keys: string[]; pattern: RegExp } { if (str instanceof RegExp) return { keys: [], pattern: str }; - let c: string, + var c: string, o: number, tmp: string | undefined, ext: number, @@ -86,13 +101,11 @@ export function parse( while (tmp = arr.shift()) { c = tmp[0]; if (c === "*") { - //@ts-ignore keys.push("wild"); pattern += "/(.*)"; } else if (c === ":") { o = tmp.indexOf("?", 1); ext = tmp.indexOf(".", 1); - //@ts-ignore keys.push(tmp.substring(1, !!~o ? o : !!~ext ? ext : tmp.length)); pattern += !!~o && !~ext ? "(?:/([^/]+?))?" : "/([^/]+?)"; if (!!~ext) pattern += (!!~o ? "?" : "") + "\\" + tmp.substring(ext); @@ -133,82 +146,22 @@ export type Route = { handlers: RouteFn[]; }; export class Server { - static kv: Deno.Kv; - static kvFs: DenoKvFs; - static setKv(kv: Deno.Kv) { - Server.kv = kv; - Server.kvFs = new DenoKvFs(Server.kv); - } - static getClientIp( - req: Request, - info: { remoteAddr: Deno.Addr | Record } = { remoteAddr: {} }, - ): string { - const fwd = req.headers.get("forwarded"); - if (fwd) { - const m = fwd.match(/for=(?:(?:"([^"]+)")|\[([^\]]+)\]|([^;,\s]+))/i); - const ip = m?.[1] || m?.[2] || m?.[3]; - if (ip) return ip; - } - const xff = req.headers.get("x-forwarded-for"); - if (xff) { - const ip = xff.split(",")[0].trim(); - if (ip) return ip; - } - const xrip = req.headers.get("x-real-ip"); - if (xrip) return xrip; - - const ra = info.remoteAddr as Deno.NetAddr; - return "hostname" in ra ? ra.hostname : "unknown"; - } - #ac = new AbortController(); // NOTE: This is transpiled into the constructor, therefore equivalent to this.routes = []; #routes: Route[] = []; - //@ts-ignore - server: Deno.HttpServer; - openedSockets: Map = new Map(); // NOTE: Using .bind can significantly increase perf compared to arrow functions. - public all: Function = this.#add.bind(this, "ALL"); - public get: Function = this.#add.bind(this, "GET"); - public head: Function = this.#add.bind(this, "HEAD"); - public patch: Function = this.#add.bind(this, "PATCH"); - public options: Function = this.#add.bind(this, "OPTIONS"); - public connect: Function = this.#add.bind(this, "CONNECT"); - public delete: Function = this.#add.bind(this, "DELETE"); - public trace: Function = this.#add.bind(this, "TRACE"); - public post: Function = this.#add.bind(this, "POST"); - public put: Function = this.#add.bind(this, "PUT"); + public all = this.#add.bind(this, "ALL"); + public get = this.#add.bind(this, "GET"); + public head = this.#add.bind(this, "HEAD"); + public patch = this.#add.bind(this, "PATCH"); + public options = this.#add.bind(this, "OPTIONS"); + public connect = this.#add.bind(this, "CONNECT"); + public delete = this.#add.bind(this, "DELETE"); + public trace = this.#add.bind(this, "TRACE"); + public post = this.#add.bind(this, "POST"); + public put = this.#add.bind(this, "PUT"); - public resetRoutes() { - this.#routes = []; - } - - acceptOrRejectSocketConn = async (ctx: Context): Promise => { - return undefined; - }; - onSocketMessage = async ( - id: string, - socket: WebSocket, - event: any, - ): Promise => { - }; - onSocketClosed = async (id: string, socket: WebSocket): Promise => { - }; - onSocketOpen = async (id: string, socket: WebSocket): Promise => { - }; - onSocketError = async (id: string, socket: WebSocket): Promise => { - }; - - public useAtBeginning(...handlers: RouteFn[]): Server { - this.#routes.unshift({ - keys: [], - method: "ALL", - handlers, - }); - return this; - } - - public use(...handlers: RouteFn[]): Server { + public use(...handlers: RouteFn[]) { this.#routes.push({ keys: [], method: "ALL", @@ -242,155 +195,103 @@ export class Server { ctx, async () => await this.#middlewareHandler(fns, fnIndex + 1, ctx), ); - } catch (e: any) { + } catch (e) { ctx.error = e; } } } - async serveHandler( - request: Request, - ): Promise { + async #handleRequest(conn: Deno.Conn) { try { - const req = request; - const url = new URL(request.url); - const requestHandlers: RouteFn[] = []; - const params: Params = {}; - const len = this.#routes.length; - let hasRoute = false; - for (let i = 0; i < len; i++) { - const r = this.#routes[i]; - const keyLength = r.keys.length; - let matches: null | string[] = null; - if ( - r.pattern === undefined || - (req.method === r.method && - (matches = r.pattern.exec( - url.pathname, - ))) - ) { - if (r.pattern) { - hasRoute = true; - if (keyLength > 0) { - if (matches) { - let inc = 0; - while (inc < keyLength) { - const prevInc = inc; - const m = matches[++inc]; - if (m != undefined) { - params[r.keys[prevInc]] = decodeURIComponent(m); + const httpConn = Deno.serveHttp(conn); + for await (const requestEvent of httpConn) { + const req = requestEvent.request; + const url = new URL(requestEvent.request.url); + const requestHandlers: RouteFn[] = []; + const params: Params = {}; + const len = this.#routes.length; + var hasRoute = false; + for (var i = 0; i < len; i++) { + const r = this.#routes[i]; + const keyLength = r.keys.length; + var matches: null | string[] = null; + if ( + r.pattern === undefined || + (req.method === r.method && + (matches = r.pattern.exec( + url.pathname, + ))) + ) { + if (r.pattern) { + hasRoute = true; + if (keyLength > 0) { + if (matches) { + var inc = 0; + while (inc < keyLength) { + params[r.keys[inc]] = decodeURIComponent(matches[++inc]); } } } } + requestHandlers.push(...r.handlers); } - requestHandlers.push(...r.handlers); } - } - const ctx = new Context( - params, - url, - req, - hasRoute, - ); - if (req.headers.get("upgrade") == "websocket") { - const connectId = await this.acceptOrRejectSocketConn(ctx); - if (!connectId) { - ctx.res.status = 500; - return new Response(ctx.res.body, { - status: ctx.res.status, - }); - } - const { socket, response } = Deno.upgradeWebSocket(req); - const existingSocket = this.openedSockets.get(connectId); - if (existingSocket) { - existingSocket.close(); - } - this.openedSockets.set(connectId, socket); - socket.onmessage = async (event) => { - try { - await this.onSocketMessage(connectId, socket, event); - } catch (e) { - console.log(e); - } - }; - socket.onclose = async () => { - try { - await this.onSocketClosed(connectId, socket); - } catch (e) { - console.log(e); - } - this.openedSockets.delete(connectId); - }; - socket.onerror = async () => { - try { - await this.onSocketError(connectId, socket); - } catch (e) { - console.log(e); - } - this.openedSockets.delete(connectId); - }; - socket.onopen = async () => { + var ctx = new Context( + conn, + httpConn, + requestEvent, + params, + url, + req, + hasRoute, + ); + await this.#middlewareHandler(requestHandlers, 0, ctx); + if (!ctx.error) { try { - await this.onSocketOpen(connectId, socket); + for (const p of ctx.postProcessors) { + await p(ctx); + } } catch (e) { - console.log(e); + ctx.error = e; } - this.openedSockets.delete(connectId); - }; - return response; - } - await this.#middlewareHandler(requestHandlers, 0, ctx); - if (!ctx.error) { - try { - for (const p of ctx.postProcessors) { - await p(ctx); - } - } catch (e: any) { - ctx.error = e; } - } - if (ctx.res instanceof Response) { if (ctx.error) { - console.log(ctx.error); + ctx.res.status = 500; + ctx.res.headers.set( + "Content-Type", + "application/json", + ); + ctx.res.body = JSON.stringify({ + msg: (ctx.error.message || ctx.error), + stack: ctx.error.stack, + }); } - return ctx.res; - } - if (ctx.error) { - ctx.res.status = 500; - ctx.res.headers.set( - "Content-Type", - "application/json", + await requestEvent.respondWith( + new Response(ctx.res.body, { + headers: ctx.res.headers, + status: ctx.res.status, + statusText: ctx.res.statusText, + }), ); - ctx.res.body = JSON.stringify({ - msg: (ctx.error.message || JSON.stringify(ctx.error)), - stack: ctx.error.stack, - }); } - return new Response(ctx.res.body, { - headers: ctx.res.headers, - status: ctx.res.status, - statusText: ctx.res.statusText, - }); } catch (e) { console.log(e); - return new Response(); } } - public async listen(options: any) { //save as Deno.Serve options - if (this.server) { - this.#ac.abort(); - await this.server.finished; + + public async listen(serverParams: any) { + const server = (serverParams.certFile || serverParams.port === 443) + ? Deno.listenTls(serverParams) + : Deno.listen(serverParams); + try { + for await (const conn of server) { + this.#handleRequest(conn); + } + } catch (e) { + console.log(e); + if (e.name === "NotConnected") { + await this.listen(serverParams); + } } - this.#ac = new AbortController(); - //@ts-ignore - this.server = Deno.serve( - { ...options, signal: this.#ac.signal }, - (request: Request, info: Deno.ServeHandlerInfo) => - this.serveHandler(request), - ); } - fetch = async (_req: Request): Promise => { - return await this.serveHandler(_req); - }; } diff --git a/vendor/deno_kv_fs/index.ts b/vendor/deno_kv_fs/index.ts deleted file mode 100644 index 0c28427..0000000 --- a/vendor/deno_kv_fs/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { - DenoKvFs, - type DirList, - type File, - type FileStatus, - type ReadOptions, - type SaveOptions, -} from "../../deps.ts"; diff --git a/vendor/jose/index.ts b/vendor/jose/index.ts deleted file mode 100644 index 32d31c9..0000000 --- a/vendor/jose/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { jose } from "../../deps.ts";