Skip to content

Commit 6941a5d

Browse files
Hage Yaapahacksparrow
authored andcommitted
feat: add HTTPs protocol support
Add built-in support for HTTPS protocol.
1 parent e2546b7 commit 6941a5d

File tree

13 files changed

+456
-92
lines changed

13 files changed

+456
-92
lines changed

packages/http-server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"@loopback/testlab": "^0.10.11",
3030
"@types/node": "^10.1.2",
3131
"@types/p-event": "^1.3.0",
32-
"@types/request-promise-native": "^1.0.14",
32+
"@types/request-promise-native": "^1.0.15",
3333
"request-promise-native": "^1.0.5"
3434
},
3535
"files": [

packages/http-server/src/http-server.ts

Lines changed: 73 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,60 @@
33
// This file is licensed under the MIT License.
44
// License text available at https://opensource.org/licenses/MIT
55

6-
import {createServer, Server, ServerRequest, ServerResponse} from 'http';
6+
import {ServerRequest, ServerResponse} from 'http';
7+
import * as http from 'http';
8+
import * as https from 'https';
79
import {AddressInfo} from 'net';
10+
811
import * as pEvent from 'p-event';
912

10-
export type HttpRequestListener = (
11-
req: ServerRequest,
12-
res: ServerResponse,
13-
) => void;
13+
export type RequestListener = (req: ServerRequest, res: ServerResponse) => void;
1414

1515
/**
16-
* Object for specifyig the HTTP / HTTPS server options
16+
* Basic HTTP server listener options
17+
*
18+
* @export
19+
* @interface ListenerOptions
1720
*/
18-
export type HttpServerOptions = {
19-
port?: number;
21+
export interface ListenerOptions {
2022
host?: string;
21-
protocol?: HttpProtocol;
22-
};
23+
port?: number;
24+
}
2325

26+
/**
27+
* HTTP server options
28+
*
29+
* @export
30+
* @interface HttpOptions
31+
*/
32+
export interface HttpOptions extends ListenerOptions {
33+
protocol?: 'http';
34+
}
35+
36+
/**
37+
* HTTPS server options
38+
*
39+
* @export
40+
* @interface HttpsOptions
41+
*/
42+
export interface HttpsOptions extends ListenerOptions, https.ServerOptions {
43+
protocol: 'https';
44+
}
45+
46+
/**
47+
* Possible server options
48+
*
49+
* @export
50+
* @type HttpServerOptions
51+
*/
52+
export type HttpServerOptions = HttpOptions | HttpsOptions;
53+
54+
/**
55+
* Supported protocols
56+
*
57+
* @export
58+
* @type HttpProtocol
59+
*/
2460
export type HttpProtocol = 'http' | 'https'; // Will be extended to `http2` in the future
2561

2662
/**
@@ -35,44 +71,51 @@ export class HttpServer {
3571
private _listening: boolean = false;
3672
private _protocol: HttpProtocol;
3773
private _address: AddressInfo;
38-
private httpRequestListener: HttpRequestListener;
39-
private httpServer: Server;
74+
private requestListener: RequestListener;
75+
private server: http.Server | https.Server;
76+
private serverOptions?: HttpServerOptions;
4077

4178
/**
42-
* @param httpServerOptions
43-
* @param httpRequestListener
79+
* @param requestListener
80+
* @param serverOptions
4481
*/
4582
constructor(
46-
httpRequestListener: HttpRequestListener,
47-
httpServerOptions?: HttpServerOptions,
83+
requestListener: RequestListener,
84+
serverOptions?: HttpServerOptions,
4885
) {
49-
this.httpRequestListener = httpRequestListener;
50-
if (!httpServerOptions) httpServerOptions = {};
51-
this._port = httpServerOptions.port || 0;
52-
this._host = httpServerOptions.host || undefined;
53-
this._protocol = httpServerOptions.protocol || 'http';
86+
this.requestListener = requestListener;
87+
this.serverOptions = serverOptions;
88+
this._port = serverOptions ? serverOptions.port || 0 : 0;
89+
this._host = serverOptions ? serverOptions.host : undefined;
90+
this._protocol = serverOptions ? serverOptions.protocol || 'http' : 'http';
5491
}
5592

5693
/**
5794
* Starts the HTTP / HTTPS server
5895
*/
5996
public async start() {
60-
this.httpServer = createServer(this.httpRequestListener);
61-
this.httpServer.listen(this._port, this._host);
62-
await pEvent(this.httpServer, 'listening');
97+
if (this._protocol === 'https') {
98+
this.server = https.createServer(
99+
this.serverOptions as https.ServerOptions,
100+
this.requestListener,
101+
);
102+
} else {
103+
this.server = http.createServer(this.requestListener);
104+
}
105+
this.server.listen(this._port, this._host);
106+
await pEvent(this.server, 'listening');
63107
this._listening = true;
64-
this._address = this.httpServer.address() as AddressInfo;
108+
this._address = this.server.address() as AddressInfo;
65109
}
66110

67111
/**
68112
* Stops the HTTP / HTTPS server
69113
*/
70114
public async stop() {
71-
if (this.httpServer) {
72-
this.httpServer.close();
73-
await pEvent(this.httpServer, 'close');
74-
this._listening = false;
75-
}
115+
if (!this.server) return;
116+
this.server.close();
117+
await pEvent(this.server, 'close');
118+
this._listening = false;
76119
}
77120

78121
/**
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDaDCCAlACCQDSmMwp5CM+CzANBgkqhkiG9w0BAQUFADB2MQswCQYDVQQGEwJV
3+
UzELMAkGA1UECAwCQ0ExCzAJBgNVBAcMAlNGMQswCQYDVQQKDAJMQjEMMAoGA1UE
4+
CwwDTEI0MRIwEAYDVQQDDAlsb2NhbGhvc3QxHjAcBgkqhkiG9w0BCQEWD2xiNEBl
5+
eGFtcGxlLmNvbTAeFw0xODA2MjgxNDMwNTdaFw0xOTA2MjgxNDMwNTdaMHYxCzAJ
6+
BgNVBAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UEBwwCU0YxCzAJBgNVBAoMAkxC
7+
MQwwCgYDVQQLDANMQjQxEjAQBgNVBAMMCWxvY2FsaG9zdDEeMBwGCSqGSIb3DQEJ
8+
ARYPbGI0QGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
9+
AQEA3mV25nB7LprWwnw2esZbzuS9vG68Eqcjiu9K0ZO9Ym8al70Wz1Q7ytqfuP4c
10+
DEjEAngvbkrT3W1ZaXUOQz5vxAa5OaLpB7moOZ3cldVyDTwlExBvFrXB5Qqrh/7Y
11+
c7OVvtb3Dah1wzvRHEt8I0EXPnojjae2uxmTu3ThZqACcpZS5SQC3hA3VOmcRpMS
12+
xKd8tvbbYYb37aaldOJkxcKge0C5adpOB8MsDvWZagBDCWaN41Wc/mER71Q1UMrz
13+
BrGB0Let4IibvUcW5nlUlfzu9qjY6ZXdb4cTDA7q6xTHmaIwhLklsI/K2Mda1YC5
14+
aIu558Kxaq1e3RWb0Hl/RpEQSQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQCSdHKL
15+
juogGyAvUH0cAOahhvuUOnfpjcOWscRa1VLpI8lR9hWX5CLt3IIqT3gVFTl8bQbq
16+
joOfUB+ArusERMtay8l/dI83l6BxOkjhz8IcKT89W5emsHPDk6l0DAMmcrZAgMM5
17+
Ow9Rt3M5dEJ7tY3xWS9WpM3WCSpou+4AZt9PLE/sCqSjkDCO0/+ao1Pr9HORP40n
18+
NOPjSqMjlesUVlfJQTi0Rscal3BQG4+2cNG+p8KzR6KLEJruORuHzRqLWh3jkUKU
19+
snB9FTDkj9kSq287SidEcF2tfi2X6ptAoxv/jdFx6unZ1q3wI0qSDZYaEAbwlO84
20+
q3Y/oEQkYu19Wzta
21+
-----END CERTIFICATE-----

packages/http-server/test/integration/http-server.integration.ts

Lines changed: 112 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,42 @@
22
// Node module: @loopback/http-server
33
// This file is licensed under the MIT License.
44
// License text available at https://opensource.org/licenses/MIT
5-
import {HttpServer} from '../../';
5+
import {HttpServer, HttpOptions, HttpServerOptions} from '../../';
66
import {supertest, expect} from '@loopback/testlab';
77
import * as makeRequest from 'request-promise-native';
88
import {ServerRequest, ServerResponse, get, IncomingMessage} from 'http';
9+
import * as https from 'https';
10+
import * as path from 'path';
11+
import * as fs from 'fs';
12+
import * as url from 'url';
913

1014
describe('HttpServer (integration)', () => {
1115
let server: HttpServer | undefined;
1216

1317
afterEach(stopServer);
1418

15-
process.env.TRAVIS
16-
? // tslint:disable-next-line:no-unused-expression
17-
it.skip
18-
: it('formats IPv6 url correctly', async () => {
19-
server = new HttpServer(dummyRequestHandler, {host: '::1'});
20-
await server.start();
21-
expect(server.address!.family).to.equal('IPv6');
22-
const response = await getAsync(server.url);
23-
expect(response.statusCode).to.equal(200);
24-
});
19+
itSkippedOnTravis('formats IPv6 url correctly', async () => {
20+
server = new HttpServer(dummyRequestHandler, {
21+
host: '::1',
22+
} as HttpOptions);
23+
await server.start();
24+
expect(server.address!.family).to.equal('IPv6');
25+
const response = await getAsync(server.url);
26+
expect(response.statusCode).to.equal(200);
27+
});
2528

2629
it('starts server', async () => {
27-
server = new HttpServer(dummyRequestHandler);
30+
const serverOptions = givenServerOptions();
31+
server = new HttpServer(dummyRequestHandler, serverOptions);
2832
await server.start();
29-
supertest(server.url)
33+
await supertest(server.url)
3034
.get('/')
3135
.expect(200);
3236
});
3337

3438
it('stops server', async () => {
35-
// Explicitly setting host to IPv4 address so test runs on Travis
36-
server = new HttpServer(dummyRequestHandler, {host: '127.0.0.1'});
39+
const serverOptions = givenServerOptions();
40+
server = new HttpServer(dummyRequestHandler, serverOptions);
3741
await server.start();
3842
await server.stop();
3943
await expect(
@@ -122,7 +126,7 @@ describe('HttpServer (integration)', () => {
122126
expect(server.address).to.be.undefined();
123127
});
124128

125-
it('exports started', async () => {
129+
it('exports listening', async () => {
126130
server = new HttpServer(dummyRequestHandler);
127131
await server.start();
128132
expect(server.listening).to.be.true();
@@ -134,8 +138,38 @@ describe('HttpServer (integration)', () => {
134138
server = new HttpServer(dummyRequestHandler);
135139
await server.start();
136140
const port = server.port;
137-
const anotherServer = new HttpServer(dummyRequestHandler, {port: port});
138-
expect(anotherServer.start()).to.be.rejectedWith(/EADDRINUSE/);
141+
const anotherServer = new HttpServer(dummyRequestHandler, {
142+
port: port,
143+
});
144+
await expect(anotherServer.start()).to.be.rejectedWith(/EADDRINUSE/);
145+
});
146+
147+
it('supports HTTPS protocol with key and certificate files', async () => {
148+
const serverOptions = givenServerOptions();
149+
const httpsServer: HttpServer = givenHttpsServer(serverOptions);
150+
await httpsServer.start();
151+
const response = await httpsGetAsync(httpsServer.url);
152+
expect(response.statusCode).to.equal(200);
153+
});
154+
155+
it('supports HTTPS protocol with a pfx file', async () => {
156+
const options = {usePfx: true};
157+
const serverOptions = givenServerOptions();
158+
Object.assign(serverOptions, options);
159+
const httpsServer: HttpServer = givenHttpsServer(serverOptions);
160+
await httpsServer.start();
161+
const response = await httpsGetAsync(httpsServer.url);
162+
expect(response.statusCode).to.equal(200);
163+
});
164+
165+
itSkippedOnTravis('handles IPv6 loopback address in HTTPS', async () => {
166+
const httpsServer: HttpServer = givenHttpsServer({
167+
host: '::1',
168+
});
169+
await httpsServer.start();
170+
expect(httpsServer.address!.family).to.equal('IPv6');
171+
const response = await httpsGetAsync(httpsServer.url);
172+
expect(response.statusCode).to.equal(200);
139173
});
140174

141175
function dummyRequestHandler(req: ServerRequest, res: ServerResponse): void {
@@ -147,9 +181,67 @@ describe('HttpServer (integration)', () => {
147181
await server.stop();
148182
}
149183

150-
function getAsync(url: string): Promise<IncomingMessage> {
184+
function getAsync(urlString: string): Promise<IncomingMessage> {
185+
return new Promise((resolve, reject) => {
186+
get(urlString, resolve).on('error', reject);
187+
});
188+
}
189+
190+
function givenHttpsServer({
191+
usePfx,
192+
host,
193+
}: {
194+
usePfx?: boolean;
195+
host?: string;
196+
}): HttpServer {
197+
const options: HttpServerOptions = {protocol: 'https', host};
198+
if (usePfx) {
199+
const pfxPath = path.join(__dirname, 'pfx.pfx');
200+
options.pfx = fs.readFileSync(pfxPath);
201+
options.passphrase = 'loopback4';
202+
} else {
203+
const keyPath = path.join(__dirname, 'key.pem');
204+
const certPath = path.join(__dirname, 'cert.pem');
205+
options.key = fs.readFileSync(keyPath);
206+
options.cert = fs.readFileSync(certPath);
207+
}
208+
return new HttpServer(dummyRequestHandler, options);
209+
}
210+
211+
function httpsGetAsync(urlString: string): Promise<IncomingMessage> {
212+
const agent = new https.Agent({
213+
rejectUnauthorized: false,
214+
});
215+
216+
const urlOptions = url.parse(urlString);
217+
const options = {agent, ...urlOptions};
218+
151219
return new Promise((resolve, reject) => {
152-
get(url, resolve).on('error', reject);
220+
https.get(options, resolve).on('error', reject);
153221
});
154222
}
223+
224+
function givenServerOptions(
225+
options: Partial<HttpServerOptions> = {},
226+
): HttpServerOptions {
227+
const defaults = process.env.TRAVIS ? {host: '127.0.0.1'} : {};
228+
return Object.assign(defaults, options);
229+
}
230+
231+
// tslint:disable-next-line:no-any
232+
type TestCallbackRetval = void | PromiseLike<any>;
233+
234+
function itSkippedOnTravis(
235+
expectation: string,
236+
callback?: (
237+
this: Mocha.ITestCallbackContext,
238+
done: MochaDone,
239+
) => TestCallbackRetval,
240+
): void {
241+
if (process.env.TRAVIS) {
242+
it.skip(`[SKIPPED ON TRAVIS] ${expectation}`, callback);
243+
} else {
244+
it(expectation, callback);
245+
}
246+
}
155247
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDeZXbmcHsumtbC
3+
fDZ6xlvO5L28brwSpyOK70rRk71ibxqXvRbPVDvK2p+4/hwMSMQCeC9uStPdbVlp
4+
dQ5DPm/EBrk5oukHuag5ndyV1XINPCUTEG8WtcHlCquH/thzs5W+1vcNqHXDO9Ec
5+
S3wjQRc+eiONp7a7GZO7dOFmoAJyllLlJALeEDdU6ZxGkxLEp3y29tthhvftpqV0
6+
4mTFwqB7QLlp2k4HwywO9ZlqAEMJZo3jVZz+YRHvVDVQyvMGsYHQt63giJu9Rxbm
7+
eVSV/O72qNjpld1vhxMMDurrFMeZojCEuSWwj8rYx1rVgLloi7nnwrFqrV7dFZvQ
8+
eX9GkRBJAgMBAAECggEABHSh8jH0tdVSUiks6j7JHhcFGh5Z1EHW+3SZ2iMMm0lA
9+
jiOyrkqwu/qvUoR8yV431xjTUnFbV0dWkD9RHtXEZXgBA/+YjZgRn73i6nmRRGSd
10+
FYmxwBG6Jb2V/C6F5aOGb4FdB8AFQ/kR0nBMt2QZdB38Ul020v7LL+lCjszL38HL
11+
qPuZLbvQi4Vs4J8JpO8k2ST3gQhaYWb36XOACaD20mL2xHNpOO5vyBJSnNVb6ueg
12+
jpy1QV/1JOEumYLXFGOudk2QRm/yN2ym4gwpqD3sWIM9iYZsc84K61oibhLRZFtO
13+
luUnuRLrNrzpZfZga2EqEPUEy0IHLij1S9+H2MQTFQKBgQD+A9fwVDnY//PdSDBp
14+
+xlggP2N3RdINi1EStPiLsy//Ok7xSCTXgE09iouZsjaP9r6nSKlG3SO18Hvy4CI
15+
whRzu95Z2vZQLYHuCCwLnqIhpM7xnFL93ueud7ATiE3fGFhJNUMGTYTQ+ZmwFuFQ
16+
7eddWrqVOEqezy2btpnsIVkINwKBgQDgIl4sZ7fl98S64+K0fcB0rCnrciL7OBap
17+
aucHuzmjydaVWW5WkzUOMxh+er2Zeqt1+0cTjnV6J7DJ96d/R8eWkNjTVtULJezf
18+
z91titYbB3O6TwYLx4IzXoweHuC/uLhE27Jxnvgz2IZacK1fKvql1lM5MaP7GDZ8
19+
VPvmiUFrfwKBgEABs+4JKzJ0/Hwr7pcmALUCi+GtbmpxzGJDALUj2dAe6J55A8Ze
20+
j6tKxEJBACeOo5027z3vdyVvVJ0aCF9tmD25f0PhGuQFM5JJWN/sryoPH15eZ8M0
21+
4ehinGmvlP+8YLLBywvRiMAnxQRMH6aG7B/n9tAXCSaPSgzMrGiF1qttAoGBAJ51
22+
Dbk9FpFZ6tbqF7PdF7wkn3padgq/q53Y+z7HCcgXAUMTN+OzLRY933pD0ll4lVHS
23+
9XwJAlr7RoxzLxLYL23uN6yqPfIkvOO6dGRmfFodmZ7FEZQwV4dzt4Hv+JrywCvG
24+
WtDjP7x/vvSfpqKaoxute6b6xmDVzGd4OaLRtNOHAoGAUyockJhGQEkUG9A21DXv
25+
hqR343WeUne1tqwfxkg0DQIBAaaFgGkeL1DjdHhE5ZNz+F/t5LRcvMkZDShRK0u3
26+
Ytnw2XtSJYtCrPlnDrt1/59dBr7S1nbhStI5xfPojctd0DbVvhC5UfQMKSNHOLCs
27+
tUWwM07FtltvXMoC0xXf5sI=
28+
-----END PRIVATE KEY-----
2.42 KB
Binary file not shown.

0 commit comments

Comments
 (0)