feat: add reserved port validation (#55237)
### Fixing a bug - [x] Related issues linked using `fixes #number` - [x] Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs - [x] Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md Closes NEXT- Fixes #55050 Co-authored-by: Steven <229881+styfle@users.noreply.github.com> Co-authored-by: Tim Neutkens <6324199+timneutkens@users.noreply.github.com>
This commit is contained in:
parent
92e1b3fef9
commit
3afba0d12d
5 changed files with 210 additions and 23 deletions
27
errors/reserved-port.mdx
Normal file
27
errors/reserved-port.mdx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
title: Reserved Port
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why This Error Occurred
|
||||||
|
|
||||||
|
Server was started on a reserved port. For example, `4045` is reserved for the Network Paging Protocol (npp).
|
||||||
|
|
||||||
|
```
|
||||||
|
next start -p 4045
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```
|
||||||
|
next dev --port 4045
|
||||||
|
```
|
||||||
|
|
||||||
|
Starting the server on a reserved port will result in an error.
|
||||||
|
|
||||||
|
## Possible Ways to Fix It
|
||||||
|
|
||||||
|
Change the provided port to ensure it's not listed in the [Port Blocking](https://fetch.spec.whatwg.org/#port-blocking) section of WHATWG's fetch spec.
|
||||||
|
|
||||||
|
## Useful Links
|
||||||
|
|
||||||
|
- https://fetch.spec.whatwg.org/#port-blocking
|
|
@ -18,6 +18,10 @@ import uploadTrace from '../trace/upload-trace'
|
||||||
import { startServer } from '../server/lib/start-server'
|
import { startServer } from '../server/lib/start-server'
|
||||||
import { loadEnvConfig } from '@next/env'
|
import { loadEnvConfig } from '@next/env'
|
||||||
import { trace } from '../trace'
|
import { trace } from '../trace'
|
||||||
|
import {
|
||||||
|
getReservedPortExplanation,
|
||||||
|
isPortIsReserved,
|
||||||
|
} from '../lib/helpers/get-reserved-port'
|
||||||
|
|
||||||
let dir: string
|
let dir: string
|
||||||
let config: NextConfigComplete
|
let config: NextConfigComplete
|
||||||
|
@ -167,6 +171,11 @@ const nextDev: CliCommand = async (args) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = getPort(args)
|
const port = getPort(args)
|
||||||
|
|
||||||
|
if (isPortIsReserved(port)) {
|
||||||
|
printAndExit(getReservedPortExplanation(port), 1)
|
||||||
|
}
|
||||||
|
|
||||||
// If neither --port nor PORT were specified, it's okay to retry new ports.
|
// If neither --port nor PORT were specified, it's okay to retry new ports.
|
||||||
const allowRetry =
|
const allowRetry =
|
||||||
args['--port'] === undefined && process.env.PORT === undefined
|
args['--port'] === undefined && process.env.PORT === undefined
|
||||||
|
|
|
@ -4,6 +4,10 @@ import { startServer } from '../server/lib/start-server'
|
||||||
import { getPort, printAndExit } from '../server/lib/utils'
|
import { getPort, printAndExit } from '../server/lib/utils'
|
||||||
import { getProjectDir } from '../lib/get-project-dir'
|
import { getProjectDir } from '../lib/get-project-dir'
|
||||||
import { CliCommand } from '../lib/commands'
|
import { CliCommand } from '../lib/commands'
|
||||||
|
import {
|
||||||
|
getReservedPortExplanation,
|
||||||
|
isPortIsReserved,
|
||||||
|
} from '../lib/helpers/get-reserved-port'
|
||||||
|
|
||||||
const nextStart: CliCommand = async (args) => {
|
const nextStart: CliCommand = async (args) => {
|
||||||
if (args['--help']) {
|
if (args['--help']) {
|
||||||
|
@ -31,6 +35,10 @@ const nextStart: CliCommand = async (args) => {
|
||||||
const host = args['--hostname']
|
const host = args['--hostname']
|
||||||
const port = getPort(args)
|
const port = getPort(args)
|
||||||
|
|
||||||
|
if (isPortIsReserved(port)) {
|
||||||
|
printAndExit(getReservedPortExplanation(port), 1)
|
||||||
|
}
|
||||||
|
|
||||||
const isExperimentalTestProxy = args['--experimental-test-proxy']
|
const isExperimentalTestProxy = args['--experimental-test-proxy']
|
||||||
|
|
||||||
const keepAliveTimeoutArg: number | undefined = args['--keepAliveTimeout']
|
const keepAliveTimeoutArg: number | undefined = args['--keepAliveTimeout']
|
||||||
|
|
96
packages/next/src/lib/helpers/get-reserved-port.ts
Normal file
96
packages/next/src/lib/helpers/get-reserved-port.ts
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
/** https://fetch.spec.whatwg.org/#port-blocking */
|
||||||
|
export const KNOWN_RESERVED_PORTS = {
|
||||||
|
1: 'tcpmux',
|
||||||
|
7: 'echo',
|
||||||
|
9: 'discard',
|
||||||
|
11: 'systat',
|
||||||
|
13: 'daytime',
|
||||||
|
15: 'netstat',
|
||||||
|
17: 'qotd',
|
||||||
|
19: 'chargen',
|
||||||
|
20: 'ftp-data',
|
||||||
|
21: 'ftp',
|
||||||
|
22: 'ssh',
|
||||||
|
23: 'telnet',
|
||||||
|
25: 'smtp',
|
||||||
|
37: 'time',
|
||||||
|
42: 'name',
|
||||||
|
43: 'nicname',
|
||||||
|
53: 'domain',
|
||||||
|
69: 'tftp',
|
||||||
|
77: 'rje',
|
||||||
|
79: 'finger',
|
||||||
|
87: 'link',
|
||||||
|
95: 'supdup',
|
||||||
|
101: 'hostname',
|
||||||
|
102: 'iso-tsap',
|
||||||
|
103: 'gppitnp',
|
||||||
|
104: 'acr-nema',
|
||||||
|
109: 'pop2',
|
||||||
|
110: 'pop3',
|
||||||
|
111: 'sunrpc',
|
||||||
|
113: 'auth',
|
||||||
|
115: 'sftp',
|
||||||
|
117: 'uucp-path',
|
||||||
|
119: 'nntp',
|
||||||
|
123: 'ntp',
|
||||||
|
135: 'epmap',
|
||||||
|
137: 'netbios-ns',
|
||||||
|
139: 'netbios-ssn',
|
||||||
|
143: 'imap',
|
||||||
|
161: 'snmp',
|
||||||
|
179: 'bgp',
|
||||||
|
389: 'ldap',
|
||||||
|
427: 'svrloc',
|
||||||
|
465: 'submissions',
|
||||||
|
512: 'exec',
|
||||||
|
513: 'login',
|
||||||
|
514: 'shell',
|
||||||
|
515: 'printer',
|
||||||
|
526: 'tempo',
|
||||||
|
530: 'courier',
|
||||||
|
531: 'chat',
|
||||||
|
532: 'netnews',
|
||||||
|
540: 'uucp',
|
||||||
|
548: 'afp',
|
||||||
|
554: 'rtsp',
|
||||||
|
556: 'remotefs',
|
||||||
|
563: 'nntps',
|
||||||
|
587: 'submission',
|
||||||
|
601: 'syslog-conn',
|
||||||
|
636: 'ldaps',
|
||||||
|
989: 'ftps-data',
|
||||||
|
990: 'ftps',
|
||||||
|
993: 'imaps',
|
||||||
|
995: 'pop3s',
|
||||||
|
1719: 'h323gatestat',
|
||||||
|
1720: 'h323hostcall',
|
||||||
|
1723: 'pptp',
|
||||||
|
2049: 'nfs',
|
||||||
|
3659: 'apple-sasl',
|
||||||
|
4045: 'npp',
|
||||||
|
5060: 'sip',
|
||||||
|
5061: 'sips',
|
||||||
|
6000: 'x11',
|
||||||
|
6566: 'sane-port',
|
||||||
|
6665: 'ircu',
|
||||||
|
6666: 'ircu',
|
||||||
|
6667: 'ircu',
|
||||||
|
6668: 'ircu',
|
||||||
|
6669: 'ircu',
|
||||||
|
6697: 'ircs-u',
|
||||||
|
10080: 'amanda',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
type ReservedPort = keyof typeof KNOWN_RESERVED_PORTS
|
||||||
|
|
||||||
|
export function isPortIsReserved(port: number): port is ReservedPort {
|
||||||
|
return port in KNOWN_RESERVED_PORTS
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReservedPortExplanation(port: ReservedPort): string {
|
||||||
|
return (
|
||||||
|
`Bad port: "${port}" is reserved for ${KNOWN_RESERVED_PORTS[port]}\n` +
|
||||||
|
'Read more: https://nextjs.org/docs/messages/reserved-port'
|
||||||
|
)
|
||||||
|
}
|
|
@ -19,6 +19,37 @@ import stripAnsi from 'strip-ansi'
|
||||||
const dirBasic = join(__dirname, '../basic')
|
const dirBasic = join(__dirname, '../basic')
|
||||||
const dirDuplicateSass = join(__dirname, '../duplicate-sass')
|
const dirDuplicateSass = join(__dirname, '../duplicate-sass')
|
||||||
|
|
||||||
|
const runAndCaptureOutput = async ({ port }) => {
|
||||||
|
let stdout = ''
|
||||||
|
let stderr = ''
|
||||||
|
|
||||||
|
let app = http.createServer((_, res) => {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/plain' })
|
||||||
|
res.end('OK')
|
||||||
|
})
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
app.on('error', reject)
|
||||||
|
app.on('listening', () => resolve())
|
||||||
|
app.listen(port)
|
||||||
|
})
|
||||||
|
|
||||||
|
await launchApp(dirBasic, port, {
|
||||||
|
stdout: true,
|
||||||
|
stderr: true,
|
||||||
|
onStdout(msg) {
|
||||||
|
stdout += msg
|
||||||
|
},
|
||||||
|
onStderr(msg) {
|
||||||
|
stderr += msg
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await new Promise((resolve) => app.close(resolve))
|
||||||
|
|
||||||
|
return { stdout, stderr }
|
||||||
|
}
|
||||||
|
|
||||||
const testExitSignal = async (
|
const testExitSignal = async (
|
||||||
killSignal = '',
|
killSignal = '',
|
||||||
args = [],
|
args = [],
|
||||||
|
@ -208,6 +239,32 @@ describe('CLI Usage', () => {
|
||||||
'Invalid keep alive timeout provided, expected a non negative number'
|
'Invalid keep alive timeout provided, expected a non negative number'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should not start on a port out of range', async () => {
|
||||||
|
const invalidPort = '300001'
|
||||||
|
const { stderr } = await runNextCommand(
|
||||||
|
['start', '--port', invalidPort],
|
||||||
|
{
|
||||||
|
stderr: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(stderr).toContain(`options.port should be >= 0 and < 65536.`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should not start on a reserved port', async () => {
|
||||||
|
const reservedPort = '4045'
|
||||||
|
const { stderr } = await runNextCommand(
|
||||||
|
['start', '--port', reservedPort],
|
||||||
|
{
|
||||||
|
stderr: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(stderr).toContain(
|
||||||
|
`Bad port: "${reservedPort}" is reserved for npp`
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('no command', () => {
|
describe('no command', () => {
|
||||||
|
@ -464,30 +521,8 @@ describe('CLI Usage', () => {
|
||||||
|
|
||||||
test('-p conflict', async () => {
|
test('-p conflict', async () => {
|
||||||
const port = await findPort()
|
const port = await findPort()
|
||||||
|
const { stderr, stdout } = await runAndCaptureOutput({ port })
|
||||||
|
|
||||||
let app = http.createServer((_, res) => {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/plain' })
|
|
||||||
res.end('OK')
|
|
||||||
})
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
// This code catches EADDRINUSE error if the port is already in use
|
|
||||||
app.on('error', reject)
|
|
||||||
app.on('listening', () => resolve())
|
|
||||||
app.listen(port)
|
|
||||||
})
|
|
||||||
let stdout = '',
|
|
||||||
stderr = ''
|
|
||||||
await launchApp(dirBasic, port, {
|
|
||||||
stdout: true,
|
|
||||||
stderr: true,
|
|
||||||
onStdout(msg) {
|
|
||||||
stdout += msg
|
|
||||||
},
|
|
||||||
onStderr(msg) {
|
|
||||||
stderr += msg
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await new Promise((resolve) => app.close(resolve))
|
|
||||||
expect(stderr).toMatch('already in use')
|
expect(stderr).toMatch('already in use')
|
||||||
expect(stdout).not.toMatch('ready')
|
expect(stdout).not.toMatch('ready')
|
||||||
expect(stdout).not.toMatch('started')
|
expect(stdout).not.toMatch('started')
|
||||||
|
@ -495,6 +530,18 @@ describe('CLI Usage', () => {
|
||||||
expect(stripAnsi(stdout).trim()).toBeFalsy()
|
expect(stripAnsi(stdout).trim()).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('-p reserved', async () => {
|
||||||
|
const TCP_MUX_PORT = 1
|
||||||
|
const { stderr, stdout } = await runAndCaptureOutput({
|
||||||
|
port: TCP_MUX_PORT,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(stdout).toMatch('')
|
||||||
|
expect(stderr).toMatch(
|
||||||
|
`Bad port: "${TCP_MUX_PORT}" is reserved for tcpmux`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test('--hostname', async () => {
|
test('--hostname', async () => {
|
||||||
const port = await findPort()
|
const port = await findPort()
|
||||||
let output = ''
|
let output = ''
|
||||||
|
|
Loading…
Reference in a new issue