diff options
| author | Jannis R <mail@jannisr.de> | 2020-05-02 02:54:13 +0200 |
|---|---|---|
| committer | Jannis R <mail@jannisr.de> | 2020-05-02 16:35:49 +0200 |
| commit | 40909d0f57ccfb246c4b7d887c4763a70157567e (patch) | |
| tree | 28356f69835d76ce9e852cea28f764377f85efcf /lib/parser.js | |
| parent | fff4d0666f44781a7998174dbc700322f496676d (diff) | |
basic req parser
Diffstat (limited to 'lib/parser.js')
| -rw-r--r-- | lib/parser.js | 98 |
1 files changed, 98 insertions, 0 deletions
diff --git a/lib/parser.js b/lib/parser.js new file mode 100644 index 0000000..956c12e --- /dev/null +++ b/lib/parser.js @@ -0,0 +1,98 @@ +'use strict' + +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. + +const CRLF = '\r\n' +const MAX_HEADER_SIZE = 2048 // cutoff + +const createParser = () => { + let headerParsed = false + let peek = Buffer.alloc(0) + + const invalid = () => { + peek = null + out.destroy(new Error('invalid Gemini request')) + } + + const onData = (data) => { + if (headerParsed) { + out.push(data) + return; + } + + peek = Buffer.concat([peek, data], peek.length + data.length) + if ( + data.indexOf(CRLF) < 0 && + 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() + + headerParsed = true + out.emit('header', { + type: ( + statusCode >= 10 && statusCode < 20 + ? 'request' : 'response' + ), + statusCode, statusMsg, + meta, + }) + + const iBody = iCRLF + 2 + if (peek.length > (iBody + 1)) { + // `data` contains the beginning of the body + out.push(peek.slice(iBody)) + } + peek = null // allow garbage collection + } + + const out = new Transform({ + write: (chunk, _, cb) => { + onData(chunk) + cb() + }, + writev: (chunks, cb) => { + for (let i = 0; i < chunks.length; i++) { + onData(chunks[i].chunk) + } + cb() + }, + }) + return out +} + +// const p = createParser() +// p.on('error', console.error) +// 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('le.org/foo?bar#baz\r\nhel')) +// p.end(b('lo server!')) + +module.exports = { + MESSAGES, + createParser, +} |
