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 => new Promise((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 = ` IIch.space
` .replaceAll(/ +/g, ' ') .replaceAll('\n', ''); const footer = `
` .replaceAll(/ +/g, ' ') .replaceAll('\n', ''); const tags: Record = { '"': '"', '&': '&', "'": ''', '<': '<', '>': '>', }; 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 = false; } else { parts.push(`
${clean}
`); } } else { if (line.startsWith('### ')) { parts.push(`

${clean.slice(4)}

`); } else if (line.startsWith('## ')) { parts.push(`

${clean.slice(3)}

`); } else if (line.startsWith('# ')) { parts.push(`

${clean.slice(2)}

`); } else if (line.startsWith('=> ')) { const [, href, ...rest] = clean.split(' '); let label = rest.join(' '); if (label === '') { label = href; } parts.push(`${label}`); } else if (line.startsWith('```')) { parts.push('
');
        pre = !pre;
      } else {
        parts.push(`

${clean}

`); } } } 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(` `); 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 { 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(` `); } else { res.writeHead(404); 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'); }); };