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