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

Move static serving to next server #33475

Merged
merged 3 commits into from
Jan 19, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 6 additions & 138 deletions packages/next/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,12 @@ import type { PreviewData } from 'next/types'
import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin'
import type { BaseNextRequest, BaseNextResponse } from './base-http'

import { join, relative, resolve, sep } from 'path'
import { join, resolve } from 'path'
import { parse as parseQs, stringify as stringifyQs } from 'querystring'
import { format as formatUrl, parse as parseUrl } from 'url'
import { getRedirectStatus, modifyRouteRegex } from '../lib/load-custom-routes'
import {
CLIENT_PUBLIC_FILES_PATH,
CLIENT_STATIC_FILES_PATH,
CLIENT_STATIC_FILES_RUNTIME,
PRERENDER_MANIFEST,
ROUTES_MANIFEST,
SERVERLESS_DIRECTORY,
Expand Down Expand Up @@ -190,6 +188,8 @@ export default abstract class Server {
protected abstract getBuildId(): string
protected abstract generatePublicRoutes(): Route[]
protected abstract generateImageRoutes(): Route[]
protected abstract generateStaticRotes(): Route[]
protected abstract generateFsStaticRoutes(): Route[]
protected abstract generateCatchAllMiddlewareRoute(): Route | undefined
protected abstract getFilesystemPaths(): Set<string>
protected abstract getMiddleware(): {
Expand Down Expand Up @@ -227,12 +227,6 @@ export default abstract class Server {
}
): Promise<void>

protected abstract sendStatic(
req: BaseNextRequest,
res: BaseNextResponse,
path: string
): Promise<void>

protected abstract runApi(
req: BaseNextRequest,
res: BaseNextResponse,
Expand Down Expand Up @@ -714,64 +708,10 @@ export default abstract class Server {
} {
const publicRoutes = this.generatePublicRoutes()
const imageRoutes = this.generateImageRoutes()

const staticFilesRoute = this.hasStaticDir
? [
{
// It's very important to keep this route's param optional.
// (but it should support as many params as needed, separated by '/')
// Otherwise this will lead to a pretty simple DOS attack.
// See more: https://github.com/vercel/next.js/issues/2617
match: route('/static/:path*'),
name: 'static catchall',
fn: async (req, res, params, parsedUrl) => {
const p = join(this.dir, 'static', ...params.path)
await this.serveStatic(req, res, p, parsedUrl)
return {
finished: true,
}
},
} as Route,
]
: []
const staticFilesRoutes = this.generateStaticRotes()

const fsRoutes: Route[] = [
{
match: route('/_next/static/:path*'),
type: 'route',
name: '_next/static catchall',
fn: async (req, res, params, parsedUrl) => {
// make sure to 404 for /_next/static itself
if (!params.path) {
await this.render404(req, res, parsedUrl)
return {
finished: true,
}
}

if (
params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
params.path[0] === 'chunks' ||
params.path[0] === 'css' ||
params.path[0] === 'image' ||
params.path[0] === 'media' ||
params.path[0] === this.buildId ||
params.path[0] === 'pages' ||
params.path[1] === 'pages'
) {
this.setImmutableAssetCacheControl(res)
}
const p = join(
this.distDir,
CLIENT_STATIC_FILES_PATH,
...(params.path || [])
)
await this.serveStatic(req, res, p, parsedUrl)
return {
finished: true,
}
},
},
...this.generateFsStaticRoutes(),
{
match: route('/_next/data/:path*'),
type: 'route',
Expand Down Expand Up @@ -860,7 +800,7 @@ export default abstract class Server {
},
},
...publicRoutes,
...staticFilesRoute,
...staticFilesRoutes,
]

const restrictedRedirectPaths = ['/_next'].map((p) =>
Expand Down Expand Up @@ -2063,78 +2003,6 @@ export default abstract class Server {
return this.renderError(null, req, res, pathname!, query, setHeaders)
}

public async serveStatic(
req: BaseNextRequest,
res: BaseNextResponse,
path: string,
parsedUrl?: UrlWithParsedQuery
): Promise<void> {
if (!this.isServeableUrl(path)) {
return this.render404(req, res, parsedUrl)
}

if (!(req.method === 'GET' || req.method === 'HEAD')) {
res.statusCode = 405
res.setHeader('Allow', ['GET', 'HEAD'])
return this.renderError(null, req, res, path)
}

try {
await this.sendStatic(req, res, path)
} catch (error) {
if (!isError(error)) throw error
const err = error as Error & { code?: string; statusCode?: number }
if (err.code === 'ENOENT' || err.statusCode === 404) {
this.render404(req, res, parsedUrl)
} else if (err.statusCode === 412) {
res.statusCode = 412
return this.renderError(err, req, res, path)
} else {
throw err
}
}
}

protected isServeableUrl(untrustedFileUrl: string): boolean {
// This method mimics what the version of `send` we use does:
// 1. decodeURIComponent:
// https://github.com/pillarjs/send/blob/0.17.1/index.js#L989
// https://github.com/pillarjs/send/blob/0.17.1/index.js#L518-L522
// 2. resolve:
// https://github.com/pillarjs/send/blob/de073ed3237ade9ff71c61673a34474b30e5d45b/index.js#L561

let decodedUntrustedFilePath: string
try {
// (1) Decode the URL so we have the proper file name
decodedUntrustedFilePath = decodeURIComponent(untrustedFileUrl)
} catch {
return false
}

// (2) Resolve "up paths" to determine real request
const untrustedFilePath = resolve(decodedUntrustedFilePath)

// don't allow null bytes anywhere in the file path
if (untrustedFilePath.indexOf('\0') !== -1) {
return false
}

// Check if .next/static, static and public are in the path.
// If not the path is not available.
if (
(untrustedFilePath.startsWith(join(this.distDir, 'static') + sep) ||
untrustedFilePath.startsWith(join(this.dir, 'static') + sep) ||
untrustedFilePath.startsWith(join(this.dir, 'public') + sep)) === false
) {
return false
}

// Check against the real filesystem paths
const filesystemUrls = this.getFilesystemPaths()
const resolved = relative(this.dir, untrustedFilePath)
return filesystemUrls.has(resolved)
}

protected get _isLikeServerless(): boolean {
return isTargetLikeServerless(this.nextConfig.target)
}
Expand Down
163 changes: 156 additions & 7 deletions packages/next/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ import type { FetchEventResult } from './web/types'
import type { ParsedNextUrl } from '../shared/lib/router/utils/parse-next-url'

import fs from 'fs'
import { join, relative } from 'path'
import { join, relative, resolve, sep } from 'path'
import { IncomingMessage, ServerResponse } from 'http'

import {
PAGES_MANIFEST,
BUILD_ID_FILE,
SERVER_DIRECTORY,
MIDDLEWARE_MANIFEST,
CLIENT_STATIC_FILES_PATH,
CLIENT_STATIC_FILES_RUNTIME,
} from '../shared/lib/constants'
import { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin'
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
Expand Down Expand Up @@ -132,6 +134,69 @@ export default class NextNodeServer extends BaseServer {
]
}

protected generateStaticRotes(): Route[] {
return this.hasStaticDir
? [
{
// It's very important to keep this route's param optional.
// (but it should support as many params as needed, separated by '/')
// Otherwise this will lead to a pretty simple DOS attack.
// See more: https://github.com/vercel/next.js/issues/2617
match: route('/static/:path*'),
name: 'static catchall',
fn: async (req, res, params, parsedUrl) => {
const p = join(this.dir, 'static', ...params.path)
await this.serveStatic(req, res, p, parsedUrl)
return {
finished: true,
}
},
} as Route,
]
: []
}

protected generateFsStaticRoutes(): Route[] {
return [
{
match: route('/_next/static/:path*'),
type: 'route',
name: '_next/static catchall',
fn: async (req, res, params, parsedUrl) => {
// make sure to 404 for /_next/static itself
if (!params.path) {
await this.render404(req, res, parsedUrl)
return {
finished: true,
}
}

if (
params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
params.path[0] === 'chunks' ||
params.path[0] === 'css' ||
params.path[0] === 'image' ||
params.path[0] === 'media' ||
params.path[0] === this.buildId ||
params.path[0] === 'pages' ||
params.path[1] === 'pages'
) {
this.setImmutableAssetCacheControl(res)
}
const p = join(
this.distDir,
CLIENT_STATIC_FILES_PATH,
...(params.path || [])
)
await this.serveStatic(req, res, p, parsedUrl)
return {
finished: true,
}
},
},
]
}

protected generatePublicRoutes(): Route[] {
if (!fs.existsSync(this.publicDir)) return []

Expand Down Expand Up @@ -575,12 +640,96 @@ export default class NextNodeServer extends BaseServer {
path: string,
parsedUrl?: UrlWithParsedQuery
): Promise<void> {
return super.serveStatic(
this.normalizeReq(req),
this.normalizeRes(res),
path,
parsedUrl
)
if (!this.isServeableUrl(path)) {
return this.render404(req, res, parsedUrl)
}

if (!(req.method === 'GET' || req.method === 'HEAD')) {
res.statusCode = 405
res.setHeader('Allow', ['GET', 'HEAD'])
return this.renderError(null, req, res, path)
}

try {
await this.sendStatic(
req as NodeNextRequest,
res as NodeNextResponse,
path
)
} catch (error) {
if (!isError(error)) throw error
const err = error as Error & { code?: string; statusCode?: number }
if (err.code === 'ENOENT' || err.statusCode === 404) {
this.render404(req, res, parsedUrl)
} else if (err.statusCode === 412) {
res.statusCode = 412
return this.renderError(err, req, res, path)
} else {
throw err
}
}
}

protected getStaticRoutes(): Route[] {
return this.hasStaticDir
? [
{
// It's very important to keep this route's param optional.
// (but it should support as many params as needed, separated by '/')
// Otherwise this will lead to a pretty simple DOS attack.
// See more: https://github.com/vercel/next.js/issues/2617
match: route('/static/:path*'),
name: 'static catchall',
fn: async (req, res, params, parsedUrl) => {
const p = join(this.dir, 'static', ...params.path)
await this.serveStatic(req, res, p, parsedUrl)
return {
finished: true,
}
},
} as Route,
]
: []
}

protected isServeableUrl(untrustedFileUrl: string): boolean {
// This method mimics what the version of `send` we use does:
// 1. decodeURIComponent:
// https://github.com/pillarjs/send/blob/0.17.1/index.js#L989
// https://github.com/pillarjs/send/blob/0.17.1/index.js#L518-L522
// 2. resolve:
// https://github.com/pillarjs/send/blob/de073ed3237ade9ff71c61673a34474b30e5d45b/index.js#L561

let decodedUntrustedFilePath: string
try {
// (1) Decode the URL so we have the proper file name
decodedUntrustedFilePath = decodeURIComponent(untrustedFileUrl)
} catch {
return false
}

// (2) Resolve "up paths" to determine real request
const untrustedFilePath = resolve(decodedUntrustedFilePath)

// don't allow null bytes anywhere in the file path
if (untrustedFilePath.indexOf('\0') !== -1) {
return false
}

// Check if .next/static, static and public are in the path.
// If not the path is not available.
if (
(untrustedFilePath.startsWith(join(this.distDir, 'static') + sep) ||
untrustedFilePath.startsWith(join(this.dir, 'static') + sep) ||
untrustedFilePath.startsWith(join(this.dir, 'public') + sep)) === false
) {
return false
}

// Check against the real filesystem paths
const filesystemUrls = this.getFilesystemPaths()
const resolved = relative(this.dir, untrustedFilePath)
return filesystemUrls.has(resolved)
}

protected getMiddlewareInfo(params: {
Expand Down
1 change: 1 addition & 0 deletions packages/next/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export type PageConfig = {
*/
externalResolver?: true
}
runtime?: 'nodejs' | 'edge'
ijjk marked this conversation as resolved.
Show resolved Hide resolved
env?: Array<string>
unstable_runtimeJS?: false
unstable_JsPreload?: false
Expand Down