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
3 changes: 3 additions & 0 deletions packages/create-nuxt/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import { provider } from 'std-env'
import { checkEngines } from '../../nuxt-cli/src/utils/engines'
import { getCreateCommand, isPinnedCreateInvocation } from '../../nuxt-cli/src/utils/headless'
import { debug, logger } from '../../nuxt-cli/src/utils/logger'
import { setupProxySupport } from '../../nuxt-cli/src/utils/network'
import { scheduleSelfUpdateNudge } from '../../nuxt-cli/src/utils/update-check'
import { description, name, version } from '../package.json'
import { setupInitCompletions } from './completions'
import init from './init'

setupProxySupport()

const _main = defineCommand({
meta: {
name,
Expand Down
29 changes: 29 additions & 0 deletions packages/create-nuxt/test/unit/proxy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { afterEach, describe, expect, it, vi } from 'vitest'

import { setGlobalProxyFromEnv, startTunnelProxy } from '../../../nuxt-cli/test/utils/proxy'

describe.skipIf(!setGlobalProxyFromEnv)('proxy support', () => {
afterEach(() => {
vi.unstubAllEnvs()
vi.resetModules()
})

it.each([
['create-nuxt', () => import('../../src/main')],
['nuxi', () => import('../../../nuxi/src/main')],
])('should route %s requests through HTTP_PROXY', async (_name, load) => {
const proxy = await startTunnelProxy()
try {
for (const key of ['HTTPS_PROXY', 'https_proxy', 'http_proxy', 'ALL_PROXY', 'all_proxy', 'NO_PROXY', 'no_proxy', 'NODE_USE_ENV_PROXY', 'NODE_OPTIONS']) {
vi.stubEnv(key, undefined)
}
vi.stubEnv('HTTP_PROXY', proxy.proxyUrl)
await load()
expect(await fetch('http://nuxt.invalid/').then(r => r.text())).toBe('ok')
expect(proxy.tunnelled).toEqual(['nuxt.invalid:80'])
}
finally {
proxy.close()
}
})
})
3 changes: 3 additions & 0 deletions packages/nuxi/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ import { isNuxiCommand, nuxiCommands } from '../../nuxt-cli/src/commands/_utils'
import { setupGlobalConsole } from '../../nuxt-cli/src/utils/console'
import { checkEngines } from '../../nuxt-cli/src/utils/engines'
import { debug, logger } from '../../nuxt-cli/src/utils/logger'
import { setupProxySupport } from '../../nuxt-cli/src/utils/network'
import { findInPath, withLocalBinPath } from '../../nuxt-cli/src/utils/path-env'
import { description, name, version } from '../package.json'

const commands = {
init: () => import('../../create-nuxt/src/init').then(m => m.default || m),
} as const

setupProxySupport()

const _main = defineCommand({
meta: {
name: name.endsWith('nightly') ? name : 'nuxi',
Expand Down
3 changes: 0 additions & 3 deletions packages/nuxt-cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ import { templateNames } from './utils/templates/names'
import { findUnknownFlags, replaceFlag, suggestFlags } from './utils/unknown-args'
import { scheduleUpdateNudge } from './utils/update-lazy'

// Node.js only reads `NODE_USE_ENV_PROXY` during bootstrap, so this cannot make
// the current process proxy-aware; it propagates the setting to child processes
// (package manager installs, dev server) and records the state for error hints.
setupProxySupport()

const _main = defineCommand({
Expand Down
16 changes: 12 additions & 4 deletions packages/nuxt-cli/src/utils/network.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import http from 'node:http'
import process from 'node:process'

import { styleText } from 'node:util'
Expand Down Expand Up @@ -52,15 +53,18 @@ export function isEnvProxyActive(env: NodeJS.ProcessEnv = process.env, execArgv:

export type ProxySetupResult = 'unused' | 'active' | 'children-only' | 'unsupported'

type EnableGlobalProxy = (env: NodeJS.ProcessEnv) => unknown

const setGlobalProxyFromEnv = (http as { setGlobalProxyFromEnv?: EnableGlobalProxy }).setGlobalProxyFromEnv

let envProxyActive: boolean | undefined
let proxyHintShown = false

/**
* Propagate Node.js' built-in proxy support to child processes (package manager
* installs, the dev server) when proxy environment variables are set, and record
* whether the current process is itself proxy-aware so failures can say so.
* Route requests from this process and its children through the proxy
* environment variables, and record whether this process is proxy-aware.
*/
export function setupProxySupport(env: NodeJS.ProcessEnv = process.env, flags?: NodeFlags): ProxySetupResult {
export function setupProxySupport(env: NodeJS.ProcessEnv = process.env, flags?: NodeFlags, enableGlobalProxy: EnableGlobalProxy | null = setGlobalProxyFromEnv ?? null): ProxySetupResult {
proxyHintShown = false

if (!hasProxyEnv(env)) {
Expand All @@ -73,6 +77,10 @@ export function setupProxySupport(env: NodeJS.ProcessEnv = process.env, flags?:
}

envProxyActive = isEnvProxyActive(env, process.execArgv, flags)
if (!envProxyActive && enableGlobalProxy && env.NODE_USE_ENV_PROXY !== '0') {
enableGlobalProxy(env)
envProxyActive = true
}
env.NODE_USE_ENV_PROXY ||= '1'

return envProxyActive ? 'active' : 'children-only'
Expand Down
24 changes: 23 additions & 1 deletion packages/nuxt-cli/test/unit/utils/network.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { stripVTControlCharacters } from 'node:util'

import { downloadTemplate } from 'giget'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { setGlobalProxyFromEnv, startTunnelProxy } from '../../utils/proxy'

const logs: Array<[string, string]> = []

Expand Down Expand Up @@ -80,10 +81,31 @@ describe('setupProxySupport', () => {

it('propagates proxy support to child processes', () => {
const env = { HTTP_PROXY: 'http://localhost:3128' } as NodeJS.ProcessEnv
expect(setupProxySupport(env, MODERN_NODE)).toBe('children-only')
expect(setupProxySupport(env, MODERN_NODE, null)).toBe('children-only')
expect(env.NODE_USE_ENV_PROXY).toBe('1')
})

it.skipIf(!setGlobalProxyFromEnv)('routes fetch through the proxy while honouring NO_PROXY', async () => {
const proxy = await startTunnelProxy()
try {
const env = { HTTP_PROXY: proxy.proxyUrl, NO_PROXY: '127.0.0.1' } as NodeJS.ProcessEnv
expect(setupProxySupport(env, MODERN_NODE)).toBe('active')
expect(await fetch('http://nuxt.invalid/').then(r => r.text())).toBe('ok')
expect(await fetch(proxy.targetUrl).then(r => r.text())).toBe('ok')
expect(proxy.tunnelled).toEqual(['nuxt.invalid:80'])
expect(getProxyHint('refused', { env, flags: MODERN_NODE })).toBeUndefined()
}
finally {
proxy.close()
}
})

it('does not enable the proxy when NODE_USE_ENV_PROXY is explicitly disabled', () => {
const enable = vi.fn()
expect(setupProxySupport({ HTTPS_PROXY: 'http://localhost:3128', NODE_USE_ENV_PROXY: '0' }, MODERN_NODE, enable)).toBe('children-only')
expect(enable).not.toHaveBeenCalled()
})

it('reports Node.js versions that cannot use the proxy', () => {
const env = { HTTP_PROXY: 'http://localhost:3128' } as NodeJS.ProcessEnv
expect(setupProxySupport(env, OLD_NODE)).toBe('unsupported')
Expand Down
43 changes: 43 additions & 0 deletions packages/nuxt-cli/test/utils/proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Server } from 'node:http'
import type { AddressInfo } from 'node:net'

import http, { createServer } from 'node:http'
import { connect } from 'node:net'

export const setGlobalProxyFromEnv = (http as { setGlobalProxyFromEnv?: (env: NodeJS.ProcessEnv) => () => void }).setGlobalProxyFromEnv

function listen(server: Server) {
return new Promise<number>(resolve => server.listen(0, '127.0.0.1', () => resolve((server.address() as AddressInfo).port)))
}

/**
* Start a CONNECT proxy that tunnels every request to a local server responding
* with `ok`, recording the requested `host:port` of each tunnel.
*/
export async function startTunnelProxy() {
const tunnelled: string[] = []
const target = createServer((_req, res) => res.end('ok'))
const proxy = createServer()
const [proxyPort, targetPort] = await Promise.all([listen(proxy), listen(target)])
proxy.on('connect', (req, socket) => {
tunnelled.push(req.url!)
const upstream = connect(targetPort, '127.0.0.1', () => {
socket.write('HTTP/1.1 200 Connection Established\r\n\r\n')
upstream.pipe(socket).pipe(upstream)
})
})
const restoreDispatcher = setGlobalProxyFromEnv?.({})

return {
proxyUrl: `http://127.0.0.1:${proxyPort}`,
targetUrl: `http://127.0.0.1:${targetPort}/`,
tunnelled,
close() {
restoreDispatcher?.()
for (const server of [proxy, target]) {
server.closeAllConnections()
server.close()
}
},
}
}
Loading