Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add reserved port validation #55237

Merged
merged 11 commits into from Sep 12, 2023
27 changes: 27 additions & 0 deletions errors/reserved-port.mdx
@@ -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
9 changes: 9 additions & 0 deletions packages/next/src/cli/next-dev.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions packages/next/src/cli/next-start.ts
Expand Up @@ -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']) {
Expand Down Expand Up @@ -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']
Expand Down
96 changes: 96 additions & 0 deletions packages/next/src/lib/helpers/get-reserved-port.ts
@@ -0,0 +1,96 @@
/** https://fetch.spec.whatwg.org/#port-blocking */
export const KNOWN_RESERVED_PORTS = {
olingern marked this conversation as resolved.
Show resolved Hide resolved
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'
)
}
93 changes: 70 additions & 23 deletions test/integration/cli/test/index.test.js
Expand Up @@ -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 = [],
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -464,37 +521,27 @@ 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')
expect(stdout).not.toMatch(`${port}`)
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 = ''
Expand Down