Skip to content

Commit 9f8de8a

Browse files
YoungSxjahooma
authored andcommitted
fix: avoid DNS lookup after proxied release CONNECT (#506)
1 parent 0f261bf commit 9f8de8a

File tree

10 files changed

+786
-357
lines changed

10 files changed

+786
-357
lines changed

cli/release-staging/http.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
const http = require('http')
2+
const https = require('https')
3+
const tls = require('tls')
4+
5+
function createReleaseHttpClient({
6+
env = process.env,
7+
userAgent,
8+
requestTimeout,
9+
httpModule = http,
10+
httpsModule = https,
11+
tlsModule = tls,
12+
}) {
13+
function getProxyUrl() {
14+
return (
15+
env.HTTPS_PROXY ||
16+
env.https_proxy ||
17+
env.HTTP_PROXY ||
18+
env.http_proxy ||
19+
null
20+
)
21+
}
22+
23+
function shouldBypassProxy(hostname) {
24+
const noProxy = env.NO_PROXY || env.no_proxy || ''
25+
if (!noProxy) return false
26+
27+
const domains = noProxy
28+
.split(',')
29+
.map((domain) => domain.trim().toLowerCase().replace(/:\d+$/, ''))
30+
const host = hostname.toLowerCase()
31+
32+
return domains.some((domain) => {
33+
if (domain === '*') return true
34+
if (domain.startsWith('.')) {
35+
return host.endsWith(domain) || host === domain.slice(1)
36+
}
37+
return host === domain || host.endsWith(`.${domain}`)
38+
})
39+
}
40+
41+
function connectThroughProxy(proxyUrl, targetHost, targetPort) {
42+
return new Promise((resolve, reject) => {
43+
const proxy = new URL(proxyUrl)
44+
const isHttpsProxy = proxy.protocol === 'https:'
45+
const connectOptions = {
46+
hostname: proxy.hostname,
47+
port: proxy.port || (isHttpsProxy ? 443 : 80),
48+
method: 'CONNECT',
49+
path: `${targetHost}:${targetPort}`,
50+
headers: {
51+
Host: `${targetHost}:${targetPort}`,
52+
},
53+
}
54+
55+
if (proxy.username || proxy.password) {
56+
const auth = Buffer.from(
57+
`${decodeURIComponent(proxy.username || '')}:${decodeURIComponent(
58+
proxy.password || '',
59+
)}`,
60+
).toString('base64')
61+
connectOptions.headers['Proxy-Authorization'] = `Basic ${auth}`
62+
}
63+
64+
const transport = isHttpsProxy ? httpsModule : httpModule
65+
const req = transport.request(connectOptions)
66+
67+
req.on('connect', (res, socket) => {
68+
if (res.statusCode === 200) {
69+
resolve(socket)
70+
return
71+
}
72+
73+
socket.destroy()
74+
reject(new Error(`Proxy CONNECT failed with status ${res.statusCode}`))
75+
})
76+
77+
req.on('error', (error) => {
78+
reject(new Error(`Proxy connection failed: ${error.message}`))
79+
})
80+
81+
req.setTimeout(requestTimeout, () => {
82+
req.destroy()
83+
reject(new Error('Proxy connection timeout.'))
84+
})
85+
86+
req.end()
87+
})
88+
}
89+
90+
async function buildRequestOptions(url, options = {}) {
91+
const parsedUrl = new URL(url)
92+
const reqOptions = {
93+
hostname: parsedUrl.hostname,
94+
port: parsedUrl.port || 443,
95+
path: parsedUrl.pathname + parsedUrl.search,
96+
headers: {
97+
'User-Agent': userAgent,
98+
...options.headers,
99+
},
100+
}
101+
102+
const proxyUrl = getProxyUrl()
103+
if (!proxyUrl || shouldBypassProxy(parsedUrl.hostname)) {
104+
return reqOptions
105+
}
106+
107+
const tunnelSocket = await connectThroughProxy(
108+
proxyUrl,
109+
parsedUrl.hostname,
110+
parsedUrl.port || 443,
111+
)
112+
113+
class TunnelAgent extends httpsModule.Agent {
114+
createConnection(_options, callback) {
115+
const secureSocket = tlsModule.connect({
116+
socket: tunnelSocket,
117+
servername: parsedUrl.hostname,
118+
})
119+
120+
if (typeof callback === 'function') {
121+
if (typeof secureSocket.once === 'function') {
122+
let settled = false
123+
const finish = (error) => {
124+
if (settled) return
125+
settled = true
126+
callback(error || null, error ? undefined : secureSocket)
127+
}
128+
129+
secureSocket.once('secureConnect', () => finish(null))
130+
secureSocket.once('error', (error) => finish(error))
131+
} else {
132+
callback(null, secureSocket)
133+
}
134+
}
135+
136+
return secureSocket
137+
}
138+
}
139+
140+
reqOptions.agent = new TunnelAgent({ keepAlive: false })
141+
return reqOptions
142+
}
143+
144+
async function httpGet(url, options = {}) {
145+
const reqOptions = await buildRequestOptions(url, options)
146+
147+
return new Promise((resolve, reject) => {
148+
const req = httpsModule.get(reqOptions, (res) => {
149+
if (res.statusCode === 301 || res.statusCode === 302) {
150+
res.resume()
151+
httpGet(new URL(res.headers.location, url).href, options)
152+
.then(resolve)
153+
.catch(reject)
154+
return
155+
}
156+
157+
resolve(res)
158+
})
159+
160+
req.on('error', reject)
161+
req.setTimeout(options.timeout || requestTimeout, () => {
162+
req.destroy()
163+
reject(new Error('Request timeout.'))
164+
})
165+
})
166+
}
167+
168+
return {
169+
getProxyUrl,
170+
httpGet,
171+
}
172+
}
173+
174+
module.exports = {
175+
createReleaseHttpClient,
176+
}

cli/release-staging/index.js

Lines changed: 6 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ const http = require('http')
66
const https = require('https')
77
const os = require('os')
88
const path = require('path')
9-
const tls = require('tls')
109
const zlib = require('zlib')
1110

1211
const tar = require('tar')
12+
const { createReleaseHttpClient } = require('./http')
1313

1414
const packageName = 'codecane'
1515

@@ -66,6 +66,11 @@ function createConfig(packageName) {
6666
}
6767

6868
const CONFIG = createConfig(packageName)
69+
const { getProxyUrl, httpGet } = createReleaseHttpClient({
70+
env: process.env,
71+
userAgent: CONFIG.userAgent,
72+
requestTimeout: CONFIG.requestTimeout,
73+
})
6974

7075
function getPostHogConfig() {
7176
const apiKey =
@@ -131,76 +136,6 @@ function trackUpdateFailed(errorMessage, version, context = {}) {
131136
}
132137
}
133138

134-
function getProxyUrl() {
135-
return (
136-
process.env.HTTPS_PROXY ||
137-
process.env.https_proxy ||
138-
process.env.HTTP_PROXY ||
139-
process.env.http_proxy ||
140-
null
141-
)
142-
}
143-
144-
function shouldBypassProxy(hostname) {
145-
const noProxy = process.env.NO_PROXY || process.env.no_proxy || ''
146-
if (!noProxy) return false
147-
const domains = noProxy.split(',').map((d) => d.trim().toLowerCase().replace(/:\d+$/, ''))
148-
const host = hostname.toLowerCase()
149-
return domains.some((d) => {
150-
if (d === '*') return true
151-
if (d.startsWith('.')) return host.endsWith(d) || host === d.slice(1)
152-
return host === d || host.endsWith('.' + d)
153-
})
154-
}
155-
156-
function connectThroughProxy(proxyUrl, targetHost, targetPort) {
157-
return new Promise((resolve, reject) => {
158-
const proxy = new URL(proxyUrl)
159-
const isHttpsProxy = proxy.protocol === 'https:'
160-
const connectOptions = {
161-
hostname: proxy.hostname,
162-
port: proxy.port || (isHttpsProxy ? 443 : 80),
163-
method: 'CONNECT',
164-
path: `${targetHost}:${targetPort}`,
165-
headers: {
166-
Host: `${targetHost}:${targetPort}`,
167-
},
168-
}
169-
170-
if (proxy.username || proxy.password) {
171-
const auth = Buffer.from(
172-
`${decodeURIComponent(proxy.username || '')}:${decodeURIComponent(proxy.password || '')}`,
173-
).toString('base64')
174-
connectOptions.headers['Proxy-Authorization'] = `Basic ${auth}`
175-
}
176-
177-
const transport = isHttpsProxy ? https : http
178-
const req = transport.request(connectOptions)
179-
180-
req.on('connect', (res, socket) => {
181-
if (res.statusCode === 200) {
182-
resolve(socket)
183-
} else {
184-
socket.destroy()
185-
reject(
186-
new Error(`Proxy CONNECT failed with status ${res.statusCode}`),
187-
)
188-
}
189-
})
190-
191-
req.on('error', (err) => {
192-
reject(new Error(`Proxy connection failed: ${err.message}`))
193-
})
194-
195-
req.setTimeout(CONFIG.requestTimeout, () => {
196-
req.destroy()
197-
reject(new Error('Proxy connection timeout.'))
198-
})
199-
200-
req.end()
201-
})
202-
}
203-
204139
const PLATFORM_TARGETS = {
205140
'linux-x64': `${packageName}-linux-x64.tar.gz`,
206141
'linux-arm64': `${packageName}-linux-arm64.tar.gz`,
@@ -225,54 +160,6 @@ const term = {
225160
},
226161
}
227162

228-
async function httpGet(url, options = {}) {
229-
const parsedUrl = new URL(url)
230-
const proxyUrl = getProxyUrl()
231-
232-
const reqOptions = {
233-
hostname: parsedUrl.hostname,
234-
path: parsedUrl.pathname + parsedUrl.search,
235-
headers: {
236-
'User-Agent': CONFIG.userAgent,
237-
...options.headers,
238-
},
239-
}
240-
241-
if (proxyUrl && !shouldBypassProxy(parsedUrl.hostname)) {
242-
const tunnelSocket = await connectThroughProxy(
243-
proxyUrl,
244-
parsedUrl.hostname,
245-
parsedUrl.port || 443,
246-
)
247-
reqOptions.agent = false
248-
reqOptions.createConnection = () =>
249-
tls.connect({
250-
socket: tunnelSocket,
251-
servername: parsedUrl.hostname,
252-
})
253-
}
254-
255-
return new Promise((resolve, reject) => {
256-
const req = https.get(reqOptions, (res) => {
257-
if (res.statusCode === 302 || res.statusCode === 301) {
258-
res.resume()
259-
return httpGet(new URL(res.headers.location, url).href, options)
260-
.then(resolve)
261-
.catch(reject)
262-
}
263-
resolve(res)
264-
})
265-
266-
req.on('error', reject)
267-
268-
const timeout = options.timeout || CONFIG.requestTimeout
269-
req.setTimeout(timeout, () => {
270-
req.destroy()
271-
reject(new Error('Request timeout.'))
272-
})
273-
})
274-
}
275-
276163
async function getLatestVersion() {
277164
try {
278165
const res = await httpGet(

cli/release-staging/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
},
1313
"files": [
1414
"index.js",
15+
"http.js",
1516
"postinstall.js",
1617
"README.md"
1718
],

0 commit comments

Comments
 (0)