nfp_sites/saproxy/api/proxy.mjs

177 lines
4.1 KiB
JavaScript

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)
}
}