diff options
| -rw-r--r-- | lib/parser.js | 38 | ||||
| -rw-r--r-- | lib/response.js | 69 |
2 files changed, 78 insertions, 29 deletions
diff --git a/lib/parser.js b/lib/parser.js index 956c12e..a66a323 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -3,16 +3,14 @@ const {Transform} = require('stream') const {MESSAGES} = require('./statuses') -// https://gemini.circumlunar.space/docs/spec-spec.txt, 1.3.1 -// > Gemini response headers look like this: -// > <STATUS><whitespace><META><CR><LF> -// > <STATUS> is a two-digit numeric status code [...]. -// > <whitespace> is any non-zero number of consecutive spaces or tabs. -// > <META> is a UTF-8 encoded string of maximum length 1024, whose meaning is -// > <STATUS> dependent. +// https://gemini.circumlunar.space/docs/spec-spec.txt, 1.2 +// > Gemini requests are a single CRLF-terminated line with the +// > following structure: <URL><CR><LF> +// > <URL> is a UTF-8 encoded absolute URL, of maximum length +// > 1024 bytes. [...] const CRLF = '\r\n' -const MAX_HEADER_SIZE = 2048 // cutoff +const MAX_HEADER_SIZE = 1024 + CRLF.length const createParser = () => { let headerParsed = false @@ -35,30 +33,12 @@ const createParser = () => { peek.length < MAX_HEADER_SIZE ) return; // keep peeking - const statusCodeAndSpace = peek.slice(0, 3).toString('utf8') - if (!/\d{2} /.test(statusCodeAndSpace)) return invalid() const iCRLF = peek.indexOf(CRLF) if (iCRLF < 0) return invalid() - let statusCode = parseInt(statusCodeAndSpace) - let statusMsg = MESSAGES[statusCode] - if (!statusMsg) { - statusCode = Math.floor(statusCode / 10) * 10 - statusMsg = MESSAGES[statusCode] - if (!statusMsg) return invalid() - } - - const meta = peek.slice(3, iCRLF).toString('utf8').trim() - + const url = peek.slice(0, iCRLF).toString('utf8') headerParsed = true - out.emit('header', { - type: ( - statusCode >= 10 && statusCode < 20 - ? 'request' : 'response' - ), - statusCode, statusMsg, - meta, - }) + out.emit('header', {url}) const iBody = iCRLF + 2 if (peek.length > (iBody + 1)) { @@ -88,7 +68,7 @@ const createParser = () => { // p.on('header', h => console.log('header', h)) // p.on('data', d => console.log('data', d.toString('utf8'))) // const b = str => Buffer.from(str, 'utf8') -// p.write(b('12 gemini://examp')) +// p.write(b('gemini://examp')) // p.write(b('le.org/foo?bar#baz\r\nhel')) // p.end(b('lo server!')) diff --git a/lib/response.js b/lib/response.js new file mode 100644 index 0000000..ad27c75 --- /dev/null +++ b/lib/response.js @@ -0,0 +1,69 @@ +'use strict' + +const {Transform} = require('stream') +const {CODES} = require('./statuses') + +// https://gemini.circumlunar.space/docs/spec-spec.txt, 1.3.1 +// > Gemini response headers look like this: +// > <STATUS><whitespace><META><CR><LF> +// > <STATUS> is a two-digit numeric status code, as described below in +// > 1.3.2 and in Appendix 1. +// > <whitespace> is any non-zero number of consecutive spaces or tabs. +// > <META> is a UTF-8 encoded string of maximum length 1024, whose +// > meaning is <STATUS> dependent. + +const createResponse = () => { + let headerSent = false + const _sendHeader = () => { + if (typeof res.statusCode !== 'number') { + throw new Error('invalid res.statusCode') + } + const cat = Math.floor(res.statusCode / 10) + + if (cat === 2 && res.mimeType) { + res.meta = res.mimeType // todo: validate + } + + res.push(`${res.statusCode} ${res.meta}\r\n`) + headerSent = true + + if (cat !== 2) res.push(null) // end + } + + const sendHeader = (statusCode, meta = '') => { + if (headerSent) throw new Error('header already sent') + res.statusCode = statusCode + res.meta = meta + _sendHeader() + } + + const write = (chunk, _, cb) => { + if (!headerSent) _sendHeader() + res.push(chunk) + cb(null) + } + + const res = new Transform({write}) + res.statusCode = CODES.SUCCESS + res.meta = '' + res.mimeType = null + + // API + res.sendHeader = sendHeader + res.prompt = (promptMsg) => { + if (typeof promptMsg !== 'string') { + throw new Error('invalid promptMsg') + } + sendHeader(10, promptMsg) + res.push(null) + } + res.gone = () => { + sendHeader(51) + res.push(null) + } + // todo: redirect(), serverUnavailable(), slowDown(), badRequest() + + return res +} + +module.exports = createResponse |
