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 { loadEnvConfig } from '@next/env'
|
||||
import { trace } from '../trace'
|
||||
import {
|
||||
getReservedPortExplanation,
|
||||
isPortIsReserved,
|
||||
} from '../lib/helpers/get-reserved-port'
|
||||
|
||||
let dir: string
|
||||
let config: NextConfigComplete
|
||||
|
@ -167,6 +171,11 @@ const nextDev: CliCommand = async (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.
|
||||
const allowRetry =
|
||||
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 { getProjectDir } from '../lib/get-project-dir'
|
||||
import { CliCommand } from '../lib/commands'
|
||||
import {
|
||||
getReservedPortExplanation,
|
||||
isPortIsReserved,
|
||||
} from '../lib/helpers/get-reserved-port'
|
||||
|
||||
const nextStart: CliCommand = async (args) => {
|
||||
if (args['--help']) {
|
||||
|
@ -31,6 +35,10 @@ const nextStart: CliCommand = async (args) => {
|
|||
const host = args['--hostname']
|
||||
const port = getPort(args)
|
||||
|
||||
if (isPortIsReserved(port)) {
|
||||
printAndExit(getReservedPortExplanation(port), 1)
|
||||
}
|
||||
|
||||
const isExperimentalTestProxy = args['--experimental-test-proxy']
|
||||
|
||||
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 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 (
|
||||
killSignal = '',
|
||||
args = [],
|
||||
|
@ -208,6 +239,32 @@ describe('CLI Usage', () => {
|
|||
'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', () => {
|
||||
|
@ -464,30 +521,8 @@ describe('CLI Usage', () => {
|
|||
|
||||
test('-p conflict', async () => {
|
||||
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(stdout).not.toMatch('ready')
|
||||
expect(stdout).not.toMatch('started')
|
||||
|
@ -495,6 +530,18 @@ describe('CLI Usage', () => {
|
|||
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 () => {
|
||||
const port = await findPort()
|
||||
let output = ''
|
||||
|
|
Loading…
Reference in a new issue