From efa7cbfd74c1c06ce4bee382b06729fe84643699 Mon Sep 17 00:00:00 2001 From: Evert Pot Date: Sun, 24 Apr 2022 20:57:13 -0400 Subject: [PATCH] Add a 'absoluteUrl' property to ctx.request This keeps on coming back, and maybe it is better to have a standard convention for this after all. This also adds a ctx.request.publicBaseUrl and ctx.response.publicBaseUrl, but perhaps these should actually be called ctx.request.origin and ctx.response.origin --- src/application.ts | 48 +++++++++++++++++++++++++++++++++++------ src/context.ts | 7 ++++++ src/memory-request.ts | 4 ++-- src/memory-response.ts | 4 ++-- src/node/request.ts | 4 ++-- src/node/response.ts | 14 +++++++++--- src/request.ts | 22 ++++++++++++++++++- src/response.ts | 9 +++++++- test/application.ts | 1 + test/conditional.ts | 16 +++++++------- test/context.ts | 48 ++++++++++++++++++++--------------------- test/header-helpers.ts | 4 ++-- test/memory-request.ts | 10 ++++++++- test/memory-response.ts | 2 +- test/node/push.ts | 42 ++++++++++++++++++++---------------- test/node/response.ts | 2 +- test/request.ts | 2 +- 17 files changed, 165 insertions(+), 74 deletions(-) diff --git a/src/application.ts b/src/application.ts index 65f4b9f..9c4a924 100644 --- a/src/application.ts +++ b/src/application.ts @@ -122,8 +122,8 @@ export default class Application extends EventEmitter { }); wss.on('connection', async(ws, req) => { - const request = new NodeRequest(req); - const response = new MemoryResponse(); + const request = new NodeRequest(req, this.publicBaseUrl); + const response = new MemoryResponse(this.publicBaseUrl); const context = new Context(request, response); context.webSocket = ws; @@ -180,8 +180,8 @@ export default class Application extends EventEmitter { // We don't have an existing Websocket server. Lets make one. this.wss = new WebSocket.Server({ noServer: true }); this.wss.on('connection', async(ws, req) => { - const request = new NodeRequest(req); - const response = new MemoryResponse(); + const request = new NodeRequest(req, this.publicBaseUrl); + const response = new MemoryResponse(this.publicBaseUrl); const context = new Context(request, response); context.webSocket = ws; @@ -213,12 +213,12 @@ export default class Application extends EventEmitter { let request: Request; if (typeof arg1 === 'string') { - request = new MemoryRequest(arg1, path!, headers, body); + request = new MemoryRequest(arg1, path!, this.publicBaseUrl, headers, body); } else { request = arg1; } - const context = new Context(request, new MemoryResponse()); + const context = new Context(request, new MemoryResponse(this.publicBaseUrl)); try { await this.handle(context); @@ -247,9 +247,43 @@ export default class Application extends EventEmitter { req: NodeHttpRequest, res: NodeHttpResponse ): Context { - const context = new Context(new NodeRequest(req), new NodeResponse(res)); + const context = new Context( + new NodeRequest(req, this.publicBaseUrl), + new NodeResponse(res, this.publicBaseUrl) + ); return context; } + private _publicBaseUrl?: string; + + /** + * The public base url of the application. + * + * This can be auto-detected, but will often be wrong when your server is + * running behind a reverse proxy or load balancer. + * + * To provide this, set the process.env.PUBLIC_URI property. + */ + get publicBaseUrl(): string { + + if (this._publicBaseUrl) { + return this._publicBaseUrl; + } + + if (process.env.PUBLIC_URI) { + return process.env.PUBLIC_URI; + } + + const port = process.env.PORT ? +process.env.PORT : 80; + return 'http://localhost' + (port?':' + port : ''); + + } + + set publicBaseUrl(baseUrl: string) { + + this._publicBaseUrl = new URL(baseUrl).origin; + + } + } diff --git a/src/context.ts b/src/context.ts index 3c5dc41..81b14c2 100644 --- a/src/context.ts +++ b/src/context.ts @@ -53,6 +53,13 @@ export class Context { } + get absoluteUrl(): string { + + return this.request.absoluteUrl; + + } + + /** * HTTP method * diff --git a/src/memory-request.ts b/src/memory-request.ts index f9dd616..16799f2 100644 --- a/src/memory-request.ts +++ b/src/memory-request.ts @@ -15,9 +15,9 @@ export class MemoryRequest extends Request { */ body: T; - constructor(method: string, requestTarget: string, headers?: HeadersInterface | HeadersObject, body: any = null) { + constructor(method: string, requestTarget: string, publicBaseUrl: string, headers?: HeadersInterface | HeadersObject, body: any = null, absoluteUrl?: string) { - super(method, requestTarget); + super(method, requestTarget, publicBaseUrl); if (headers?.get !== undefined) { this.headers = headers as HeadersInterface; } else { diff --git a/src/memory-response.ts b/src/memory-response.ts index 0dd3041..131f2b4 100644 --- a/src/memory-response.ts +++ b/src/memory-response.ts @@ -3,9 +3,9 @@ import { Response, Body } from './response'; export class MemoryResponse extends Response { - constructor() { + constructor(publicBaseUrl: string) { - super(); + super(publicBaseUrl); this.headers = new Headers(); this.status = 200; (this.body as any) = null; diff --git a/src/node/request.ts b/src/node/request.ts index ccc6bd1..74c213d 100644 --- a/src/node/request.ts +++ b/src/node/request.ts @@ -11,9 +11,9 @@ export class NodeRequest extends Request { */ private inner: NodeHttpRequest; - constructor(inner: NodeHttpRequest) { + constructor(inner: NodeHttpRequest, publicBaseUrl: string) { - super(inner.method!, inner.url!); + super(inner.method!, inner.url!, publicBaseUrl); this.inner = inner; // @ts-expect-error ignoring that headers might be undefined this.headers = new Headers(this.inner.headers); diff --git a/src/node/response.ts b/src/node/response.ts index b5abf00..00d3360 100644 --- a/src/node/response.ts +++ b/src/node/response.ts @@ -17,7 +17,7 @@ export class NodeResponse implements Response { private bodyValue!: T; private explicitStatus: boolean; - constructor(inner: NodeHttpResponse) { + constructor(inner: NodeHttpResponse, publicBaseUrl: string) { // The default response status is 404. this.inner = inner; @@ -27,6 +27,7 @@ export class NodeResponse implements Response { this.body = null; this.status = 404; this.explicitStatus = false; + this.publicBaseUrl = publicBaseUrl; } @@ -149,8 +150,8 @@ export class NodeResponse implements Response { } const pushCtx = new Context( - new MemoryRequest('GET', '|||DELIBERATELY_INVALID|||'), - new MemoryResponse() + new MemoryRequest('GET', '|||DELIBERATELY_INVALID|||', this.publicBaseUrl), + new MemoryResponse(this.publicBaseUrl) ); await invokeMiddlewares(pushCtx, [callback]); @@ -232,6 +233,13 @@ export class NodeResponse implements Response { this.headers.set('Location', addr); } + /** + * Public base URL + * + * This will be used to determine the absoluteUrl + */ + readonly publicBaseUrl: string; + } export default NodeResponse; diff --git a/src/request.ts b/src/request.ts index 08bb50e..29c01cb 100644 --- a/src/request.ts +++ b/src/request.ts @@ -13,10 +13,11 @@ export type Encoding = 'utf-8' | 'ascii' | 'hex'; */ export abstract class Request { - constructor(method: string, requestTarget: string) { + constructor(method: string, requestTarget: string, publicBaseUrl: string) { this.method = method; this.requestTarget = requestTarget; this.headers = new Headers(); + this.publicBaseUrl = publicBaseUrl; } /** @@ -210,6 +211,25 @@ export abstract class Request { } + /** + * Returns the absolute url for this request. + * + * This may not always be correct, because it's based on a best guess. + * If you have a reverse proxy in front of your curveball server, you may + * need to provide a 'publicBaseUrl' argument when constructing the server. + */ + get absoluteUrl(): string { + + return this.publicBaseUrl + this.requestTarget; + + } + + /** + * Public base URL + * + * This will be used to determine the absoluteUrl + */ + readonly publicBaseUrl: string; } export default Request; diff --git a/src/response.ts b/src/response.ts index 295dff9..80e3627 100644 --- a/src/response.ts +++ b/src/response.ts @@ -18,10 +18,11 @@ export type Body = */ export abstract class Response { - constructor() { + constructor(publicBaseUrl: string) { this.headers = new Headers(); this.status = 200; + this.publicBaseUrl = publicBaseUrl; } @@ -134,6 +135,12 @@ export abstract class Response { this.headers.set('Location', addr); } + /** + * Public base URL + * + * This will be used to determine the absoluteUrl + */ + readonly publicBaseUrl: string; } export default Response; diff --git a/test/application.ts b/test/application.ts index 064994b..2c6e81a 100644 --- a/test/application.ts +++ b/test/application.ts @@ -314,6 +314,7 @@ describe('Application', () => { const request = new MemoryRequest( 'POST', '/', + application.publicBaseUrl, { foo: 'bar' }, 'request-body' ); diff --git a/test/conditional.ts b/test/conditional.ts index cbd1e34..cea1bea 100644 --- a/test/conditional.ts +++ b/test/conditional.ts @@ -18,7 +18,7 @@ describe('conditionals', () => { for (const [status, method, header] of tests) { it(`should return ${status} when doing ${method} with If-Match: ${header}`, () => { - const request = new MemoryRequest(method, '/foo', { 'If-Match': header }); + const request = new MemoryRequest(method, '/foo', 'http://localhost', { 'If-Match': header }); expect(conditionalCheck(request, null, '"a"')).to.eql(status); }); @@ -38,7 +38,7 @@ describe('conditionals', () => { for (const [status, method, header] of tests) { it(`should return ${status} when doing ${method} with If-Match: ${header}`, () => { - const request = new MemoryRequest(method, '/foo', { 'If-Match': header }); + const request = new MemoryRequest(method, '/foo', 'http://localhost', { 'If-Match': header }); expect(conditionalCheck(request, null, null)).to.eql(status); }); @@ -63,7 +63,7 @@ describe('conditionals', () => { for (const [status, method, header] of tests) { it(`should return ${status} when doing ${method} with If-None-Match: ${header}`, () => { - const request = new MemoryRequest(method, '/foo', { 'If-None-Match': header }); + const request = new MemoryRequest(method, '/foo', 'http://localhost', { 'If-None-Match': header }); expect(conditionalCheck(request, null, '"a"')).to.eql(status); }); @@ -83,7 +83,7 @@ describe('conditionals', () => { for (const [status, method, header] of tests) { it(`should return ${status} when doing ${method} with If-None-Match: ${header}`, () => { - const request = new MemoryRequest(method, '/foo', { 'If-None-Match': header }); + const request = new MemoryRequest(method, '/foo', 'http://localhost', { 'If-None-Match': header }); expect(conditionalCheck(request, null, null)).to.eql(status); }); @@ -103,7 +103,7 @@ describe('conditionals', () => { for (const [status, method, headerDate] of tests) { it(`should return ${status} when doing ${method} with If-Modified-Since: ${headerDate}`, () => { - const request = new MemoryRequest(method, '/foo', { 'If-Modified-Since': headerDate }); + const request = new MemoryRequest(method, '/foo', 'http://localhost', { 'If-Modified-Since': headerDate }); expect(conditionalCheck(request, new Date('2020-03-06 00:00:00'), null)).to.eql(status); }); @@ -115,7 +115,7 @@ describe('conditionals', () => { it('should return 200', () => { - const request = new MemoryRequest('GET', '/foo', { 'If-Modified-Since': 'Thu, 7 Mar 2019 14:49:00 GMT' }); + const request = new MemoryRequest('GET', '/foo', 'http://localhost', { 'If-Modified-Since': 'Thu, 7 Mar 2019 14:49:00 GMT' }); expect(conditionalCheck(request, null, null)).to.eql(200); }); @@ -134,7 +134,7 @@ describe('conditionals', () => { for (const [status, method, headerDate] of tests) { it(`should return ${status} when doing ${method} with If-Unmodified-Since: ${headerDate}`, () => { - const request = new MemoryRequest(method, '/foo', { 'If-Unmodified-Since': headerDate }); + const request = new MemoryRequest(method, '/foo', 'http://localhost', { 'If-Unmodified-Since': headerDate }); expect(conditionalCheck(request, new Date('2020-03-06 00:00:00'), null)).to.eql(status); }); @@ -146,7 +146,7 @@ describe('conditionals', () => { it('should return 412', () => { - const request = new MemoryRequest('GET', '/foo', { 'If-Unmodified-Since': 'Thu, 7 Mar 2019 14:49:00 GMT' }); + const request = new MemoryRequest('GET', '/foo', 'http://localhost', { 'If-Unmodified-Since': 'Thu, 7 Mar 2019 14:49:00 GMT' }); expect(conditionalCheck(request, null, null)).to.eql(412); }); diff --git a/test/context.ts b/test/context.ts index 0abeae4..39a126b 100644 --- a/test/context.ts +++ b/test/context.ts @@ -7,8 +7,8 @@ describe('Context', () => { it('should instantiate correctly', () => { - const request = new Request('GET', '/'); - const response = new Response(); + const request = new Request('GET', '/', 'http://localhost'); + const response = new Response('http://localhost'); const context = new Context( request, @@ -22,8 +22,8 @@ describe('Context', () => { it('should forward the "method" property to the request', () => { - const request = new Request('GET', '/'); - const response = new Response(); + const request = new Request('GET', '/', 'http://localhost'); + const response = new Response('http://localhost'); const context = new Context( request, @@ -36,8 +36,8 @@ describe('Context', () => { it('should forward the "path" property to the request', () => { - const request = new Request('GET', '/foo'); - const response = new Response(); + const request = new Request('GET', '/foo', 'http://localhost'); + const response = new Response('http://localhost'); const context = new Context( request, @@ -49,8 +49,8 @@ describe('Context', () => { }); it('should forward the "query" property to the request', () => { - const request = new Request('GET', '/foo?a=b'); - const response = new Response(); + const request = new Request('GET', '/foo?a=b', 'http://localhost'); + const response = new Response('http://localhost'); const context = new Context( request, @@ -63,8 +63,8 @@ describe('Context', () => { it('should forward the "accepts" method to the request', () => { - const request = new Request('GET', '/foo', {Accept: 'text/html'}); - const response = new Response(); + const request = new Request('GET', '/foo', 'http://localhost', {Accept: 'text/html'}); + const response = new Response('http://localhost'); const context = new Context( request, @@ -77,8 +77,8 @@ describe('Context', () => { it('should forward the "status" property to the response', () => { - const request = new Request('GET', '/foo'); - const response = new Response(); + const request = new Request('GET', '/foo', 'http://localhost'); + const response = new Response('http://localhost'); response.status = 414; const context = new Context( @@ -97,8 +97,8 @@ describe('Context', () => { it('should forward the "push" method to the response', () => { let called = false; - const request = new Request('GET', '/foo'); - const response = new Response(); + const request = new Request('GET', '/foo', 'http://localhost'); + const response = new Response('http://localhost'); response.push = () => { called = true; @@ -123,8 +123,8 @@ describe('Context', () => { it('should forward the "sendInformational" method to the response', () => { let called = false; - const request = new Request('GET', '/foo'); - const response = new Response(); + const request = new Request('GET', '/foo', 'http:/localhost'); + const response = new Response('http://localhost'); response.sendInformational = () => { called = true; @@ -147,8 +147,8 @@ describe('Context', () => { it('should return null if the underlying request isn\'t socket-based', () => { - const request = new Request('GET', '/foo'); - const response = new Response(); + const request = new Request('GET', '/foo', 'http://localhost'); + const response = new Response('http://localhost'); const context = new Context( request, @@ -159,9 +159,9 @@ describe('Context', () => { }); it('should call the ip() method on the request if it\'s socket-based', () => { - const request = new Request('GET', '/foo'); + const request = new Request('GET', '/foo', 'http://localhost'); (request as any).ip = () => '127.0.0.1'; - const response = new Response(); + const response = new Response('http://localhost'); const context = new Context( request, @@ -179,8 +179,8 @@ describe('Context', () => { const newTarget = '/bar'; const defaultStatus = 303; - const request = new Request('GET', originalTarget); - const response = new Response(); + const request = new Request('GET', originalTarget, 'http://localhost'); + const response = new Response('http://localhost'); const context = new Context( request, @@ -199,8 +199,8 @@ describe('Context', () => { const newTarget = '/bar'; const newStatus = 301; - const request = new Request('GET', originalTarget); - const response = new Response(); + const request = new Request('GET', originalTarget, 'http://localhost'); + const response = new Response('http://localhost'); const context = new Context( request, diff --git a/test/header-helpers.ts b/test/header-helpers.ts index 0dac482..16c887d 100644 --- a/test/header-helpers.ts +++ b/test/header-helpers.ts @@ -36,7 +36,7 @@ describe('Header helpers', () => { it(`should return ${test[2]} for a Content-Type of ${test[0]} and an argument ${test[1]}`, () => { - const request = new MemoryRequest('GET', '/'); + const request = new MemoryRequest('GET', '/', 'http://localhost'); request.headers.set('Content-Type', test[0]); expect(is(request, test[1])).to.eql(test[2]); @@ -46,7 +46,7 @@ describe('Header helpers', () => { it('should return false when no Content-Type was set', () => { - const request = new MemoryRequest('GET', '/'); + const request = new MemoryRequest('GET', '/', 'http://localhost'); expect(is(request, 'json')).to.eql(false); }); diff --git a/test/memory-request.ts b/test/memory-request.ts index 741e773..b26b612 100644 --- a/test/memory-request.ts +++ b/test/memory-request.ts @@ -7,6 +7,7 @@ function getReq() { return new MemoryRequest( 'POST', '/foo?a=1&b=2', + 'http://localhost', { 'X-FOO': 'BAR', 'Accept': 'text/html', @@ -48,7 +49,7 @@ describe('MemoryRequest', () => { it('should work with HeadersInterface', async () => { const headers = new Headers(); - const request = new MemoryRequest('GET', '/', headers); + const request = new MemoryRequest('GET', '/', 'http://localhost', headers); expect(request.headers).to.equal(headers); @@ -125,6 +126,7 @@ describe('MemoryRequest', () => { const req = new MemoryRequest( 'POST', '/', + 'http://localhost', {}, 'hello' ); @@ -139,6 +141,7 @@ describe('MemoryRequest', () => { const req = new MemoryRequest( 'POST', '/', + 'http://localhost', {}, 'hello' ); @@ -153,6 +156,7 @@ describe('MemoryRequest', () => { const req = new MemoryRequest( 'POST', '/', + 'http://localhost', {} ); @@ -166,6 +170,7 @@ describe('MemoryRequest', () => { const req = new MemoryRequest( 'POST', '/', + 'http://localhost', {}, { foo: 'bar' }, ); @@ -180,6 +185,7 @@ describe('MemoryRequest', () => { const req = new MemoryRequest( 'POST', '/', + 'http://localhost', {}, Buffer.from('hello') ); @@ -195,6 +201,7 @@ describe('MemoryRequest', () => { const req = new MemoryRequest( 'POST', '/', + 'http://localhost', {}, buffer ); @@ -213,6 +220,7 @@ describe('MemoryRequest', () => { const req = new MemoryRequest( 'POST', '/', + 'http://localhost', {}, 'hello' ); diff --git a/test/memory-response.ts b/test/memory-response.ts index 2859fe0..3b531b7 100644 --- a/test/memory-response.ts +++ b/test/memory-response.ts @@ -3,7 +3,7 @@ import { MemoryResponse } from '../src/memory-response'; function getRes() { - const response = new MemoryResponse(); + const response = new MemoryResponse('http://localhost'); response.headers.set('Content-Type', 'text/html; charset=utf-8'); response.status = 200; diff --git a/test/node/push.ts b/test/node/push.ts index dfc6eea..f809c6b 100644 --- a/test/node/push.ts +++ b/test/node/push.ts @@ -289,11 +289,14 @@ describe('NodeResponse http/2 push', () => { }); it('should throw an error when no path was set', async () => { - const response = new NodeResponse({ - stream: { - pushAllowed: true - } - } as any); + const response = new NodeResponse( + { + stream: { + pushAllowed: true + } + } as any, + 'http://localhost', + ); let err; try { @@ -314,14 +317,17 @@ describe('NodeResponse http/2 push', () => { it('should handle stream errors', async () => { - const response = new NodeResponse({ - stream: { - pushStream(headers: any, callback: any) { - callback(new Error('hi')); + const response = new NodeResponse( + { + stream: { + pushStream(headers: any, callback: any) { + callback(new Error('hi')); + }, + pushAllowed: true }, - pushAllowed: true - } - } as any); + } as any, + 'http://localhost' + ); let err; try { @@ -364,8 +370,8 @@ describe('push() function', () => { await push( stream as any, new Context( - new MemoryRequest('GET', '/push-resource'), - new MemoryResponse() + new MemoryRequest('GET', '/push-resource', 'http://localhost'), + new MemoryResponse('http://localhost') ) ); @@ -404,8 +410,8 @@ describe('push() function', () => { await push( stream as any, new Context( - new MemoryRequest('GET', '/push-resource'), - new MemoryResponse() + new MemoryRequest('GET', '/push-resource', 'http://localhost'), + new MemoryResponse('http://localhost') ) ); @@ -441,8 +447,8 @@ describe('push() function', () => { await push( stream as any, new Context( - new MemoryRequest('GET', '/push-resource'), - new MemoryResponse() + new MemoryRequest('GET', '/push-resource', 'http://localhost'), + new MemoryResponse('http://localhost') ) ); } catch (e: any) { diff --git a/test/node/response.ts b/test/node/response.ts index b350284..357c4c9 100644 --- a/test/node/response.ts +++ b/test/node/response.ts @@ -15,7 +15,7 @@ function getRes() { inner.setHeader('Content-Type', 'text/html; charset=utf-8'); inner.statusCode = 200; - const outer = new NodeResponse(inner); + const outer = new NodeResponse(inner, 'http://localhost'); return outer; } diff --git a/test/request.ts b/test/request.ts index afc6f21..bd7c263 100644 --- a/test/request.ts +++ b/test/request.ts @@ -8,7 +8,7 @@ class FakeRequest extends Request { constructor(method: string, path: string, headers: HeadersObject, body: any = '') { - super(method, path); + super(method, path, 'http://localhost'); this.headers = new Headers(headers); this.body = Buffer.from(body);