saproxy: Created basic proxy service
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
This commit is contained in:
parent
31f2ecc09b
commit
840f23908e
9 changed files with 427 additions and 0 deletions
1
saproxy/.npmrc
Normal file
1
saproxy/.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
package-lock=false
|
35
saproxy/api/config.mjs
Normal file
35
saproxy/api/config.mjs
Normal file
|
@ -0,0 +1,35 @@
|
|||
import Nconf from 'nconf-lite'
|
||||
|
||||
const nconf = new Nconf()
|
||||
|
||||
// Helper method for global usage.
|
||||
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
|
||||
|
||||
// Config follow the following priority check order:
|
||||
// 1. Enviroment variables
|
||||
// 2. package.json
|
||||
// 3. config/config.json
|
||||
// 4. config/config.default.json
|
||||
|
||||
// Load enviroment variables as first priority
|
||||
nconf.env({
|
||||
separator: '__',
|
||||
whitelist: [
|
||||
'NODE_ENV',
|
||||
],
|
||||
parseValues: true,
|
||||
})
|
||||
|
||||
|
||||
// Load empty overrides that can be overwritten later
|
||||
nconf.overrides({})
|
||||
|
||||
nconf.defaults({
|
||||
"NODE_ENV": "development",
|
||||
"frontend": {
|
||||
"url": "http://beta01.nfp.moe"
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
export default nconf
|
176
saproxy/api/proxy.mjs
Normal file
176
saproxy/api/proxy.mjs
Normal file
|
@ -0,0 +1,176 @@
|
|||
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)
|
||||
}
|
||||
}
|
109
saproxy/api/server.mjs
Normal file
109
saproxy/api/server.mjs
Normal file
|
@ -0,0 +1,109 @@
|
|||
import { Flaska, QueryHandler, JsonHandler } from 'flaska'
|
||||
import config from './config.mjs'
|
||||
import StaticRoutes from './static_routes.mjs'
|
||||
import Proxy from './proxy.mjs'
|
||||
|
||||
export default class Server {
|
||||
constructor(http, port, core, opts = {}) {
|
||||
Object.assign(this, opts)
|
||||
this.http = http
|
||||
this.port = port
|
||||
this.core = core
|
||||
|
||||
this.flaskaOptions = {
|
||||
appendHeaders: {
|
||||
'Content-Security-Policy': `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'`,
|
||||
},
|
||||
log: this.core.log,
|
||||
nonce: ['script-src'],
|
||||
nonceCacheLength: 50,
|
||||
}
|
||||
|
||||
this.init()
|
||||
}
|
||||
|
||||
init() { }
|
||||
|
||||
runCreateServer() {
|
||||
// Create our server
|
||||
this.flaska = new Flaska(this.flaskaOptions, this.http)
|
||||
|
||||
// configure our server
|
||||
console.log(config.get('NODE_ENV'))
|
||||
if (config.get('NODE_ENV') === 'development') {
|
||||
this.flaska.devMode()
|
||||
}
|
||||
|
||||
this.flaska.onerror(err => {
|
||||
console.log(err)
|
||||
ctx.log.error(err)
|
||||
ctx.status = 500
|
||||
})
|
||||
|
||||
this.flaska.before(function(ctx) {
|
||||
ctx.state.started = new Date().getTime()
|
||||
ctx.req.ip = ctx.req.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress
|
||||
ctx.log = ctx.log.child({
|
||||
id: Math.random().toString(36).substring(2, 14),
|
||||
})
|
||||
})
|
||||
|
||||
let healthChecks = 0
|
||||
let healthCollectLimit = 60 * 60 * 12
|
||||
|
||||
this.flaska.after(function(ctx) {
|
||||
if (ctx.aborted && ctx.status === 200) {
|
||||
ctx.status = 299
|
||||
}
|
||||
let ended = new Date().getTime()
|
||||
var requestTime = ended - ctx.state.started
|
||||
|
||||
let status = ''
|
||||
let level = 'info'
|
||||
if (ctx.status >= 400) {
|
||||
status = ctx.status + ' '
|
||||
level = 'warn'
|
||||
}
|
||||
if (ctx.status >= 500) {
|
||||
level = 'error'
|
||||
}
|
||||
|
||||
if (ctx.url === '/health' || ctx.url === '/api/health') {
|
||||
healthChecks++
|
||||
if (healthChecks >= healthCollectLimit) {
|
||||
ctx.log[level]({
|
||||
duration: Math.round(ended),
|
||||
status: ctx.status,
|
||||
}, `<-- ${status}${ctx.method} ${ctx.url} {has happened ${healthChecks} times}`)
|
||||
healthChecks = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.log[level]({
|
||||
duration: requestTime,
|
||||
status: ctx.status,
|
||||
ip: ctx.req.ip,
|
||||
}, (ctx.aborted ? '[ABORT]' : '<--') + ` ${status}${ctx.method} ${ctx.url}`)
|
||||
})
|
||||
}
|
||||
|
||||
registerRoutes() {
|
||||
var stat = new StaticRoutes()
|
||||
var proxy = new Proxy()
|
||||
stat.register(this)
|
||||
proxy.register(this)
|
||||
}
|
||||
|
||||
runStartListen() {
|
||||
return this.flaska.listenAsync(this.port).then(() => {
|
||||
this.core.log.info('Server is listening on port ' + this.port)
|
||||
})
|
||||
}
|
||||
|
||||
run() {
|
||||
this.runCreateServer()
|
||||
this.registerRoutes()
|
||||
return this.runStartListen()
|
||||
}
|
||||
}
|
17
saproxy/api/static_routes.mjs
Normal file
17
saproxy/api/static_routes.mjs
Normal file
|
@ -0,0 +1,17 @@
|
|||
import config from './config.mjs'
|
||||
|
||||
export default class StaticRoutes {
|
||||
constructor(opts = {}) {
|
||||
Object.assign(this, { })
|
||||
}
|
||||
|
||||
register(server) {
|
||||
server.flaska.get('/api/health', this.health.bind(this))
|
||||
}
|
||||
|
||||
health(ctx) {
|
||||
ctx.body = {
|
||||
environment: config.get('NODE_ENV'),
|
||||
}
|
||||
}
|
||||
}
|
8
saproxy/build-package.json
Normal file
8
saproxy/build-package.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"scripts": {
|
||||
"build": "echo done;"
|
||||
},
|
||||
"dependencies": {
|
||||
"service-core": "^3.0.0-beta.17"
|
||||
}
|
||||
}
|
24
saproxy/dev.mjs
Normal file
24
saproxy/dev.mjs
Normal file
|
@ -0,0 +1,24 @@
|
|||
import fs from 'fs'
|
||||
import { ServiceCore } from 'service-core'
|
||||
import * as index from './index.mjs'
|
||||
|
||||
const port = 4120
|
||||
|
||||
var core = new ServiceCore('nfp_moe', import.meta.url, port, '')
|
||||
|
||||
let config = {
|
||||
frontend: {
|
||||
url: 'http://localhost:' + port
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
config = JSON.parse(fs.readFileSync('./config.json'))
|
||||
} catch {}
|
||||
|
||||
config.port = port
|
||||
|
||||
core.setConfig(config)
|
||||
core.init(index).then(function() {
|
||||
return core.run()
|
||||
})
|
11
saproxy/index.mjs
Normal file
11
saproxy/index.mjs
Normal file
|
@ -0,0 +1,11 @@
|
|||
import config from './api/config.mjs'
|
||||
|
||||
export function start(http, port, ctx) {
|
||||
config.sources[1].store = ctx.config
|
||||
|
||||
return import('./api/server.mjs')
|
||||
.then(function(module) {
|
||||
let server = new module.default(http, port, ctx)
|
||||
return server.run()
|
||||
})
|
||||
}
|
46
saproxy/package.json
Normal file
46
saproxy/package.json
Normal file
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "saproxy",
|
||||
"version": "1.0.0",
|
||||
"port": 4130,
|
||||
"description": "Proxy guy for SA",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node --experimental-modules index.mjs",
|
||||
"dev:server": "node dev.mjs | bunyan",
|
||||
"dev": "npm-watch dev:server"
|
||||
},
|
||||
"watch": {
|
||||
"dev:server": {
|
||||
"patterns": [
|
||||
"api/*",
|
||||
"base/*",
|
||||
"../base/*"
|
||||
],
|
||||
"extensions": "js,mjs",
|
||||
"quiet": true,
|
||||
"inherit": true
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.nfp.is/nfp/nfp_sites.git"
|
||||
},
|
||||
"author": "Jonatan Nilsson",
|
||||
"license": "WTFPL",
|
||||
"bugs": {
|
||||
"url": "https://git.nfp.is/nfp/nfp_sites/issues"
|
||||
},
|
||||
"homepage": "https://git.nfp.is/nfp/nfp_sites",
|
||||
"dependencies": {
|
||||
"bunyan-lite": "^1.2.1",
|
||||
"flaska": "^1.3.3",
|
||||
"multipipe": "^4.0.0",
|
||||
"nconf-lite": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"service-core": "^3.0.0-beta.17"
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue