feat: Support creation of HTTPS server

This commit is contained in:
Joachim Van Herwegen 2021-05-27 13:00:52 +02:00
parent afc662ca9a
commit 7faad0aef0
9 changed files with 246 additions and 55 deletions

View File

@ -0,0 +1,53 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld",
"import": [
"files-scs:config/http/handler/default.json",
"files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/static/default.json",
"files-scs:config/identity/handler/default.json",
"files-scs:config/identity/email/default.json",
"files-scs:config/init/handler/default.json",
"files-scs:config/ldp/authentication/dpop-bearer.json",
"files-scs:config/ldp/authorization/webacl.json",
"files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json",
"files-scs:config/pod/handler/static.json",
"files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/resource-store/file.json",
"files-scs:config/util/auxiliary/acl.json",
"files-scs:config/util/identifiers/suffix.json",
"files-scs:config/util/index/default.json",
"files-scs:config/util/logging/winston.json",
"files-scs:config/util/representation-conversion/default.json",
"files-scs:config/util/resource-locker/memory.json",
"files-scs:config/util/variables/default.json"
],
"@graph": [
{
"comment": [
"An example of what a config could look like if HTTPS is required.",
"The http/server-factory import above has been omitted since that feature is set below."
]
},
{
"comment": "The key/cert values should be replaces with paths to the correct files. The 'options' block can be removed if not needed.",
"@id": "urn:solid-server:default:ServerFactory",
"@type": "WebSocketServerFactory",
"baseServerFactory": {
"@id": "urn:solid-server:default:HttpServerFactory",
"@type": "BaseHttpServerFactory",
"handler": { "@id": "urn:solid-server:default:HttpHandler" },
"options_https": true,
"options_key": "/path/to/server.key",
"options_cert": "/path/to/server.cert"
},
"webSocketHandler": {
"@type": "UnsecureWebSocketsProtocol",
"source": { "@id": "urn:solid-server:default:ResourceStore" }
}
}
]
}

View File

@ -16,6 +16,7 @@ and then pass the request along.
The factory used to create the actual server object.
* *no-websockets*: Only HTTP.
* *websockets*: HTTP and websockets.
* *https-example*: An example configuration to use HTTPS directly at the server (instead of at a reverse proxy).
## Static
Support for static files that should be found at a specific path.

View File

@ -0,0 +1,22 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld",
"@graph": [
{
"comment": "An example of how to set up a server with HTTPS",
"@id": "urn:solid-server:default:ServerFactory",
"@type": "WebSocketServerFactory",
"baseServerFactory": {
"@id": "urn:solid-server:default:HttpServerFactory",
"@type": "BaseHttpServerFactory",
"handler": { "@id": "urn:solid-server:default:HttpHandler" },
"options_https": true,
"options_key": "/path/to/server.key",
"options_cert": "/path/to/server.cert"
},
"webSocketHandler": {
"@type": "UnsecureWebSocketsProtocol",
"source": { "@id": "urn:solid-server:default:ResourceStore" }
}
}
]
}

View File

@ -5,9 +5,7 @@
"comment": "Creates a server that supports HTTP requests.",
"@id": "urn:solid-server:default:ServerFactory",
"@type": "BaseHttpServerFactory",
"handler": {
"@id": "urn:solid-server:default:HttpHandler"
}
"handler": { "@id": "urn:solid-server:default:HttpHandler" }
}
]
}

View File

@ -1,11 +1,33 @@
import { readFileSync } from 'fs';
import type { Server, IncomingMessage, ServerResponse } from 'http';
import { createServer } from 'http';
import { createServer as createHttpServer } from 'http';
import { createServer as createHttpsServer } from 'https';
import { URL } from 'url';
import { getLoggerFor } from '../logging/LogUtil';
import { isNativeError } from '../util/errors/ErrorUtil';
import { guardStream } from '../util/GuardedStream';
import type { HttpHandler } from './HttpHandler';
import type { HttpServerFactory } from './HttpServerFactory';
/**
* Options to be used when creating the server.
* Due to Components.js not supporting external types, this has been simplified (for now?).
* The common https keys here (key/cert/pfx) will be interpreted as file paths that need to be read
* before passing the options to the `createServer` function.
*/
export interface BaseHttpServerOptions {
/**
* If the server should start as an http or https server.
*/
https?: boolean;
key?: string;
cert?: string;
pfx?: string;
passphrase?: string;
}
/**
* HttpServerFactory based on the native Node.js http module
*/
@ -14,19 +36,26 @@ export class BaseHttpServerFactory implements HttpServerFactory {
/** The main HttpHandler */
private readonly handler: HttpHandler;
private readonly options: BaseHttpServerOptions;
public constructor(handler: HttpHandler) {
public constructor(handler: HttpHandler, options: BaseHttpServerOptions = { https: false }) {
this.handler = handler;
this.options = { ...options };
}
/**
* Creates and starts an HTTP server
* Creates and starts an HTTP(S) server
* @param port - Port on which the server listens
*/
public startServer(port: number): Server {
this.logger.info(`Starting server at http://localhost:${port}/`);
const protocol = this.options.https ? 'https' : 'http';
const url = new URL(`${protocol}://localhost:${port}/`).href;
this.logger.info(`Starting server at ${url}`);
const server = createServer(
const createServer = this.options.https ? createHttpsServer : createHttpServer;
const options = this.createServerOptions();
const server = createServer(options,
async(request: IncomingMessage, response: ServerResponse): Promise<void> => {
try {
this.logger.info(`Received ${request.method} request for ${request.url}`);
@ -45,9 +74,19 @@ export class BaseHttpServerFactory implements HttpServerFactory {
response.writeHead(404).end();
}
}
},
);
});
return server.listen(port);
}
private createServerOptions(): BaseHttpServerOptions {
const options = { ...this.options };
for (const id of [ 'key', 'cert', 'pfx' ] as const) {
const val = options[id];
if (val) {
options[id] = readFileSync(val, 'utf8');
}
}
return options;
}
}

View File

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDbTCCAlWgAwIBAgIUFoZojFFFwF4/zB9XlZ3zXgZyvo0wDQYJKoZIhvcNAQEL
BQAwRjELMAkGA1UEBhMCQkUxEzARBgNVBAgMClNvbWUtU3RhdGUxDjAMBgNVBAoM
BVNvbGlkMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMjEwNTI3MDkwMTQxWhcNMjEw
NjI2MDkwMTQxWjBGMQswCQYDVQQGEwJCRTETMBEGA1UECAwKU29tZS1TdGF0ZTEO
MAwGA1UECgwFU29saWQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAK5C/0PCj2If698eYOCvp4z87oY0YZSI5MZnsbv2
NWNAA+9ODvycA0oStyqzmGUw4sajqFrJZuYjDZWELnhEgv7bpwGDncHh/KjwPkwl
w0RQkELSUfFPO6uAz/JwaF2XiI1XS11I2EvzSJNPlgrqKWNiAOZ59Vm5Seda+16m
nhnqFJCSQ+bImdGVdnGQUHzHjidwoyv5QFOW6MZd+1jwY+HZFoWFYy9QMAHMR2/I
pKnsfb3zeQYyEhSxwDwwrCiIFfPhUX+wezsVS3KrGZyUeg8Z8/Jb7p3Vk5fXItxP
t2IcOKrU/fnXDdcghRylcX93fs6DzCh9p4x/gyUsAazoDVECAwEAAaNTMFEwHQYD
VR0OBBYEFAS3dWyE56NTg8K+RabiIwSII6njMB8GA1UdIwQYMBaAFAS3dWyE56NT
g8K+RabiIwSII6njMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB
ADFPYohoV7scihIW+nW2KQuflNYc026CSUBbjIh3CtUQKI4uM8d9zY6+hDX7WtF2
asR29a6f1/BMKDWlHCbBbP17x4FXZwpUCnLPqQS9XEVvC4B+s4hc3wgxMFewvClX
isAZPIpbrOiqpF1unRHmKzyuGsmQmul22YRfeqj3y6qVry/iSdTtjZQY7oy33prC
2MZWxs5C+YSRbpSYmYg6T7jDqpvcuWn4Ta5L04g7lsksE5/hSBML6cLGpCmNbqnx
l1ZJDs9o4DAzMoRux2N1p+ndY7Rly19bOazTBeYGZ7WwWh8LH/TBuM8o2w74dlgq
tUDcKM3uEHZQrIrscK27ef4=
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCuQv9Dwo9iH+vf
HmDgr6eM/O6GNGGUiOTGZ7G79jVjQAPvTg78nANKErcqs5hlMOLGo6hayWbmIw2V
hC54RIL+26cBg53B4fyo8D5MJcNEUJBC0lHxTzurgM/ycGhdl4iNV0tdSNhL80iT
T5YK6iljYgDmefVZuUnnWvtepp4Z6hSQkkPmyJnRlXZxkFB8x44ncKMr+UBTlujG
XftY8GPh2RaFhWMvUDABzEdvyKSp7H2983kGMhIUscA8MKwoiBXz4VF/sHs7FUty
qxmclHoPGfPyW+6d1ZOX1yLcT7diHDiq1P351w3XIIUcpXF/d37Og8wofaeMf4Ml
LAGs6A1RAgMBAAECggEBAKlDSEDf9XfBO9Gf50e3No76ixDuRi4JffW9eOTytE6w
OmIyNtplC8jiPuoKQidgAZYiFwAACqPVPneRSbXmDjtQzXnqBszxHgJWQJykPXPY
sRdGxPMYHARs/Q8m4iiubKOlO/3jKL01FLSJpFr7sbHn2qoDoi5BjKhdNjZsrrrJ
ikoTCZwmHtxxOd98Rb1/VOt/pQsNaOTtfshMVu/gsw7Ruz7jWMMjSklNBXC2rcIh
GGJWO2f/tRpbA+R5efnuUgLFTCHgopsccHDr8c+QhEZkTuKW/jTItspGkylNL0An
n7rlkj7rDztwXMfS/ntNJQYki74s0+5NNYkfQtV+ZQUCgYEA1t0YzCZl4YnRn5tw
kubXhWhiiAeUjg5tYy9enXmbvybzB6qEPU3H3gyOC53B38bEm3fhcDfS74qPz1Pk
XiVDfBNRhMrrOjacTmrzxy+x6B+x0ZcdNhHYlfOvoVKpGs8mT1rlmg6BeFQ12E5K
ho+PekAGZx/Mt9MUcPQGVde66ecCgYEAz5/qlljDawUfnSHflXWl9ql9QGQY0WM4
w8KFtbG2K6CAhYduIirJ9U6b20GGBW0NKaFip88zGTelYm8TLUBl/gWDuCKbrV3/
CFdgo3MUuC8+GIYt+dh+nX1t59KHLERNtNQx2pwDEK9/S250PoYf+O3b0THafZ7I
bpwmQShyGAcCgYEAwFIU0R8JkHBQ/sEeaY9QmCwQDdxjHyhQxzfuQ5xHSTkuzczW
Ix1M6jdoqYMitw9uig4q7sw49YqcIKLhxVcraZLNI8SR+oBJNnPLEp5havl7q7PM
RMqCh+4gZZDcpo+Gpf8hhty3DKKrs5qYYIt9jJpkYMf48Q1xvYzfYtT/jD8CgYAm
5BmZF/9i6I7HbDTpViREU/M2QIm1jxRu9tz879Dj0yi/2mJy2/kAjjz7kQZ9tbOl
fKlyLYmwy4+bJJs++rUgJABMWY83pkfDVDqx4ziaV58WEOxDxJ3S+k/AANt5G0JD
AQxlmpuoYHdDtejoXU9X3ZYzVVdL+JYqwe0Yf27/uQKBgQDR1WFoSAppR3ocPzn8
jP+xF9tNxDJ3Wpohcnw7JAoakbMzP4LQtjwPmRTf9ORgqDjKGanP5m+gTfiPOUos
7eWNVFNKg+7X7Ej8Rst9s6JNrEhsKgz0u0vAvjwsZDCsy6WwrlrdZZkvwb5S+3Ls
M5aHThIzFI2fJX8HGAcnudkKIw==
-----END PRIVATE KEY-----

View File

@ -1,8 +1,13 @@
import type { Server } from 'http';
import request from 'supertest';
import type { BaseHttpServerOptions } from '../../../src/server/BaseHttpServerFactory';
import { BaseHttpServerFactory } from '../../../src/server/BaseHttpServerFactory';
import type { HttpHandler } from '../../../src/server/HttpHandler';
import type { HttpResponse } from '../../../src/server/HttpResponse';
import { joinFilePath } from '../../../src/util/PathUtil';
import { getPort } from '../../util/Util';
const port = getPort('BaseHttpServerFactory');
const handler: jest.Mocked<HttpHandler> = {
handleSafe: jest.fn(async(input: { response: HttpResponse }): Promise<void> => {
@ -11,15 +16,35 @@ const handler: jest.Mocked<HttpHandler> = {
}),
} as any;
describe('BaseHttpServerFactory', (): void => {
describe('A BaseHttpServerFactory', (): void => {
let server: Server;
const options: [string, BaseHttpServerOptions | undefined][] = [
[ 'http', undefined ],
[ 'https', {
https: true,
key: joinFilePath(__dirname, '../../assets/https/server.key'),
cert: joinFilePath(__dirname, '../../assets/https/server.cert'),
}],
];
describe.each(options)('with %s', (protocol, httpOptions): void => {
let rejectTls: string | undefined;
beforeAll(async(): Promise<void> => {
const factory = new BaseHttpServerFactory(handler);
server = factory.startServer(5555);
// Allow self-signed certificate
rejectTls = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const factory = new BaseHttpServerFactory(handler, httpOptions);
server = factory.startServer(port);
});
beforeEach(async(): Promise<void> => {
jest.clearAllMocks();
});
afterAll(async(): Promise<void> => {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = rejectTls;
server.close();
});
@ -66,3 +91,4 @@ describe('BaseHttpServerFactory', (): void => {
expect(res.text).toContain('Unknown error.');
});
});
});

View File

@ -6,6 +6,7 @@ import type { SystemError } from '../../src/util/errors/SystemError';
/* eslint-disable @typescript-eslint/naming-convention */
const portNames = [
// Integration
'DynamicPods',
'Identity',
'LpdHandlerWithAuth',
@ -17,6 +18,8 @@ const portNames = [
'SparqlStorage',
'Subdomains',
'WebSocketsProtocol',
// Unit
'BaseHttpServerFactory',
] as const;
export function getPort(name: typeof portNames[number]): number {