Add Cache-Tag headers
This commit is contained in:
14
server.js
14
server.js
@ -1,12 +1,16 @@
|
|||||||
const path = require('path')
|
|
||||||
const throng = require('throng')
|
const throng = require('throng')
|
||||||
const { startServer } = require('./server/index')
|
const createServer = require('./server/createServer')
|
||||||
|
|
||||||
const port = parseInt(process.env.PORT, 10) || 5000
|
const port = parseInt(process.env.PORT, 10) || 5000
|
||||||
const publicDir = path.resolve(__dirname, 'build')
|
|
||||||
|
|
||||||
throng({
|
throng({
|
||||||
workers: process.env.WEB_CONCURRENCY || 1,
|
workers: process.env.WEB_CONCURRENCY || 1,
|
||||||
start: (id) => startServer({ id, port, publicDir }),
|
lifetime: Infinity,
|
||||||
lifetime: Infinity
|
start: function (id) {
|
||||||
|
const server = createServer()
|
||||||
|
|
||||||
|
server.listen(port, function () {
|
||||||
|
console.log('Server #%s listening on port %s, Ctrl+C to stop', id, port)
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,23 +1,34 @@
|
|||||||
/*eslint-disable no-console*/
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
const http = require('http')
|
const http = require('http')
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const cors = require('cors')
|
const cors = require('cors')
|
||||||
const morgan = require('morgan')
|
const morgan = require('morgan')
|
||||||
const middleware = require('./middleware')
|
|
||||||
const { fetchStats } = require('./cloudflare')
|
const { fetchStats } = require('./cloudflare')
|
||||||
|
const parseURL = require('./middleware/parseURL')
|
||||||
|
const checkBlacklist = require('./middleware/checkBlacklist')
|
||||||
|
const fetchPackage = require('./middleware/fetchPackage')
|
||||||
|
const findFile = require('./middleware/findFile')
|
||||||
|
const serveFile = require('./middleware/serveFile')
|
||||||
|
|
||||||
const fs = require('fs')
|
morgan.token('fwd', function (req) {
|
||||||
const path = require('path')
|
return req.get('x-forwarded-for').replace(/\s/g, '')
|
||||||
|
})
|
||||||
|
|
||||||
const sendHomePage = (publicDir) => {
|
function sendHomePage(publicDir) {
|
||||||
const html = fs.readFileSync(path.join(publicDir, 'index.html'), 'utf8')
|
const html = fs.readFileSync(path.join(publicDir, 'index.html'), 'utf8')
|
||||||
|
|
||||||
return (req, res, next) => {
|
return function (req, res, next) {
|
||||||
fetchStats((error, stats) => {
|
fetchStats(function (error, stats) {
|
||||||
if (error) {
|
if (error) {
|
||||||
next(error)
|
next(error)
|
||||||
} else {
|
} else {
|
||||||
res.set('Cache-Control', 'public, max-age=60')
|
res.set({
|
||||||
|
'Cache-Control': 'public, max-age=60',
|
||||||
|
'Cache-Tag': 'home'
|
||||||
|
})
|
||||||
|
|
||||||
res.send(
|
res.send(
|
||||||
// Replace the __SERVER_DATA__ token that was added to the
|
// Replace the __SERVER_DATA__ token that was added to the
|
||||||
// HTML file in the build process (see scripts/build.js).
|
// HTML file in the build process (see scripts/build.js).
|
||||||
@ -30,15 +41,13 @@ const sendHomePage = (publicDir) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorHandler = (err, req, res, next) => {
|
function errorHandler(err, req, res, next) {
|
||||||
res.status(500).send('<p>Internal Server Error</p>')
|
res.status(500).type('text').send('Internal Server Error')
|
||||||
console.error(err.stack)
|
console.error(err.stack)
|
||||||
next(err)
|
next(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
morgan.token('fwd', (req) => req.get('x-forwarded-for').replace(/\s/g, ''))
|
function createServer() {
|
||||||
|
|
||||||
const createServer = (config) => {
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
app.disable('x-powered-by')
|
app.disable('x-powered-by')
|
||||||
@ -53,20 +62,24 @@ const createServer = (config) => {
|
|||||||
app.use(errorHandler)
|
app.use(errorHandler)
|
||||||
app.use(cors())
|
app.use(cors())
|
||||||
|
|
||||||
app.get('/', sendHomePage(config.publicDir))
|
app.get('/', sendHomePage('build'))
|
||||||
|
|
||||||
app.use(express.static(config.publicDir, {
|
app.use(express.static('build', {
|
||||||
maxAge: '365d'
|
maxAge: '365d'
|
||||||
}))
|
}))
|
||||||
|
|
||||||
app.use(middleware())
|
app.use(parseURL)
|
||||||
|
app.use(checkBlacklist)
|
||||||
|
app.use(fetchPackage)
|
||||||
|
app.use(findFile)
|
||||||
|
app.use(serveFile)
|
||||||
|
|
||||||
const server = http.createServer(app)
|
const server = http.createServer(app)
|
||||||
|
|
||||||
// Heroku dynos automatically timeout after 30s. Set our
|
// Heroku dynos automatically timeout after 30s. Set our
|
||||||
// own timeout here to force sockets to close before that.
|
// own timeout here to force sockets to close before that.
|
||||||
// https://devcenter.heroku.com/articles/request-timeout
|
// https://devcenter.heroku.com/articles/request-timeout
|
||||||
server.setTimeout(25000, (socket) => {
|
server.setTimeout(25000, function (socket) {
|
||||||
const message = `Timeout of 25 seconds exceeded`
|
const message = `Timeout of 25 seconds exceeded`
|
||||||
|
|
||||||
socket.end([
|
socket.end([
|
||||||
@ -83,22 +96,4 @@ const createServer = (config) => {
|
|||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultServerConfig = {
|
module.exports = createServer
|
||||||
id: 1,
|
|
||||||
port: parseInt(process.env.PORT, 10) || 5000,
|
|
||||||
publicDir: 'public'
|
|
||||||
}
|
|
||||||
|
|
||||||
const startServer = (serverConfig = {}) => {
|
|
||||||
const config = Object.assign({}, defaultServerConfig, serverConfig)
|
|
||||||
const server = createServer(config)
|
|
||||||
|
|
||||||
server.listen(config.port, () => {
|
|
||||||
console.log('Server #%s listening on port %s, Ctrl+C to stop', config.id, config.port)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
createServer,
|
|
||||||
startServer
|
|
||||||
}
|
|
@ -5,7 +5,7 @@ const blacklist = require('../PackageBlacklist').blacklist
|
|||||||
*/
|
*/
|
||||||
function checkBlacklist(req, res, next) {
|
function checkBlacklist(req, res, next) {
|
||||||
if (blacklist.includes(req.packageName)) {
|
if (blacklist.includes(req.packageName)) {
|
||||||
res.status(403).send(`Package ${req.packageName} is blacklisted`)
|
res.status(403).type('text').send(`Package ${req.packageName} is blacklisted`)
|
||||||
} else {
|
} else {
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
@ -11,11 +11,11 @@ function fetchPackage(req, res, next) {
|
|||||||
PackageInfo.get(req.packageName, function (error, packageInfo) {
|
PackageInfo.get(req.packageName, function (error, packageInfo) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
return res.status(500).send(`Cannot get info for package "${req.packageName}"`)
|
return res.status(500).type('text').send(`Cannot get info for package "${req.packageName}"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packageInfo == null || packageInfo.versions == null)
|
if (packageInfo == null || packageInfo.versions == null)
|
||||||
return res.status(404).send(`Cannot find package "${req.packageName}"`)
|
return res.status(404).type('text').send(`Cannot find package "${req.packageName}"`)
|
||||||
|
|
||||||
req.packageInfo = packageInfo
|
req.packageInfo = packageInfo
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ function fetchPackage(req, res, next) {
|
|||||||
PackageCache.get(req.packageConfig, function (error, outputDir) {
|
PackageCache.get(req.packageConfig, function (error, outputDir) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
res.status(500).send(`Cannot fetch package ${req.packageSpec}`)
|
res.status(500).type('text').send(`Cannot fetch package ${req.packageSpec}`)
|
||||||
} else {
|
} else {
|
||||||
req.packageDir = outputDir
|
req.packageDir = outputDir
|
||||||
next()
|
next()
|
||||||
@ -42,7 +42,7 @@ function fetchPackage(req, res, next) {
|
|||||||
if (maxVersion) {
|
if (maxVersion) {
|
||||||
res.redirect(PackageURL.create(req.packageName, maxVersion, req.filename, req.search))
|
res.redirect(PackageURL.create(req.packageName, maxVersion, req.filename, req.search))
|
||||||
} else {
|
} else {
|
||||||
res.status(404).send(`Cannot find package ${req.packageSpec}`)
|
res.status(404).type('text').send(`Cannot find package ${req.packageSpec}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -51,7 +51,7 @@ function findFile(req, res, next) {
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
res.status(404).send(`Cannot find file "${req.filename}" in package ${req.packageSpec}`)
|
res.status(404).type('text').send(`Cannot find file "${req.filename}" in package ${req.packageSpec}`)
|
||||||
} else if (stats.isDirectory() && req.pathname[req.pathname.length - 1] !== '/') {
|
} else if (stats.isDirectory() && req.pathname[req.pathname.length - 1] !== '/') {
|
||||||
// Append / to directory URLs.
|
// Append / to directory URLs.
|
||||||
res.redirect(`${req.pathname}/${req.search}`)
|
res.redirect(`${req.pathname}/${req.search}`)
|
||||||
@ -71,7 +71,7 @@ function findFile(req, res, next) {
|
|||||||
|
|
||||||
if (queryMain) {
|
if (queryMain) {
|
||||||
if (!(queryMain in packageConfig))
|
if (!(queryMain in packageConfig))
|
||||||
return res.status(404).send(`Cannot find field "${queryMain}" in ${req.packageSpec} package config`)
|
return res.status(404).type('text').send(`Cannot find field "${queryMain}" in ${req.packageSpec} package config`)
|
||||||
|
|
||||||
mainFilename = packageConfig[queryMain]
|
mainFilename = packageConfig[queryMain]
|
||||||
} else {
|
} else {
|
||||||
@ -93,7 +93,7 @@ function findFile(req, res, next) {
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
res.status(404).send(`Cannot find main file "${mainFilename}" in package ${req.packageSpec}`)
|
res.status(404).type('text').send(`Cannot find main file "${mainFilename}" in package ${req.packageSpec}`)
|
||||||
} else {
|
} else {
|
||||||
req.file = file.replace(req.packageDir, '')
|
req.file = file.replace(req.packageDir, '')
|
||||||
req.stats = stats
|
req.stats = stats
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
const express = require('express')
|
|
||||||
const parseURL = require('./parseURL')
|
|
||||||
const checkBlacklist = require('./checkBlacklist')
|
|
||||||
const fetchPackage = require('./fetchPackage')
|
|
||||||
const findFile = require('./findFile')
|
|
||||||
const serveFile = require('./serveFile')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and returns a function that can be used in the "request"
|
|
||||||
* event of a standard node HTTP server. Supported URL schemes are:
|
|
||||||
*
|
|
||||||
* /history@1.12.5/umd/History.min.js (recommended)
|
|
||||||
* /history@1.12.5 (package.json's main is implied)
|
|
||||||
*
|
|
||||||
* Additionally, the following URLs are supported but will return a
|
|
||||||
* temporary (302) redirect:
|
|
||||||
*
|
|
||||||
* /history (redirects to version, latest is implied)
|
|
||||||
* /history/umd/History.min.js (redirects to version, latest is implied)
|
|
||||||
* /history@latest/umd/History.min.js (redirects to version)
|
|
||||||
* /history@^1/umd/History.min.js (redirects to max satisfying version)
|
|
||||||
*/
|
|
||||||
function createRequestHandler() {
|
|
||||||
const app = express.Router()
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
parseURL,
|
|
||||||
checkBlacklist,
|
|
||||||
fetchPackage,
|
|
||||||
findFile,
|
|
||||||
serveFile
|
|
||||||
)
|
|
||||||
|
|
||||||
return app
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = createRequestHandler
|
|
@ -19,12 +19,12 @@ function parseURL(req, res, next) {
|
|||||||
const url = PackageURL.parse(req.url)
|
const url = PackageURL.parse(req.url)
|
||||||
|
|
||||||
if (url == null)
|
if (url == null)
|
||||||
return res.status(403).send(`Invalid URL: ${req.url}`)
|
return res.status(403).type('text').send(`Invalid URL: ${req.url}`)
|
||||||
|
|
||||||
// Do not allow unrecognized query parameters because
|
// Do not allow unrecognized query parameters because
|
||||||
// some people use them to bust the cache.
|
// some people use them to bust the cache.
|
||||||
if (!queryIsValid(url.query))
|
if (!queryIsValid(url.query))
|
||||||
return res.status(403).send(`Invalid query: ${JSON.stringify(url.query)}`)
|
return res.status(403).type('text').send(`Invalid query: ${JSON.stringify(url.query)}`)
|
||||||
|
|
||||||
req.packageName = url.packageName
|
req.packageName = url.packageName
|
||||||
req.packageVersion = url.packageVersion
|
req.packageVersion = url.packageVersion
|
||||||
|
@ -31,7 +31,7 @@ function sendFile(res, file, stats) {
|
|||||||
|
|
||||||
stream.on('error', (error) => {
|
stream.on('error', (error) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
res.status(500).send('There was an error serving this file')
|
res.status(500).type('text').send('There was an error serving this file')
|
||||||
})
|
})
|
||||||
|
|
||||||
stream.pipe(res)
|
stream.pipe(res)
|
||||||
@ -46,15 +46,21 @@ function serveFile(req, res, next) {
|
|||||||
Metadata.get(req.packageDir, req.file, req.stats, MaximumDepth, function (error, metadata) {
|
Metadata.get(req.packageDir, req.file, req.stats, MaximumDepth, function (error, metadata) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
res.status(500).send(`Cannot generate JSON metadata for ${req.packageSpec}${req.filename}`)
|
res.status(500).type('text').send(`Cannot generate JSON metadata for ${req.packageSpec}${req.filename}`)
|
||||||
} else {
|
} else {
|
||||||
// Cache metadata for 1 year.
|
// Cache metadata for 1 year.
|
||||||
res.set('Cache-Control', 'public, max-age=31536000').send(metadata)
|
res.set({
|
||||||
|
'Cache-Control': 'public, max-age=31536000',
|
||||||
|
'Cache-Tag': 'meta'
|
||||||
|
}).send(metadata)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if (req.stats.isFile()) {
|
} else if (req.stats.isFile()) {
|
||||||
// Cache files for 1 year.
|
// Cache files for 1 year.
|
||||||
res.set('Cache-Control', 'public, max-age=31536000')
|
res.set({
|
||||||
|
'Cache-Control': 'public, max-age=31536000',
|
||||||
|
'Cache-Tag': 'file'
|
||||||
|
})
|
||||||
|
|
||||||
// TODO: use res.sendFile instead of our own sendFile?
|
// TODO: use res.sendFile instead of our own sendFile?
|
||||||
sendFile(res, path.join(req.packageDir, req.file), req.stats)
|
sendFile(res, path.join(req.packageDir, req.file), req.stats)
|
||||||
@ -62,14 +68,17 @@ function serveFile(req, res, next) {
|
|||||||
generateDirectoryIndexHTML(req.packageInfo, req.packageVersion, req.packageDir, req.file, function (error, html) {
|
generateDirectoryIndexHTML(req.packageInfo, req.packageVersion, req.packageDir, req.file, function (error, html) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
res.status(500).send(`Cannot generate index page for ${req.packageSpec}${req.filename}`)
|
res.status(500).type('text').send(`Cannot generate index page for ${req.packageSpec}${req.filename}`)
|
||||||
} else {
|
} else {
|
||||||
// Cache HTML directory listings for 1 minute.
|
// Cache HTML directory listings for 1 minute.
|
||||||
res.set('Cache-Control', 'public, max-age=60').send(html)
|
res.set({
|
||||||
|
'Cache-Control': 'public, max-age=60',
|
||||||
|
'Cache-Tag': 'index'
|
||||||
|
}).send(html)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
res.status(403).send(`Cannot serve ${req.packageSpec}${req.filename}; it's not a file`)
|
res.status(403).type('text').send(`Cannot serve ${req.packageSpec}${req.filename}; it's not a file`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user