Last active
April 19, 2026 06:38
-
-
Save higuma/3a851c9f7d4a87583f00642e0292153d to your computer and use it in GitHub Desktop.
Lightweight node.js server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| // Lightweight node.js server | |
| // Author: Yuji Miyane (https://github.com/higuma) | |
| // Date: 2026-04-19 | |
| import fs from 'node:fs'; | |
| import http from 'node:http'; | |
| import path from 'node:path'; | |
| // Supported MIME types | |
| const MIME_TYPES = (function(mimeExtensions) { | |
| const mimeTypes = { default: 'application/octet-stream' }; | |
| for (let mime in mimeExtensions) { | |
| const extensions = mimeExtensions[mime]; | |
| for (let i = 0; i < extensions.length; i++) { | |
| mimeTypes[extensions[i]] = mime; | |
| } | |
| } | |
| return mimeTypes; | |
| })({ | |
| // taken from https://github.com/broofa/mime/blob/main/types/standard.ts | |
| 'application/andrew-inset': ['ez'], | |
| 'application/appinstaller': ['appinstaller'], | |
| 'application/applixware': ['aw'], | |
| 'application/appx': ['appx'], | |
| 'application/appxbundle': ['appxbundle'], | |
| 'application/atom+xml': ['atom'], | |
| 'application/atomcat+xml': ['atomcat'], | |
| 'application/atomdeleted+xml': ['atomdeleted'], | |
| 'application/atomsvc+xml': ['atomsvc'], | |
| 'application/atsc-dwd+xml': ['dwd'], | |
| 'application/atsc-held+xml': ['held'], | |
| 'application/atsc-rsat+xml': ['rsat'], | |
| 'application/automationml-aml+xml': ['aml'], | |
| 'application/automationml-amlx+zip': ['amlx'], | |
| 'application/bdoc': ['bdoc'], | |
| 'application/calendar+xml': ['xcs'], | |
| 'application/ccxml+xml': ['ccxml'], | |
| 'application/cdfx+xml': ['cdfx'], | |
| 'application/cdmi-capability': ['cdmia'], | |
| 'application/cdmi-container': ['cdmic'], | |
| 'application/cdmi-domain': ['cdmid'], | |
| 'application/cdmi-object': ['cdmio'], | |
| 'application/cdmi-queue': ['cdmiq'], | |
| 'application/cpl+xml': ['cpl'], | |
| 'application/cu-seeme': ['cu'], | |
| 'application/cwl': ['cwl'], | |
| 'application/dash+xml': ['mpd'], | |
| 'application/dash-patch+xml': ['mpp'], | |
| 'application/davmount+xml': ['davmount'], | |
| 'application/dicom': ['dcm'], | |
| 'application/docbook+xml': ['dbk'], | |
| 'application/dssc+der': ['dssc'], | |
| 'application/dssc+xml': ['xdssc'], | |
| 'application/ecmascript': ['ecma'], | |
| 'application/emma+xml': ['emma'], | |
| 'application/emotionml+xml': ['emotionml'], | |
| 'application/epub+zip': ['epub'], | |
| 'application/exi': ['exi'], | |
| 'application/express': ['exp'], | |
| 'application/fdf': ['fdf'], | |
| 'application/fdt+xml': ['fdt'], | |
| 'application/font-tdpfr': ['pfr'], | |
| 'application/geo+json': ['geojson'], | |
| 'application/gml+xml': ['gml'], | |
| 'application/gpx+xml': ['gpx'], | |
| 'application/gxf': ['gxf'], | |
| 'application/gzip': ['gz'], | |
| 'application/hjson': ['hjson'], | |
| 'application/hyperstudio': ['stk'], | |
| 'application/inkml+xml': ['ink', 'inkml'], | |
| 'application/ipfix': ['ipfix'], | |
| 'application/its+xml': ['its'], | |
| 'application/java-archive': ['jar', 'war', 'ear'], | |
| 'application/java-serialized-object': ['ser'], | |
| 'application/java-vm': ['class'], | |
| 'application/javascript': ['*js'], | |
| 'application/json': ['json', 'map'], | |
| 'application/json5': ['json5'], | |
| 'application/jsonml+json': ['jsonml'], | |
| 'application/ld+json': ['jsonld'], | |
| 'application/lgr+xml': ['lgr'], | |
| 'application/lost+xml': ['lostxml'], | |
| 'application/mac-binhex40': ['hqx'], | |
| 'application/mac-compactpro': ['cpt'], | |
| 'application/mads+xml': ['mads'], | |
| 'application/manifest+json': ['webmanifest'], | |
| 'application/marc': ['mrc'], | |
| 'application/marcxml+xml': ['mrcx'], | |
| 'application/mathematica': ['ma', 'nb', 'mb'], | |
| 'application/mathml+xml': ['mathml'], | |
| 'application/mbox': ['mbox'], | |
| 'application/media-policy-dataset+xml': ['mpf'], | |
| 'application/mediaservercontrol+xml': ['mscml'], | |
| 'application/metalink+xml': ['metalink'], | |
| 'application/metalink4+xml': ['meta4'], | |
| 'application/mets+xml': ['mets'], | |
| 'application/mmt-aei+xml': ['maei'], | |
| 'application/mmt-usd+xml': ['musd'], | |
| 'application/mods+xml': ['mods'], | |
| 'application/mp21': ['m21', 'mp21'], | |
| 'application/mp4': ['*mp4', '*mpg4', 'mp4s', 'm4p'], | |
| 'application/msix': ['msix'], | |
| 'application/msixbundle': ['msixbundle'], | |
| 'application/msword': ['doc', 'dot'], | |
| 'application/mxf': ['mxf'], | |
| 'application/n-quads': ['nq'], | |
| 'application/n-triples': ['nt'], | |
| 'application/node': ['cjs'], | |
| 'application/octet-stream': [ | |
| 'bin', | |
| 'dms', | |
| 'lrf', | |
| 'mar', | |
| 'so', | |
| 'dist', | |
| 'distz', | |
| 'pkg', | |
| 'bpk', | |
| 'dump', | |
| 'elc', | |
| 'deploy', | |
| 'exe', | |
| 'dll', | |
| 'deb', | |
| 'dmg', | |
| 'iso', | |
| 'img', | |
| 'msi', | |
| 'msp', | |
| 'msm', | |
| 'buffer', | |
| ], | |
| 'application/oda': ['oda'], | |
| 'application/oebps-package+xml': ['opf'], | |
| 'application/ogg': ['ogx'], | |
| 'application/omdoc+xml': ['omdoc'], | |
| 'application/onenote': [ | |
| 'onetoc', | |
| 'onetoc2', | |
| 'onetmp', | |
| 'onepkg', | |
| 'one', | |
| 'onea', | |
| ], | |
| 'application/oxps': ['oxps'], | |
| 'application/p2p-overlay+xml': ['relo'], | |
| 'application/patch-ops-error+xml': ['xer'], | |
| 'application/pdf': ['pdf'], | |
| 'application/pgp-encrypted': ['pgp'], | |
| 'application/pgp-keys': ['asc'], | |
| 'application/pgp-signature': ['sig', '*asc'], | |
| 'application/pics-rules': ['prf'], | |
| 'application/pkcs10': ['p10'], | |
| 'application/pkcs7-mime': ['p7m', 'p7c'], | |
| 'application/pkcs7-signature': ['p7s'], | |
| 'application/pkcs8': ['p8'], | |
| 'application/pkix-attr-cert': ['ac'], | |
| 'application/pkix-cert': ['cer'], | |
| 'application/pkix-crl': ['crl'], | |
| 'application/pkix-pkipath': ['pkipath'], | |
| 'application/pkixcmp': ['pki'], | |
| 'application/pls+xml': ['pls'], | |
| 'application/postscript': ['ai', 'eps', 'ps'], | |
| 'application/provenance+xml': ['provx'], | |
| 'application/pskc+xml': ['pskcxml'], | |
| 'application/raml+yaml': ['raml'], | |
| 'application/rdf+xml': ['rdf', 'owl'], | |
| 'application/reginfo+xml': ['rif'], | |
| 'application/relax-ng-compact-syntax': ['rnc'], | |
| 'application/resource-lists+xml': ['rl'], | |
| 'application/resource-lists-diff+xml': ['rld'], | |
| 'application/rls-services+xml': ['rs'], | |
| 'application/route-apd+xml': ['rapd'], | |
| 'application/route-s-tsid+xml': ['sls'], | |
| 'application/route-usd+xml': ['rusd'], | |
| 'application/rpki-ghostbusters': ['gbr'], | |
| 'application/rpki-manifest': ['mft'], | |
| 'application/rpki-roa': ['roa'], | |
| 'application/rsd+xml': ['rsd'], | |
| 'application/rss+xml': ['rss'], | |
| 'application/rtf': ['rtf'], | |
| 'application/sbml+xml': ['sbml'], | |
| 'application/scvp-cv-request': ['scq'], | |
| 'application/scvp-cv-response': ['scs'], | |
| 'application/scvp-vp-request': ['spq'], | |
| 'application/scvp-vp-response': ['spp'], | |
| 'application/sdp': ['sdp'], | |
| 'application/senml+xml': ['senmlx'], | |
| 'application/sensml+xml': ['sensmlx'], | |
| 'application/set-payment-initiation': ['setpay'], | |
| 'application/set-registration-initiation': ['setreg'], | |
| 'application/shf+xml': ['shf'], | |
| 'application/sieve': ['siv', 'sieve'], | |
| 'application/smil+xml': ['smi', 'smil'], | |
| 'application/sparql-query': ['rq'], | |
| 'application/sparql-results+xml': ['srx'], | |
| 'application/sql': ['sql'], | |
| 'application/srgs': ['gram'], | |
| 'application/srgs+xml': ['grxml'], | |
| 'application/sru+xml': ['sru'], | |
| 'application/ssdl+xml': ['ssdl'], | |
| 'application/ssml+xml': ['ssml'], | |
| 'application/swid+xml': ['swidtag'], | |
| 'application/tei+xml': ['tei', 'teicorpus'], | |
| 'application/thraud+xml': ['tfi'], | |
| 'application/timestamped-data': ['tsd'], | |
| 'application/toml': ['toml'], | |
| 'application/trig': ['trig'], | |
| 'application/ttml+xml': ['ttml'], | |
| 'application/ubjson': ['ubj'], | |
| 'application/urc-ressheet+xml': ['rsheet'], | |
| 'application/urc-targetdesc+xml': ['td'], | |
| 'application/voicexml+xml': ['vxml'], | |
| 'application/wasm': ['wasm'], | |
| 'application/watcherinfo+xml': ['wif'], | |
| 'application/widget': ['wgt'], | |
| 'application/winhlp': ['hlp'], | |
| 'application/wsdl+xml': ['wsdl'], | |
| 'application/wspolicy+xml': ['wspolicy'], | |
| 'application/xaml+xml': ['xaml'], | |
| 'application/xcap-att+xml': ['xav'], | |
| 'application/xcap-caps+xml': ['xca'], | |
| 'application/xcap-diff+xml': ['xdf'], | |
| 'application/xcap-el+xml': ['xel'], | |
| 'application/xcap-ns+xml': ['xns'], | |
| 'application/xenc+xml': ['xenc'], | |
| 'application/xfdf': ['xfdf'], | |
| 'application/xhtml+xml': ['xhtml', 'xht'], | |
| 'application/xliff+xml': ['xlf'], | |
| 'application/xml': ['xml', 'xsl', 'xsd', 'rng'], | |
| 'application/xml-dtd': ['dtd'], | |
| 'application/xop+xml': ['xop'], | |
| 'application/xproc+xml': ['xpl'], | |
| 'application/xslt+xml': ['*xsl', 'xslt'], | |
| 'application/xspf+xml': ['xspf'], | |
| 'application/xv+xml': ['mxml', 'xhvml', 'xvml', 'xvm'], | |
| 'application/yang': ['yang'], | |
| 'application/yin+xml': ['yin'], | |
| 'application/zip': ['zip'], | |
| 'application/zip+dotlottie': ['lottie'], | |
| 'audio/3gpp': ['*3gpp'], | |
| 'audio/aac': ['adts', 'aac'], | |
| 'audio/adpcm': ['adp'], | |
| 'audio/amr': ['amr'], | |
| 'audio/basic': ['au', 'snd'], | |
| 'audio/midi': ['mid', 'midi', 'kar', 'rmi'], | |
| 'audio/mobile-xmf': ['mxmf'], | |
| 'audio/mp3': ['*mp3'], | |
| 'audio/mp4': ['m4a', 'mp4a', 'm4b'], | |
| 'audio/mpeg': ['mpga', 'mp2', 'mp2a', 'mp3', 'm2a', 'm3a'], | |
| 'audio/ogg': ['oga', 'ogg', 'spx', 'opus'], | |
| 'audio/s3m': ['s3m'], | |
| 'audio/silk': ['sil'], | |
| 'audio/wav': ['wav'], | |
| 'audio/wave': ['*wav'], | |
| 'audio/webm': ['weba'], | |
| 'audio/xm': ['xm'], | |
| 'font/collection': ['ttc'], | |
| 'font/otf': ['otf'], | |
| 'font/ttf': ['ttf'], | |
| 'font/woff': ['woff'], | |
| 'font/woff2': ['woff2'], | |
| 'image/aces': ['exr'], | |
| 'image/apng': ['apng'], | |
| 'image/avci': ['avci'], | |
| 'image/avcs': ['avcs'], | |
| 'image/avif': ['avif'], | |
| 'image/bmp': ['bmp', 'dib'], | |
| 'image/cgm': ['cgm'], | |
| 'image/dicom-rle': ['drle'], | |
| 'image/dpx': ['dpx'], | |
| 'image/emf': ['emf'], | |
| 'image/fits': ['fits'], | |
| 'image/g3fax': ['g3'], | |
| 'image/gif': ['gif'], | |
| 'image/heic': ['heic'], | |
| 'image/heic-sequence': ['heics'], | |
| 'image/heif': ['heif'], | |
| 'image/heif-sequence': ['heifs'], | |
| 'image/hej2k': ['hej2'], | |
| 'image/ief': ['ief'], | |
| 'image/jaii': ['jaii'], | |
| 'image/jais': ['jais'], | |
| 'image/jls': ['jls'], | |
| 'image/jp2': ['jp2', 'jpg2'], | |
| 'image/jpeg': ['jpg', 'jpeg', 'jpe'], | |
| 'image/jph': ['jph'], | |
| 'image/jphc': ['jhc'], | |
| 'image/jpm': ['jpm', 'jpgm'], | |
| 'image/jpx': ['jpx', 'jpf'], | |
| 'image/jxl': ['jxl'], | |
| 'image/jxr': ['jxr'], | |
| 'image/jxra': ['jxra'], | |
| 'image/jxrs': ['jxrs'], | |
| 'image/jxs': ['jxs'], | |
| 'image/jxsc': ['jxsc'], | |
| 'image/jxsi': ['jxsi'], | |
| 'image/jxss': ['jxss'], | |
| 'image/ktx': ['ktx'], | |
| 'image/ktx2': ['ktx2'], | |
| 'image/pjpeg': ['jfif'], | |
| 'image/png': ['png'], | |
| 'image/sgi': ['sgi'], | |
| 'image/svg+xml': ['svg', 'svgz'], | |
| 'image/t38': ['t38'], | |
| 'image/tiff': ['tif', 'tiff'], | |
| 'image/tiff-fx': ['tfx'], | |
| 'image/webp': ['webp'], | |
| 'image/wmf': ['wmf'], | |
| 'message/disposition-notification': ['disposition-notification'], | |
| 'message/global': ['u8msg'], | |
| 'message/global-delivery-status': ['u8dsn'], | |
| 'message/global-disposition-notification': ['u8mdn'], | |
| 'message/global-headers': ['u8hdr'], | |
| 'message/rfc822': ['eml', 'mime', 'mht', 'mhtml'], | |
| 'model/3mf': ['3mf'], | |
| 'model/gltf+json': ['gltf'], | |
| 'model/gltf-binary': ['glb'], | |
| 'model/iges': ['igs', 'iges'], | |
| 'model/jt': ['jt'], | |
| 'model/mesh': ['msh', 'mesh', 'silo'], | |
| 'model/mtl': ['mtl'], | |
| 'model/obj': ['obj'], | |
| 'model/prc': ['prc'], | |
| 'model/step': ['step', 'stp', 'stpnc', 'p21', '210'], | |
| 'model/step+xml': ['stpx'], | |
| 'model/step+zip': ['stpz'], | |
| 'model/step-xml+zip': ['stpxz'], | |
| 'model/stl': ['stl'], | |
| 'model/u3d': ['u3d'], | |
| 'model/vrml': ['wrl', 'vrml'], | |
| 'model/x3d+binary': ['*x3db', 'x3dbz'], | |
| 'model/x3d+fastinfoset': ['x3db'], | |
| 'model/x3d+vrml': ['*x3dv', 'x3dvz'], | |
| 'model/x3d+xml': ['x3d', 'x3dz'], | |
| 'model/x3d-vrml': ['x3dv'], | |
| 'text/cache-manifest': ['appcache', 'manifest'], | |
| 'text/calendar': ['ics', 'ifb'], | |
| 'text/coffeescript': ['coffee', 'litcoffee'], | |
| 'text/css': ['css'], | |
| 'text/csv': ['csv'], | |
| 'text/html': ['html', 'htm', 'shtml'], | |
| 'text/jade': ['jade'], | |
| 'text/javascript': ['js', 'mjs'], | |
| 'text/jsx': ['jsx'], | |
| 'text/less': ['less'], | |
| 'text/markdown': ['md', 'markdown'], | |
| 'text/mathml': ['mml'], | |
| 'text/mdx': ['mdx'], | |
| 'text/n3': ['n3'], | |
| 'text/plain': ['txt', 'text', 'conf', 'def', 'list', 'log', 'in', 'ini'], | |
| 'text/richtext': ['rtx'], | |
| 'text/rtf': ['*rtf'], | |
| 'text/sgml': ['sgml', 'sgm'], | |
| 'text/shex': ['shex'], | |
| 'text/slim': ['slim', 'slm'], | |
| 'text/spdx': ['spdx'], | |
| 'text/stylus': ['stylus', 'styl'], | |
| 'text/tab-separated-values': ['tsv'], | |
| 'text/troff': ['t', 'tr', 'roff', 'man', 'me', 'ms'], | |
| 'text/turtle': ['ttl'], | |
| 'text/uri-list': ['uri', 'uris', 'urls'], | |
| 'text/vcard': ['vcard'], | |
| 'text/vtt': ['vtt'], | |
| 'text/wgsl': ['wgsl'], | |
| 'text/xml': ['*xml'], | |
| 'text/yaml': ['yaml', 'yml'], | |
| 'video/3gpp': ['3gp', '3gpp'], | |
| 'video/3gpp2': ['3g2'], | |
| 'video/h261': ['h261'], | |
| 'video/h263': ['h263'], | |
| 'video/h264': ['h264'], | |
| 'video/iso.segment': ['m4s'], | |
| 'video/jpeg': ['jpgv'], | |
| 'video/jpm': ['*jpm', '*jpgm'], | |
| 'video/mj2': ['mj2', 'mjp2'], | |
| 'video/mp2t': ['ts', 'm2t', 'm2ts', 'mts'], | |
| 'video/mp4': ['mp4', 'mp4v', 'mpg4'], | |
| 'video/mpeg': ['mpeg', 'mpg', 'mpe', 'm1v', 'm2v'], | |
| 'video/ogg': ['ogv'], | |
| 'video/quicktime': ['qt', 'mov'], | |
| 'video/webm': ['webm'], | |
| }); | |
| // HTTP status codes | |
| const STATUS_CODES = { | |
| 200: 'OK', | |
| 302: 'Found', | |
| 404: 'Not Found', | |
| }; | |
| // show help message and exit | |
| function showHelp(exitCode) { | |
| console.log('Syntax: node-serve [-h] [-d DIRECTORY] [PORT]'); | |
| console.log(); | |
| console.log('Positional arguments:'); | |
| console.log(' PORT port number (default: 8000)'); | |
| console.log(); | |
| console.log('Options:'); | |
| console.log(' -h, --help show this help message and exit'); | |
| console.log(' -d, --directory DIRECTORY serve this directory (default: current directory)'); | |
| process.exit(exitCode); | |
| } | |
| // show command line error message and exit | |
| function syntaxError(message) { | |
| console.log(message); | |
| console.log(); | |
| showHelp(1); | |
| } | |
| // display runtime error message and exit | |
| function runtimeError(err) { | |
| console.log(`Error: ${err.message}`); | |
| process.exit(err.errno); | |
| } | |
| // detect file existance and its type | |
| async function getFileType(localPath) { | |
| try { | |
| let stats = await fs.promises.stat(localPath); | |
| return stats.isFile() ? 'file' | |
| : stats.isDirectory() ? 'directory' | |
| : stats.isBlockDevice() ? 'block device' | |
| : stats.isCharacterDevice() ? 'character device' | |
| : stats.isFIFO() ? 'fifo' | |
| : stats.isSocket() ? 'socket' | |
| : stats.isSymbolicLink() ? 'symbolic link' | |
| : 'unknown'; | |
| } catch (err) { | |
| return err.code === 'ENOENT' ? 'not exist' : 'unknown error'; | |
| } | |
| } | |
| // check for a normal file | |
| async function isFile(localPath) { | |
| return (await getFileType(localPath)) === 'file'; | |
| } | |
| // check for a directory | |
| async function isDirectory(localPath) { | |
| return (await getFileType(localPath)) === 'directory'; | |
| } | |
| // Server settings with default values | |
| let PORT = 8000; // port number | |
| let DIRECTORY = process.cwd(); // local root directory | |
| // command line parser | |
| async function parseArgv() { | |
| let argv = process.argv.slice(2); | |
| while (argv.length > 0) { | |
| let arg = argv.shift(); | |
| if (arg === '-h' || arg === '--help' || arg === '-?' || arg === '?') { | |
| showHelp(0); | |
| } else if (arg === '-d' || arg === '--directory') { | |
| // parse DIRECTORY | |
| if (argv.length === 0) { | |
| syntaxError('Error (-d, --directory): DIRECTORY is not specified'); | |
| } | |
| arg = argv.shift(); | |
| const dir = path.isAbsolute(arg) ? path.join(arg) : path.join(process.cwd(), arg); | |
| if (await isDirectory(dir)) { | |
| DIRECTORY= dir; | |
| } else { | |
| syntaxError(`Error (-d, --directory): ${arg} is not a directory`); | |
| } | |
| } else { | |
| // parse PORT | |
| const port = Number(arg); | |
| if (Number.isNaN(port)) { | |
| syntaxError(`Error: invalid option or argument: ${arg}`); | |
| } | |
| if (Number.isInteger(port) && port >= 0 && port < 65536) { | |
| PORT = port; | |
| } | |
| if (argv.length > 0) { | |
| syntaxError(`Error: unused argument/option: ${argv.join(' ')}`); | |
| } | |
| } | |
| } | |
| } | |
| // analize URL and lookup local file/directory | |
| async function lookupFile(url) { | |
| let statusCode = 404; // defaults to 'Not Found' | |
| let localPath = null; // used on 200 OK | |
| let listing = false; // used on 200 OK | |
| let location = null; // used on 302 Found | |
| if (url.endsWith('/')) { | |
| localPath = path.join(DIRECTORY, url); | |
| if (await isDirectory(localPath)) { | |
| statusCode = 200; | |
| const index = path.join(localPath, 'index.html'); | |
| if (await isFile(index)) { | |
| localPath = index; | |
| } else { | |
| listing = true; | |
| } | |
| } | |
| } else { | |
| localPath = path.join(DIRECTORY, url); | |
| switch (await getFileType(localPath)) { | |
| case 'file': | |
| statusCode = 200; | |
| break; | |
| case 'directory': | |
| statusCode = 302; | |
| location = `${encodeURI(url)}/`; | |
| break; | |
| case 'not exist': | |
| localPath = `${localPath}.html`; | |
| if (await isFile(localPath)) { | |
| statusCode = 200; | |
| } | |
| break; | |
| } | |
| } | |
| return { statusCode, localPath, listing, location }; | |
| } | |
| // Serve directory listing | |
| async function serveDirectory(res, url, localPath) { | |
| res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); | |
| res.write("<!DOCTYPE HTML>\n"); | |
| res.write("<html lang=\"en\">\n"); | |
| res.write("<head>\n"); | |
| res.write("<meta charset=\"utf-8\">\n"); | |
| res.write(`<title>Directory listing for ${url}</title>\n`); | |
| res.write("<body>\n"); | |
| res.write(`<h1>Directory listing for ${url}</h1>\n`); | |
| res.write("<hr>\n"); | |
| res.write("<ul>\n"); | |
| try { | |
| for (const file of await fs.promises.readdir(localPath)) { | |
| res.write(`<li><a href="${file}">${file}</a></li>`); | |
| } | |
| } catch (err) { | |
| res.write(`<p>Error: ${err}</p>\n`); | |
| } | |
| res.write("</ul>\n"); | |
| res.write("<hr>\n"); | |
| res.write("</body>\n"); | |
| res.end("</html>\n"); | |
| } | |
| // Serve file | |
| function serveFile(res, localPath) { | |
| const ext = path.extname(localPath).slice(1).toLowerCase(); | |
| let mimeType = MIME_TYPES[ext] || MIME_TYPES.default; | |
| if (mimeType.startsWith('text/')) { | |
| mimeType += '; charset=utf-8'; | |
| } | |
| res.writeHead(200, { 'Content-Type': mimeType }); | |
| fs.createReadStream(localPath).pipe(res); | |
| } | |
| // HTTP request handler | |
| async function handleRequest(req, res) { | |
| const url = decodeURI(req.url).replace(/\?.*$/, ''); | |
| const { statusCode, localPath, listing, location } = await lookupFile(url); | |
| const message = `${statusCode} ${STATUS_CODES[statusCode]}`; | |
| switch (statusCode) { | |
| case 200: | |
| if (listing) { | |
| await serveDirectory(res, url, localPath); | |
| } else { | |
| serveFile(res, localPath); | |
| } | |
| break; | |
| case 302: | |
| res.writeHead(302, { 'Content-Type': 'text/plain', 'Location': location }); | |
| res.end(message); | |
| break; | |
| case 404: | |
| default: | |
| res.writeHead(404, { 'Content-Type': 'text/plain' }); | |
| res.end(message); | |
| break; | |
| } | |
| console.log(`${(new Date).toLocaleString()} ${req.method} ${req.url} → ${message}`); | |
| } | |
| // MAIN | |
| // ---- | |
| await parseArgv(); | |
| try { | |
| const server = http.createServer(handleRequest); | |
| server.on('error', function(err) { runtimeError(err) }); | |
| server.listen(PORT); | |
| console.log('Serving HTTP (Ctrl-C to exit)'); | |
| console.log('────────────────────────────────────'); | |
| console.log(`- local: http://localhost:${PORT}`); | |
| console.log(`- Network: http://127.0.0.1:${PORT}`); | |
| console.log('────────────────────────────────────'); | |
| } catch (err) { | |
| runtimeError(err); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment