|
| 1 | +#!/usr/bin/env qjs |
| 2 | +///@ts-check |
| 3 | +/// <reference path="../doc/globals.d.ts" /> |
| 4 | +/// <reference path="../doc/os.d.ts" /> |
| 5 | +import * as os from "os"; |
| 6 | + |
| 7 | +const MIMES = new Map([ |
| 8 | + ['html', 'text/html'], |
| 9 | + ['txt', 'text/plain'], |
| 10 | + ['css', 'text/css'], |
| 11 | + ['c', 'text/plain'], |
| 12 | + ['h', 'text/plain'], |
| 13 | + ['json', 'application/json'], |
| 14 | + ['mjs', 'application/javascript'], |
| 15 | + ['js', 'application/javascript'], |
| 16 | + ['', 'application/octet-stream'], |
| 17 | +]); |
| 18 | +/** @template T @param {os.Result<T>} result */ |
| 19 | +function must(result) { |
| 20 | + if (typeof result === "number" && result < 0) throw result; |
| 21 | + return /** @type {T} */ (result) |
| 22 | +} |
| 23 | +/**@param {os.FileDescriptor} fd */ |
| 24 | +async function* recvLines(fd) { |
| 25 | + const chunk = new Uint8Array(1); |
| 26 | + let line = ''; |
| 27 | + while (await os.recv(fd, chunk.buffer) > 0) { |
| 28 | + const char = String.fromCharCode(...chunk); |
| 29 | + if (char == '\n') { |
| 30 | + yield line; |
| 31 | + line = ''; |
| 32 | + } else line += char; |
| 33 | + } |
| 34 | + if (line) yield line; |
| 35 | +} |
| 36 | +/** @param {os.FileDescriptor} fd @param {string[]} lines */ |
| 37 | +function sendLines(fd, lines) { |
| 38 | + const buf = Uint8Array.from(lines.join('\r\n'), c => c.charCodeAt(0)); |
| 39 | + return os.send(fd, buf.buffer); |
| 40 | +} |
| 41 | +//USAGE: qjs http_server.js [PORT=8080 [HOST=localhost]] |
| 42 | +const [port = "8080", host = "localhost"] = scriptArgs.slice(1); |
| 43 | +const [ai] = os.getaddrinfo(host, port); |
| 44 | +//if (!ai.length) throw `Unable to getaddrinfo(${host}, ${port})`; |
| 45 | +const sock_srv = must(os.socket(os.AF_INET, os.SOCK_STREAM)); |
| 46 | +must(os.setsockopt(sock_srv, os.SO_REUSEADDR, new Uint32Array([1]).buffer)); |
| 47 | +must(os.bind(sock_srv, ai)); |
| 48 | +must(os.listen(sock_srv)); |
| 49 | +//os.signal(os.SIGINT, ()=>os.close(sock_srv)); // don't work |
| 50 | +console.log(`Listening on http://${host}:${port} (${ai.addr}:${ai.port}) ...`); |
| 51 | +const openCmd = { linux: "xdg-open", darwin: "open", win32: "start" }[os.platform]; |
| 52 | +if (openCmd) os.exec([openCmd, `http://${host}:${port}`]); |
| 53 | +while (true) { // TODO: break on SIG* |
| 54 | + const [sock_cli] = await os.accept(sock_srv); |
| 55 | + |
| 56 | + const lines = recvLines(sock_cli); |
| 57 | + const [method, path, http_ver] = ((await lines.next()).value || '').split(' '); |
| 58 | + let safe_path = '.' + path.replaceAll(/\.+/g, '.'); // may += index.html later |
| 59 | + console.log(method, safe_path, http_ver); |
| 60 | + |
| 61 | + const headers = new Map() |
| 62 | + for await (const line of lines) { |
| 63 | + const header = line.trimEnd(); |
| 64 | + if (!header) break; |
| 65 | + const sepIdx = header.indexOf(': '); |
| 66 | + headers.set(header.slice(0, sepIdx), header.slice(sepIdx + 2)); |
| 67 | + } |
| 68 | + |
| 69 | + let [obj, err] = os.stat(safe_path); |
| 70 | + if (obj?.mode & os.S_IFDIR && safe_path.endsWith('/') && os.stat(safe_path + 'index.html')[0]) { |
| 71 | + safe_path += 'index.html'; |
| 72 | + [obj, err] = os.stat(safe_path); |
| 73 | + } |
| 74 | + if (err) { |
| 75 | + await sendLines(sock_cli, ['HTTP/1.1 404', '', safe_path, 'errno:' + err]) |
| 76 | + } else if (obj?.mode & os.S_IFDIR) { |
| 77 | + if (!safe_path.endsWith('/')) |
| 78 | + await sendLines(sock_cli, ['HTTP/1.1 301', `Location: ${safe_path}/`, '']); |
| 79 | + else |
| 80 | + await sendLines(sock_cli, ['HTTP/1.1 200', 'Content-Type: text/html', '', |
| 81 | + os.readdir(safe_path)[0]?.filter(e => e[0] != '.').map(e => `<li><a href="${e}">${e}</a></li>`).join('') |
| 82 | + ]); |
| 83 | + } else { |
| 84 | + const mime = MIMES.get(safe_path.split('.').at(-1) || '') || MIMES.get(''); |
| 85 | + await sendLines(sock_cli, ['HTTP/1.1 200', `Content-Type: ${mime}`, '', '']); |
| 86 | + const fd = must(os.open(safe_path)); |
| 87 | + const fbuf = new Uint8Array(4096); |
| 88 | + for (let got = 0; (got = os.read(fd, fbuf.buffer, 0, fbuf.byteLength)) > 0;) { |
| 89 | + await os.send(sock_cli, fbuf.buffer, got); |
| 90 | + } |
| 91 | + } |
| 92 | + |
| 93 | + os.close(sock_cli); |
| 94 | +} |
0 commit comments