Skip to content

Commit

Permalink
feat: add reserved port validation (#55237)
Browse files Browse the repository at this point in the history
### 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>
  • Loading branch information
3 people committed Sep 12, 2023
1 parent 92e1b3f commit 3afba0d
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 23 deletions.
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 = {
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

0 comments on commit 3afba0d

Please sign in to comment.