Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ jobs:
with:
node-version: 22.x
- name: Update npm
run: npm install npm@latest -g
run: |
npm install npm@~11.10.0 -g # Workaround for https://github.com/npm/cli/issues/9151
npm install npm@latest -g
- name: Install Dependencies
run: npm install
- name: Pack
Expand Down
4 changes: 2 additions & 2 deletions gyp/pylib/gyp/simple_copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ def deepcopy(x):
return _deepcopy_dispatch[type(x)](x)
except KeyError:
raise Error(
"Unsupported type %s for deepcopy. Use copy.deepcopy "
+ "or expand simple_copy support." % type(x)
f"Unsupported type {type(x)} for deepcopy. Use copy.deepcopy "
+ "or expand simple_copy support."
)


Expand Down
63 changes: 55 additions & 8 deletions lib/download.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const fetch = require('make-fetch-happen')
const { Readable } = require('stream')
const { EnvHttpProxyAgent } = require('undici')
const { promises: fs } = require('graceful-fs')
const log = require('./log')

Expand All @@ -10,19 +11,65 @@ async function download (gyp, url) {
'User-Agent': `node-gyp v${gyp.version} (node ${process.version})`,
Connection: 'keep-alive'
},
proxy: gyp.opts.proxy,
noProxy: gyp.opts.noproxy
dispatcher: await createDispatcher(gyp)
}

const cafile = gyp.opts.cafile
if (cafile) {
requestOpts.ca = await readCAFile(cafile)
let res
try {
res = await fetch(url, requestOpts)
} catch (err) {
// Built-in fetch wraps low-level errors in "TypeError: fetch failed" with
// the underlying error on .cause. Callers inspect .code (e.g. ENOTFOUND).
if (err.cause) {
throw err.cause
}
throw err
}

const res = await fetch(url, requestOpts)
log.http(res.status, res.url)

return res
const body = res.body ? Readable.fromWeb(res.body) : Readable.from([])
return {
status: res.status,
url: res.url,
body,
text: async () => {
let data = ''
body.setEncoding('utf8')
for await (const chunk of body) {
data += chunk
}
return data
}
}
}

async function createDispatcher (gyp) {
Comment thread
gengjiawen marked this conversation as resolved.
const env = process.env
const hasProxyEnv = env.http_proxy || env.HTTP_PROXY || env.https_proxy || env.HTTPS_PROXY
if (!gyp.opts.proxy && !gyp.opts.cafile && !hasProxyEnv) {
return undefined
}

const opts = {}
if (gyp.opts.cafile) {
const ca = await readCAFile(gyp.opts.cafile)
// EnvHttpProxyAgent forwards opts to both its internal Agent (direct) and
// ProxyAgent (proxied). Agent reads TLS config from `connect`; ProxyAgent
// reads it from `requestTls` (origin) / `proxyTls` (proxy). Set all three
// so the custom CA is applied regardless of which path a request takes.
opts.connect = { ca }
opts.requestTls = { ca }
opts.proxyTls = { ca }
}
if (gyp.opts.proxy) {
opts.httpProxy = gyp.opts.proxy
opts.httpsProxy = gyp.opts.proxy
}
if (gyp.opts.noproxy) {
opts.noProxy = gyp.opts.noproxy
}
return new EnvHttpProxyAgent(opts)
}

async function readCAFile (filename) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@
"env-paths": "^2.2.0",
"exponential-backoff": "^3.1.1",
"graceful-fs": "^4.2.6",
"make-fetch-happen": "^15.0.0",
"nopt": "^9.0.0",
"proc-log": "^6.0.0",
"semver": "^7.3.5",
"tar": "^7.5.4",
"tinyglobby": "^0.2.12",
"undici": "^6.25.0",
"which": "^6.0.0"
},
"engines": {
Expand Down
79 changes: 72 additions & 7 deletions test/test-download.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const fs = require('fs/promises')
const path = require('path')
const http = require('http')
const https = require('https')
const net = require('net')
const install = require('../lib/install')
const { download, readCAFile } = require('../lib/download')
const { FULL_TEST, devDir, platformTimeout } = require('./common')
Expand Down Expand Up @@ -69,13 +70,24 @@ describe('download', function () {
})

it('download over http with proxy', async function () {
const server = http.createServer((_, res) => {
const server = http.createServer((req, res) => {
assert.strictEqual(req.headers['user-agent'], `node-gyp v42 (node ${process.version})`)
res.end('ok')
})

const pserver = http.createServer((req, res) => {
assert.strictEqual(req.headers['user-agent'], `node-gyp v42 (node ${process.version})`)
res.end('proxy ok')
let proxyUsed = false
const pserver = http.createServer()
pserver.on('connect', (req, clientSocket, head) => {
proxyUsed = true
const [targetHost, targetPort] = req.url.split(':')
const serverSocket = net.connect(targetPort, targetHost, () => {
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n')
serverSocket.write(head)
serverSocket.pipe(clientSocket)
clientSocket.pipe(serverSocket)
})
Comment thread
MarshallOfSound marked this conversation as resolved.
clientSocket.on('error', () => serverSocket.destroy())
serverSocket.on('error', () => clientSocket.destroy())
})

after(() => Promise.all([
Expand All @@ -96,7 +108,56 @@ describe('download', function () {
}
const url = `http://${host}:${port}`
const res = await download(gyp, url)
assert.strictEqual(await res.text(), 'proxy ok')
assert.strictEqual(await res.text(), 'ok')
assert.strictEqual(proxyUsed, true)
})

it('download over https with proxy and custom ca', async function () {
const cafile = path.join(__dirname, 'fixtures/ca-proxy.crt')
await fs.writeFile(cafile, certs['ca.crt'], 'utf8')

const server = https.createServer({
ca: await readCAFile(cafile),
cert: certs['server.crt'],
key: certs['server.key']
}, (_, res) => res.end('ok'))

let proxyUsed = false
const pserver = http.createServer()
pserver.on('connect', (req, clientSocket, head) => {
proxyUsed = true
const [targetHost, targetPort] = req.url.split(':')
const serverSocket = net.connect(targetPort, targetHost, () => {
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n')
serverSocket.write(head)
serverSocket.pipe(clientSocket)
clientSocket.pipe(serverSocket)
})
clientSocket.on('error', () => serverSocket.destroy())
serverSocket.on('error', () => clientSocket.destroy())
})

after(async () => {
await new Promise((resolve) => server.close(resolve))
await new Promise((resolve) => pserver.close(resolve))
await fs.unlink(cafile)
})

const host = 'localhost'
await new Promise((resolve) => server.listen(0, host, resolve))
const { port } = server.address()
await new Promise((resolve) => pserver.listen(port + 1, host, resolve))
const gyp = {
opts: {
cafile,
proxy: `http://${host}:${port + 1}`,
noproxy: 'bad'
},
version: '42'
}
const res = await download(gyp, `https://${host}:${port}`)
assert.strictEqual(await res.text(), 'ok')
assert.strictEqual(proxyUsed, true)
})

it('download over http with noproxy', async function () {
Expand All @@ -105,8 +166,11 @@ describe('download', function () {
res.end('ok')
})

const pserver = http.createServer((_, res) => {
res.end('proxy ok')
let proxyUsed = false
const pserver = http.createServer()
pserver.on('connect', (_, socket) => {
proxyUsed = true
socket.destroy()
})

after(() => Promise.all([
Expand All @@ -128,6 +192,7 @@ describe('download', function () {
const url = `http://${host}:${port}`
const res = await download(gyp, url)
assert.strictEqual(await res.text(), 'ok')
assert.strictEqual(proxyUsed, false)
})

it('download with missing cafile', async function () {
Expand Down
Loading