💾 Archived View for iich.space › src › gateway › index.ts captured on 2022-03-01 at 15:59:55.
⬅️ Previous capture (2021-12-03)
-=-=-=-=-=-=-
import { readFileSync } from 'fs'; import { createServer as httpCreateServer } from 'http'; import { createServer as httpsCreateServer } from 'https'; import { connect } from 'tls'; import { URL } from 'url'; import { createLogger } from '@/log'; import { createBan } from '~/db/admin/queries'; import { hasBan } from '~/db/queries'; import { generateHash } from '~/util/hash'; const BLACKLIST = ['wp-includes', 'phpunit', 'microsoft', 'aspx', '.env']; const log = createLogger(); const request = async (path: string): Promise<Buffer> => new Promise<Buffer>((ok) => { let response = Buffer.from([]); const url = path.slice(0, 1) === '/' ? `gemini://localhost:1966${path}` : path; const socket = connect( { host: 'localhost', port: 1965, rejectUnauthorized: false }, () => { socket.write(url + '\r\n'); }, ) .on('data', (data) => (response = Buffer.concat([response, data]))) .on('end', () => ok(response)); }); const GEMINI = '♊︎'; const stylesheet = readFileSync('src/gateway/style.css'); const header = ` <html> <head> <title>IIch.space</title> <link href="https://fonts.googleapis.com/css?family=Nunito:200,400,600,800&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Fira+Mono&display=swap" rel="stylesheet"> <meta name="viewport" content="initial-scale=1.0"> <link href="/style.css" rel="stylesheet"> </head> <body> <div id="header"> <div class="container"> <a href="/">${GEMINI} IIch.space</a> </div> </div> <div class="content container"> ` .replaceAll(/ +/g, ' ') .replaceAll('\n', ''); const footer = ` </div> <script> function replace(e) { e.preventDefault(); if (this.nextSibling.tagName === "IMG") { this.nextSibling.remove(); } else { const img = document.createElement('img'); img.src = this.href; this.parentElement.insertBefore(img, this.nextSibling); } } document.querySelectorAll('a[href*=png]').forEach((node) => node.onclick = replace); </script> </body> </html> ` .replaceAll(/ +/g, ' ') .replaceAll('\n', ''); const tags: Record<string, string> = { '"': '"', '&': '&', "'": ''', '<': '<', '>': '>', }; const escapeHTML = (str: string): string => str.replace(/[&<>'"]/g, (tag) => tags[tag]); const generateHTML = (page: string) => { const lines = page.split('\n'); const parts = []; let pre = false; for (const line of lines) { const clean = escapeHTML(line); if (pre) { if (line.startsWith('```')) { parts.push('</pre>'); pre = false; } else { parts.push(`<div>${clean}</div>`); } } else { if (line.startsWith('### ')) { parts.push(`<h3>${clean.slice(4)}</h3>`); } else if (line.startsWith('## ')) { parts.push(`<h2>${clean.slice(3)}</h2>`); } else if (line.startsWith('# ')) { parts.push(`<h1>${clean.slice(2)}</h1>`); } else if (line.startsWith('=> ')) { const [, href, ...rest] = clean.split(' '); let label = rest.join(' '); if (label === '') { label = href; } parts.push(`<a href="${href}">${label}</a>`); } else if (line.startsWith('```')) { parts.push('<pre>'); pre = !pre; } else { parts.push(`<p>${clean}</p>`); } } } return parts.join(''); }; const redirectServer = httpCreateServer((req, res) => { const remote = generateHash(req.socket.remoteAddress!); log.info(remote, req.url); const url = new URL(req.url || '/', `http://${req.headers.host}`); url.protocol = 'https'; res.writeHead(302, url.href); res.write(` <head> <meta http-equiv="Refresh" content="0; URL=${url.href}"> </head> `); res.end(); }); const server = httpsCreateServer( { cert: readFileSync(process.env.SSL_CERT_PATH!), key: readFileSync(process.env.SSL_KEY_PATH!), }, async (req, res) => { try { const remote = generateHash(req.socket.remoteAddress!); if (hasBan(remote)) { res.writeHead(500); res.end('ip banned'); return; } if (req.url) { if (req.url === '/style.css') { res.write(stylesheet); res.end(); return; } if (req.url === '/favicon.ico') { res.writeHead(404); res.end('404'); return; } log.info(remote, req.url); const urlSearch = req.url.toLowerCase(); for (const term of BLACKLIST) { if (urlSearch.includes(term)) { createBan(remote); res.writeHead(500); res.end('blacklisted url - ip banned'); log.info('ip banned', remote); return; } } const href = `https://${req.headers.host}${req.url}`; const url = new URL(href); url.search = ''; const response = await request(url.href); const index = response.indexOf('\r\n'); const status = response.slice(0, index); const body = response.slice(index + 2); const [code, meta] = status.toString().split(' '); if (code === '20') { if (meta === 'image/png') { res.writeHead(200, { 'Content-length': body.length, 'Content-type': 'image/png', }); res.write(body); } else if (meta !== 'text/gemini') { res.writeHead(200, { 'Content-type': `${meta}; charset=utf-8`, }); res.write(body); } else { res.setHeader('Content-type', 'text/html; charset=utf-8'); res.write(header); res.write(generateHTML(body.toString())); res.write(footer); } } else if (code.startsWith('1') || code.startsWith('3')) { const redirect = `gemini://${url.hostname}${url.pathname}`; res.writeHead(302, redirect); res.write(` <head> <meta http-equiv="Refresh" content="0; URL=${redirect}"> </head> `); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('404'); return; } } } catch (e) { console.error(e); } res.end(); }, ); export const startGateway = (): void => { redirectServer.listen(1080, () => { log.info('http server started and listening on port 1080'); }); server.listen(1443, () => { log.info('https server started and listening on port 1443'); }); };