summaryrefslogtreecommitdiffstats
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
parentfff4d0666f44781a7998174dbc700322f496676d (diff)
basic req parser
-rw-r--r--lib/parser.js98
-rw-r--r--lib/statuses.js59
-rw-r--r--package.json1
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": [