Merge pull request #35198 from github/repo-sync

Repo sync
This commit is contained in:
docs-bot 2024-11-05 16:30:48 -05:00 коммит произвёл GitHub
Родитель ea09c2bf46 629632fc80
Коммит bf0b5666f0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
11 изменённых файлов: 193 добавлений и 99 удалений

Просмотреть файл

@ -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)