import https from 'https' import pipe from 'multipipe' import { Transform, compose } from 'stream' import config from './config.mjs' export default class Proxy { constructor(opts = {}) { Object.assign(this, { agent: opts.agent || new https.Agent({ keepAlive: true, maxSockets: 2, keepAliveMsecs: 1000 * 60 * 60, secureProtocol: 'TLSv1_2_method', rejectUnauthorized: false, }), cache: new Map(), defaultRemap: 'default', remap: new Map([ ['default', '/frettatengt/serstok-malefni/taktu-tvaer/'] ]) }) } register(server) { server.flaska.on404(this.proxy.bind(this)) } getTime(time) { if (!time) return 60 let m = time.match(/max-age=(\d+)/) if (!m) return 60 return Number(m[1]) || 60 } getVaryKey(req, vary) { if (!vary) { return 'null' } let out = [] for (let v of vary.split(',')) { out.push(req.headers[v.trim().toLowerCase()] || 'null') } return out.join(', ') } cleanHeaders(headers) { var set = new Set() for (let key of Object.keys(headers)) { if (set.has(key.toLowerCase())) { delete headers[key] } else { set.add(key) } } return headers } cloneHeaders(headers) { var out = {} for (let key of Object.keys(headers)) { if (key !== 'content-length' && key !== 'content-type') { out[key] = headers[key] } } return out } proxyCache(req, path, time, res) { let cache = this.cache.get(path) if (!cache) { this.cache.set(path, cache = { path: path, time: this.getTime(time), created: new Date(), status: res.statusCode, type: res.headers['content-type'], vary: (res.headers['vary'] || '').toLowerCase(), data: new Map() }) } let varyKey = this.getVaryKey(req, cache.vary) if (cache.data.has(varyKey)) return res let item = { headers: this.cleanHeaders(res.headers), data: [], done: false, } cache.data.set(varyKey, item) const capture = new Transform({ decodeStrings: false, destroy(err, cb) { if (!err) { if (item.data.length) { item.data = Buffer.concat(item.data) item.done = true } else { cache.data.delete(varyKey, item) } } cb(err) }, transform(chunk, encoding, callback) { item.data.push(chunk) callback(null, chunk); }, }); return pipe(res, capture) } getCache(req, path) { let cache = this.cache.get(path) if (!cache) return null if ((new Date() - cache.created) / 1000 > cache.time) { this.cache.delete(path) return null } let varyKey = this.getVaryKey(req, cache.vary) let data = cache.data.get(varyKey) if (!data) return null if (!data.done) return null return { varyKey: varyKey, res: cache, data: cache.data.get(varyKey) } } async proxy(ctx) { let url = ctx.req.url if (!url.startsWith('/_')) { url = this.remap.get(ctx.req.headers['host']) if (!url) { url = this.remap.get(this.defaultRemap) } url += ctx.req.url.slice(1) } let cache = this.getCache(ctx.req, url) if (cache) { ctx.status = cache.res.status ctx.type = cache.res.type ctx.body = cache.data.data ctx.headers = this.cloneHeaders(cache.data.headers) console.log('found cache', cache) return } ctx.req.headers.host = 'www.sa.is' var res = await new Promise((res, rej) => { var req = https.request({ hostname: 'www.sa.is', path: url, method: ctx.req.method, headers: ctx.req.headers, agent: this.agent, }, results => { res(results) }) req.on('error', rej) req.end() }) ctx.status = res.statusCode ctx.headers = this.cloneHeaders(res.headers) ctx.type = res.headers['content-type'] ctx.body = this.proxyCache(ctx.req, url, ctx.headers['cache-control'], res) } }