@thi.ng/server 0.9.1 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -1
- package/README.md +2 -2
- package/api.d.ts +52 -0
- package/package.json +20 -20
- package/server.js +2 -0
- package/interceptors/static.d.ts +0 -96
- package/interceptors/static.js +0 -73
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
- **Last updated**: 2025-04-
|
|
3
|
+
- **Last updated**: 2025-04-30T12:52:32Z
|
|
4
4
|
- **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
|
|
5
5
|
|
|
6
6
|
All notable changes to this project will be documented in this file.
|
|
@@ -11,6 +11,15 @@ See [Conventional Commits](https://conventionalcommits.org/) for commit guidelin
|
|
|
11
11
|
**Note:** Unlisted _patch_ versions only involve non-code or otherwise excluded changes
|
|
12
12
|
and/or version bumps of transitive dependencies.
|
|
13
13
|
|
|
14
|
+
## [0.10.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/server@0.10.0) (2025-04-16)
|
|
15
|
+
|
|
16
|
+
#### 🚀 Features
|
|
17
|
+
|
|
18
|
+
- add `#` in URL safety check, update `RequestCtx` ([f0a7025](https://github.com/thi-ng/umbrella/commit/f0a7025))
|
|
19
|
+
- respond with HTTP 400 if request URL contains `#`
|
|
20
|
+
- this stops maliciously contructed requests via HTTP `request-target`
|
|
21
|
+
- add docs
|
|
22
|
+
|
|
14
23
|
## [0.9.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/server@0.9.0) (2025-03-22)
|
|
15
24
|
|
|
16
25
|
#### 🚀 Features
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://mastodon.thi.ng/@toxi)
|
|
8
8
|
|
|
9
9
|
> [!NOTE]
|
|
10
|
-
> This is one of
|
|
10
|
+
> This is one of 206 standalone projects, maintained as part
|
|
11
11
|
> of the [@thi.ng/umbrella](https://github.com/thi-ng/umbrella/) monorepo
|
|
12
12
|
> and anti-framework.
|
|
13
13
|
>
|
|
@@ -164,7 +164,7 @@ For Node.js REPL:
|
|
|
164
164
|
const ser = await import("@thi.ng/server");
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
-
Package sizes (brotli'd, pre-treeshake): ESM: 6.
|
|
167
|
+
Package sizes (brotli'd, pre-treeshake): ESM: 6.27 KB
|
|
168
168
|
|
|
169
169
|
## Dependencies
|
|
170
170
|
|
package/api.d.ts
CHANGED
|
@@ -85,17 +85,69 @@ export interface CompiledServerRoute<CTX extends RequestCtx = RequestCtx> extend
|
|
|
85
85
|
handlers: Partial<Record<Method, CompiledHandler<CTX>>>;
|
|
86
86
|
}
|
|
87
87
|
export interface RequestCtx {
|
|
88
|
+
/**
|
|
89
|
+
* Server instance.
|
|
90
|
+
*/
|
|
88
91
|
server: Server;
|
|
92
|
+
/**
|
|
93
|
+
* Logger instance.
|
|
94
|
+
*/
|
|
89
95
|
logger: ILogger;
|
|
96
|
+
/**
|
|
97
|
+
* Parsed request URL.
|
|
98
|
+
*
|
|
99
|
+
* @remarks
|
|
100
|
+
* Route handlers SHOULD only use this url instead of
|
|
101
|
+
* {@link RequestCtx.req}'s `.url` property. This ensures, that the
|
|
102
|
+
* hash-fragment (`#...`) part is properly dealt with, avoiding security
|
|
103
|
+
* issues related to HTTP `request-target`
|
|
104
|
+
* (https://datatracker.ietf.org/doc/html/rfc9112#section-3.2).
|
|
105
|
+
*
|
|
106
|
+
* Note: The server already responds to any request containing a `#` in its
|
|
107
|
+
* URL with a HTTP 400.
|
|
108
|
+
*
|
|
109
|
+
* Also see {@link RequestCtx.path} and {@link RequestCtx.query}.
|
|
110
|
+
*/
|
|
111
|
+
url: URL;
|
|
112
|
+
/**
|
|
113
|
+
* HTTP request instance.
|
|
114
|
+
*/
|
|
90
115
|
req: IncomingMessage;
|
|
116
|
+
/**
|
|
117
|
+
* HTTP server response instance.
|
|
118
|
+
*/
|
|
91
119
|
res: ServerResponse;
|
|
120
|
+
/**
|
|
121
|
+
* Pre-compiled route handler and its interceptors
|
|
122
|
+
*/
|
|
92
123
|
route: CompiledServerRoute;
|
|
124
|
+
/**
|
|
125
|
+
* Parsed route details.
|
|
126
|
+
*/
|
|
93
127
|
match: RouteMatch;
|
|
128
|
+
/**
|
|
129
|
+
* Request method (possibly adapted). Also see {@link RequestCtx.origMethod}.
|
|
130
|
+
*/
|
|
94
131
|
method: Method;
|
|
132
|
+
/**
|
|
133
|
+
* Original request method. Also see {@link RequestCtx.method}.
|
|
134
|
+
*/
|
|
95
135
|
origMethod: Method;
|
|
136
|
+
/**
|
|
137
|
+
* URI-decoded path part of request URL.
|
|
138
|
+
*/
|
|
96
139
|
path: string;
|
|
140
|
+
/**
|
|
141
|
+
* Parsed query string params (aka URL search params).
|
|
142
|
+
*/
|
|
97
143
|
query: Record<string, any>;
|
|
144
|
+
/**
|
|
145
|
+
* Parsed cookies, if any.
|
|
146
|
+
*/
|
|
98
147
|
cookies?: Record<string, string>;
|
|
148
|
+
/**
|
|
149
|
+
* Server session (only if {@link SessionInterceptor} is used).
|
|
150
|
+
*/
|
|
99
151
|
session?: ServerSession;
|
|
100
152
|
}
|
|
101
153
|
export type HandlerResult = MaybePromise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thi.ng/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Minimal HTTP server with declarative routing, static file serving and freely extensible via pre/post interceptors",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -39,26 +39,26 @@
|
|
|
39
39
|
"tool:tangle": "../../node_modules/.bin/tangle src/**/*.ts"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@thi.ng/api": "^8.11.
|
|
43
|
-
"@thi.ng/arrays": "^2.
|
|
44
|
-
"@thi.ng/cache": "^2.3.
|
|
45
|
-
"@thi.ng/checks": "^3.7.
|
|
46
|
-
"@thi.ng/errors": "^2.5.
|
|
47
|
-
"@thi.ng/file-io": "^2.1.
|
|
48
|
-
"@thi.ng/leaky-bucket": "^0.2.
|
|
49
|
-
"@thi.ng/logger": "^3.1.
|
|
50
|
-
"@thi.ng/mime": "^2.7.
|
|
51
|
-
"@thi.ng/paths": "^5.2.
|
|
52
|
-
"@thi.ng/router": "^4.1.
|
|
53
|
-
"@thi.ng/strings": "^3.9.
|
|
54
|
-
"@thi.ng/timestamp": "^1.1.
|
|
55
|
-
"@thi.ng/uuid": "^1.1.
|
|
42
|
+
"@thi.ng/api": "^8.11.27",
|
|
43
|
+
"@thi.ng/arrays": "^2.11.0",
|
|
44
|
+
"@thi.ng/cache": "^2.3.32",
|
|
45
|
+
"@thi.ng/checks": "^3.7.7",
|
|
46
|
+
"@thi.ng/errors": "^2.5.33",
|
|
47
|
+
"@thi.ng/file-io": "^2.1.36",
|
|
48
|
+
"@thi.ng/leaky-bucket": "^0.2.5",
|
|
49
|
+
"@thi.ng/logger": "^3.1.8",
|
|
50
|
+
"@thi.ng/mime": "^2.7.9",
|
|
51
|
+
"@thi.ng/paths": "^5.2.10",
|
|
52
|
+
"@thi.ng/router": "^4.1.27",
|
|
53
|
+
"@thi.ng/strings": "^3.9.12",
|
|
54
|
+
"@thi.ng/timestamp": "^1.1.12",
|
|
55
|
+
"@thi.ng/uuid": "^1.1.24"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@types/node": "^22.
|
|
59
|
-
"esbuild": "^0.25.
|
|
60
|
-
"typedoc": "^0.28.
|
|
61
|
-
"typescript": "^5.8.
|
|
58
|
+
"@types/node": "^22.15.3",
|
|
59
|
+
"esbuild": "^0.25.3",
|
|
60
|
+
"typedoc": "^0.28.3",
|
|
61
|
+
"typescript": "^5.8.3"
|
|
62
62
|
},
|
|
63
63
|
"keywords": [
|
|
64
64
|
"cookie",
|
|
@@ -167,5 +167,5 @@
|
|
|
167
167
|
"status": "alpha",
|
|
168
168
|
"year": 2024
|
|
169
169
|
},
|
|
170
|
-
"gitHead": "
|
|
170
|
+
"gitHead": "4354686a6fb1f82c09ea48f92f87786191b231a0\n"
|
|
171
171
|
}
|
package/server.js
CHANGED
|
@@ -90,6 +90,7 @@ class Server {
|
|
|
90
90
|
}
|
|
91
91
|
async listener(req, res) {
|
|
92
92
|
try {
|
|
93
|
+
if (req.url.includes("#")) return res.badRequest();
|
|
93
94
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
94
95
|
if (this.opts.host && !isMatchingHost(url.hostname, this.host)) {
|
|
95
96
|
this.logger.debug(
|
|
@@ -123,6 +124,7 @@ class Server {
|
|
|
123
124
|
// @ts-ignore
|
|
124
125
|
server: this,
|
|
125
126
|
logger: this.logger,
|
|
127
|
+
url,
|
|
126
128
|
req,
|
|
127
129
|
res,
|
|
128
130
|
path,
|
package/interceptors/static.d.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import type { Fn, MaybePromise, Predicate } from "@thi.ng/api";
|
|
2
|
-
import { type HashAlgo } from "@thi.ng/file-io";
|
|
3
|
-
import type { OutgoingHttpHeaders } from "node:http";
|
|
4
|
-
import type { Interceptor, RequestCtx, ServerRoute } from "./api.js";
|
|
5
|
-
/**
|
|
6
|
-
* Static file configuration options.
|
|
7
|
-
*/
|
|
8
|
-
export interface StaticOpts<CTX extends RequestCtx = RequestCtx> {
|
|
9
|
-
/**
|
|
10
|
-
* Path to local root directory for static assets. Also see
|
|
11
|
-
* {@link StaticOpts.prefix}
|
|
12
|
-
*
|
|
13
|
-
* @defaultValue `.` (current cwd)
|
|
14
|
-
*/
|
|
15
|
-
rootDir: string;
|
|
16
|
-
/**
|
|
17
|
-
* Filter predicate to exclude files from being served. Called with the
|
|
18
|
-
* absolute local file path. If the function returns false, the server
|
|
19
|
-
* produces a 404 response. By default all files (within
|
|
20
|
-
* {@link StaticOpts.rootDir}) will be allowed.
|
|
21
|
-
*/
|
|
22
|
-
filter: Predicate<string>;
|
|
23
|
-
/**
|
|
24
|
-
* Base URL prefix for static assets. Also see {@link StaticOpts.rootDir}
|
|
25
|
-
*
|
|
26
|
-
* @defaultValue "static"
|
|
27
|
-
*/
|
|
28
|
-
prefix: string;
|
|
29
|
-
/**
|
|
30
|
-
* Additional route specific interceptors.
|
|
31
|
-
*/
|
|
32
|
-
intercept: Interceptor<CTX>[];
|
|
33
|
-
/**
|
|
34
|
-
* Additional common headers (e.g. cache control) for all static files
|
|
35
|
-
*/
|
|
36
|
-
headers: OutgoingHttpHeaders;
|
|
37
|
-
/**
|
|
38
|
-
* If true (default: false), files will be served with brotli, gzip or deflate
|
|
39
|
-
* compression (if the client supports it).
|
|
40
|
-
*
|
|
41
|
-
* @defaultValue false
|
|
42
|
-
*/
|
|
43
|
-
compress: boolean;
|
|
44
|
-
/**
|
|
45
|
-
* User defined function to compute an Etag value for given file path. The
|
|
46
|
-
* file is guaranteed to exist when this function is called.
|
|
47
|
-
*/
|
|
48
|
-
etag: Fn<string, MaybePromise<string>>;
|
|
49
|
-
/**
|
|
50
|
-
* If true, the route will have its `auth` flag enabled, e.g. for use with
|
|
51
|
-
* the {@link authenticateWith} interceptor.
|
|
52
|
-
*
|
|
53
|
-
* @defaultValue false
|
|
54
|
-
*/
|
|
55
|
-
auth: boolean;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Defines a configurable {@link ServerRoute} and handler for serving static
|
|
59
|
-
* files from a local directory (optionally with compression, see
|
|
60
|
-
* {@link StaticOpts.compress} for details).
|
|
61
|
-
*
|
|
62
|
-
* @param opts
|
|
63
|
-
*/
|
|
64
|
-
export declare const staticFiles: <CTX extends RequestCtx = RequestCtx>({
|
|
65
|
-
prefix,
|
|
66
|
-
rootDir,
|
|
67
|
-
intercept,
|
|
68
|
-
filter,
|
|
69
|
-
compress,
|
|
70
|
-
auth,
|
|
71
|
-
etag,
|
|
72
|
-
headers,
|
|
73
|
-
}?: Partial<StaticOpts<CTX>>) => ServerRoute<CTX>;
|
|
74
|
-
/**
|
|
75
|
-
* Etag header value function for {@link StaticOpts.etag}. Computes Etag based
|
|
76
|
-
* on file modified date.
|
|
77
|
-
*
|
|
78
|
-
* @remarks
|
|
79
|
-
* Also see {@link etagFileHash}.
|
|
80
|
-
*
|
|
81
|
-
* @param path
|
|
82
|
-
*/
|
|
83
|
-
export declare const etagFileTimeModified: (path: string) => string;
|
|
84
|
-
/**
|
|
85
|
-
* Higher-order Etag header value function for {@link StaticOpts.etag}. Computes
|
|
86
|
-
* Etag value by computing the hash digest of a given file. Uses MD5 by default.
|
|
87
|
-
*
|
|
88
|
-
* @remarks
|
|
89
|
-
* Also see {@link etagFileTimeModified}.
|
|
90
|
-
*
|
|
91
|
-
* @param algo
|
|
92
|
-
*/
|
|
93
|
-
export declare const etagFileHash: (
|
|
94
|
-
algo?: HashAlgo
|
|
95
|
-
) => (path: string) => Promise<string>;
|
|
96
|
-
//# sourceMappingURL=static.d.ts.map
|
package/interceptors/static.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { fileHash as $fileHash } from "@thi.ng/file-io";
|
|
2
|
-
import { preferredTypeForPath } from "@thi.ng/mime";
|
|
3
|
-
import { existsSync, statSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { isUnmodified } from "../utils/cache.js";
|
|
6
|
-
const staticFiles = ({
|
|
7
|
-
prefix = "static",
|
|
8
|
-
rootDir = ".",
|
|
9
|
-
intercept = [],
|
|
10
|
-
filter = () => true,
|
|
11
|
-
compress = false,
|
|
12
|
-
auth = false,
|
|
13
|
-
etag,
|
|
14
|
-
headers
|
|
15
|
-
} = {}) => ({
|
|
16
|
-
id: "__static",
|
|
17
|
-
match: [prefix, "+"],
|
|
18
|
-
auth,
|
|
19
|
-
handlers: {
|
|
20
|
-
head: {
|
|
21
|
-
fn: async (ctx) => {
|
|
22
|
-
const path = join(rootDir, ...ctx.match.rest);
|
|
23
|
-
ctx.logger.debug("path", path);
|
|
24
|
-
const $headers = await __fileHeaders(
|
|
25
|
-
path,
|
|
26
|
-
ctx,
|
|
27
|
-
filter,
|
|
28
|
-
etag,
|
|
29
|
-
headers
|
|
30
|
-
);
|
|
31
|
-
if (!$headers) return;
|
|
32
|
-
ctx.res.writeHead(200, {
|
|
33
|
-
"content-type": preferredTypeForPath(path),
|
|
34
|
-
...$headers
|
|
35
|
-
});
|
|
36
|
-
},
|
|
37
|
-
intercept
|
|
38
|
-
},
|
|
39
|
-
get: {
|
|
40
|
-
fn: async (ctx) => {
|
|
41
|
-
const path = join(rootDir, ...ctx.match.rest);
|
|
42
|
-
ctx.logger.debug("path", path);
|
|
43
|
-
const $headers = await __fileHeaders(
|
|
44
|
-
path,
|
|
45
|
-
ctx,
|
|
46
|
-
filter,
|
|
47
|
-
etag,
|
|
48
|
-
headers
|
|
49
|
-
);
|
|
50
|
-
if (!$headers) return;
|
|
51
|
-
return ctx.server.sendFile(ctx, path, $headers, compress);
|
|
52
|
-
},
|
|
53
|
-
intercept
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
const __fileHeaders = async (path, ctx, filter, etag, headers) => {
|
|
58
|
-
if (!(existsSync(path) && filter(path))) {
|
|
59
|
-
return ctx.res.missing();
|
|
60
|
-
}
|
|
61
|
-
if (etag) {
|
|
62
|
-
const etagValue = await etag(path);
|
|
63
|
-
return isUnmodified(etagValue, ctx.req.headers["if-none-match"]) ? ctx.res.unmodified() : { ...headers, etag: etagValue };
|
|
64
|
-
}
|
|
65
|
-
return { ...headers };
|
|
66
|
-
};
|
|
67
|
-
const etagFileTimeModified = (path) => String(statSync(path).mtimeMs);
|
|
68
|
-
const etagFileHash = (algo = "md5") => (path) => $fileHash(path, void 0, algo);
|
|
69
|
-
export {
|
|
70
|
-
etagFileHash,
|
|
71
|
-
etagFileTimeModified,
|
|
72
|
-
staticFiles
|
|
73
|
-
};
|