“7dc17a6cf015b175c78f3c459498b05a0d6b2955”上不存在“git@gitcode.net:btwise/openssl.git”
next-server.ts 10.4 KB
Newer Older
1 2
/* eslint-disable import/first */
import {IncomingMessage, ServerResponse} from 'http'
A
Arunoda Susiripala 已提交
3
import { resolve, join, sep } from 'path'
4 5
import { parse as parseUrl, UrlWithParsedQuery } from 'url'
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
6
import fs from 'fs'
7
import {renderToHTML} from './render'
8
import {sendHTML} from './send-html'
9
import {serveStatic} from './serve-static'
10
import Router, {route, Route} from './router'
11
import { isInternalUrl, isBlockedPage } from './utils'
T
Tim Neutkens 已提交
12
import loadConfig from 'next-server/next-config'
13
import {PHASE_PRODUCTION_SERVER, BUILD_ID_FILE, CLIENT_STATIC_FILES_PATH, CLIENT_STATIC_FILES_RUNTIME} from 'next-server/constants'
14
import * as envConfig from '../lib/runtime-config'
15
import {loadComponents} from './load-components'
16 17 18 19 20 21 22

type NextConfig = any

type ServerConstructor = {
  dir?: string,
  staticMarkup?: boolean,
  quiet?: boolean,
23
  conf?: NextConfig,
24
}
25

N
nkzawa 已提交
26
export default class Server {
27 28 29 30 31 32 33 34 35 36
  dir: string
  quiet: boolean
  nextConfig: NextConfig
  distDir: string
  buildId: string
  renderOpts: {
    staticMarkup: boolean,
    buildId: string,
    generateEtags: boolean,
    runtimeConfig?: {[key: string]: any},
37
    assetPrefix?: string,
38 39 40
  }
  router: Router

41
  public constructor({ dir = '.', staticMarkup = false, quiet = false, conf = null }: ServerConstructor = {}) {
N
nkzawa 已提交
42
    this.dir = resolve(dir)
N
Naoyuki Kanezawa 已提交
43
    this.quiet = quiet
T
Tim Neutkens 已提交
44
    const phase = this.currentPhase()
45
    this.nextConfig = loadConfig(phase, this.dir, conf)
46
    this.distDir = join(this.dir, this.nextConfig.distDir)
T
Tim Neutkens 已提交
47

48 49 50 51
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
    const {serverRuntimeConfig = {}, publicRuntimeConfig, assetPrefix, generateEtags} = this.nextConfig

T
Tim Neutkens 已提交
52
    this.buildId = this.readBuildId()
53 54
    this.renderOpts = {
      staticMarkup,
55
      buildId: this.buildId,
56
      generateEtags,
57
    }
N
Naoyuki Kanezawa 已提交
58

59 60 61 62
    // Only the `publicRuntimeConfig` key is exposed to the client side
    // It'll be rendered as part of __NEXT_DATA__ on the client side
    if (publicRuntimeConfig) {
      this.renderOpts.runtimeConfig = publicRuntimeConfig
63 64
    }

65 66 67
    // Initialize next/config with the environment configuration
    envConfig.setConfig({
      serverRuntimeConfig,
68
      publicRuntimeConfig,
69 70
    })

71 72
    const routes = this.generateRoutes()
    this.router = new Router(routes)
73
    this.setAssetPrefix(assetPrefix)
N
Naoyuki Kanezawa 已提交
74
  }
N
nkzawa 已提交
75

76
  private currentPhase(): string {
77
    return PHASE_PRODUCTION_SERVER
78 79
  }

80
  private logError(...args: any): void {
81 82
    if (this.quiet) return
    // tslint:disable-next-line
83 84 85
    console.error(...args)
  }

86
  private handleRequest(req: IncomingMessage, res: ServerResponse, parsedUrl?: UrlWithParsedQuery): Promise<void> {
87
    // Parse url if parsedUrl not provided
88
    if (!parsedUrl || typeof parsedUrl !== 'object') {
89 90
      const url: any = req.url
      parsedUrl = parseUrl(url, true)
91
    }
92

93 94 95
    // Parse the querystring ourselves if the user doesn't handle querystring parsing
    if (typeof parsedUrl.query === 'string') {
      parsedUrl.query = parseQs(parsedUrl.query)
N
Naoyuki Kanezawa 已提交
96
    }
97

98
    res.statusCode = 200
99
    return this.run(req, res, parsedUrl)
100
      .catch((err) => {
101
        this.logError(err)
102
        res.statusCode = 500
103
        res.end('Internal Server Error')
104
      })
105 106
  }

107
  public getRequestHandler() {
108
    return this.handleRequest.bind(this)
N
nkzawa 已提交
109 110
  }

111
  public setAssetPrefix(prefix?: string) {
112
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
113 114
  }

115
  // Backwards compatibility
116
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
117

T
Tim Neutkens 已提交
118
  // Backwards compatibility
119
  private async close(): Promise<void> {}
T
Tim Neutkens 已提交
120

121
  private setImmutableAssetCacheControl(res: ServerResponse) {
T
Tim Neutkens 已提交
122
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
123 124
  }

125
  private generateRoutes(): Route[] {
126
    const routes: Route[] = [
T
Tim Neutkens 已提交
127
      {
128
        match: route('/_next/static/:path*'),
129
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
130 131 132 133 134
          // The commons folder holds commonschunk files
          // The chunks folder holds dynamic entries
          // The buildId folder holds pages and potentially other assets. As buildId changes per build it can be long-term cached.
          if (params.path[0] === CLIENT_STATIC_FILES_RUNTIME || params.path[0] === 'chunks' || params.path[0] === this.buildId) {
            this.setImmutableAssetCacheControl(res)
135
          }
T
Tim Neutkens 已提交
136
          const p = join(this.distDir, CLIENT_STATIC_FILES_PATH, ...(params.path || []))
137
          await this.serveStatic(req, res, p, parsedUrl)
138
        },
139
      },
T
Tim Neutkens 已提交
140
      {
141
        match: route('/_next/:path*'),
T
Tim Neutkens 已提交
142
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
143
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
144
          await this.render404(req, res, parsedUrl)
145
        },
T
Tim Neutkens 已提交
146 147
      },
      {
148 149 150
        // 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.
T
Tim Neutkens 已提交
151
        // See more: https://github.com/zeit/next.js/issues/2617
152
        match: route('/static/:path*'),
153
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
154
          const p = join(this.dir, 'static', ...(params.path || []))
155
          await this.serveStatic(req, res, p, parsedUrl)
156 157
        },
      },
T
Tim Neutkens 已提交
158
    ]
159

160
    if (this.nextConfig.useFileSystemPublicRoutes) {
161 162 163
      // 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.
164
      // See more: https://github.com/zeit/next.js/issues/2617
T
Tim Neutkens 已提交
165
      routes.push({
166
        match: route('/:path*'),
167
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
168
          const { pathname, query } = parsedUrl
169
          if (!pathname) {
170 171
            throw new Error('pathname is undefined')
          }
T
Tim Neutkens 已提交
172
          await this.render(req, res, pathname, query, parsedUrl)
173
        },
T
Tim Neutkens 已提交
174
      })
175
    }
N
nkzawa 已提交
176

T
Tim Neutkens 已提交
177 178 179
    return routes
  }

180
  private async run(req: IncomingMessage, res: ServerResponse, parsedUrl: UrlWithParsedQuery) {
181 182 183 184 185 186 187 188 189 190 191 192
    try {
      const fn = this.router.match(req, res, parsedUrl)
      if (fn) {
        await fn()
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
193 194 195
    }

    if (req.method === 'GET' || req.method === 'HEAD') {
196
      await this.render404(req, res, parsedUrl)
197 198
    } else {
      res.statusCode = 501
199
      res.end('Not Implemented')
N
nkzawa 已提交
200 201 202
    }
  }

203 204 205 206 207
  private async sendHTML(req: IncomingMessage, res: ServerResponse, html: string) {
    const {generateEtags} = this.renderOpts
    return sendHTML(req, res, html, {generateEtags})
  }

208
  public async render(req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}, parsedUrl?: UrlWithParsedQuery): Promise<void> {
209 210
    const url: any = req.url
    if (isInternalUrl(url)) {
211 212 213
      return this.handleRequest(req, res, parsedUrl)
    }

214
    if (isBlockedPage(pathname)) {
215
      return this.render404(req, res, parsedUrl)
216 217
    }

N
Naoyuki Kanezawa 已提交
218
    const html = await this.renderToHTML(req, res, pathname, query)
219 220
    // Request was ended by the user
    if (html === null) {
221 222 223
      return
    }

224
    if (this.nextConfig.poweredByHeader) {
225
      res.setHeader('X-Powered-By', 'Next.js ' + process.env.NEXT_VERSION)
226
    }
227
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
228
  }
N
nkzawa 已提交
229

230 231 232 233 234
  private async renderToHTMLWithComponents(req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}, opts: any) {
    const result = await loadComponents(this.distDir, this.buildId, pathname)
    return renderToHTML(req, res, pathname, query, {...result, ...opts})
  }

235
  public async renderToHTML(req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}): Promise<string|null> {
N
Naoyuki Kanezawa 已提交
236
    try {
237
      // To make sure the try/catch is executed
238
      const html = await this.renderToHTMLWithComponents(req, res, pathname, query, this.renderOpts)
239
      return html
N
Naoyuki Kanezawa 已提交
240
    } catch (err) {
N
Naoyuki Kanezawa 已提交
241
      if (err.code === 'ENOENT') {
T
Tim Neutkens 已提交
242
        res.statusCode = 404
N
Naoyuki Kanezawa 已提交
243 244
        return this.renderErrorToHTML(null, req, res, pathname, query)
      } else {
245
        this.logError(err)
N
Naoyuki Kanezawa 已提交
246 247
        res.statusCode = 500
        return this.renderErrorToHTML(err, req, res, pathname, query)
N
nkzawa 已提交
248 249
      }
    }
N
Naoyuki Kanezawa 已提交
250 251
  }

252
  public async renderError(err: Error|null, req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}): Promise<void> {
253
    res.setHeader('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate')
N
Naoyuki Kanezawa 已提交
254
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
255
    if (html === null) {
256 257
      return
    }
258
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
259 260
  }

261
  public async renderErrorToHTML(err: Error|null, req: IncomingMessage, res: ServerResponse, _pathname: string, query: ParsedUrlQuery = {}) {
262
    return this.renderToHTMLWithComponents(req, res, '/_error', query, {...this.renderOpts, err})
N
Naoyuki Kanezawa 已提交
263 264
  }

265
  public async render404(req: IncomingMessage, res: ServerResponse, parsedUrl?: UrlWithParsedQuery): Promise<void> {
266 267
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
268
    if (!pathname) {
269 270
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
271
    res.statusCode = 404
272
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
273
  }
N
Naoyuki Kanezawa 已提交
274

275
  public async serveStatic(req: IncomingMessage, res: ServerResponse, path: string, parsedUrl?: UrlWithParsedQuery): Promise<void> {
A
Arunoda Susiripala 已提交
276
    if (!this.isServeableUrl(path)) {
277
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
278 279
    }

N
Naoyuki Kanezawa 已提交
280
    try {
281
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
282
    } catch (err) {
T
Tim Neutkens 已提交
283
      if (err.code === 'ENOENT' || err.statusCode === 404) {
284
        this.render404(req, res, parsedUrl)
N
Naoyuki Kanezawa 已提交
285 286 287 288 289 290
      } else {
        throw err
      }
    }
  }

291
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
292 293
    const resolved = resolve(path)
    if (
294
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
A
Arunoda Susiripala 已提交
295 296 297 298 299 300 301 302 303
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

304
  private readBuildId(): string {
305 306 307 308 309 310 311 312 313
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
        throw new Error(`Could not find a valid build in the '${this.distDir}' directory! Try building your app with 'next build' before starting the server.`)
      }

      throw err
314
    }
315
  }
316
}