summaryrefslogtreecommitdiffstats
path: root/lib/parser.js
diff options
context:
space:
mode:
authorJannis R <mail@jannisr.de>2020-05-02 02:54:13 +0200
committerJannis R <mail@jannisr.de>2020-05-02 16:35:49 +0200
commit40909d0f57ccfb246c4b7d887c4763a70157567e (patch)
tree28356f69835d76ce9e852cea28f764377f85efcf /lib/parser.js
parentfff4d0666f44781a7998174dbc700322f496676d (diff)
basic req parser
Diffstat (limited to 'lib/parser.js')
-rw-r--r--lib/parser.js98
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,
+}