Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

api-joe

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

api-joe - npm Package Compare versions

Comparing version 0.4.3 to 0.4.4

11

CHANGELOG.md

@@ -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 @@

26

lib/proxy.js

@@ -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 @@

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc