From 1b5af6f4bdfca939e780ae70bd6e1819425824f7 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sat, 2 May 2020 15:32:36 +0200 Subject: move code, server example :memo: --- example.js | 3 -- examples/server.js | 37 ++++++++++++++++++++++++ index.js | 78 ++----------------------------------------------- lib/parser.js | 78 ------------------------------------------------- lib/request-parser.js | 75 +++++++++++++++++++++++++++++++++++++++++++++++ lib/util.js | 14 +++++++++ package.json | 4 ++- server.js | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 212 insertions(+), 157 deletions(-) delete mode 100644 example.js create mode 100644 examples/server.js delete mode 100644 lib/parser.js create mode 100644 lib/request-parser.js create mode 100644 lib/util.js create mode 100644 server.js diff --git a/example.js b/example.js deleted file mode 100644 index eba3ac4..0000000 --- a/example.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict' - -// todo diff --git a/examples/server.js b/examples/server.js new file mode 100644 index 0000000..bff88e3 --- /dev/null +++ b/examples/server.js @@ -0,0 +1,37 @@ +'use strict' + +const createCert = require('create-cert') +const { + createServer: createGeminiServer, + DEFAULT_PORT, +} = require('..') + +const onRequest = (req, res) => { + if (req.path === '/foo') { + res.write('foo,') + res.end('bar') + } else { + res.gone() + } +} + +const onError = (err) => { + console.error(err) + process.exit(1) +} + +createCert('example.org') +.then((keys) => { + const server = createGeminiServer({ + tlsOpt: keys, + // todo: SNICallback + }, onRequest) + server.on('error', onError) + + server.listen(DEFAULT_PORT, (err) => { + if (err) return onError(err) + const {address, port} = server.address() + console.info(`listening on ${address}:${port}`) + }) +}) +.catch(onError) diff --git a/index.js b/index.js index 470b21d..ee44e81 100644 --- a/index.js +++ b/index.js @@ -1,78 +1,6 @@ 'use strict' -const {createServer: createTlsServer} = require('tls') -const {EventEmitter} = require('events') -const {createParser} = require('./lib/parser') -const createResponse = require('./lib/response') - -const ALPN_ID = 'gemini' // todo: clarify this - -const createGeminiServer = (opt = {}, onRequest) => { - if (typeof opt === 'function') { - onRequest = opt - opt = {} - } - const { - tlsOpt, - } = { - tlsOpt: {}, - ...opt, - } - - const onConnection = (socket) => { - const req = createParser() - socket.pipe(req) - socket.once('error', (err) => { - socket.unpipe(req) - req.destroy(err) - }) - - const close = () => { - socket.destroy() - req.destroy() - } - let timeout = setTimeout(close, 20 * 1000) - - req.once('header', (header) => { - clearTimeout(timeout) - - // prepare req - req.socket = socket - req.url = header.url - const url = new URL(header.url, 'http://foo/') - req.path = url.pathname - // todo: req.abort(), req.destroy() - - // prepare res - const res = createResponse() - res.pipe(socket) - res.once('error', (err) => { - console.error('error', err) - res.unpipe(socket) - socket.destroy(err) - }) - Object.defineProperty(res, 'socket', {value: socket}) - - onRequest(req, res) - server.emit('request', req, res) - }) - } - - const server = createTlsServer({ - // ALPNProtocols: [ALPN_ID], - // https://gemini.circumlunar.space/docs/spec-spec.txt#1.4.1 - // > Servers MUST use TLS version 1.2 or higher and SHOULD use TLS version - // > 1.3 or higher. - minVersion: 'TLSv1.2', - ...tlsOpt, - }, onConnection) - - return server +module.exports = { + createServer: require('./server'), + ...require('./lib/util'), } - -// https://gemini.circumlunar.space/docs/spec-spec.txt, 1. -// > When Gemini is served over TCP/IP, servers should listen on port 1965 -// > (the first manned Gemini mission, Gemini 3, flew in March '65). -createGeminiServer.DEFAULT_PORT = 1965 - -module.exports = createGeminiServer diff --git a/lib/parser.js b/lib/parser.js deleted file mode 100644 index a66a323..0000000 --- a/lib/parser.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict' - -const {Transform} = require('stream') -const {MESSAGES} = require('./statuses') - -// https://gemini.circumlunar.space/docs/spec-spec.txt, 1.2 -// > Gemini requests are a single CRLF-terminated line with the -// > following structure: -// > is a UTF-8 encoded absolute URL, of maximum length -// > 1024 bytes. [...] - -const CRLF = '\r\n' -const MAX_HEADER_SIZE = 1024 + CRLF.length - -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 iCRLF = peek.indexOf(CRLF) - if (iCRLF < 0) return invalid() - - const url = peek.slice(0, iCRLF).toString('utf8') - headerParsed = true - out.emit('header', {url}) - - 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('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/request-parser.js b/lib/request-parser.js new file mode 100644 index 0000000..04c5cd6 --- /dev/null +++ b/lib/request-parser.js @@ -0,0 +1,75 @@ +'use strict' + +const {Transform} = require('stream') +const {MESSAGES} = require('./statuses') + +// https://gemini.circumlunar.space/docs/spec-spec.txt, 1.2 +// > Gemini requests are a single CRLF-terminated line with the +// > following structure: +// > is a UTF-8 encoded absolute URL, of maximum length +// > 1024 bytes. [...] + +const CRLF = '\r\n' +const MAX_HEADER_SIZE = 1024 + CRLF.length + +const createRequestParser = () => { + 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 iCRLF = peek.indexOf(CRLF) + if (iCRLF < 0) return invalid() + + const url = peek.slice(0, iCRLF).toString('utf8') + headerParsed = true + out.emit('header', {url}) + + 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 = createRequestParser() +// 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('gemini://examp')) +// p.write(b('le.org/foo?bar#baz\r\nhel')) +// p.end(b('lo server!')) + +module.exports = createRequestParser diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..3d46bfb --- /dev/null +++ b/lib/util.js @@ -0,0 +1,14 @@ +'use strict' + +// todo: clarify if ALPN is wanted & if this ID is correct +const ALPN_ID = 'gemini' + +// https://gemini.circumlunar.space/docs/spec-spec.txt, 1. +// > When Gemini is served over TCP/IP, servers should listen on port 1965 +// > (the first manned Gemini mission, Gemini 3, flew in March '65). +const DEFAULT_PORT = 1965 + +module.exports = { + ALPN_ID, + DEFAULT_PORT, +} diff --git a/package.json b/package.json index a7c8fa3..e2fe031 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,9 @@ "main": "index.js", "files": [ "index.js", + "server.js", "lib", - "example.js" + "examples" ], "keywords": [ "gemini", @@ -24,6 +25,7 @@ "dependencies": { }, "devDependencies": { + "create-cert": "^1.0.6" }, "scripts": { } diff --git a/server.js b/server.js new file mode 100644 index 0000000..344728c --- /dev/null +++ b/server.js @@ -0,0 +1,80 @@ +'use strict' + +const {createServer: createTlsServer} = require('tls') +const {EventEmitter} = require('events') +const createParser = require('./lib/request-parser') +const createResponse = require('./lib/response') +const {ALPN_ID} = require('./lib/util') + +const createGeminiServer = (opt = {}, onRequest) => { + if (typeof opt === 'function') { + onRequest = opt + opt = {} + } + const { + tlsOpt, + } = { + tlsOpt: {}, + ...opt, + } + + const onConnection = (socket) => { + const req = createParser() + socket.pipe(req) + socket.once('error', (err) => { + socket.unpipe(req) + req.destroy(err) + }) + + const close = () => { + socket.destroy() + req.destroy() + } + let timeout = setTimeout(close, 20 * 1000) + + req.once('header', (header) => { + clearTimeout(timeout) + + // prepare req + req.socket = socket + req.url = header.url + const url = new URL(header.url, 'http://foo/') + req.path = url.pathname + // todo: req.abort(), req.destroy() + + // prepare res + const res = createResponse() + res.pipe(socket) + res.once('error', (err) => { + console.error('error', err) + res.unpipe(socket) + socket.destroy(err) + }) + Object.defineProperty(res, 'socket', {value: socket}) + + onRequest(req, res) + server.emit('request', req, res) + }) + } + + const server = createTlsServer({ + ALPNProtocols: [ALPN_ID], + // https://gemini.circumlunar.space/docs/spec-spec.txt, 1.4.1 + // > Servers MUST use TLS version 1.2 or higher and SHOULD use TLS version + // > 1.3 or higher. + minVersion: 'TLSv1.2', + requestCert: !!alwaysRequireClientCert, + // > Gemini requests typically will be made without a client + // > certificate being sent to the server. If a requested resource + // > is part of a server-side application which requires persistent + // > state, a Gemini server can [...] request that the client repeat + // the request with a "transient certificate" to initiate a client + // > certificate section. + rejectUnauthorized: false, + ...tlsOpt, + }, onConnection) + + return server +} + +module.exports = createGeminiServer -- cgit v1.3