| import type { Response, NextFunction } from 'express' |
| import { fetchWithRetry } from '@/frame/lib/fetch-utils' |
|
|
| import statsd from '@/observability/lib/statsd' |
| import { |
| firstVersionDeprecatedOnNewSite, |
| lastVersionWithoutArchivedRedirectsFile, |
| deprecatedWithFunctionalRedirects, |
| firstReleaseStoredInBlobStorage, |
| } from '@/versions/lib/enterprise-server-releases' |
| import patterns from '@/frame/lib/patterns' |
| import versionSatisfiesRange from '@/versions/lib/version-satisfies-range' |
| import { isArchivedVersion } from '@/archives/lib/is-archived-version' |
| import { setFastlySurrogateKey, SURROGATE_ENUMS } from '@/frame/middleware/set-fastly-surrogate-key' |
| import { readCompressedJsonFileFallbackLazily } from '@/frame/lib/read-json-file' |
| import { archivedCacheControl, languageCacheControl } from '@/frame/middleware/cache-control' |
| import { pathLanguagePrefixed, languagePrefixPathRegex } from '@/languages/lib/languages-server' |
| import getRedirect, { splitPathByLanguage } from '@/redirects/lib/get-redirect' |
| import getRemoteJSON from '@/frame/lib/get-remote-json' |
| import { ExtendedRequest } from '@/types' |
|
|
| const OLD_PUBLIC_AZURE_BLOB_URL = 'https://githubdocs.azureedge.net' |
| |
| const OLD_AZURE_BLOB_ENTERPRISE_DIR = `${OLD_PUBLIC_AZURE_BLOB_URL}/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` |
| |
| |
| const ENTERPRISE_GH_PAGES_URL_PREFIX = 'https://github.github.com/docs-ghes-' |
|
|
| type ArchivedRedirects = { |
| [url: string]: string | null |
| } |
| |
| |
| |
| const archivedRedirects: () => ArchivedRedirects = readCompressedJsonFileFallbackLazily( |
| './src/redirects/lib/static/archived-redirects-from-213-to-217.json', |
| ) |
|
|
| type ArchivedFrontmatterURLs = { |
| [url: string]: string[] |
| } |
| const archivedFrontmatterValidURLS: () => ArchivedFrontmatterURLs = |
| readCompressedJsonFileFallbackLazily( |
| './src/redirects/lib/static/archived-frontmatter-valid-urls.json', |
| ) |
|
|
| |
| |
| const cacheAggressively = (res: Response) => { |
| archivedCacheControl(res) |
|
|
| |
| |
| |
| |
| |
| setFastlySurrogateKey(res, SURROGATE_ENUMS.MANUAL) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const retryConfiguration = { limit: 3 } |
| |
| |
| |
| |
| |
| |
| const timeoutConfiguration = { response: 1500 } |
|
|
| |
| |
| |
|
|
| export default async function archivedEnterpriseVersions( |
| req: ExtendedRequest, |
| res: Response, |
| next: NextFunction, |
| ) { |
| const { isArchived, requestedVersion } = isArchivedVersion(req) |
| if (!isArchived || !requestedVersion) return next() |
|
|
| |
| if (patterns.assetPaths.test(req.path)) return next() |
|
|
| const redirectCode = pathLanguagePrefixed(req.path) ? 301 : 302 |
|
|
| |
| if (deprecatedWithFunctionalRedirects.includes(requestedVersion)) { |
| const redirectTo = req.context ? getRedirect(req.path, req.context) : undefined |
| if (redirectTo) { |
| if (redirectCode === 302) { |
| languageCacheControl(res) |
| } |
| archivedCacheControl(res) |
| return res.redirect(redirectCode, redirectTo) |
| } |
|
|
| const redirectJson = await getRemoteJSON(getProxyPath('redirects.json', requestedVersion), { |
| retry: retryConfiguration, |
| |
| |
| |
| |
| |
| timeout: { response: 1000 }, |
| }) |
| if (!req.context) throw new Error('No context on request') |
| const [language, withoutLanguage] = splitPathByLanguage(req.path, req.context.userLanguage) |
| const newRedirectTo = redirectJson[withoutLanguage] |
| if (newRedirectTo && newRedirectTo !== withoutLanguage) { |
| if (redirectCode === 302) { |
| languageCacheControl(res) |
| } |
| archivedCacheControl(res) |
| return res.redirect(redirectCode, `/${language}${newRedirectTo}`) |
| } |
| } |
| |
| if ( |
| req.path.startsWith('/en/') && |
| versionSatisfiesRange(requestedVersion, `<${firstVersionDeprecatedOnNewSite}`) |
| ) { |
| archivedCacheControl(res) |
| return res.redirect(redirectCode, req.baseUrl + req.path.replace(/^\/en/, '')) |
| } |
|
|
| |
| if ( |
| versionSatisfiesRange(requestedVersion, `>=${firstVersionDeprecatedOnNewSite}`) && |
| versionSatisfiesRange(requestedVersion, `<=${lastVersionWithoutArchivedRedirectsFile}`) |
| ) { |
| const [language, withoutLanguagePath] = splitByLanguage(req.path) |
|
|
| |
| |
|
|
| const newPath = withoutLanguagePath && archivedRedirects()[withoutLanguagePath] |
| |
| |
| |
| |
| |
| if (newPath !== undefined && (newPath || !language)) { |
| |
| |
| const redirect = `/${language || 'en'}${newPath || withoutLanguagePath}` |
| cacheAggressively(res) |
| return res.redirect(redirectCode, redirect) |
| } |
| } |
| |
| |
| if ( |
| versionSatisfiesRange(requestedVersion, `>${lastVersionWithoutArchivedRedirectsFile}`) && |
| !deprecatedWithFunctionalRedirects.includes(requestedVersion) |
| ) { |
| const redirectJson = await getRemoteJSON(getProxyPath('redirects.json', requestedVersion), { |
| retry: retryConfiguration, |
| |
| |
| |
| |
| |
| timeout: { response: 1000 }, |
| }) |
|
|
| |
| if (redirectJson[req.path]) { |
| res.set('x-robots-tag', 'noindex') |
| cacheAggressively(res) |
| return res.redirect(redirectCode, redirectJson[req.path]) |
| } |
| } |
| |
| const doGet = () => |
| fetchWithRetry( |
| getProxyPath(req.path, requestedVersion), |
| {}, |
| { |
| retries: retryConfiguration.limit, |
| timeout: timeoutConfiguration.response, |
| throwHttpErrors: false, |
| }, |
| ) |
|
|
| const statsdTags = [`version:${requestedVersion}`] |
| const r = await statsd.asyncTimer(doGet, 'archive_enterprise_proxy', [ |
| ...statsdTags, |
| `path:${req.path}`, |
| ])() |
|
|
| if (r.status === 200) { |
| const body = await r.text() |
| const [, withoutLanguagePath] = splitByLanguage(req.path) |
| const isDeveloperPage = withoutLanguagePath?.startsWith( |
| `/enterprise/${requestedVersion}/developer`, |
| ) |
| res.set('x-robots-tag', 'noindex') |
|
|
| |
| const staticRedirect = body.match(patterns.staticRedirect) |
| if (staticRedirect) { |
| cacheAggressively(res) |
| return res.redirect(redirectCode, staticRedirect[1]) |
| } |
|
|
| res.set('content-type', r.headers.get('content-type') || '') |
|
|
| cacheAggressively(res) |
|
|
| |
| |
| |
| if ( |
| versionSatisfiesRange(requestedVersion, `>=${firstReleaseStoredInBlobStorage}`) && |
| versionSatisfiesRange(requestedVersion, `<=3.9`) |
| ) { |
| |
| |
| const host = req.get('x-host') || req.get('x-forwarded-host') || req.get('host') |
| const modifiedBody = 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}://${host}/enterprise-server@${requestedVersion}/`, |
| ) |
|
|
| return res.send(modifiedBody) |
| } |
|
|
| |
| |
| |
| |
| |
| if (versionSatisfiesRange(requestedVersion, `<${firstReleaseStoredInBlobStorage}`)) { |
| let modifiedBody = body.replaceAll( |
| `${OLD_GITHUB_IMAGES_ENTERPRISE_DIR}/${requestedVersion}`, |
| `${ENTERPRISE_GH_PAGES_URL_PREFIX}${requestedVersion}`, |
| ) |
| if (versionSatisfiesRange(requestedVersion, '<=2.18') && isDeveloperPage) { |
| modifiedBody = modifiedBody.replaceAll( |
| `${OLD_DEVELOPER_SITE_CONTAINER}/${requestedVersion}`, |
| `${ENTERPRISE_GH_PAGES_URL_PREFIX}${requestedVersion}/developer`, |
| ) |
| |
| modifiedBody = modifiedBody.replaceAll( |
| `="/enterprise/${requestedVersion}`, |
| `="/enterprise/${requestedVersion}/developer`, |
| ) |
| |
| modifiedBody = modifiedBody.replaceAll( |
| 'href="/changes', |
| 'href="https://developer.github.com/changes', |
| ) |
| } |
|
|
| |
| modifiedBody = modifiedBody.replaceAll( |
| /="(\.\.\/)*assets/g, |
| `="${ENTERPRISE_GH_PAGES_URL_PREFIX}${requestedVersion}/assets`, |
| ) |
|
|
| |
| if (requestedVersion === '2.16' && req.path === '/en/enterprise/2.16') { |
| modifiedBody = modifiedBody.replaceAll('ref="/en/enterprise', 'ref="/en/enterprise/2.16') |
| } |
|
|
| |
| modifiedBody = modifiedBody.replaceAll('<div id="search-results-container"></div>', '') |
|
|
| return res.send(modifiedBody) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let modifiedBody = body.replaceAll( |
| /="(\.\.\/)*assets/g, |
| `="${ENTERPRISE_GH_PAGES_URL_PREFIX}${requestedVersion}/assets`, |
| ) |
|
|
| |
| if (requestedVersion === '2.16' && req.path === '/en/enterprise/2.16') { |
| modifiedBody = modifiedBody.replaceAll('ref="/en/enterprise', 'ref="/en/enterprise/2.16') |
| } |
|
|
| |
| |
| modifiedBody = modifiedBody.replaceAll('<div id="search-results-container"></div>', '') |
|
|
| return res.send(modifiedBody) |
| } |
| |
| |
| |
| if ( |
| versionSatisfiesRange(requestedVersion, `>=${firstVersionDeprecatedOnNewSite}`) && |
| versionSatisfiesRange(requestedVersion, `<=${lastVersionWithoutArchivedRedirectsFile}`) |
| ) { |
| const statsTags = [`path:${req.path}`] |
| const fallbackRedirect = getFallbackRedirect(req) |
| if (fallbackRedirect) { |
| statsTags.push(`fallback:${fallbackRedirect}`) |
| statsd.increment('middleware.trying_fallback_redirect_success', 1, statsTags) |
| cacheAggressively(res) |
| return res.redirect(redirectCode, fallbackRedirect) |
| } |
| statsd.increment('middleware.trying_fallback_redirect_failure', 1, statsTags) |
| } |
|
|
| return next() |
| } |
|
|
| function getProxyPath(reqPath: string, requestedVersion: string) { |
| const [, withoutLanguagePath] = splitByLanguage(reqPath) |
| const isDeveloperPage = withoutLanguagePath?.startsWith( |
| `/enterprise/${requestedVersion}/developer`, |
| ) |
|
|
| |
| if (isDeveloperPage) { |
| const enterprisePath = `/enterprise/${requestedVersion}` |
| const newReqPath = reqPath.replace(enterprisePath, '') |
| return ENTERPRISE_GH_PAGES_URL_PREFIX + requestedVersion + newReqPath |
| } |
|
|
| |
| if (versionSatisfiesRange(requestedVersion, `>${lastVersionWithoutArchivedRedirectsFile}`)) { |
| const newReqPath = reqPath.includes('redirects.json') ? `/${reqPath}` : `${reqPath}/index.html` |
| return ENTERPRISE_GH_PAGES_URL_PREFIX + requestedVersion + newReqPath |
| } |
|
|
| |
| |
| if (versionSatisfiesRange(requestedVersion, `>=2.13`)) { |
| return `${ENTERPRISE_GH_PAGES_URL_PREFIX + requestedVersion + reqPath}/index.html` |
| } |
|
|
| |
| const enterprisePath = `/enterprise/${requestedVersion}` |
| const newReqPath = reqPath.replace(enterprisePath, '') |
| return ENTERPRISE_GH_PAGES_URL_PREFIX + requestedVersion + newReqPath |
| } |
|
|
| |
| |
| const fallbackRedirectLookups = new Map() |
|
|
| function getFallbackRedirect(req: ExtendedRequest) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if (!fallbackRedirectLookups.size) { |
| for (const [destination, sources] of Object.entries(archivedFrontmatterValidURLS())) { |
| for (const source of sources) { |
| fallbackRedirectLookups.set(source, destination) |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| const [language, withoutLanguage] = splitPathByLanguage(req.path) |
| const fallback = fallbackRedirectLookups.get(withoutLanguage) |
| if (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] |
| } |
|
|