@derhuerst/gemini
Version:
Experimental Gemini server & client.
98 lines (85 loc) • 2.73 kB
JavaScript
import {Transform} from 'node:stream'
import {CODES} from './statuses.js'
// 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 createGeminiResponse = () => {
let headerSent = false
const _sendHeader = () => {
if (!res.writable) {
// todo: debug-log: "response has already been closed/destroyed, cannot send header"
return;
}
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
res.sendHeader = sendHeader
// convenience API
res.prompt = (promptMsg) => {
if (typeof promptMsg !== 'string') throw new Error('invalid promptMsg')
sendHeader(CODES.INPUT, promptMsg)
}
res.redirect = (url, permanent = false) => {
sendHeader(permanent ? CODES.REDIRECT_PERMANENT : CODES.REDIRECT_TEMPORARY, url)
}
res.proxyError = (msg) => {
if (typeof msg !== 'string') throw new Error('invalid msg')
sendHeader(CODES.PROXY_ERROR, msg)
}
res.slowDown = (waitForSeconds) => {
if (!Number.isInteger(waitForSeconds)) {
throw new Error('invalid waitForSeconds')
}
sendHeader(CODES.SLOW_DOWN, waitForSeconds + '')
}
res.notFound = () => {
sendHeader(CODES.NOT_FOUND)
}
res.gone = () => {
sendHeader(CODES.GONE)
}
res.badRequest = (msg) => {
if (typeof msg !== 'string') throw new Error('invalid msg')
sendHeader(CODES.BAD_REQUEST, msg)
}
res.requestTransientClientCert = (reason) => {
if (typeof reason !== 'string') throw new Error('invalid reason')
sendHeader(CODES.TRANSIENT_CERT_REQUESTED, reason)
}
res.requestAuthorizedClientCert = (reason) => {
if (typeof reason !== 'string') throw new Error('invalid reason')
sendHeader(CODES.AUTHORISED_CERT_REQUIRED, reason)
}
return res
}
export {
createGeminiResponse as createResponse,
}