# Node.js Dumbserver

Dumbserver, or the simplest workable HTTP server. It is supposed to operate in the 'traditional' way, that is, mapping paths in requests to the directory structure.

Why did I write it: I needed a simple development server for a website I'm trying to make. Instead of using Express or something like that for this purpose, I thought I'd make my own simplistic server as an exercise.

I don't know Javascript very well and I suppose that especially here they may be multiple subtleties I don't know about and/or failed to handle. Pointing these out would be much appreciated.

Also I suppose that reading the whole file into memory and then writing it to the socket is a poor thing to do, I should instead pipe the file stream into the socket? But I'm not sure how to do this. createReadStream will only give me errors after I start reading it, but then it is too late: I need to set an erroneous HTTP code before I start writing to the response object ;/

const {createServer} = require('http');
const {join} = require('path')
const {getType} = require('mime/lite')

const argv = require('minimist')(process.argv.slice(2))

const dir = argv.www
const host = argv.host
const port = argv.port
if(dir === undefined || host == undefined || port == undefined) {
console.error(Usage: ${process.argv[1]} --www=C:\\directory\\of\\your\\webpage --host=example.com --port=80) process.exit(1) } function decodePathname(pathname, err) { try { return decodeURIComponent(pathname) } catch(e) { if(e instanceof(URIError)) { err.code = 400 return } else { throw e } } } function splitPathname(pathname, err) { const decodedPathname = decodePathname(pathname, err) if(decodedPathname) { return pathname.split('/') } } function constructOsPath(pathname, err) { const parts = splitPathname(pathname, err) if(parts) { let res = dir for(const part of parts) { res = join(res, part) // Defense against escaping from dir by stacking ..'s in request URL // Checking this on each step to prevent guessing directory structure by requests such as http://example.com/../../var/www/index.html if(!res.startsWith(dir)) { err.code = 403 return } } return {path: res, mime: getType(res)} } } async function getResponse(pathname, err) { const pathMime = constructOsPath(pathname, err) if(pathMime) { try { return {content: await readFile(pathMime.path), mime: pathMime.mime} } catch(e) { if(e instanceof(Error)) { if(e.code == 'ENOENT') { err.code = 404 return } else if(e.code == 'EACCESS') { err.code = 403 return } else if(e.code == 'EISDIR') { err.code = 422 // Not sure about code here? Is 422 appropriate really? return; } else { throw e } } else { throw e } } } } async function processRequest(req, res) { if(!['GET', 'HEAD'].includes(req.method)) { res.code = 501 return } const url = new URL(req.url, http://${req.headers.host})
const pathname = url.pathname

const err = {}

// Bad, I know, but idk what else can be done
// - I should do a createReadStream and then pipe this stream to the response!
//    - But then what if this stream errors out? I should send HTTP 500 but can't do so if I've already started sending a response
// - I should not read the file when responding to a HEAD request!
//    - I shouldn't, but otherwise it's hard to guarantee that HEAD returns same exact status as GET would
const contentMime = await getResponse(pathname, err)

if(contentMime) {
if(req.method == 'GET') {
res.end(contentMime.content)
} else if(req.method == 'HEAD') {
res.end()
}
} else {
res.statusCode = err.code
res.end()
}
}

const server = createServer((req, res) => {
const now = new Date(Date.now())
console.log(${now.toISOString()}${req.socket.remoteAddress} -> ${req.method}${req.url})

processRequest(req, res).catch(e => {
console.error(e)

res.code = 500
res.end()
})
})

server.on('error', e => console.error(e))

server.listen(port, host, () => console.log('Server listening'))