Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CAS-6139/ip headers #96

Merged
merged 9 commits into from
Jan 28, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,19 @@ const castle = Castle({ apiSecret: 'YOUR SECRET HERE' });

#### Config options

| Config option | Explanation |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| apiSecret | `string` - This can be found in the castle dashboard. |
| timeout | `number` - Time before returning the failover strategy. Default value is 500. |
| allowlisted | `string[]` - An array of strings matching the headers you want to pass fully to the service. |
| denylisted | `string[]` - An array of of strings matching the headers you do not want to pass fully to the service. |
| Config option | Explanation |
| ----------------- | ----------- |
| apiSecret | `string` - This can be found in the castle dashboard. |
| timeout | `number` - Time before returning the failover strategy. Default value is 500. |
| allowlisted | `string[]` - An array of strings matching the headers you want to pass fully to the service. |
| denylisted | `string[]` - An array of of strings matching the headers you do not want to pass fully to the service. |
| failoverStrategy | `string` - If the request to our service would for some reason time out, this is where you select the automatic response from `authenticate`. Options are `allow`, `deny`, `challenge`. |
| logLevel | `string` - Corresponds to standard log levels: `trace`, `debug`, `info`, `warn`, `error`, `fatal`. Useful levels are `info` and `error`. |
| doNotTrack | `boolean` - False by default, setting it to true turns off all requests and triggers automatic failover on `authenticate`. Used for development and testing. |
| logLevel | `string` - Corresponds to standard log levels: `trace`, `debug`, `info`, `warn`, `error`, `fatal`. Useful levels are `info` and `error`. |
| doNotTrack | `boolean` - False by default, setting it to true turns off all requests and triggers automatic failover on `authenticate`. Used for development and testing. |
| ipHeaders | `string[]` - IP Headers to look for a client IP address. |
| trustedProxies | `string[]` - Trusted public proxies list. |
| trustProxyChain | `boolean` - False by default, defines if trusting all of the proxy IPs in X-Forwarded-For is enabled. |
| trustedProxyDepth | `number` - Number of trusted proxies used in the chain. |

## Actions

Expand Down
19 changes: 15 additions & 4 deletions src/Castle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import fetch from 'node-fetch';
import AbortController from 'abort-controller';
import pino from 'pino';

import { DEFAULT_ALLOWLIST } from './constants';
import {
DEFAULT_ALLOWLIST,
DEFAULT_API_URL,
DEFAULT_TIMEOUT,
} from './constants';
import { AuthenticateResult, Configuration, Payload } from './models';
import {
CommandAuthenticateService,
Expand All @@ -14,9 +18,6 @@ import {
} from './failover/failover.module';
import { LoggerService } from './logger/logger.module';

const DEFAULT_API_URL = 'https://api.castle.io/v1';
const DEFAULT_TIMEOUT = 1000;

// The body on the request is a stream and can only be
// read once, by default. This is a workaround so that the
// logging functions can read the body independently
Expand Down Expand Up @@ -51,6 +52,10 @@ export class Castle {
failoverStrategy = FailoverStrategy.allow,
logLevel = 'error',
doNotTrack = false,
ipHeaders = [],
trustedProxies = [],
trustProxyChain = false,
trustedProxyDepth = 0,
}: Configuration) {
if (!apiSecret) {
throw new Error(
Expand All @@ -70,6 +75,12 @@ export class Castle {
failoverStrategy,
logLevel,
doNotTrack,
ipHeaders,
trustedProxies: trustedProxies.length
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to check this ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't thanks for catching!

? trustedProxies.map((proxy) => new RegExp(proxy))
: [],
trustProxyChain,
trustedProxyDepth,
};
this.logger = pino({
prettyPrint: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const DEFAULT_API_URL = 'https://api.castle.io/v1';
export const DEFAULT_TIMEOUT = 1000;
export const DEFAULT_ALLOWLIST = [
'accept',
'accept-charset',
Expand All @@ -23,3 +25,17 @@ export const DEFAULT_ALLOWLIST = [
'x-castle-client-id',
'x-requested-with',
];
export const TRUSTED_PROXIES = [
new RegExp(
[
/^127\.0\.0\.1$|/,
/^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|/,
/^::1\Z|\Afd[0-9a-f]{2}:.+|/,
/^localhost$|/,
/^unix$|/,
/^unix:/,
]
.map((r) => r.source)
.join('')
),
];
2 changes: 1 addition & 1 deletion src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from './default-allowlist';
export * from './configuration';
2 changes: 2 additions & 0 deletions src/context/services/context-get-default.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Configuration } from '../../models';
import { HeadersExtractService } from '../../headers/headers.module';
import { IPsExtractService } from '../../ips/ips.module';
import { version } from '../../../package.json';

export const ContextGetDefaultService = {
call: (context: any, configuration: Configuration) => {
return {
client_id: context.client_id || false,
headers: HeadersExtractService.call(context.headers, configuration),
ip: IPsExtractService.call(context.headers, configuration),
library: {
name: 'castle-node',
version,
Expand Down
2 changes: 1 addition & 1 deletion src/headers/services/headers-extract.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const ALWAYS_DENYLISTED = ['cookie', 'authorization'];
export const HeadersExtractService = {
call: (
headers: IncomingHttpHeaders,
{ allowlisted, denylisted }: Configuration
{ allowlisted = [], denylisted = [] }: Configuration
) => {
return reduce(
headers,
Expand Down
1 change: 1 addition & 0 deletions src/ips/ips.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './services';
1 change: 1 addition & 0 deletions src/ips/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ips-extract.service';
67 changes: 67 additions & 0 deletions src/ips/services/ips-extract.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { IncomingHttpHeaders } from 'http';
import { Configuration } from '../../models';
import { TRUSTED_PROXIES } from '../../constants';

// ordered list of ip headers for ip extraction
const DEFAULT = ['x-forwarded-for', 'remote-addr'];
// list of header which are used with proxy depth setting
const DEPTH_RELATED = ['x-forwarded-for'];

const checkInternal = (ipAddress: string, proxies: any[]) => {
return proxies.some((proxyRegexp) => proxyRegexp.test(ipAddress));
};

const limitProxyDepth = (ips, ipHeader, trustedProxyDepth) => {
if (DEPTH_RELATED.includes(ipHeader)) {
ips.splice(ips.length - 1, trustedProxyDepth);
}
return ips;
};

const IPsFrom = (ipHeader, headers, trustedProxyDepth) => {
if (!headers) {
return [];
}
const headerValue = headers[ipHeader];

if (!headerValue) {
return [];
}
const ips = headerValue.trim().split(/[,\s]+/);
return limitProxyDepth(ips, ipHeader, trustedProxyDepth);
};

const removeProxies = (ips, trustProxyChain, proxiesList) => {
if (trustProxyChain) {
return ips[0];
}
const filteredIps = ips.filter((ip) => !checkInternal(ip, proxiesList));
return filteredIps[filteredIps.length - 1];
};

export const IPsExtractService = {
call: (
headers: IncomingHttpHeaders,
{
ipHeaders = [],
trustedProxies = [],
trustProxyChain = false,
trustedProxyDepth = 0,
}: Configuration
) => {
const ipHeadersList = ipHeaders.length ? ipHeaders : DEFAULT;
const proxiesList = trustedProxies.concat(TRUSTED_PROXIES);
let allIPs = [];
for (const ipHeader of ipHeadersList) {
const IPs = IPsFrom(ipHeader, headers, trustedProxyDepth);
const IPValue = removeProxies(IPs, trustProxyChain, proxiesList);
if (IPValue) {
return IPValue;
}
allIPs = [...allIPs, ...IPs];
}

// fallback to first listed ip
return allIPs[0];
},
};
4 changes: 4 additions & 0 deletions src/models/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ export type Configuration = {
failoverStrategy?: FailoverStrategy;
logLevel?: pino.Level;
doNotTrack?: boolean;
ipHeaders?: string[];
trustedProxies?: RegExp[];
trustProxyChain?: boolean;
trustedProxyDepth?: number;
};
1 change: 1 addition & 0 deletions test/Castle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const sampleRequestData = {
client_id: 'clientid',
headers: {
Cookie: 'SECRET=pleasedontbehere',
'x-forwarded-for': '8.8.8.8',
},
},
};
Expand Down
10 changes: 9 additions & 1 deletion test/context/services/context-get-default.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,29 @@ import { version } from '../../../package.json';
describe('ContextGetDefaultService', () => {
describe('call', () => {
const expected = {
headers: {},
headers: {
'x-forwarded-for': '1.2.3.4',
},
library: {
name: 'castle-node',
version,
},
client_id: 'client_id',
ip: '1.2.3.4',
};

const config = {
apiSecret: 'test',
apiUrl: 'castle.io',
denylisted: [],
allowlisted: [],
};

const context = {
client_id: 'client_id',
headers: {
'x-forwarded-for': '1.2.3.4',
},
};

it('generates default context', () => {
Expand Down
8 changes: 6 additions & 2 deletions test/core/services/core-generate-request-body.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ describe('CoreGenerateRequestBody', () => {
context: {
ip: '127.0.0.1',
client_id: 'client_id',
headers: {},
headers: {
'x-forwarded-for': '127.0.0.1',
},
library: {
name: 'castle-node',
version,
Expand All @@ -37,7 +39,9 @@ describe('CoreGenerateRequestBody', () => {
context: {
ip: '127.0.0.1',
client_id: 'client_id',
headers: {},
headers: {
'x-forwarded-for': '127.0.0.1',
},
},
};

Expand Down
Loading