Skip to content

Commit f67c612

Browse files
alan-agius4clydin
authored andcommitted
fix(@angular-devkit/build-angular): ERR_SSL_PROTOCOL_ERROR when using HTTPS reverse proxy
With this change we set the publicHost to `0.0.0.0:0`, when it's not provided. This solved issues where previously the publicHost needed to be specified directly to get around `ERR_SSL_PROTOCOL_ERROR` error when proxing https -> http. NB: this was also the behaviour in version 10 https://github.com/angular/angular-cli/blob/c252968225c0ecac6c4d8ee08e14ae64de8791a8/packages/angular_devkit/build_angular/src/dev-server/index.ts#L170 Closes #19403
1 parent ddd114c commit f67c612

File tree

6 files changed

+256
-173
lines changed

6 files changed

+256
-173
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
"@types/express": "^4.16.0",
102102
"@types/find-cache-dir": "^3.0.0",
103103
"@types/glob": "^7.1.1",
104+
"@types/http-proxy": "^1.17.4",
104105
"@types/inquirer": "^7.3.0",
105106
"@types/jasmine": "~3.6.0",
106107
"@types/karma": "^5.0.0",
@@ -153,6 +154,7 @@
153154
"gh-got": "^9.0.0",
154155
"git-raw-commits": "^2.0.0",
155156
"glob": "7.1.6",
157+
"http-proxy": "^1.18.1",
156158
"husky": "^4.0.10",
157159
"inquirer": "7.3.3",
158160
"jasmine": "^3.3.1",

packages/angular_devkit/build_angular/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,8 @@ LARGE_SPECS = {
282282
"@npm//@types/node-fetch",
283283
"@npm//express",
284284
"@npm//node-fetch",
285+
"@npm//@types/http-proxy",
286+
"@npm//http-proxy",
285287
"@npm//puppeteer",
286288
],
287289
},
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
// tslint:disable: no-implicit-dependencies
9+
import { Architect, BuilderRun } from '@angular-devkit/architect';
10+
import { tags } from '@angular-devkit/core';
11+
import { createProxyServer } from 'http-proxy';
12+
import { HTTPResponse } from 'puppeteer/lib/cjs/puppeteer/api-docs-entry';
13+
import { Browser } from 'puppeteer/lib/cjs/puppeteer/common/Browser';
14+
import { Page } from 'puppeteer/lib/cjs/puppeteer/common/Page';
15+
import puppeteer from 'puppeteer/lib/cjs/puppeteer/node';
16+
import { debounceTime, switchMap, take } from 'rxjs/operators';
17+
import { createArchitect, host } from '../test-utils';
18+
19+
// tslint:disable-next-line: no-any
20+
declare const document: any;
21+
22+
interface ProxyInstance {
23+
server: typeof createProxyServer extends () => infer R ? R : never;
24+
url: string;
25+
}
26+
27+
let proxyPort = 9100;
28+
function createProxy(target: string, secure: boolean): ProxyInstance {
29+
proxyPort++;
30+
31+
const server = createProxyServer({
32+
ws: true,
33+
target,
34+
secure,
35+
ssl: secure && {
36+
key: tags.stripIndents`
37+
-----BEGIN RSA PRIVATE KEY-----
38+
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDEBRUsUz4rdcMt
39+
CQGLvG3SzUinsmgdgOyTNQNA0eOMyRSrmS8L+F/kSLUnqqu4mzdeqDzo2Xj553jK
40+
dRqMCRFGJuGnQ/VIbW2A+ywgrqILuDyF5i4PL1aQW4yJ7TnXfONKfpswQArlN6DF
41+
gBYJtoJlf8XD1sOeJpsv/O46/ix/wngQ+GwQQ2cfqxQT0fE9SBCY23VNt3SPUJ3k
42+
9etJMvJ9U9GHSb1CFdNQe7Gyx7xdKf1TazB27ElNZEg2aF99if47uRskYjvvFivy
43+
7nxGx/ccIwjwNMpk29AsKG++0sn1yTK7tD5Px6aCSVK0BKbdXZS2euJor8hASGBJ
44+
3GpVGJvdAgMBAAECggEAapYo8TVCdPdP7ckb4hPP0/R0MVu9aW2VNmZ5ImH+zar5
45+
ZmWhQ20HF2bBupP/VB5yeTIaDLNUKO9Iqy4KBWNY1UCHKyC023FFPgFV+V98FctU
46+
faqwGOmwtEZToRwxe48ZOISndhEc247oCPyg/x8SwIY9z0OUkwaDFBEAqWtUXxM3
47+
/SPpCT5ilLgxnRgVB8Fj5Z0q7ThnxNVOmVC1OSIakEj46PzmMXn1pCKLOCUmAAOQ
48+
BnrOZuty2b8b2M/GHsktLZwojQQJmArnIBymTXQTVhaGgKSyOv1qvHLp9L1OJf0/
49+
Xm+/TqT6ztzhzlftcObdfQZZ5JuoEwlvyrsGFlA3MQKBgQDiQC3KYMG8ViJkWrv6
50+
XNAFEoAjVEKrtirGWJ66YfQ9KSJ7Zttrd1Y1V1OLtq3z4YMH39wdQ8rOD+yR8mWV
51+
6Tnsxma6yJXAH8uan8iVbxjIZKF1hnvNCxUoxYmWOmTLcEQMzmxvTzAiR+s6R6Uj
52+
9LgGqppt30nM4wnOhOJU6UxqbwKBgQDdy03KidbPZuycJSy1C9AIt0jlrxDsYm+U
53+
fZrB6mHEZcgoZS5GbLKinQCdGcgERa05BXvJmNbfZtT5a37YEnbjsTImIhDiBP5P
54+
nW36/9a3Vg1svd1KP2206/Bh3gfZbgTsQg4YogXgjf0Uzuvw18btgTtLVpVyeuqz
55+
TU3eeF30cwKBgQCN6lvOmapsDEs+T3uhqx4AUH53qp63PmjOSUAnANJGmsq6ROZV
56+
HmHAy6nn9Qpf85BRHCXhZWiMoIhvc3As/EINNtWxS6hC/q6jqp4SvcD50cVFBroY
57+
/16iWGXZCX+37A+DSOfTWgSDPEFcKRx41UOpStHbITgVgEPieo/NWxlHmQKBgQDX
58+
JOLs2RB6V0ilnpnjdPXzvncD9fHgmwvJap24BPeZX3HtXViqD76oZsu1mNCg9EW3
59+
zk3pnEyyoDlvSIreZerVq4kN3HWsCVP3Pqr0kz9g0CRtmy8RWr28hjHDfXD3xPUZ
60+
iGnMEz7IOHOKv722/liFAprV1cNaLUmFbDNg3jmlaQKBgQDG5WwngPhOHmjTnSml
61+
amfEz9a4yEhQqpqgVNW5wwoXOf6DbjL2m/maJh01giThj7inMcbpkZlIclxD0Eu6
62+
Lof+ctCeqSAJvaVPmd+nv8Yp26zsF1yM8ax9xXjrIvv9fSbycNveGTDCsNNTiYoW
63+
QyvMqmN1kGy20SZbQDD/fLfqBQ==
64+
-----END RSA PRIVATE KEY-----
65+
`,
66+
cert: tags.stripIndents`
67+
-----BEGIN CERTIFICATE-----
68+
MIIDXTCCAkWgAwIBAgIJALz8gD/gAt0OMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
69+
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
70+
aWRnaXRzIFB0eSBMdGQwHhcNMTgxMDIzMTgyMTQ5WhcNMTkxMDIzMTgyMTQ5WjBF
71+
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
72+
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
73+
CgKCAQEAxAUVLFM+K3XDLQkBi7xt0s1Ip7JoHYDskzUDQNHjjMkUq5kvC/hf5Ei1
74+
J6qruJs3Xqg86Nl4+ed4ynUajAkRRibhp0P1SG1tgPssIK6iC7g8heYuDy9WkFuM
75+
ie0513zjSn6bMEAK5TegxYAWCbaCZX/Fw9bDniabL/zuOv4sf8J4EPhsEENnH6sU
76+
E9HxPUgQmNt1Tbd0j1Cd5PXrSTLyfVPRh0m9QhXTUHuxsse8XSn9U2swduxJTWRI
77+
NmhffYn+O7kbJGI77xYr8u58Rsf3HCMI8DTKZNvQLChvvtLJ9ckyu7Q+T8emgklS
78+
tASm3V2UtnriaK/IQEhgSdxqVRib3QIDAQABo1AwTjAdBgNVHQ4EFgQUDZBhVKdb
79+
3BRhLIhuuE522Vsul0IwHwYDVR0jBBgwFoAUDZBhVKdb3BRhLIhuuE522Vsul0Iw
80+
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEABh9WWZwWLgb9/DcTxL72
81+
6pI96t4jiF79Q+pPefkaIIi0mE6yodWrTAsBQu9I6bNRaEcCSoiXkP2bqskD/UGg
82+
LwUFgSrDOAA3UjdHw3QU5g2NocduG7mcFwA40TB98sOsxsUyYlzSyWzoiQWwPYwb
83+
hek1djuWkqPXsTjlj54PTPN/SjTFmo4p5Ip6nbRf2nOREl7v0rJpGbJvXiCMYyd+
84+
Zv+j4mRjCGo8ysMR2HjCUGkYReLAgKyyz3M7i8vevJhKslyOmy6Txn4F0nPVumaU
85+
DDIy4xXPW1STWfsmSYJfYW3wa0wk+pJQ3j2cTzkPQQ8gwpvM3U9DJl43uwb37v6I
86+
7Q==
87+
-----END CERTIFICATE-----
88+
`,
89+
},
90+
})
91+
.listen(proxyPort);
92+
93+
return {
94+
server,
95+
url: `${secure ? 'https' : 'http'}://localhost:${proxyPort}`,
96+
};
97+
}
98+
99+
async function goToPageAndWaitForSockJs(page: Page, url: string): Promise<void> {
100+
const socksRequest = `${url.endsWith('/') ? url : url + '/'}sockjs-node/info?t=`;
101+
102+
await Promise.all([
103+
page.waitForResponse((r: HTTPResponse) => r.url().startsWith(socksRequest) && r.status() === 200),
104+
page.goto(url),
105+
]);
106+
}
107+
108+
describe('Dev Server Builder live-reload', () => {
109+
const target = { project: 'app', target: 'serve' };
110+
const overrides = { hmr: false, watch: true, port: 0, liveReload: true };
111+
let architect: Architect;
112+
let browser: Browser;
113+
let page: Page;
114+
let runs: BuilderRun[];
115+
let proxy: ProxyInstance | undefined;
116+
117+
beforeAll(async () => {
118+
browser = await puppeteer.launch({
119+
// MacOSX users need to set the local binary manually because Chrome has lib files with
120+
// spaces in them which Bazel does not support in runfiles
121+
// See: https://github.com/angular/angular-cli/pull/17624
122+
// tslint:disable-next-line: max-line-length
123+
// executablePath: '/Users/<USERNAME>/git/angular-cli/node_modules/puppeteer/.local-chromium/mac-809590/chrome-mac/Chromium.app/Contents/MacOS/Chromium',
124+
args: [
125+
'--no-sandbox',
126+
'--disable-gpu',
127+
'--ignore-certificate-errors',
128+
'--ignore-urlfetcher-cert-requests',
129+
],
130+
});
131+
});
132+
133+
afterAll(async () => {
134+
await browser.close();
135+
});
136+
137+
beforeEach(async () => {
138+
await host.initialize().toPromise();
139+
architect = (await createArchitect(host.root())).architect;
140+
141+
host.writeMultipleFiles({
142+
'src/app/app.component.html': `
143+
<p>{{title}}</p>
144+
`,
145+
});
146+
147+
runs = [];
148+
page = await browser.newPage();
149+
});
150+
151+
afterEach(async () => {
152+
proxy?.server.close();
153+
proxy = undefined;
154+
await host.restore().toPromise();
155+
await page.close();
156+
await Promise.all(runs.map(r => r.stop()));
157+
});
158+
159+
it('works without proxy', async () => {
160+
const run = await architect.scheduleTarget(target, overrides);
161+
runs.push(run);
162+
163+
let buildCount = 0;
164+
await run.output
165+
.pipe(
166+
debounceTime(1000),
167+
switchMap(async buildEvent => {
168+
expect(buildEvent.success).toBe(true);
169+
const url = buildEvent.baseUrl as string;
170+
switch (buildCount) {
171+
case 0:
172+
await goToPageAndWaitForSockJs(page, url);
173+
host.replaceInFile('src/app/app.component.ts', `'app'`, `'app-live-reload'`);
174+
break;
175+
case 1:
176+
const innerText = await page.evaluate(() => document.querySelector('p').innerText);
177+
expect(innerText).toBe('app-live-reload');
178+
break;
179+
}
180+
181+
buildCount++;
182+
}),
183+
take(2),
184+
)
185+
.toPromise();
186+
}, 30000);
187+
188+
it('works without http -> http proxy', async () => {
189+
const run = await architect.scheduleTarget(target, overrides);
190+
runs.push(run);
191+
192+
let proxy: ProxyInstance | undefined;
193+
let buildCount = 0;
194+
await run.output
195+
.pipe(
196+
debounceTime(1000),
197+
switchMap(async buildEvent => {
198+
expect(buildEvent.success).toBe(true);
199+
const url = buildEvent.baseUrl as string;
200+
switch (buildCount) {
201+
case 0:
202+
proxy = createProxy(url, false);
203+
await goToPageAndWaitForSockJs(page, proxy.url);
204+
host.replaceInFile('src/app/app.component.ts', `'app'`, `'app-live-reload'`);
205+
break;
206+
case 1:
207+
const innerText = await page.evaluate(() => document.querySelector('p').innerText);
208+
expect(innerText).toBe('app-live-reload');
209+
break;
210+
}
211+
212+
buildCount++;
213+
}),
214+
take(2),
215+
)
216+
.toPromise();
217+
}, 30000);
218+
219+
it('works without https -> http proxy', async () => {
220+
const run = await architect.scheduleTarget(target, overrides);
221+
runs.push(run);
222+
223+
let proxy: ProxyInstance | undefined;
224+
let buildCount = 0;
225+
await run.output
226+
.pipe(
227+
debounceTime(1000),
228+
switchMap(async buildEvent => {
229+
expect(buildEvent.success).toBe(true);
230+
const url = buildEvent.baseUrl as string;
231+
switch (buildCount) {
232+
case 0:
233+
proxy = createProxy(url, true);
234+
await goToPageAndWaitForSockJs(page, proxy.url);
235+
host.replaceInFile('src/app/app.component.ts', `'app'`, `'app-live-reload'`);
236+
break;
237+
case 1:
238+
const innerText = await page.evaluate(() => document.querySelector('p').innerText);
239+
expect(innerText).toBe('app-live-reload');
240+
break;
241+
}
242+
243+
buildCount++;
244+
}),
245+
take(2),
246+
)
247+
.toPromise();
248+
}, 30000);
249+
});

packages/angular_devkit/build_angular/src/webpack/configs/dev-server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ export function getDevServerConfig(
5656

5757
const parsedHost = url.parse(publicHost);
5858
publicHost = parsedHost.host;
59+
} else {
60+
publicHost = '0.0.0.0:0';
5961
}
6062

6163
if (!watch) {

0 commit comments

Comments
 (0)