diff options
author | Casper Beyer <caspervonb@pm.me> | 2021-02-02 19:05:46 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-02 12:05:46 +0100 |
commit | 6abf126c2a7a451cded8c6b5e6ddf1b69c84055d (patch) | |
tree | fd94c013a19fcb38954844085821ec1601c20e18 /std/http/file_server.ts | |
parent | a2b5d44f1aa9d64f448a2a3cc2001272e2f60b98 (diff) |
chore: remove std directory (#9361)
This removes the std folder from the tree.
Various parts of the tests are pretty tightly dependent
on std (47 direct imports and 75 indirect imports, not
counting the cli tests that use them as fixtures) so I've
added std as a submodule for now.
Diffstat (limited to 'std/http/file_server.ts')
-rw-r--r-- | std/http/file_server.ts | 454 |
1 files changed, 0 insertions, 454 deletions
diff --git a/std/http/file_server.ts b/std/http/file_server.ts deleted file mode 100644 index 8fd2e7484..000000000 --- a/std/http/file_server.ts +++ /dev/null @@ -1,454 +0,0 @@ -#!/usr/bin/env -S deno run --allow-net --allow-read -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. - -// This program serves files in the current directory over HTTP. -// TODO(bartlomieju): Stream responses instead of reading them into memory. -// TODO(bartlomieju): Add tests like these: -// https://github.com/indexzero/http-server/blob/master/test/http-server-test.js - -import { extname, posix } from "../path/mod.ts"; -import { - HTTPSOptions, - listenAndServe, - listenAndServeTLS, - Response, - ServerRequest, -} from "./server.ts"; -import { parse } from "../flags/mod.ts"; -import { assert } from "../_util/assert.ts"; - -interface EntryInfo { - mode: string; - size: string; - url: string; - name: string; -} - -export interface FileServerArgs { - _: string[]; - // -p --port - p?: number; - port?: number; - // --cors - cors?: boolean; - // --no-dir-listing - "dir-listing"?: boolean; - // --host - host?: string; - // -c --cert - c?: string; - cert?: string; - // -k --key - k?: string; - key?: string; - // -h --help - h?: boolean; - help?: boolean; -} - -const encoder = new TextEncoder(); - -const serverArgs = parse(Deno.args) as FileServerArgs; -const target = posix.resolve(serverArgs._[0] ?? ""); - -const MEDIA_TYPES: Record<string, string> = { - ".md": "text/markdown", - ".html": "text/html", - ".htm": "text/html", - ".json": "application/json", - ".map": "application/json", - ".txt": "text/plain", - ".ts": "text/typescript", - ".tsx": "text/tsx", - ".js": "application/javascript", - ".jsx": "text/jsx", - ".gz": "application/gzip", - ".css": "text/css", - ".wasm": "application/wasm", - ".mjs": "application/javascript", -}; - -/** Returns the content-type based on the extension of a path. */ -function contentType(path: string): string | undefined { - return MEDIA_TYPES[extname(path)]; -} - -function modeToString(isDir: boolean, maybeMode: number | null): string { - const modeMap = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"]; - - if (maybeMode === null) { - return "(unknown mode)"; - } - const mode = maybeMode.toString(8); - if (mode.length < 3) { - return "(unknown mode)"; - } - let output = ""; - mode - .split("") - .reverse() - .slice(0, 3) - .forEach((v): void => { - output = modeMap[+v] + output; - }); - output = `(${isDir ? "d" : "-"}${output})`; - return output; -} - -function fileLenToString(len: number): string { - const multiplier = 1024; - let base = 1; - const suffix = ["B", "K", "M", "G", "T"]; - let suffixIndex = 0; - - while (base * multiplier < len) { - if (suffixIndex >= suffix.length - 1) { - break; - } - base *= multiplier; - suffixIndex++; - } - - return `${(len / base).toFixed(2)}${suffix[suffixIndex]}`; -} - -/** - * Returns an HTTP Response with the requested file as the body - * @param req The server request context used to cleanup the file handle - * @param filePath Path of the file to serve - */ -export async function serveFile( - req: ServerRequest, - filePath: string, -): Promise<Response> { - const [file, fileInfo] = await Promise.all([ - Deno.open(filePath), - Deno.stat(filePath), - ]); - const headers = new Headers(); - headers.set("content-length", fileInfo.size.toString()); - const contentTypeValue = contentType(filePath); - if (contentTypeValue) { - headers.set("content-type", contentTypeValue); - } - req.done.then(() => { - file.close(); - }); - return { - status: 200, - body: file, - headers, - }; -} - -// TODO(bartlomieju): simplify this after deno.stat and deno.readDir are fixed -async function serveDir( - req: ServerRequest, - dirPath: string, -): Promise<Response> { - const dirUrl = `/${posix.relative(target, dirPath)}`; - const listEntry: EntryInfo[] = []; - for await (const entry of Deno.readDir(dirPath)) { - const filePath = posix.join(dirPath, entry.name); - const fileUrl = posix.join(dirUrl, entry.name); - if (entry.name === "index.html" && entry.isFile) { - // in case index.html as dir... - return serveFile(req, filePath); - } - // Yuck! - let fileInfo = null; - try { - fileInfo = await Deno.stat(filePath); - } catch (e) { - // Pass - } - listEntry.push({ - mode: modeToString(entry.isDirectory, fileInfo?.mode ?? null), - size: entry.isFile ? fileLenToString(fileInfo?.size ?? 0) : "", - name: entry.name, - url: fileUrl, - }); - } - listEntry.sort((a, b) => - a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1 - ); - const formattedDirUrl = `${dirUrl.replace(/\/$/, "")}/`; - const page = encoder.encode(dirViewerTemplate(formattedDirUrl, listEntry)); - - const headers = new Headers(); - headers.set("content-type", "text/html"); - - const res = { - status: 200, - body: page, - headers, - }; - return res; -} - -function serveFallback(req: ServerRequest, e: Error): Promise<Response> { - if (e instanceof URIError) { - return Promise.resolve({ - status: 400, - body: encoder.encode("Bad Request"), - }); - } else if (e instanceof Deno.errors.NotFound) { - return Promise.resolve({ - status: 404, - body: encoder.encode("Not Found"), - }); - } else { - return Promise.resolve({ - status: 500, - body: encoder.encode("Internal server error"), - }); - } -} - -function serverLog(req: ServerRequest, res: Response): void { - const d = new Date().toISOString(); - const dateFmt = `[${d.slice(0, 10)} ${d.slice(11, 19)}]`; - const s = `${dateFmt} "${req.method} ${req.url} ${req.proto}" ${res.status}`; - console.log(s); -} - -function setCORS(res: Response): void { - if (!res.headers) { - res.headers = new Headers(); - } - res.headers.append("access-control-allow-origin", "*"); - res.headers.append( - "access-control-allow-headers", - "Origin, X-Requested-With, Content-Type, Accept, Range", - ); -} - -function dirViewerTemplate(dirname: string, entries: EntryInfo[]): string { - return html` - <!DOCTYPE html> - <html lang="en"> - <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <meta http-equiv="X-UA-Compatible" content="ie=edge" /> - <title>Deno File Server</title> - <style> - :root { - --background-color: #fafafa; - --color: rgba(0, 0, 0, 0.87); - } - @media (prefers-color-scheme: dark) { - :root { - --background-color: #303030; - --color: #fff; - } - } - @media (min-width: 960px) { - main { - max-width: 960px; - } - body { - padding-left: 32px; - padding-right: 32px; - } - } - @media (min-width: 600px) { - main { - padding-left: 24px; - padding-right: 24px; - } - } - body { - background: var(--background-color); - color: var(--color); - font-family: "Roboto", "Helvetica", "Arial", sans-serif; - font-weight: 400; - line-height: 1.43; - font-size: 0.875rem; - } - a { - color: #2196f3; - text-decoration: none; - } - a:hover { - text-decoration: underline; - } - table th { - text-align: left; - } - table td { - padding: 12px 24px 0 0; - } - </style> - </head> - <body> - <main> - <h1>Index of ${dirname}</h1> - <table> - <tr> - <th>Mode</th> - <th>Size</th> - <th>Name</th> - </tr> - ${ - entries.map( - (entry) => - html` - <tr> - <td class="mode"> - ${entry.mode} - </td> - <td> - ${entry.size} - </td> - <td> - <a href="${entry.url}">${entry.name}</a> - </td> - </tr> - `, - ) - } - </table> - </main> - </body> - </html> - `; -} - -function html(strings: TemplateStringsArray, ...values: unknown[]): string { - const l = strings.length - 1; - let html = ""; - - for (let i = 0; i < l; i++) { - let v = values[i]; - if (v instanceof Array) { - v = v.join(""); - } - const s = strings[i] + v; - html += s; - } - html += strings[l]; - return html; -} - -function normalizeURL(url: string): string { - let normalizedUrl = url; - try { - normalizedUrl = decodeURI(normalizedUrl); - } catch (e) { - if (!(e instanceof URIError)) { - throw e; - } - } - - try { - //allowed per https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html - const absoluteURI = new URL(normalizedUrl); - normalizedUrl = absoluteURI.pathname; - } catch (e) { //wasn't an absoluteURI - if (!(e instanceof TypeError)) { - throw e; - } - } - - if (normalizedUrl[0] !== "/") { - throw new URIError("The request URI is malformed."); - } - - normalizedUrl = posix.normalize(normalizedUrl); - const startOfParams = normalizedUrl.indexOf("?"); - return startOfParams > -1 - ? normalizedUrl.slice(0, startOfParams) - : normalizedUrl; -} - -function main(): void { - const CORSEnabled = serverArgs.cors ? true : false; - const port = serverArgs.port ?? serverArgs.p ?? 4507; - const host = serverArgs.host ?? "0.0.0.0"; - const addr = `${host}:${port}`; - const tlsOpts = {} as HTTPSOptions; - tlsOpts.certFile = serverArgs.cert ?? serverArgs.c ?? ""; - tlsOpts.keyFile = serverArgs.key ?? serverArgs.k ?? ""; - const dirListingEnabled = serverArgs["dir-listing"] ?? true; - - if (tlsOpts.keyFile || tlsOpts.certFile) { - if (tlsOpts.keyFile === "" || tlsOpts.certFile === "") { - console.log("--key and --cert are required for TLS"); - serverArgs.h = true; - } - } - - if (serverArgs.h ?? serverArgs.help) { - console.log(`Deno File Server - Serves a local directory in HTTP. - - INSTALL: - deno install --allow-net --allow-read https://deno.land/std/http/file_server.ts - - USAGE: - file_server [path] [options] - - OPTIONS: - -h, --help Prints help information - -p, --port <PORT> Set port - --cors Enable CORS via the "Access-Control-Allow-Origin" header - --host <HOST> Hostname (default is 0.0.0.0) - -c, --cert <FILE> TLS certificate file (enables TLS) - -k, --key <FILE> TLS key file (enables TLS) - --no-dir-listing Disable directory listing - - All TLS options are required when one is provided.`); - Deno.exit(); - } - - const handler = async (req: ServerRequest): Promise<void> => { - let response: Response | undefined; - try { - const normalizedUrl = normalizeURL(req.url); - let fsPath = posix.join(target, normalizedUrl); - if (fsPath.indexOf(target) !== 0) { - fsPath = target; - } - const fileInfo = await Deno.stat(fsPath); - if (fileInfo.isDirectory) { - if (dirListingEnabled) { - response = await serveDir(req, fsPath); - } else { - throw new Deno.errors.NotFound(); - } - } else { - response = await serveFile(req, fsPath); - } - } catch (e) { - console.error(e.message); - response = await serveFallback(req, e); - } finally { - if (CORSEnabled) { - assert(response); - setCORS(response); - } - serverLog(req, response!); - try { - await req.respond(response!); - } catch (e) { - console.error(e.message); - } - } - }; - - let proto = "http"; - if (tlsOpts.keyFile || tlsOpts.certFile) { - proto += "s"; - tlsOpts.hostname = host; - tlsOpts.port = port; - listenAndServeTLS(tlsOpts, handler); - } else { - listenAndServe(addr, handler); - } - console.log(`${proto.toUpperCase()} server listening on ${proto}://${addr}/`); -} - -if (import.meta.main) { - main(); -} |