зеркало из https://github.com/github/docs.git
Коммит
bf0b5666f0
|
@ -1,5 +1,3 @@
|
|||
import path from 'path'
|
||||
|
||||
import got from 'got'
|
||||
import type { Response, NextFunction } from 'express'
|
||||
|
||||
|
@ -14,13 +12,7 @@ import type { ExtendedRequest } from '@/types'
|
|||
|
||||
// This module handles requests for the CSS and JS assets for
|
||||
// deprecated GitHub Enterprise versions by routing them to static content in
|
||||
// help-docs-archived-enterprise-versions
|
||||
//
|
||||
// Note that as of GHES 3.2, we no longer store assets for deprecated versions
|
||||
// in help-docs-archived-enterprise-versions. Instead, we store them in the
|
||||
// Azure blob storage `githubdocs` in the `enterprise` container. All HTML files
|
||||
// have been updated to use references to this blob storage for all assets.
|
||||
//
|
||||
// one of the docs-ghes-<release number> repos.
|
||||
// See also ./archived-enterprise-versions.js for non-CSS/JS paths
|
||||
|
||||
export default async function archivedEnterpriseVersionsAssets(
|
||||
|
@ -33,12 +25,13 @@ export default async function archivedEnterpriseVersionsAssets(
|
|||
// or /_next/static/foo.css
|
||||
if (!patterns.assetPaths.test(req.path)) return next()
|
||||
|
||||
// We now know the URL is either /enterprise/2.22/_next/static/foo.css
|
||||
// or the regular /_next/static/foo.css. But we're only going to
|
||||
// bother looking it up on https://github.github.com/help-docs-archived-enterprise-versions
|
||||
// if the URL has the enterprise bit in it, or if the path was
|
||||
// /_next/static/foo.css *and* its Referrer had the enterprise
|
||||
// bit in it.
|
||||
// The URL is either in the format
|
||||
// /enterprise/2.22/_next/static/foo.css,
|
||||
// /enterprise-server@<release>,
|
||||
// or /_next/static/foo.css.
|
||||
// If the URL is prefixed with the enterprise version and release number
|
||||
// or if the Referrer contains the enterprise version and release number,
|
||||
// then we'll fetch it from the docs-ghes-<release number> repo.
|
||||
if (
|
||||
!(
|
||||
patterns.getEnterpriseVersionNumber.test(req.path) ||
|
||||
|
@ -59,12 +52,17 @@ export default async function archivedEnterpriseVersionsAssets(
|
|||
const { isArchived, requestedVersion } = isArchivedVersion(req)
|
||||
if (!isArchived || !requestedVersion) return next()
|
||||
|
||||
const assetPath = req.path.replace(`/enterprise/${requestedVersion}`, '')
|
||||
// In all of the `docs-ghes-<relase number` repos, the asset directories
|
||||
// are at the root. This removes the version and release number from the
|
||||
// asset path so that we can proxy the request to the correct location.
|
||||
const newEnterprisePrefix = `/enterprise-server@${requestedVersion}`
|
||||
const legacyEnterprisePrefix = `/enterprise/${requestedVersion}`
|
||||
const assetPath = req.path.replace(newEnterprisePrefix, '').replace(legacyEnterprisePrefix, '')
|
||||
|
||||
// Just to be absolutely certain that the path can not contain
|
||||
// a URL that might trip up the GET we're about to make.
|
||||
if (
|
||||
assetPath.includes('..') ||
|
||||
assetPath.includes('../') ||
|
||||
assetPath.includes('://') ||
|
||||
(assetPath.includes(':') && assetPath.includes('@'))
|
||||
) {
|
||||
|
@ -72,12 +70,10 @@ export default async function archivedEnterpriseVersionsAssets(
|
|||
return res.status(404).type('text/plain').send('Asset path not valid')
|
||||
}
|
||||
|
||||
const proxyPath = path.join('/', requestedVersion, assetPath)
|
||||
|
||||
const proxyPath = `https://github.github.com/docs-ghes-${requestedVersion}${assetPath}`
|
||||
try {
|
||||
const r = await got(
|
||||
`https://github.github.com/help-docs-archived-enterprise-versions${proxyPath}`,
|
||||
)
|
||||
const r = await got(proxyPath)
|
||||
|
||||
res.set('accept-ranges', 'bytes')
|
||||
res.set('content-type', r.headers['content-type'])
|
||||
res.set('content-length', r.headers['content-length'])
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
import path from 'path'
|
||||
|
||||
import type { Response, NextFunction } from 'express'
|
||||
import slash from 'slash'
|
||||
import got from 'got'
|
||||
|
||||
import statsd from '@/observability/lib/statsd.js'
|
||||
|
@ -25,18 +22,16 @@ import getRedirect, { splitPathByLanguage } from '@/redirects/lib/get-redirect.j
|
|||
import getRemoteJSON from '@/frame/lib/get-remote-json.js'
|
||||
import { ExtendedRequest } from '@/types'
|
||||
|
||||
const REMOTE_ENTERPRISE_STORAGE_URL = 'https://githubdocs.azureedge.net/enterprise'
|
||||
|
||||
function splitByLanguage(uri: string) {
|
||||
let language = null
|
||||
let withoutLanguage = uri
|
||||
const match = uri.match(languagePrefixPathRegex)
|
||||
if (match) {
|
||||
language = match[1]
|
||||
withoutLanguage = uri.replace(languagePrefixPathRegex, '/')
|
||||
}
|
||||
return [language, withoutLanguage]
|
||||
}
|
||||
const OLD_PUBLIC_AZURE_BLOB_URL = 'https://githubdocs.azureedge.net'
|
||||
// Old Azure Blob Storage `enterprise` container.
|
||||
const OLD_AZURE_BLOB_ENTERPRISE_DIR = `${OLD_PUBLIC_AZURE_BLOB_URL}/enterprise`
|
||||
// Old Azure Blob storage `github-images` container with
|
||||
// the root directory of 'enterprise'.
|
||||
const OLD_GITHUB_IMAGES_ENTERPRISE_DIR = `${OLD_PUBLIC_AZURE_BLOB_URL}/github-images/enterprise`
|
||||
const OLD_DEVELOPER_SITE_CONTAINER = `${OLD_PUBLIC_AZURE_BLOB_URL}/developer-site`
|
||||
// This is the new repo naming convention we use for each archived enterprise
|
||||
// version. E.g. https://github.github.com/docs-ghes-2.10
|
||||
const ENTERPRISE_GH_PAGES_URL_PREFIX = 'https://github.github.com/docs-ghes-'
|
||||
|
||||
type ArchivedRedirects = {
|
||||
[url: string]: string | null
|
||||
|
@ -93,7 +88,8 @@ const retryConfiguration = { limit: 3 }
|
|||
const timeoutConfiguration = { response: 1500 }
|
||||
|
||||
// This module handles requests for deprecated GitHub Enterprise versions
|
||||
// by routing them to static content in help-docs-archived-enterprise-versions
|
||||
// by routing them to static content in
|
||||
// one of the docs-ghes-<release number> repos.
|
||||
|
||||
export default async function archivedEnterpriseVersions(
|
||||
req: ExtendedRequest,
|
||||
|
@ -108,6 +104,7 @@ export default async function archivedEnterpriseVersions(
|
|||
|
||||
const redirectCode = pathLanguagePrefixed(req.path) ? 301 : 302
|
||||
|
||||
// Redirects for releases 3.0+
|
||||
if (deprecatedWithFunctionalRedirects.includes(requestedVersion)) {
|
||||
const redirectTo = getRedirect(req.path, req.context)
|
||||
if (redirectTo) {
|
||||
|
@ -138,8 +135,7 @@ export default async function archivedEnterpriseVersions(
|
|||
return res.redirect(redirectCode, `/${language}${newRedirectTo}`)
|
||||
}
|
||||
}
|
||||
// redirect language-prefixed URLs like /en/enterprise/2.10 -> /enterprise/2.10
|
||||
// (this only applies to versions <2.13)
|
||||
// For releases 2.13 and lower, redirect language-prefixed URLs like /en/enterprise/2.10 -> /enterprise/2.10
|
||||
if (
|
||||
req.path.startsWith('/en/') &&
|
||||
versionSatisfiesRange(requestedVersion, `<${firstVersionDeprecatedOnNewSite}`)
|
||||
|
@ -148,8 +144,7 @@ export default async function archivedEnterpriseVersions(
|
|||
return res.redirect(redirectCode, req.baseUrl + req.path.replace(/^\/en/, ''))
|
||||
}
|
||||
|
||||
// find redirects for versions between 2.13 and 2.17
|
||||
// starting with 2.18, we updated the archival script to create a redirects.json file
|
||||
// Redirects for releases 2.13 - 2.17
|
||||
if (
|
||||
versionSatisfiesRange(requestedVersion, `>=${firstVersionDeprecatedOnNewSite}`) &&
|
||||
versionSatisfiesRange(requestedVersion, `<=${lastVersionWithoutArchivedRedirectsFile}`)
|
||||
|
@ -173,7 +168,8 @@ export default async function archivedEnterpriseVersions(
|
|||
return res.redirect(redirectCode, redirect)
|
||||
}
|
||||
}
|
||||
|
||||
// Redirects for 2.18 - 3.0. Starting with 2.18, we updated the archival
|
||||
// script to create a redirects.json file
|
||||
if (
|
||||
versionSatisfiesRange(requestedVersion, `>${lastVersionWithoutArchivedRedirectsFile}`) &&
|
||||
!deprecatedWithFunctionalRedirects.includes(requestedVersion)
|
||||
|
@ -195,19 +191,25 @@ export default async function archivedEnterpriseVersions(
|
|||
return res.redirect(redirectCode, redirectJson[req.path])
|
||||
}
|
||||
}
|
||||
|
||||
const statsdTags = [`version:${requestedVersion}`]
|
||||
// Retrieve the page from the archived repo
|
||||
const doGet = () =>
|
||||
got(getProxyPath(req.path, requestedVersion), {
|
||||
throwHttpErrors: false,
|
||||
retry: retryConfiguration,
|
||||
timeout: timeoutConfiguration,
|
||||
})
|
||||
|
||||
const statsdTags = [`version:${requestedVersion}`]
|
||||
const r = await statsd.asyncTimer(doGet, 'archive_enterprise_proxy', [
|
||||
...statsdTags,
|
||||
`path:${req.path}`,
|
||||
])()
|
||||
|
||||
if (r.statusCode === 200) {
|
||||
const [, withoutLanguagePath] = splitByLanguage(req.path)
|
||||
const isDeveloperPage = withoutLanguagePath?.startsWith(
|
||||
`/enterprise/${requestedVersion}/developer`,
|
||||
)
|
||||
res.set('x-robots-tag', 'noindex')
|
||||
|
||||
// make stubbed redirect files (which exist in versions <2.13) redirect with a 301
|
||||
|
@ -221,11 +223,74 @@ export default async function archivedEnterpriseVersions(
|
|||
|
||||
cacheAggressively(res)
|
||||
|
||||
// Releases 3.2 and higher contain image asset paths with the
|
||||
// old Azure Blob Storage URL. These need to be rewritten to
|
||||
// the new archived enterprise repo URL.
|
||||
if (versionSatisfiesRange(requestedVersion, `>=${firstReleaseStoredInBlobStorage}`)) {
|
||||
r.body = r.body
|
||||
.replaceAll(
|
||||
`${OLD_AZURE_BLOB_ENTERPRISE_DIR}/${requestedVersion}/assets/cb-`,
|
||||
`${ENTERPRISE_GH_PAGES_URL_PREFIX}${requestedVersion}/assets/cb-`,
|
||||
)
|
||||
.replaceAll(
|
||||
`${OLD_AZURE_BLOB_ENTERPRISE_DIR}/${requestedVersion}/`,
|
||||
`${req.protocol}://${req.get('host')}/enterprise-server@${requestedVersion}/`,
|
||||
)
|
||||
}
|
||||
|
||||
// Releases 3.1 and lower were previously hosted in the
|
||||
// help-docs-archived-enterprise-versions repo. Only the images
|
||||
// were stored in the old Azure Blob Storage `github-images` container.
|
||||
// The image paths all need to be updated to reference the images in the
|
||||
// new archived enterprise repo's root assets directory.
|
||||
if (versionSatisfiesRange(requestedVersion, `<${firstReleaseStoredInBlobStorage}`)) {
|
||||
r.body = r.body.replaceAll(
|
||||
`${OLD_GITHUB_IMAGES_ENTERPRISE_DIR}/${requestedVersion}`,
|
||||
`${ENTERPRISE_GH_PAGES_URL_PREFIX}${requestedVersion}`,
|
||||
)
|
||||
if (versionSatisfiesRange(requestedVersion, '<=2.18') && isDeveloperPage) {
|
||||
r.body = r.body.replaceAll(
|
||||
`${OLD_DEVELOPER_SITE_CONTAINER}/${requestedVersion}`,
|
||||
`${ENTERPRISE_GH_PAGES_URL_PREFIX}${requestedVersion}/developer`,
|
||||
)
|
||||
// Update all hrefs to add /developer to the path
|
||||
r.body = r.body.replaceAll(
|
||||
`="/enterprise/${requestedVersion}`,
|
||||
`="/enterprise/${requestedVersion}/developer`,
|
||||
)
|
||||
// The changelog is the only thing remaining on developer.github.com
|
||||
r.body = r.body.replaceAll('href="/changes', 'href="https://developer.github.com/changes')
|
||||
}
|
||||
}
|
||||
|
||||
// In all releases, some assets were incorrectly scraped and contain
|
||||
// deep relative paths. For example, releases 3.4+ use the webp format
|
||||
// for images. The URLs for those images were never rewritten to pull
|
||||
// from the Azure Blob Storage container. This may be due to not
|
||||
// updating our scraping tool to handle the new image types. There
|
||||
// are additional images in older versions that also have a relative path.
|
||||
// We want to update the URLs in the format
|
||||
// "../../../../../../assets/" to prefix the assets directory with the
|
||||
// new archived enterprise repo URL.
|
||||
r.body = r.body.replaceAll(
|
||||
/="(\.\.\/)*assets/g,
|
||||
`="${ENTERPRISE_GH_PAGES_URL_PREFIX}${requestedVersion}/assets`,
|
||||
)
|
||||
|
||||
// Fix broken hrefs on the 2.16 landing page
|
||||
if (requestedVersion === '2.16' && req.path === '/en/enterprise/2.16') {
|
||||
r.body = r.body.replaceAll('ref="/en/enterprise', 'ref="/en/enterprise/2.16')
|
||||
}
|
||||
|
||||
// Remove the search results container from the page, which removes a white
|
||||
// box that prevents clicking on page links
|
||||
r.body = r.body.replaceAll('<div id="search-results-container"></div>', '')
|
||||
|
||||
return res.send(r.body)
|
||||
}
|
||||
|
||||
// from 2.13 to 2.17, we lost access to frontmatter redirects during the archival process
|
||||
// this workaround finds potentially relevant frontmatter redirects in currently supported pages
|
||||
// In releases 2.13 - 2.17, we lost access to frontmatter redirects
|
||||
// during the archival process. This workaround finds potentially
|
||||
// relevant frontmatter redirects in currently supported pages
|
||||
if (
|
||||
versionSatisfiesRange(requestedVersion, `>=${firstVersionDeprecatedOnNewSite}`) &&
|
||||
versionSatisfiesRange(requestedVersion, `<=${lastVersionWithoutArchivedRedirectsFile}`)
|
||||
|
@ -244,18 +309,35 @@ export default async function archivedEnterpriseVersions(
|
|||
return next()
|
||||
}
|
||||
|
||||
// paths are slightly different depending on the version
|
||||
// for >=2.13: /2.13/en/enterprise/2.13/user/articles/viewing-contributions-on-your-profile
|
||||
// for <2.13: /2.12/user/articles/viewing-contributions-on-your-profile
|
||||
function getProxyPath(reqPath: string, requestedVersion: string) {
|
||||
if (versionSatisfiesRange(requestedVersion, `>=${firstReleaseStoredInBlobStorage}`)) {
|
||||
const newReqPath = reqPath.includes('redirects.json') ? `/${reqPath}` : reqPath + '/index.html'
|
||||
return `${REMOTE_ENTERPRISE_STORAGE_URL}/${requestedVersion}${newReqPath}`
|
||||
const [, withoutLanguagePath] = splitByLanguage(reqPath)
|
||||
const isDeveloperPage = withoutLanguagePath?.startsWith(
|
||||
`/enterprise/${requestedVersion}/developer`,
|
||||
)
|
||||
|
||||
// This was the last release supported on developer.github.com
|
||||
if (isDeveloperPage) {
|
||||
const enterprisePath = `/enterprise/${requestedVersion}`
|
||||
const newReqPath = reqPath.replace(enterprisePath, '')
|
||||
return ENTERPRISE_GH_PAGES_URL_PREFIX + requestedVersion + newReqPath
|
||||
}
|
||||
const proxyPath = versionSatisfiesRange(requestedVersion, `>=${firstVersionDeprecatedOnNewSite}`)
|
||||
? slash(path.join('/', requestedVersion, reqPath))
|
||||
: reqPath.replace(/^\/enterprise/, '')
|
||||
return `https://github.github.com/help-docs-archived-enterprise-versions${proxyPath}`
|
||||
|
||||
// Releases 2.18 and higher
|
||||
if (versionSatisfiesRange(requestedVersion, `>${lastVersionWithoutArchivedRedirectsFile}`)) {
|
||||
const newReqPath = reqPath.includes('redirects.json') ? `/${reqPath}` : reqPath + '/index.html'
|
||||
return ENTERPRISE_GH_PAGES_URL_PREFIX + requestedVersion + newReqPath
|
||||
}
|
||||
|
||||
// Releases 2.13 - 2.17
|
||||
// redirect.json files don't exist for these versions
|
||||
if (versionSatisfiesRange(requestedVersion, `>=2.13`)) {
|
||||
return ENTERPRISE_GH_PAGES_URL_PREFIX + requestedVersion + reqPath + '/index.html'
|
||||
}
|
||||
|
||||
// Releases 2.12 and lower
|
||||
const enterprisePath = `/enterprise/${requestedVersion}`
|
||||
const newReqPath = reqPath.replace(enterprisePath, '')
|
||||
return ENTERPRISE_GH_PAGES_URL_PREFIX + requestedVersion + newReqPath
|
||||
}
|
||||
|
||||
// Module-level global cache object.
|
||||
|
@ -276,7 +358,7 @@ function getFallbackRedirect(req: ExtendedRequest) {
|
|||
//
|
||||
// The keys are valid URLs that it can redirect to. I.e. these are
|
||||
// URLs that we definitely know are valid and will be found
|
||||
// in https://github.com/github/help-docs-archived-enterprise-versions
|
||||
// in one of the docs-ghes-<release number> repos.
|
||||
// The array values are possible URLs we deem acceptable redirect
|
||||
// sources.
|
||||
// But to avoid an unnecessary, O(n), loop every time, we turn this
|
||||
|
@ -311,3 +393,14 @@ function getFallbackRedirect(req: ExtendedRequest) {
|
|||
return `/${language}${fallback}`
|
||||
}
|
||||
}
|
||||
|
||||
function splitByLanguage(uri: string) {
|
||||
let language = null
|
||||
let withoutLanguage = uri
|
||||
const match = uri.match(languagePrefixPathRegex)
|
||||
if (match) {
|
||||
language = match[1]
|
||||
withoutLanguage = uri.replace(languagePrefixPathRegex, '/')
|
||||
}
|
||||
return [language, withoutLanguage]
|
||||
}
|
||||
|
|
|
@ -41,9 +41,7 @@ function version2url(version) {
|
|||
semver.coerce(version).raw,
|
||||
semver.coerce(firstReleaseStoredInBlobStorage).raw,
|
||||
)
|
||||
return inBlobStorage
|
||||
? `https://githubdocs.azureedge.net/enterprise/${version}/redirects.json`
|
||||
: `https://github.github.com/help-docs-archived-enterprise-versions/${version}/redirects.json`
|
||||
return `https://github.github.com/docs-ghes-${version}/redirects.json`
|
||||
}
|
||||
|
||||
function withArchivedRedirectsFile(version) {
|
||||
|
|
|
@ -122,7 +122,7 @@ describe('recently deprecated redirects', () => {
|
|||
expect(res.headers.vary).toContain('accept-language')
|
||||
expect(res.headers.vary).toContain('x-user-language')
|
||||
// This is based on
|
||||
// https://github.com/github/help-docs-archived-enterprise-versions/blob/master/3.0/redirects.json
|
||||
// https://github.com/github/docs-ghes-3.0/blob/main/redirects.json
|
||||
expect(res.headers.location).toBe(
|
||||
'/en/enterprise-server@3.0/get-started/learning-about-github/githubs-products',
|
||||
)
|
||||
|
@ -309,8 +309,8 @@ describe('JS and CSS assets', () => {
|
|||
expect(result.headers['x-is-archived']).toBeUndefined()
|
||||
})
|
||||
|
||||
test('404 if the pathname contains URL characters (..)', async () => {
|
||||
const result = await get('/enterprise/2.18/dist/index..css', {
|
||||
test('404 if the pathname contains URL characters (../)', async () => {
|
||||
const result = await get('/enterprise/2.18/dist/index../css', {
|
||||
headers: {
|
||||
Referrer: '/en/enterprise/2.18',
|
||||
},
|
||||
|
|
|
@ -83,30 +83,30 @@ describe('archived enterprise static assets', () => {
|
|||
const sampleCSS = '/* nice CSS */'
|
||||
|
||||
nock('https://github.github.com')
|
||||
.get('/help-docs-archived-enterprise-versions/2.21/_next/static/foo.css')
|
||||
.get('/docs-ghes-2.21/_next/static/foo.css')
|
||||
.reply(200, sampleCSS, {
|
||||
'content-type': 'text/css',
|
||||
'content-length': `${sampleCSS.length}`,
|
||||
})
|
||||
nock('https://github.github.com')
|
||||
.get('/help-docs-archived-enterprise-versions/2.21/_next/static/only-on-proxy.css')
|
||||
.get('/docs-ghes-2.21/_next/static/only-on-proxy.css')
|
||||
.reply(200, sampleCSS, {
|
||||
'content-type': 'text/css',
|
||||
'content-length': `${sampleCSS.length}`,
|
||||
})
|
||||
nock('https://github.github.com')
|
||||
.get('/help-docs-archived-enterprise-versions/2.3/_next/static/only-on-2.3.css')
|
||||
.get('/docs-ghes-2.3/_next/static/only-on-2.3.css')
|
||||
.reply(200, sampleCSS, {
|
||||
'content-type': 'text/css',
|
||||
'content-length': `${sampleCSS.length}`,
|
||||
})
|
||||
nock('https://github.github.com')
|
||||
.get('/help-docs-archived-enterprise-versions/2.3/_next/static/fourofour.css')
|
||||
.get('/docs-ghes-2.3/_next/static/fourofour.css')
|
||||
.reply(404, 'Not found', {
|
||||
'content-type': 'text/plain',
|
||||
})
|
||||
nock('https://github.github.com')
|
||||
.get('/help-docs-archived-enterprise-versions/2.3/assets/images/site/logo.png')
|
||||
.get('/docs-ghes-2.3/assets/images/site/logo.png')
|
||||
.reply(404, 'Not found', {
|
||||
'content-type': 'text/plain',
|
||||
})
|
||||
|
|
|
@ -13,8 +13,8 @@ const inProd = process.env.NODE_ENV === 'production'
|
|||
|
||||
// Wrapper on `got()` that is able to both cache in memory and on disk.
|
||||
// The on-disk caching is in `.remotejson/`.
|
||||
// We use this for downloading `redirects.json` files from the
|
||||
// help-docs-archived-enterprise-versions repo as a proxy. A lot of those
|
||||
// We use this for downloading `redirects.json` files from one of the
|
||||
// docs-ghes-<release number> repos as a proxy. A lot of those
|
||||
// .json files are large and they're also static which makes them
|
||||
// ideal for caching.
|
||||
// Note that there's 2 layers of caching here:
|
||||
|
|
|
@ -2,9 +2,9 @@ import type { NextFunction, Request, Response } from 'express'
|
|||
import helmet from 'helmet'
|
||||
import { isArchivedVersion } from '@/archives/lib/is-archived-version.js'
|
||||
import versionSatisfiesRange from '@/versions/lib/version-satisfies-range.js'
|
||||
import { languagePrefixPathRegex } from '@/languages/lib/languages.js'
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
const AZURE_STORAGE_URL = 'githubdocs.azureedge.net'
|
||||
const GITHUB_DOMAINS = [
|
||||
"'self'",
|
||||
'github.com',
|
||||
|
@ -30,15 +30,14 @@ const DEFAULT_OPTIONS = {
|
|||
// When doing local dev, especially in Safari, you need to add `ws:`
|
||||
// which NextJS uses for the hot module reloading.
|
||||
connectSrc: ["'self'", isDev && 'ws:'].filter(Boolean) as string[],
|
||||
fontSrc: ["'self'", 'data:', AZURE_STORAGE_URL],
|
||||
imgSrc: [...GITHUB_DOMAINS, 'data:', AZURE_STORAGE_URL, 'placehold.it'],
|
||||
fontSrc: ["'self'", 'data:'],
|
||||
imgSrc: [...GITHUB_DOMAINS, 'data:', 'placehold.it'],
|
||||
objectSrc: ["'self'"],
|
||||
// For use during development only!
|
||||
// `unsafe-eval` allows us to use a performant webpack devtool setting (eval)
|
||||
// https://webpack.js.org/configuration/devtool/#devtool
|
||||
scriptSrc: ["'self'", 'data:', AZURE_STORAGE_URL, isDev && "'unsafe-eval'"].filter(
|
||||
Boolean,
|
||||
) as string[],
|
||||
scriptSrc: ["'self'", 'data:', isDev && "'unsafe-eval'"].filter(Boolean) as string[],
|
||||
scriptSrcAttr: ["'self'"],
|
||||
frameSrc: [
|
||||
...GITHUB_DOMAINS,
|
||||
isDev && 'http://localhost:3000',
|
||||
|
@ -50,7 +49,7 @@ const DEFAULT_OPTIONS = {
|
|||
'https://www.youtube-nocookie.com',
|
||||
].filter(Boolean) as string[],
|
||||
frameAncestors: isDev ? ['*'] : [...GITHUB_DOMAINS],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", 'data:', AZURE_STORAGE_URL],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", 'data:'],
|
||||
childSrc: ["'self'"], // exception for search in deprecated GHE versions
|
||||
manifestSrc: ["'self'"],
|
||||
upgradeInsecureRequests: isDev ? null : [],
|
||||
|
@ -59,7 +58,7 @@ const DEFAULT_OPTIONS = {
|
|||
}
|
||||
|
||||
const NODE_DEPRECATED_OPTIONS = structuredClone(DEFAULT_OPTIONS)
|
||||
const { directives: ndDirs } = NODE_DEPRECATED_OPTIONS.contentSecurityPolicy
|
||||
const ndDirs = NODE_DEPRECATED_OPTIONS.contentSecurityPolicy.directives
|
||||
ndDirs.scriptSrc.push(
|
||||
"'unsafe-eval'",
|
||||
"'unsafe-inline'",
|
||||
|
@ -69,12 +68,20 @@ ndDirs.scriptSrc.push(
|
|||
ndDirs.connectSrc.push('https://www.google-analytics.com')
|
||||
ndDirs.imgSrc.push('http://www.google-analytics.com', 'https://ssl.google-analytics.com')
|
||||
|
||||
const DEVELOPER_DEPRECATED_OPTIONS = structuredClone(DEFAULT_OPTIONS)
|
||||
const devDirs = DEVELOPER_DEPRECATED_OPTIONS.contentSecurityPolicy.directives
|
||||
devDirs.styleSrc.push('*.googleapis.com')
|
||||
devDirs.scriptSrc.push("'unsafe-inline'", '*.googleapis.com', 'http://www.google-analytics.com')
|
||||
devDirs.fontSrc.push('*.gstatic.com')
|
||||
devDirs.scriptSrcAttr.push("'unsafe-inline'")
|
||||
|
||||
const STATIC_DEPRECATED_OPTIONS = structuredClone(DEFAULT_OPTIONS)
|
||||
STATIC_DEPRECATED_OPTIONS.contentSecurityPolicy.directives.scriptSrc.push("'unsafe-inline'")
|
||||
|
||||
const defaultHelmet = helmet(DEFAULT_OPTIONS)
|
||||
const nodeDeprecatedHelmet = helmet(NODE_DEPRECATED_OPTIONS)
|
||||
const staticDeprecatedHelmet = helmet(STATIC_DEPRECATED_OPTIONS)
|
||||
const developerDeprecatedHelmet = helmet(DEVELOPER_DEPRECATED_OPTIONS)
|
||||
|
||||
export default function helmetMiddleware(req: Request, res: Response, next: NextFunction) {
|
||||
// Enable CORS
|
||||
|
@ -85,6 +92,14 @@ export default function helmetMiddleware(req: Request, res: Response, next: Next
|
|||
// Determine version for exceptions
|
||||
const { requestedVersion } = isArchivedVersion(req)
|
||||
|
||||
// Check if this is a legacy developer.github.com path
|
||||
const isDeveloper = req.path
|
||||
.replace(languagePrefixPathRegex, '/')
|
||||
.startsWith(`/enterprise/${requestedVersion}/developer`)
|
||||
if (versionSatisfiesRange(requestedVersion, '<=2.18') && isDeveloper) {
|
||||
return developerDeprecatedHelmet(req, res, next)
|
||||
}
|
||||
|
||||
// Exception for deprecated Enterprise docs (Node.js era)
|
||||
if (
|
||||
versionSatisfiesRange(requestedVersion, '<=2.19') &&
|
||||
|
|
|
@ -10,8 +10,6 @@ import {
|
|||
makeLanguageSurrogateKey,
|
||||
} from '#src/frame/middleware/set-fastly-surrogate-key.js'
|
||||
|
||||
const AZURE_STORAGE_URL = 'githubdocs.azureedge.net'
|
||||
|
||||
describe('server', () => {
|
||||
vi.setConfig({ testTimeout: 60 * 1000 })
|
||||
|
||||
|
@ -49,12 +47,10 @@ describe('server', () => {
|
|||
expect(csp.get('default-src')).toBe("'none'")
|
||||
|
||||
expect(csp.get('font-src').includes("'self'")).toBe(true)
|
||||
expect(csp.get('font-src').includes(AZURE_STORAGE_URL)).toBe(true)
|
||||
|
||||
expect(csp.get('connect-src').includes("'self'")).toBe(true)
|
||||
|
||||
expect(csp.get('img-src').includes("'self'")).toBe(true)
|
||||
expect(csp.get('img-src').includes(AZURE_STORAGE_URL)).toBe(true)
|
||||
|
||||
expect(csp.get('script-src').includes("'self'")).toBe(true)
|
||||
|
||||
|
|
|
@ -58,12 +58,11 @@ As a workaround for these lost redirects, we have two files in `lib/redirects/st
|
|||
which had a record of each possible redirect candidate that we should bother
|
||||
redirecting too.
|
||||
Now, this new file has been created by accurately comparing it to the actual
|
||||
content inside the `github/help-docs-archived-enterprise-versions` repo for the
|
||||
content inside one of the `github/docs-ghes-<release number>` repos for the
|
||||
version range of 2.13 to 2.17. So every key in `archived-frontmatter-valid-urls.json`
|
||||
corresponds to a file that would work.
|
||||
|
||||
Here's how the `src/archives/middleware/archived-enterprise-versions.js` fallback works: if someone tries to access an article that was updated via a now-lost frontmatter redirect (for example, an article at the path `/en/enterprise/2.15/user/articles/viewing-contributions-on-your-profile-page`), the middleware will first look for a redirect in `archived-redirects-from-213-to-217.json`. If it does not find one, it will look for it in `archived-frontmatter-valid-urls.json` that contains the requested path. If it finds it, it will redirect to it to because that file knows exactly which URLs are valid in
|
||||
`help-docs-archived-enterprise-versions`.
|
||||
Here's how the `src/archives/middleware/archived-enterprise-versions.js` fallback works: if someone tries to access an article that was updated via a now-lost frontmatter redirect (for example, an article at the path `/en/enterprise/2.15/user/articles/viewing-contributions-on-your-profile-page`), the middleware will first look for a redirect in `archived-redirects-from-213-to-217.json`. If it does not find one, it will look for it in `archived-frontmatter-valid-urls.json` that contains the requested path. If it finds it, it will redirect to it to because that file knows exactly which URLs are valid in the `docs-ghes-<release number>` repos.
|
||||
|
||||
## Tests
|
||||
|
||||
|
|
|
@ -14,15 +14,11 @@ describe('release notes', () => {
|
|||
await get('/')
|
||||
|
||||
nock('https://github.github.com')
|
||||
.get(
|
||||
'/help-docs-archived-enterprise-versions/2.19/en/enterprise-server@2.19/admin/release-notes',
|
||||
)
|
||||
.get('/docs-ghes-2.19/en/enterprise-server@2.19/admin/release-notes')
|
||||
.reply(404)
|
||||
nock('https://github.github.com')
|
||||
.get('/help-docs-archived-enterprise-versions/2.19/redirects.json')
|
||||
.reply(200, {
|
||||
emp: 'ty',
|
||||
})
|
||||
nock('https://github.github.com').get('/docs-ghes-2.19/redirects.json').reply(200, {
|
||||
emp: 'ty',
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => nock.cleanAll())
|
||||
|
|
|
@ -81,10 +81,11 @@ export const deprecated = [
|
|||
]
|
||||
export const legacyAssetVersions = ['3.0', '2.22', '2.21']
|
||||
|
||||
// As of GHES 3.2, the archived enterprise content in no longer stored
|
||||
// in the help-docs-archived-enterprise-versions repository. Instead, it
|
||||
// is stored in our githubdocs Azure blog storage, in the `enterprise`
|
||||
// container.
|
||||
// As of GHES 3.2, we started storing the scraped assets and html
|
||||
// in Azure blob storage. All enterprise deprecated veresions are
|
||||
// now stored in individual docs-ghes-<release number> repos. This
|
||||
// release number now indicates a change in the way the archived html
|
||||
// references assets.
|
||||
export const firstReleaseStoredInBlobStorage = '3.2'
|
||||
|
||||
export const all = supported.concat(deprecated)
|
||||
|
|
Загрузка…
Ссылка в новой задаче