mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Fallback to X-Forwarded-* headers
* Fallback to X-Forwarded-* headers This uses the first value from X-Forwarded-Host and X-Forwarded-Proto if they're present and the standard Forwarded header is not. * Update parseForwarded to handle X-Forwarded-* This updates the signature for parseForwarded to take in the headers and handle the logic of falling back to X-Forwarded-* headers. * Update src/util/HeaderUtil.ts Co-authored-by: Ruben Verborgh <ruben@verborgh.org> * Inline parseXForwarded helper Additionally fixes a typo, updates a unit test, and removes a typing that is no longer necessary. * Tweak handling of X-Forwarded value checking and assignment * Fix: terminology & consistency suggestions from review Co-authored-by: Ruben Verborgh <ruben@verborgh.org> Co-authored-by: Ruben Verborgh <ruben@verborgh.org> Co-authored-by: Wouter Termont <woutermont@gmail.com>
This commit is contained in:
parent
e2284c4c42
commit
de51a231e3
@ -44,7 +44,7 @@ class WebSocketListener extends EventEmitter {
|
||||
}
|
||||
|
||||
// Store the HTTP host and protocol
|
||||
const forwarded = parseForwarded(headers.forwarded);
|
||||
const forwarded = parseForwarded(headers);
|
||||
this.host = forwarded.host ?? headers.host ?? 'localhost';
|
||||
this.protocol = forwarded.proto === 'https' || (socket as any).secure ? 'https:' : 'http:';
|
||||
}
|
||||
|
@ -21,17 +21,17 @@ export class OriginalUrlExtractor extends TargetExtractor {
|
||||
throw new Error('Missing URL');
|
||||
}
|
||||
|
||||
// Extract host and protocol (possibly overridden by the Forwarded header)
|
||||
// Extract host and protocol (possibly overridden by the Forwarded/X-Forwarded-* header)
|
||||
let { host } = headers;
|
||||
let protocol = (connection as TLSSocket)?.encrypted ? 'https' : 'http';
|
||||
if (headers.forwarded) {
|
||||
const forwarded = parseForwarded(headers.forwarded);
|
||||
if (forwarded.host) {
|
||||
({ host } = forwarded);
|
||||
}
|
||||
if (forwarded.proto) {
|
||||
({ proto: protocol } = forwarded);
|
||||
}
|
||||
|
||||
// Check Forwarded/X-Forwarded-* headers
|
||||
const forwarded = parseForwarded(headers);
|
||||
if (forwarded.host) {
|
||||
({ host } = forwarded);
|
||||
}
|
||||
if (forwarded.proto) {
|
||||
({ proto: protocol } = forwarded);
|
||||
}
|
||||
|
||||
// Perform a sanity check on the host
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type { IncomingHttpHeaders } from 'http';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import type { HttpResponse } from '../server/HttpResponse';
|
||||
import { BadRequestHttpError } from './errors/BadRequestHttpError';
|
||||
@ -429,21 +430,29 @@ export interface Forwarded {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a Forwarded header value.
|
||||
* Parses a Forwarded header value and will fall back to X-Forwarded-* headers.
|
||||
*
|
||||
* @param value - The Forwarded header value.
|
||||
* @param headers - The incoming HTTP headers.
|
||||
*
|
||||
* @returns The parsed Forwarded header.
|
||||
*/
|
||||
export function parseForwarded(value = ''): Forwarded {
|
||||
export function parseForwarded(headers: IncomingHttpHeaders): Forwarded {
|
||||
const forwarded: Record<string, string> = {};
|
||||
if (value) {
|
||||
for (const pair of value.replace(/\s*,.*$/u, '').split(';')) {
|
||||
if (headers.forwarded) {
|
||||
for (const pair of headers.forwarded.replace(/\s*,.*/u, '').split(';')) {
|
||||
const components = /^(by|for|host|proto)=(.+)$/u.exec(pair);
|
||||
if (components) {
|
||||
forwarded[components[1]] = components[2];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const suffixes = [ 'host', 'proto' ];
|
||||
for (const suffix of suffixes) {
|
||||
const value = headers[`x-forwarded-${suffix}`] as string;
|
||||
if (value) {
|
||||
forwarded[suffix] = value.trim().replace(/\s*,.*/u, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
return forwarded;
|
||||
}
|
||||
|
@ -182,4 +182,20 @@ describe('An UnsecureWebSocketsProtocol', (): void => {
|
||||
expect(webSocket.messages).toHaveLength(2);
|
||||
expect(webSocket.messages.pop()).toBe('ack https://other.example/protocol/foo');
|
||||
});
|
||||
|
||||
it('respects the X-Forwarded-* headers if Forwarded header is not present.', async(): Promise<void> => {
|
||||
const webSocket = new DummySocket();
|
||||
const upgradeRequest = {
|
||||
headers: {
|
||||
'x-forwarded-host': 'other.example',
|
||||
'x-forwarded-proto': 'https',
|
||||
'sec-websocket-protocol': 'solid-0.1',
|
||||
},
|
||||
socket: {},
|
||||
} as any as HttpRequest;
|
||||
await protocol.handle({ webSocket, upgradeRequest } as any);
|
||||
webSocket.emit('message', 'sub https://other.example/protocol/foo');
|
||||
expect(webSocket.messages).toHaveLength(2);
|
||||
expect(webSocket.messages.pop()).toBe('ack https://other.example/protocol/foo');
|
||||
});
|
||||
});
|
||||
|
@ -81,4 +81,53 @@ describe('A OriginalUrlExtractor', (): void => {
|
||||
await expect(extractor.handle({ request: { url: '/foo/bar', headers } as any }))
|
||||
.resolves.toEqual({ path: 'https://pod.example/foo/bar' });
|
||||
});
|
||||
|
||||
it('should fallback to x-fowarded-* headers.', async(): Promise<void> => {
|
||||
const headers = {
|
||||
host: 'test.com',
|
||||
'x-forwarded-host': 'pod.example',
|
||||
'x-forwarded-proto': 'https',
|
||||
};
|
||||
await expect(extractor.handle({ request: { url: '/foo/bar', headers } as any }))
|
||||
.resolves.toEqual({ path: 'https://pod.example/foo/bar' });
|
||||
});
|
||||
|
||||
it('should just take x-forwarded-host if provided.', async(): Promise<void> => {
|
||||
const headers = {
|
||||
host: 'test.com',
|
||||
'x-forwarded-host': 'pod.example',
|
||||
};
|
||||
await expect(extractor.handle({ request: { url: '/foo/bar', headers } as any }))
|
||||
.resolves.toEqual({ path: 'http://pod.example/foo/bar' });
|
||||
});
|
||||
|
||||
it('should just take x-forwarded-protocol if provided.', async(): Promise<void> => {
|
||||
const headers = {
|
||||
host: 'test.com',
|
||||
'x-forwarded-proto': 'https',
|
||||
};
|
||||
await expect(extractor.handle({ request: { url: '/foo/bar', headers } as any }))
|
||||
.resolves.toEqual({ path: 'https://test.com/foo/bar' });
|
||||
});
|
||||
|
||||
it('should prefer forwarded header to x-forwarded-* headers.', async(): Promise<void> => {
|
||||
const headers = {
|
||||
host: 'test.com',
|
||||
forwarded: 'proto=http;host=pod.example',
|
||||
'x-forwarded-proto': 'https',
|
||||
'x-forwarded-host': 'anotherpod.example',
|
||||
};
|
||||
await expect(extractor.handle({ request: { url: '/foo/bar', headers } as any }))
|
||||
.resolves.toEqual({ path: 'http://pod.example/foo/bar' });
|
||||
});
|
||||
|
||||
it('should just take the first x-forwarded-* value.', async(): Promise<void> => {
|
||||
const headers = {
|
||||
host: 'test.com',
|
||||
'x-forwarded-host': 'pod.example, another.domain',
|
||||
'x-forwarded-proto': 'http,https',
|
||||
};
|
||||
await expect(extractor.handle({ request: { url: '/foo/bar', headers } as any }))
|
||||
.resolves.toEqual({ path: 'http://pod.example/foo/bar' });
|
||||
});
|
||||
});
|
||||
|
@ -188,16 +188,18 @@ describe('HeaderUtil', (): void => {
|
||||
});
|
||||
|
||||
describe('#parseForwarded', (): void => {
|
||||
it('parses an undefined value.', (): void => {
|
||||
expect(parseForwarded()).toEqual({});
|
||||
it('handles an empty set of headers.', (): void => {
|
||||
expect(parseForwarded({})).toEqual({});
|
||||
});
|
||||
|
||||
it('parses an empty string.', (): void => {
|
||||
expect(parseForwarded('')).toEqual({});
|
||||
it('handles empty string values.', (): void => {
|
||||
const headers = { forwarded: '', 'x-forwarded-host': '', 'x-forwarded-proto': '' };
|
||||
expect(parseForwarded(headers)).toEqual({});
|
||||
});
|
||||
|
||||
it('parses a Forwarded header value.', (): void => {
|
||||
expect(parseForwarded('for=192.0.2.60;proto=http;by=203.0.113.43;host=example.org')).toEqual({
|
||||
const headers = { forwarded: 'for=192.0.2.60;proto=http;by=203.0.113.43;host=example.org' };
|
||||
expect(parseForwarded(headers)).toEqual({
|
||||
by: '203.0.113.43',
|
||||
for: '192.0.2.60',
|
||||
host: 'example.org',
|
||||
@ -206,15 +208,48 @@ describe('HeaderUtil', (): void => {
|
||||
});
|
||||
|
||||
it('skips empty fields.', (): void => {
|
||||
expect(parseForwarded('for=192.0.2.60;proto=;by=;host=')).toEqual({
|
||||
const headers = { forwarded: 'for=192.0.2.60;proto=;by=;host=' };
|
||||
expect(parseForwarded(headers)).toEqual({
|
||||
for: '192.0.2.60',
|
||||
});
|
||||
});
|
||||
|
||||
it('takes only the first value into account.', (): void => {
|
||||
expect(parseForwarded('host=pod.example, for=192.0.2.43, host=other')).toEqual({
|
||||
const headers = { forwarded: 'host=pod.example, for=192.0.2.43, host=other' };
|
||||
expect(parseForwarded(headers)).toEqual({
|
||||
host: 'pod.example',
|
||||
});
|
||||
});
|
||||
|
||||
it('should fall back to X-Forwarded-Host and X-Forwarded-Proto without Forward header.', (): void => {
|
||||
const headers = { 'x-forwarded-host': 'pod.example', 'x-forwarded-proto': 'https' };
|
||||
expect(parseForwarded(headers)).toEqual({
|
||||
host: 'pod.example',
|
||||
proto: 'https',
|
||||
});
|
||||
});
|
||||
|
||||
it('should prefer Forwarded to X-Forwarded-Host and X-Forwarded-Proto with Forward header.', (): void => {
|
||||
const headers = {
|
||||
forwarded: 'proto=http;host=pod.example',
|
||||
'x-forwarded-host': 'anotherpod.example',
|
||||
'x-forwarded-proto': 'https',
|
||||
};
|
||||
expect(parseForwarded(headers)).toEqual({
|
||||
host: 'pod.example',
|
||||
proto: 'http',
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly handle multiple values with varying spaces for X-Forwarded-*.', (): void => {
|
||||
const headers = {
|
||||
'x-forwarded-host': ' pod.example ,192.0.2.60, 192.0.2.43',
|
||||
'x-forwarded-proto': ' https ,http',
|
||||
};
|
||||
expect(parseForwarded(headers)).toEqual({
|
||||
host: 'pod.example',
|
||||
proto: 'https',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user