Skip to content

Commit

Permalink
Merge pull request #106 from thiagobustamante/master
Browse files Browse the repository at this point in the history
Fix Middleware loading
  • Loading branch information
thiagobustamante committed Feb 6, 2018
2 parents 868908f + 11dc2ea commit b172142
Show file tree
Hide file tree
Showing 38 changed files with 232 additions and 76 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ An API gateway provides a single, unified entry point across one or more interna

Tree Gateway is a free and open source solution writen in Node JS that has a complete and customizable pipeline to handle your requests.
It provides:
- **Authentication**: More than 300 strategies available through an easy [passportjs](http://passportjs.org/) integration, including support to JWT tokens, Oauth, Basic and many others.
- **Authentication**: More than 480 strategies available through an easy [passportjs](http://passportjs.org/) integration, including support to JWT tokens, Oauth, Basic and many others.
- A flexible and robust **Routing system** that allows any kind of customized request pipeline.
- **Rate limits** - To control quotas for your customers and to define actions to be taken when any quota is exceeded.
- **Caching system** - Allow you to easily inject and control caching behavior for your APIs. Tree Gateway provides two kinds of cache:
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tree-gateway",
"version": "1.6.0",
"version": "2.0.0",
"homepage": "http://treegateway.org",
"description": "The Tree Gateway API Gateway",
"author": "Thiago da Rosa de Bustamante <trbustamante@gmail.com>",
Expand Down
62 changes: 44 additions & 18 deletions src/authentication/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,32 @@ export class ApiAuth {

authentication(apiRouter: express.Router, apiKey: string, api: ApiConfig, gatewayFeatures: ApiFeaturesConfig) {
const path: string = api.path;
let authentication: ApiAuthenticationConfig = api.authentication;
try {
authentication = this.resolveReferences(authentication, gatewayFeatures);
const authStrategy: auth.Strategy = this.middlewareLoader.loadMiddleware('authentication/strategy', authentication.strategy);
if (!authStrategy) {
this.logger.error('Error configuring authenticator. Invalid Strategy');
} else {
auth.use(apiKey, authStrategy);
const authentications: Array<ApiAuthenticationConfig> = this.sortMiddlewares(api.authentication, path);

const authenticator = auth.authenticate(apiKey, { session: false, failWithError: true });
if (authentication.group) {
this.createAuthenticatorForGroup(apiRouter, api, authentication, authenticator);
authentications.forEach((authentication: ApiAuthenticationConfig, index: number) => {
try {
authentication = this.resolveReferences(authentication, gatewayFeatures);
const authStrategy: auth.Strategy = this.middlewareLoader.loadMiddleware('authentication/strategy', authentication.strategy);
if (!authStrategy) {
this.logger.error('Error configuring authenticator. Invalid Strategy');
} else {
this.createAuthenticator(apiRouter, api, authentication, authenticator);
}
auth.use(`${apiKey}_${index}`, authStrategy);

const authenticator = auth.authenticate(`${apiKey}_${index}`, { session: false, failWithError: true });
if (authentication.group) {
this.createAuthenticatorForGroup(apiRouter, api, authentication, authenticator);
} else {
this.createAuthenticator(apiRouter, api, authentication, authenticator);
}

if (this.logger.isDebugEnabled) {
this.logger.debug(`Authentication Strategy [${this.middlewareLoader.getId(authentication.strategy)}] configured for path [${path}]`);
if (this.logger.isDebugEnabled) {
this.logger.debug(`Authentication Strategy [${this.middlewareLoader.getId(authentication.strategy)}] configured for path [${path}]`);
}
}
} catch (e) {
this.logger.error(`Error configuring Authentication Strategy [${this.middlewareLoader.getId(authentication.strategy)}] for path [${path}]`, e);
}
} catch (e) {
this.logger.error(`Error configuring Authentication Strategy [${this.middlewareLoader.getId(authentication.strategy)}] for path [${path}]`, e);
}
});
}

private resolveReferences(authentication: ApiAuthenticationConfig, features: ApiFeaturesConfig) {
Expand Down Expand Up @@ -132,4 +135,27 @@ export class ApiAuth {

return null;
}

private sortMiddlewares(middlewares: Array<ApiAuthenticationConfig>, path: string): Array<ApiAuthenticationConfig> {
const generalMiddlewares = _.filter(middlewares, (value) => {
if (value.group) {
return true;
}
return false;
});

if (generalMiddlewares.length > 1) {
this.logger.error(`Invalid authentication configuration for api [${path}]. Conflicting configurations for default group`);
return [];
}

if (generalMiddlewares.length > 0) {
const index = middlewares.indexOf(generalMiddlewares[0]);
if (index < middlewares.length - 1) {
const gen = middlewares.splice(index, 1);
middlewares.push(gen[0]);
}
}
return middlewares;
}
}
1 change: 1 addition & 0 deletions src/authentication/strategies/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ module.exports = function(authConfig: BasicAuthentication) {
const verifyFunction = middlewareLoader.loadMiddleware('authentication/verify', authConfig.verify);
return new BasicStrategy(verifyFunction);
};
module.exports.factory = true;
1 change: 1 addition & 0 deletions src/authentication/strategies/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ function getExtractor(extractor: string, param: string) {
return ExtractJwt.fromAuthHeader();
}
}
module.exports.factory = true;
1 change: 1 addition & 0 deletions src/authentication/strategies/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ module.exports = function(authConfig: LocalAuthentication) {
const verifyFunction = middlewareLoader.loadMiddleware('authentication/verify', authConfig.verify);
return new Strategy(opts, verifyFunction);
};
module.exports.factory = true;
6 changes: 3 additions & 3 deletions src/circuitbreaker/circuit-breaker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,19 +114,19 @@ export class ApiCircuitBreaker {
if (config.onOpen) {
const openHandler = this.middlewareLoader.loadMiddleware('circuitbreaker', config.onOpen);
breakerInfo.circuitBreaker.on('open', () => {
openHandler(path, 'open');
openHandler(path, 'open', apiId);
});
}
if (config.onClose) {
const closeHandler = this.middlewareLoader.loadMiddleware('circuitbreaker', config.onClose);
breakerInfo.circuitBreaker.on('close', () => {
closeHandler(path, 'close');
closeHandler(path, 'close', apiId);
});
}
if (config.onRejected) {
const rejectedHandler = this.middlewareLoader.loadMiddleware('circuitbreaker', config.onRejected);
breakerInfo.circuitBreaker.on('rejected', () => {
rejectedHandler(path, 'rejected');
rejectedHandler(path, 'rejected', apiId);
});
}
}
Expand Down
40 changes: 21 additions & 19 deletions src/circuitbreaker/express-circuit-breaker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { EventEmitter } from 'events';
import * as express from 'express';
import { ProxyError } from '../error/errors';

export enum State { OPEN, CLOSED, HALF_OPEN }

Expand Down Expand Up @@ -73,16 +74,15 @@ export class CircuitBreaker extends EventEmitter {
}

middleware(): express.RequestHandler {
const self = this;
return (req, res, next) => {
// self.emit('request');
if (self.isOpen() || (self.isHalfOpen() && self.options.stateHandler.halfOpenCallPending)) {
return self.fastFail(res);
} else if (self.isHalfOpen() && !self.options.stateHandler.halfOpenCallPending) {
self.options.stateHandler.halfOpenCallPending = true;
return self.invokeApi(req, res, next);
// this.emit('request');
if (this.isOpen() || (this.isHalfOpen() && this.options.stateHandler.halfOpenCallPending)) {
return this.fastFail(next);
} else if (this.isHalfOpen() && !this.options.stateHandler.halfOpenCallPending) {
this.options.stateHandler.halfOpenCallPending = true;
return this.invokeApi(req, res, next);
} else {
return self.invokeApi(req, res, next);
return this.invokeApi(req, res, next);
}
};
}
Expand All @@ -96,13 +96,13 @@ export class CircuitBreaker extends EventEmitter {
}

private invokeApi(req: express.Request, res: express.Response, next: express.NextFunction) {
const self = this;
let operationTimeout = false;
const timeoutID = setTimeout(() => {
operationTimeout = true;
self.handleTimeout(req, res);
}, self.options.timeout);
this.handleTimeout(req, res, next);
}, this.options.timeout);
const end = res.end;
const self = this;
res.end = function(...args: any[]) {
if (!operationTimeout) {
clearTimeout(timeoutID);
Expand All @@ -120,18 +120,20 @@ export class CircuitBreaker extends EventEmitter {
return next();
}

private fastFail(res: express.Response) {
res.status(this.options.rejectStatusCode);
const err = new Error(this.options.rejectMessage);
res.end(err.message);
private fastFail(next: express.NextFunction) {
const err = new ProxyError(this.options.rejectMessage, this.options.rejectStatusCode);
next(err);
// res.status(this.options.rejectStatusCode);
// res.end(err.message);
this.emit('rejected', err);
}

private handleTimeout(req: express.Request, res: express.Response) {
const err = new Error(this.options.timeoutMessage);
private handleTimeout(req: express.Request, res: express.Response, next: express.NextFunction) {
const err = new ProxyError(this.options.timeoutMessage, this.options.timeoutStatusCode);
this.handleFailure(req, err);
res.status(this.options.timeoutStatusCode);
res.end(err.message);
// res.status(this.options.timeoutStatusCode);
// res.end(err.message);
next(err);
// this.emit('timeout', (Date.now() - startTime));
}

Expand Down
4 changes: 2 additions & 2 deletions src/config/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export interface ApiConfig {
/**
* Configuration for API authentication.
*/
authentication?: ApiAuthenticationConfig;
authentication?: Array<ApiAuthenticationConfig>;
/**
* Configuration for API cache.
*/
Expand Down Expand Up @@ -97,7 +97,7 @@ export interface ApiConfig {
}

export let apiConfigValidatorSchema = Joi.object().keys({
authentication: apiAuthenticationValidatorSchema,
authentication: Joi.alternatives([Joi.array().items(apiAuthenticationValidatorSchema), apiAuthenticationValidatorSchema]),
cache: Joi.alternatives([Joi.array().items(apiCacheConfigValidatorSchema), apiCacheConfigValidatorSchema]),
circuitBreaker: Joi.alternatives([Joi.array().items(apiCircuitBreakerConfigValidatorSchema), apiCircuitBreakerConfigValidatorSchema]),
cors: Joi.alternatives([Joi.array().items(apiCorsConfigSchema), apiCorsConfigSchema]),
Expand Down
9 changes: 8 additions & 1 deletion src/error/errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

import { Errors } from 'typescript-rest';
import { HttpError, Errors } from 'typescript-rest';
import * as Joi from 'joi';
import * as _ from 'lodash';

Expand Down Expand Up @@ -70,3 +70,10 @@ export class UnavailableError extends Error {
Object.setPrototypeOf(this, UnavailableError.prototype);
}
}

export class ProxyError extends HttpError {
constructor(message: string, statusCode: number) {
super('gatewayError', statusCode, message);
Object.setPrototypeOf(this, UnavailableError.prototype);
}
}
1 change: 1 addition & 0 deletions src/error/handlers/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ module.exports = function(config: JSONAtaExpression) {
}
};
};
module.exports.factory = true;
1 change: 1 addition & 0 deletions src/error/handlers/mustache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ module.exports = function(config: MustacheConfig) {
}
};
};
module.exports.factory = true;
16 changes: 11 additions & 5 deletions src/filter/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Logger } from '../logger';
import { Inject } from 'typescript-ioc';
import { MiddlewareLoader } from '../utils/middleware-loader';
import { ApiFilter as Filter } from '../config/filter';
import { NotFoundError } from '../error/errors';

export class ApiFilter {
@Inject private middlewareLoader: MiddlewareLoader;
Expand Down Expand Up @@ -39,7 +40,8 @@ export class ApiFilter {
return next();
}
if (!res.headersSent) {
res.sendStatus(404);
next(new NotFoundError());
// res.sendStatus(404);
}
}).catch(err => {
next(err);
Expand All @@ -66,7 +68,8 @@ export class ApiFilter {
return next();
}
if (!res.headersSent) {
res.sendStatus(404);
next(new NotFoundError());
// res.sendStatus(404);
}
}).catch(err => {
next(err);
Expand All @@ -82,7 +85,8 @@ export class ApiFilter {
return next();
}
if (!res.headersSent) {
res.sendStatus(404);
next(new NotFoundError());
// res.sendStatus(404);
}
}).catch(err => {
next(err);
Expand Down Expand Up @@ -113,7 +117,8 @@ export class ApiFilter {
if (groupValidator(req, res)) {
next();
} else {
res.sendStatus(404);
next(new NotFoundError());
// res.sendStatus(404);
}
});
}
Expand All @@ -129,7 +134,8 @@ export class ApiFilter {
if (groupValidator(req, res)) {
next();
} else {
res.sendStatus(404);
next(new NotFoundError());
// res.sendStatus(404);
}
});
}
Expand Down
1 change: 1 addition & 0 deletions src/filter/filters/ipFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,4 @@ module.exports = function(config: IpFilterConfig) {
}
return getBlacklistFilter(config);
};
module.exports.factory = true;
1 change: 1 addition & 0 deletions src/proxy/interceptors/requestBodyTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ module.exports = function(config: JSONAtaExpression) {
return result;
};
};
module.exports.factory = true;
1 change: 1 addition & 0 deletions src/proxy/interceptors/requestHeaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ module.exports = function(config: RequestHeadersConfig) {
return { headers: h };
};
};
module.exports.factory = true;
1 change: 1 addition & 0 deletions src/proxy/interceptors/requestMustache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ module.exports = function(config: MustacheConfig) {
return result;
};
};
module.exports.factory = true;
1 change: 1 addition & 0 deletions src/proxy/interceptors/requestXml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ module.exports = function(config: any) {
return result;
};
};
module.exports.factory = true;
1 change: 1 addition & 0 deletions src/proxy/interceptors/responseBodyTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ module.exports = function(config: JSONAtaExpression) {
});
};
};
module.exports.factory = true;
1 change: 1 addition & 0 deletions src/proxy/interceptors/responseHeaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ module.exports = function(config: ResponseHeadersConfig) {
return result;
};
};
module.exports.factory = true;
1 change: 1 addition & 0 deletions src/proxy/interceptors/responseMustache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ module.exports = function(config: MustacheConfig) {
return result;
};
};
module.exports.factory = true;
1 change: 1 addition & 0 deletions src/proxy/interceptors/responseXml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ module.exports = function(config: any) {
});
};
};
module.exports.factory = true;

0 comments on commit b172142

Please sign in to comment.