diff --git a/errors/reserved-port.mdx b/errors/reserved-port.mdx new file mode 100644 index 000000000000..335cea914718 --- /dev/null +++ b/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 diff --git a/packages/next/src/cli/next-dev.ts b/packages/next/src/cli/next-dev.ts index e6b03751a934..83114a3556cd 100644 --- a/packages/next/src/cli/next-dev.ts +++ b/packages/next/src/cli/next-dev.ts @@ -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 diff --git a/packages/next/src/cli/next-start.ts b/packages/next/src/cli/next-start.ts index 6af517729492..67c729042b5d 100755 --- a/packages/next/src/cli/next-start.ts +++ b/packages/next/src/cli/next-start.ts @@ -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'] diff --git a/packages/next/src/lib/helpers/get-reserved-port.ts b/packages/next/src/lib/helpers/get-reserved-port.ts new file mode 100644 index 000000000000..6b5a7c8b5d99 --- /dev/null +++ b/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' + ) +} diff --git a/test/integration/cli/test/index.test.js b/test/integration/cli/test/index.test.js index 06bc0fe2eb99..1d66d05c8465 100644 --- a/test/integration/cli/test/index.test.js +++ b/test/integration/cli/test/index.test.js @@ -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 = ''