summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/parser.js38
-rw-r--r--lib/response.js69
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