Comparing version 0.4.3 to 0.4.4
@@ -8,2 +8,13 @@ # API Joe Changelog | ||
## [v0.4.4] - 2022-09-23 | ||
### Added | ||
- log entry when new services are discovered | ||
- forwarded client IP to all requests going to backend | ||
- feature to replace session | ||
### Fixed | ||
- proxy to always pass back all backend response headers | ||
- logout endpoint to not accept unauthenticated request | ||
## [v0.4.3] - 2022-06-08 | ||
@@ -10,0 +21,0 @@ |
@@ -5,3 +5,3 @@ const http = require('http'); | ||
pass({ path, res, log, query, routing, conf, body, headers, sessid, claim, method }){ | ||
pass({ path, res, log, query, routing, conf, body, headers, sessid, claim, method, ip }){ | ||
@@ -20,2 +20,4 @@ let target = routing.service.url + (routing.removePrefix | ||
newHeaders['x-joe-client-ip'] = ip; | ||
if(!conf.proxy.preserveCookies) | ||
@@ -34,11 +36,17 @@ delete newHeaders.cookie; | ||
const newReq = http.request(target, { method, headers: newHeaders }); | ||
body.stream.pipe(newReq); | ||
body.pipe(newReq); | ||
newReq.on('response', beRes => { | ||
/* istanbul ignore next */ | ||
if(beRes.headers['content-type']) | ||
res.type(beRes.headers['content-type']); | ||
res.status(beRes.statusCode); | ||
res.status(beRes.statusCode); | ||
beRes.pipe(res.resStream); | ||
let k; | ||
for(const s of beRes.rawHeaders) | ||
if(k){ | ||
res.set(k, s); | ||
k = undefined; | ||
} | ||
else | ||
k = s; | ||
beRes.pipe(res); | ||
}); | ||
@@ -48,3 +56,3 @@ | ||
res.status(503).end(); | ||
log.error({ ...err, target, class: 'proxy' }); | ||
log.error({ err, target, class: 'proxy' }); | ||
}); | ||
@@ -54,3 +62,3 @@ } | ||
res.status(503).end(); | ||
log.error({ ...err, target, class: 'proxy' }); | ||
log.error({ err, target, class: 'proxy' }); | ||
} | ||
@@ -57,0 +65,0 @@ } |
@@ -15,2 +15,7 @@ const { post, all } = require('nodecaf'); | ||
post('/mimic', async function({ call }){ | ||
await call(Session.match); | ||
call(Session.replace); | ||
}), | ||
all(async function({ call }){ | ||
@@ -17,0 +22,0 @@ call(routeRequest); |
@@ -46,2 +46,3 @@ const { pathToRegexp } = require('path-to-regexp'); | ||
services[serv.domain] = services[serv.name]; | ||
log.debug({ service: serv }, 'Discovered service %s', serv.name); | ||
} | ||
@@ -48,0 +49,0 @@ catch(err){ |
@@ -24,3 +24,2 @@ const { request, post } = require('muhb'); | ||
async create({ redisTimeout, cookieOpts, conf, res, redis, headers, body }){ | ||
body = await body.raw(); | ||
@@ -49,2 +48,3 @@ | ||
destroy({ cookieOpts, claim, sessid, res, redis, conf }){ | ||
res.unauthorized(!claim); | ||
res.clearCookie(conf.cookie.name, cookieOpts); | ||
@@ -54,4 +54,23 @@ res.end(); | ||
redis.publish('joe:logout', claim); | ||
}, | ||
async replace({ body, conf, headers, sessid, res, cookieOpts, redis, redisTimeout }){ | ||
res.unauthorized(!sessid, 'no session'); | ||
body = await body.raw(); | ||
const r = await request({ | ||
timeout: conf.replace.timeout, | ||
body, | ||
method: conf.replace.method, | ||
url: conf.replace.url, | ||
headers: { ...headers, ...conf.replace.headers, 'X-Joe-Sess-Id': sessid } | ||
}); | ||
res.badRequest(r.status !== 200, r.body); | ||
res.cookie(conf.cookie.name, sessid, cookieOpts).end(r.body); | ||
redis.set('joe:sess:' + sessid, r.body, { EX: redisTimeout }); | ||
} | ||
} |
{ | ||
"name": "api-joe", | ||
"version": "0.4.3", | ||
"version": "0.4.4", | ||
"description": "An API Gateway to easily expose your services to web clients", | ||
@@ -38,3 +38,3 @@ "main": "lib/main.js", | ||
"muhb": "^3.1.1", | ||
"nodecaf": "^0.13.1", | ||
"nodecaf": "^0.13.2", | ||
"nodecaf-redis": "^0.1.0", | ||
@@ -45,4 +45,4 @@ "path-to-regexp": "^6.2.1", | ||
"uuid": "^8.3.2", | ||
"ws": "^8.7.0" | ||
"ws": "^8.8.0" | ||
} | ||
} |
@@ -5,8 +5,9 @@ # [API Joe](https://gitlab.com/GCSBOSS/api-joe) | ||
## Get Started | ||
Our main features are: | ||
- Request routing based on various parameters such as path, headers and domain | ||
- Automatic SSL handling through Let's Encrypt | ||
- User session management | ||
- Outgoing events via WebSocket and redis publish | ||
- Replicable by offloading any state to shared remote redis db | ||
1. Install with: `npm i -g api-joe`. | ||
2. Setup your [services](#services). | ||
3. Run in the terminal with: `api-joe` (optional argument for the [config file](#configuration) path). | ||
## Get Started with Docker | ||
@@ -13,0 +14,0 @@ |
135
test/spec.js
@@ -5,3 +5,3 @@ /* eslint-env mocha */ | ||
const { v4: uuid } = require('uuid'); | ||
const { context } = require('muhb'); | ||
const muhb = require('muhb'); | ||
const Nodecaf = require('nodecaf'); | ||
@@ -14,2 +14,4 @@ const redis = require('nodecaf-redis'); | ||
const LOCAL_HOST = 'http://127.0.0.1:8236'; | ||
const SEC_LOCAL_HOST = 'https://localhost:8236'; | ||
const ACME_GATEWAY = process.env.ACME_GATEWAY || 'host.docker.internal'; | ||
@@ -28,25 +30,30 @@ const REDIS_HOST = process.env.REDIS_HOST || 'localhost'; | ||
let app; | ||
const base = context('http://127.0.0.1:8236'), secBase = context('https://localhost:8236'); | ||
const authProvider = new Nodecaf({ | ||
conf: { port: 8060, log: false }, | ||
conf: { port: 8060 }, | ||
autoParseBody: true, | ||
api({ post, get }){ | ||
routes: [ | ||
get('/', ({ res }) => res.end()) | ||
Nodecaf.get('/', ({ res }) => res.end()), | ||
post('/auth', ({ res, body }) => { | ||
Nodecaf.post('/auth', ({ res, body }) => { | ||
if(body) | ||
return res.status(400).end(); | ||
res.end('ID: ' + uuid()); | ||
}); | ||
}), | ||
get('/get', ({ res }) => res.end()); | ||
Nodecaf.post('/replace', ({ res, body }) => { | ||
if(body) | ||
return res.status(400).end(); | ||
res.end('ID: ' + uuid()); | ||
}), | ||
get('/say/:something', ({ params, res }) => res.text(params.something)); | ||
Nodecaf.get('/get', ({ res }) => res.end()), | ||
get('/headers', ({ headers, res }) => res.json(headers)); | ||
Nodecaf.get('/say/:something', ({ params, res }) => res.text(params.something)), | ||
get('/public-only', ({ res }) => res.end('public-only')); | ||
} | ||
Nodecaf.get('/headers', ({ headers, res }) => res.json(headers)), | ||
Nodecaf.get('/public-only', ({ res }) => res.end('public-only')) | ||
] | ||
}); | ||
@@ -70,3 +77,4 @@ | ||
services: SERVICES, | ||
auth: { url: 'http://localhost:8060/auth' } | ||
auth: { url: 'http://localhost:8060/auth' }, | ||
replace: { url: 'http://localhost:8060/replace' } | ||
}); | ||
@@ -84,3 +92,3 @@ await app.start(); | ||
it('Should boot just fine', async function(){ | ||
const { status } = await base.get(''); | ||
const { status } = await muhb.get(LOCAL_HOST + '/'); | ||
assert.strictEqual(status, 404); | ||
@@ -91,3 +99,3 @@ }); | ||
await app.restart({ services: null }); | ||
const { status } = await base.get(''); | ||
const { status } = await muhb.get(LOCAL_HOST + '/'); | ||
assert.strictEqual(status, 404); | ||
@@ -119,3 +127,3 @@ }); | ||
const { status } = await base.get('backend/get'); | ||
const { status } = await muhb.get(LOCAL_HOST + '/backend/get'); | ||
assert.strictEqual(status, 200); | ||
@@ -134,3 +142,3 @@ }); | ||
const { status } = await base.get('new-serv/get'); | ||
const { status } = await muhb.get(LOCAL_HOST + '/new-serv/get'); | ||
assert.strictEqual(status, 200); | ||
@@ -144,3 +152,3 @@ }); | ||
it('Should fail when service doesn\'t exist', async function(){ | ||
const { status } = await base.get('nothing'); | ||
const { status } = await muhb.get(LOCAL_HOST + '/nothing'); | ||
assert.strictEqual(status, 404); | ||
@@ -151,3 +159,3 @@ }); | ||
this.timeout(4000); | ||
const { status } = await base.get('unresponsive/foo'); | ||
const { status } = await muhb.get(LOCAL_HOST + '/unresponsive/foo'); | ||
assert.strictEqual(status, 503); | ||
@@ -157,3 +165,3 @@ }); | ||
it('Should fail when endpoint doesn\'t exist', async function(){ | ||
const { status } = await base.get('backend/public-only'); | ||
const { status } = await muhb.get(LOCAL_HOST + '/backend/public-only'); | ||
assert.strictEqual(status, 404); | ||
@@ -163,6 +171,6 @@ }); | ||
it('Should reach an exposed service endpoints', async function(){ | ||
const { status, body } = await base.get('backend/headers'); | ||
const { status, body } = await muhb.get(LOCAL_HOST + '/backend/headers'); | ||
assert.strictEqual(status, 200); | ||
assert(body.includes('date')); | ||
const { status: s2, body: b2 } = await base.get('backend/say/foobar'); | ||
const { status: s2, body: b2 } = await muhb.get(LOCAL_HOST + '/backend/say/foobar'); | ||
assert.strictEqual(s2, 200); | ||
@@ -173,6 +181,6 @@ assert.strictEqual(b2, 'foobar'); | ||
it('Should reach any endpoint of exposed service [*service.exposed]', async function(){ | ||
const { status, body } = await base.get('public/public-only'); | ||
const { status, body } = await muhb.get(LOCAL_HOST + '/public/public-only'); | ||
assert.strictEqual(status, 200); | ||
assert(body.includes('public-only')); | ||
const { status: s2 } = await base.get('public/nothing'); | ||
const { status: s2 } = await muhb.get(LOCAL_HOST + '/public/nothing'); | ||
assert.strictEqual(s2, 404); | ||
@@ -183,3 +191,3 @@ }); | ||
await app.restart({ proxy: { preserveCookies: true } }); | ||
const { body } = await base.get('backend/headers', { cookies: { foo: 'bar' } }); | ||
const { body } = await muhb.get(LOCAL_HOST + '/backend/headers', { cookies: { foo: 'bar' } }); | ||
assert(body.includes('"cookie":"foo=bar')); | ||
@@ -189,3 +197,3 @@ }); | ||
it('Should find service according to HOST header [*service.domain]', async function(){ | ||
const { status, body } = await base.get('headers', { 'host': 'hostly' }); | ||
const { status, body } = await muhb.get(LOCAL_HOST + '/headers', { 'host': 'hostly' }); | ||
assert.strictEqual(status, 200); | ||
@@ -203,3 +211,3 @@ assert(body.includes('{')); | ||
await app.restart({ auth: { url: 'http://nothing' } }); | ||
const { status } = await base.post('login'); | ||
const { status } = await muhb.post(LOCAL_HOST + '/login'); | ||
assert.strictEqual(status, 500); | ||
@@ -209,3 +217,3 @@ }); | ||
it('Should return authentication failure when not 200', async function(){ | ||
const { status } = await base.post('login', { auto: true }, 'must fail'); | ||
const { status } = await muhb.post(LOCAL_HOST + '/login', { auto: true }, 'must fail'); | ||
assert.strictEqual(status, 400); | ||
@@ -215,3 +223,3 @@ }); | ||
it('Should return ok when auth succeeds', async function(){ | ||
const { status } = await base.post('login'); | ||
const { status } = await muhb.post(LOCAL_HOST + '/login'); | ||
assert.strictEqual(status, 200); | ||
@@ -221,5 +229,5 @@ }); | ||
it('Should include claim header when proxying for logged in client', async function(){ | ||
const { status, cookies } = await base.post('login'); | ||
const { status, cookies } = await muhb.post(LOCAL_HOST + '/login'); | ||
assert.strictEqual(status, 200); | ||
const { status: s2, body } = await base.get('backend/headers', { cookies }); | ||
const { status: s2, body } = await muhb.get(LOCAL_HOST + '/backend/headers', { cookies }); | ||
assert.strictEqual(s2, 200); | ||
@@ -231,2 +239,27 @@ assert(body.includes('"x-claim":"ID')); | ||
describe('POST /mimic', function(){ | ||
it('Should return authentication failure when not 200', async function(){ | ||
const { cookies } = await muhb.post(LOCAL_HOST + '/login'); | ||
const { status } = await muhb.post(LOCAL_HOST + '/mimic', { cookies, auto: true }, 'must fail'); | ||
assert.strictEqual(status, 400); | ||
}); | ||
it('Should return ok when auth succeeds', async function(){ | ||
const { cookies } = await muhb.post(LOCAL_HOST + '/login'); | ||
const { status } = await muhb.post(LOCAL_HOST + '/mimic', { cookies }); | ||
assert.strictEqual(status, 200); | ||
}); | ||
it('Should include claim header when proxying for logged in client', async function(){ | ||
const { cookies } = await muhb.post(LOCAL_HOST + '/login'); | ||
const { status } = await muhb.post(LOCAL_HOST + '/mimic', { cookies }); | ||
assert.strictEqual(status, 200); | ||
const { status: s2, body } = await muhb.get(LOCAL_HOST + '/backend/headers', { cookies }); | ||
assert.strictEqual(s2, 200); | ||
assert(body.includes('"x-claim":"ID')); | ||
}); | ||
}); | ||
describe('POST /logout', function(){ | ||
@@ -236,4 +269,4 @@ | ||
await app.restart({ session: { timeout: '1d' } }); | ||
const { cookies } = await base.post('login'); | ||
const { headers } = await base.post('logout', { cookies }); | ||
const { cookies } = await muhb.post(LOCAL_HOST + '/login'); | ||
const { headers } = await muhb.post(LOCAL_HOST + '/logout', { cookies }); | ||
assert(headers['set-cookie'][0].indexOf('01 Jan 1970') > 0); | ||
@@ -256,4 +289,4 @@ }); | ||
beforeEach(async function(){ | ||
const { cookies } = await base.post('login'); | ||
const { cookies: c2 } = await base.post('login'); | ||
const { cookies } = await muhb.post(LOCAL_HOST + '/login'); | ||
const { cookies: c2 } = await muhb.post(LOCAL_HOST + '/login'); | ||
logClaim = await backend.get('joe:sess:' + cookies.foobaz.substring(0, 36)); | ||
@@ -417,3 +450,3 @@ await backend.sAdd('foobarg', logClaim); | ||
const { status } = await secBase.get('headers', { 'host': ACME_GATEWAY }); | ||
const { status } = await muhb.get(SEC_LOCAL_HOST + '/headers', { 'host': ACME_GATEWAY }); | ||
assert.strictEqual(status, 200); | ||
@@ -430,4 +463,4 @@ // TODO actually validate if cert is OK? | ||
await secBase.get('headers', { 'host': ACME_GATEWAY }); | ||
const { status } = await secBase.get('headers', { 'host': ACME_GATEWAY }); | ||
await muhb.get(SEC_LOCAL_HOST + '/headers', { 'host': ACME_GATEWAY }); | ||
const { status } = await muhb.get(SEC_LOCAL_HOST + '/headers', { 'host': ACME_GATEWAY }); | ||
assert.strictEqual(status, 200); | ||
@@ -446,3 +479,3 @@ // TODO actually validate if cert is OK? | ||
// Passing HOST that is unreachable to Pebble | ||
const { status } = await secBase.get('headers', { 'host': 'httpsonly' }); | ||
const { status } = await muhb.get(SEC_LOCAL_HOST + '/headers', { 'host': 'httpsonly' }); | ||
assert.strictEqual(status, 200); | ||
@@ -458,3 +491,3 @@ // TODO actually validate if cert is OK? | ||
await app.restart({ cookie: { name: 'foobar' } }); | ||
const { cookies } = await base.post('login'); | ||
const { cookies } = await muhb.post(LOCAL_HOST + '/login'); | ||
assert.strictEqual(cookies.foobar.charAt(36), '.'); | ||
@@ -465,4 +498,4 @@ }); | ||
await app.restart({ proxy: { claimHeader: 'X-Foo' } }); | ||
const { cookies } = await base.post('login'); | ||
const { body } = await base.get('backend/headers', { cookies }); | ||
const { cookies } = await muhb.post(LOCAL_HOST + '/login'); | ||
const { body } = await muhb.get(LOCAL_HOST + '/backend/headers', { cookies }); | ||
assert(body.includes('"x-foo":')); | ||
@@ -473,5 +506,5 @@ }); | ||
await app.restart({ session: { timeout: '1s' } }); | ||
const { cookies } = await base.post('login'); | ||
const { cookies } = await muhb.post(LOCAL_HOST + '/login'); | ||
await new Promise(done => setTimeout(done, 1500)); | ||
const { body } = await base.get('backend/headers', { cookies }); | ||
const { body } = await muhb.get(LOCAL_HOST + '/backend/headers', { cookies }); | ||
assert(body.indexOf('x-claim') < 0); | ||
@@ -490,3 +523,3 @@ }); | ||
await app.restart({ auth: { onSuccess: 'http://localhost:2345' } }); | ||
await base.post('login'); | ||
await muhb.post(LOCAL_HOST + '/login'); | ||
})(); | ||
@@ -500,3 +533,3 @@ }); | ||
it('Should respond to path /', async function(){ | ||
const { status } = await base.get('', { 'host': 'hostly' }); | ||
const { status } = await muhb.get(LOCAL_HOST + '/', { 'host': 'hostly' }); | ||
assert.strictEqual(status, 200); | ||
@@ -506,7 +539,7 @@ }); | ||
it('Should be ok being logged in successively (nodecaf fixed bug)', async function(){ | ||
const { cookies } = await base.post('login'); | ||
await base.post('logout', { cookies }); | ||
const { cookies: cs } = await base.post('login'); | ||
await base.post('logout', { cookies: cs }); | ||
const { headers } = await base.post('login'); | ||
const { cookies } = await muhb.post(LOCAL_HOST + '/login'); | ||
await muhb.post(LOCAL_HOST + '/logout', { cookies }); | ||
const { cookies: cs } = await muhb.post(LOCAL_HOST + '/login'); | ||
await muhb.post(LOCAL_HOST + '/logout', { cookies: cs }); | ||
const { headers } = await muhb.post(LOCAL_HOST + '/login'); | ||
assert(headers['set-cookie'][0].indexOf('Max-Age=0') < 0); | ||
@@ -513,0 +546,0 @@ }); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
50516
861
75
Updatednodecaf@^0.13.2
Updatedws@^8.8.0