-
-
Notifications
You must be signed in to change notification settings - Fork 47
/
send.ts
229 lines (189 loc) · 5.84 KB
/
send.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
/*!
* Adapted from koa-send at https://github.com/koajs/send and which is licensed
* with the MIT license.
*/
import { basename, contentType, extname, parse, sep } from "../deps.ts";
import { isAbsolute, join, normalize, resolve } from "../deps.ts";
// TODO move to library mode
interface Response {
[key: string]: any;
}
export function decodeComponent(text: string) {
try {
return decodeURIComponent(text);
} catch {
return text;
}
}
export interface SendOptions {
/** Browser cache max-age in milliseconds. (defaults to `0`) */
maxAge?: number;
/** Tell the browser the resource is immutable and can be cached
* indefinitely. (defaults to `false`) */
immutable?: boolean;
/** Allow transfer of hidden files. (defaults to `false`) */
hidden?: boolean;
/** Root directory to restrict file access. */
root: string;
/** Name of the index file to serve automatically when visiting the root
* location. (defaults to none) */
index?: string;
/** Try to serve the gzipped version of a file automatically when gzip is
* supported by a client and if the requested file with `.gz` extension
* exists. (defaults to `true`). */
gzip?: boolean;
/** Try to serve the brotli version of a file automatically when brotli is
* supported by a client and if the requested file with `.br` extension
* exists. (defaults to `true`) */
brotli?: boolean;
/** If `true`, format the path to serve static file servers and not require a
* trailing slash for directories, so that you can do both `/directory` and
* `/directory/`. (defaults to `true`) */
format?: boolean;
/** Try to match extensions from passed array to search for file when no
* extension is sufficed in URL. First found is served. (defaults to
* `undefined`) */
extensions?: string[];
}
interface RequestResponse {
request: Request;
response: Response;
}
function isHidden(root: string, path: string) {
const pathArr = path.substr(root.length).split(sep);
return !!pathArr.find((segment) => segment.startsWith("."));
}
async function exists(path: string): Promise<boolean> {
try {
return (await Deno.stat(path)).isFile;
} catch {
return false;
}
}
function toUTCString(value: number): string {
return new Date(value).toUTCString();
}
// TODO: implement send data with 'range'
/** Asynchronously fulfill a response with a file from the local file
* system. */
export async function send(
{ request, response }: RequestResponse,
path: string,
options: SendOptions = { root: "" },
): Promise<string | undefined> {
const {
brotli = true,
extensions,
format = true,
gzip = true,
index,
hidden = false,
immutable = false,
maxAge = 0,
root,
} = options;
const trailingSlash = path[path.length - 1] === "/";
path = decodeComponent(path.substr(parse(path).root.length));
if (index && trailingSlash) {
path += index;
}
// normalize
path = resolvePath(root, path);
if (!hidden && isHidden(root, path)) {
return;
}
if (!response) {
response = { headers: new Headers() };
}
let encodingExt = "";
if (brotli && (await exists(`${path}.br`))) {
path = `${path}.br`;
response.headers.set("Content-Encoding", "br");
response.headers.delete("Content-Length");
encodingExt = ".br";
} else if (gzip && (await exists(`${path}.gz`))) {
path = `${path}.gz`;
response.headers.set("Content-Encoding", "gzip");
response.headers.delete("Content-Length");
encodingExt = ".gz";
}
if (extensions && !/\.[^/]*$/.exec(path)) {
for (let ext of extensions) {
if (!/^\./.exec(ext)) {
ext = `.${ext}`;
}
if (await exists(`${path}${ext}`)) {
path += ext;
break;
}
}
}
let stats: Deno.FileInfo;
try {
stats = await Deno.stat(path);
if (stats.isDirectory) {
if (format && index) {
path += `/${index}`;
stats = await Deno.stat(path);
} else {
return;
}
}
} catch (err) {
if (err instanceof Deno.errors.NotFound) {
throw new Error(err.message); // 404
}
throw new Error(err.message); // 500
}
response.headers.set("Content-Length", String(stats.size));
// TODO: stats.modified from Stats
// if (!response.headers.has('Last-Modified') && stats.modified) {
// response.headers.set('Last-Modified', toUTCString(stats.modified));
// }
if (!response.headers.has("Cache-Control")) {
const directives = [`max-age=${(maxAge / 1000) | 0}`];
if (immutable) {
directives.push("immutable");
}
response.headers.set("Cache-Control", directives.join(","));
}
if (!response.headers.has("Content-Type")) {
const type = contentType(
encodingExt !== "" ? extname(basename(path, encodingExt)) : extname(path),
);
response.headers.set("Content-Type", type);
}
response.body = await Deno.readFile(path);
return path;
}
// Moved from:
// import { resolvePath } from './resolve-path.ts';
const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
export function resolvePath(relativePath: string): string;
export function resolvePath(rootPath: string, relativePath: string): string;
export function resolvePath(rootPath: string, relativePath?: string): string {
let path = relativePath;
let root = rootPath;
// root is optional, similar to root.resolve
if (arguments.length === 1) {
path = rootPath;
root = Deno.cwd();
}
if (path === undefined) {
throw new TypeError("Argument relativePath is required.");
}
// containing NULL bytes is malicious
if (path.includes("\0")) {
throw new Error("Malicious Path");
}
// path should never be absolute
if (isAbsolute(path)) {
throw new Error("Malicious Path");
}
// path outside root
if (UP_PATH_REGEXP.test(normalize("." + sep + path))) {
throw new Error("403");
}
// join the relative path
return normalize(join(resolve(root), path));
}