diff options
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(); -} |