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 | |
| parent | fff4d0666f44781a7998174dbc700322f496676d (diff) | |
basic req parser
| -rw-r--r-- | lib/parser.js | 98 | ||||
| -rw-r--r-- | lib/statuses.js | 59 | ||||
| -rw-r--r-- | package.json | 1 |
3 files changed, 158 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, +} diff --git a/lib/statuses.js b/lib/statuses.js new file mode 100644 index 0000000..a81f283 --- /dev/null +++ b/lib/statuses.js @@ -0,0 +1,59 @@ +'use strict' + +const CODES = Object.create(null) +const MESSAGES = [] + +// https://gemini.circumlunar.space/docs/spec-spec.txt, Appendix 1 +// https://github.com/michael-lazar/jetforce/blob/5e0fd57f936112746b13b78db807e5bb9e9c94bc/jetforce.py#L76-L106 +const INPUT = CODES.INPUT = 10 +MESSAGES[INPUT] = 'INPUT' + +const SUCCESS = CODES.SUCCESS = 20 +MESSAGES[SUCCESS] = 'SUCCESS' +const SUCCESS_END_OF_SESSION = CODES.SUCCESS_END_OF_SESSION = 21 +MESSAGES[SUCCESS_END_OF_SESSION] = 'SUCCESS – END OF SESSION' + +const REDIRECT_TEMPORARY = CODES.REDIRECT_TEMPORARY = 30 +MESSAGES[REDIRECT_TEMPORARY] = 'REDIRECT – TEMPORARY' +const REDIRECT_PERMANENT = CODES.REDIRECT_PERMANENT = 31 +MESSAGES[REDIRECT_PERMANENT] = 'REDIRECT – PERMANENT' + +const TEMPORARY_FAILURE = CODES.TEMPORARY_FAILURE = 40 +MESSAGES[TEMPORARY_FAILURE] = 'TEMPORARY FAILURE' +const SERVER_UNAVAILABLE = CODES.SERVER_UNAVAILABLE = 41 +MESSAGES[SERVER_UNAVAILABLE] = 'SERVER UNAVAILABLE' +const CGI_ERROR = CODES.CGI_ERROR = 42 +MESSAGES[CGI_ERROR] = 'CGI ERROR' +const PROXY_ERROR = CODES.PROXY_ERROR = 43 +MESSAGES[PROXY_ERROR] = 'PROXY ERROR' +const SLOW_DOWN = CODES.SLOW_DOWN = 44 +MESSAGES[SLOW_DOWN] = 'SLOW DOWN' + +const PERMANENT_FAILURE = CODES.PERMANENT_FAILURE = 50 +MESSAGES[PERMANENT_FAILURE] = 'PERMANENT FAILURE' +const NOT_FOUND = CODES.NOT_FOUND = 51 +MESSAGES[NOT_FOUND] = 'NOT FOUND' +const GONE = CODES.GONE = 52 +MESSAGES[GONE] = 'GONE' +const PROXY_REQUEST_REFUSED = CODES.PROXY_REQUEST_REFUSED = 53 +MESSAGES[PROXY_REQUEST_REFUSED] = 'PROXY REQUEST REFUSED' +const BAD_REQUEST = CODES.BAD_REQUEST = 59 +MESSAGES[BAD_REQUEST] = 'BAD REQUEST' + +const CLIENT_CERT_REQUIRED = CODES.CLIENT_CERT_REQUIRED = 60 +MESSAGES[CLIENT_CERT_REQUIRED] = 'CLIENT CERTIFICATE REQUIRED' +const TRANSIENT_CERT_REQUESTED = CODES.TRANSIENT_CERT_REQUESTED = 61 +MESSAGES[TRANSIENT_CERT_REQUESTED] = 'TRANSIENT CERTIFICATE REQUESTED' +const AUTHORISED_CERT_REQUIRED = CODES.AUTHORISED_CERT_REQUIRED = 62 +MESSAGES[AUTHORISED_CERT_REQUIRED] = 'AUTHORISED CERTIFICATE REQUIRED' +const CERTIFICATE_NOT_ACCEPTED = CODES.CERTIFICATE_NOT_ACCEPTED = 63 +MESSAGES[CERTIFICATE_NOT_ACCEPTED] = 'CERTIFICATE NOT ACCEPTED' +const FUTURE_CERT_REJECTED = CODES.FUTURE_CERT_REJECTED = 64 +MESSAGES[FUTURE_CERT_REJECTED] = 'FUTURE CERTIFICATE REJECTED' +const EXPIRED_CERT_REJECTED = CODES.EXPIRED_CERT_REJECTED = 65 +MESSAGES[EXPIRED_CERT_REJECTED] = 'EXPIRED CERTIFICATE REJECTED' + +module.exports = { + CODES, + MESSAGES, +} diff --git a/package.json b/package.json index ed35932..a7c8fa3 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "index.js", "files": [ "index.js", + "lib", "example.js" ], "keywords": [ |
