1
0
mirror of synced 2026-01-06 06:02:35 -05:00

Merge pull request #32525 from github/repo-sync

Repo sync
This commit is contained in:
docs-bot
2024-04-15 16:57:59 -04:00
committed by GitHub
8 changed files with 222 additions and 2 deletions

View File

@@ -72,6 +72,7 @@ jobs:
- rest
- search
- shielding
- tracking
# - tests
# - tools
- versions

View File

@@ -22,12 +22,12 @@ function cacheControlFactory(
!maxAge && 'no-store',
maxAge >= 60 * 60 && `stale-while-revalidate=${60 * 60}`,
maxAge >= 60 * 60 && `stale-if-error=${24 * 60 * 60}`,
maxAgeZero && 'max-age=0',
(maxAgeZero || maxAge === 0) && 'max-age=0',
]
.filter(Boolean)
.join(', ')
return (res) => {
if (process.env.NODE_ENV !== 'production' && res.hasHeader('set-cookie')) {
if (process.env.NODE_ENV !== 'production' && res.hasHeader('set-cookie') && maxAge) {
console.warn(
"You can't set a >0 cache-control header AND set-cookie or else the CDN will never respect the cache-control.",
)

View File

@@ -64,6 +64,7 @@ import mockVaPortal from './mock-va-portal.js'
import dynamicAssets from '#src/assets/middleware/dynamic-assets.js'
import contextualizeSearch from '#src/search/middleware/contextualize.js'
import shielding from '#src/shielding/middleware/index.js'
import tracking from '#src/tracking/middleware/index.js'
const { DEPLOYMENT_ENV, NODE_ENV } = process.env
const isTest = NODE_ENV === 'test' || process.env.GITHUB_ACTIONS === 'true'
@@ -209,6 +210,9 @@ export default function (app) {
app.use(mockVaPortal) // FOR TESTING.
}
// ** Possible early exits after cookies **
app.use(tracking)
// *** Headers ***
app.set('etag', false) // We will manage our own ETags if desired

View File

@@ -29,6 +29,8 @@ const RECOGNIZED_KEYS_BY_ANY = new Set([
'query',
// The drop-downs on "Webhook events and payloads"
'actionType',
// Used by the tracking middleware
'ghdomain',
])
export default function handleInvalidQuerystrings(req, res, next) {

20
src/tracking/README.md Normal file
View File

@@ -0,0 +1,20 @@
# Tracking
## Overview
This is about recording inbound links that helps with "tracking".
For example, if you arrive on Docs with `?ghdomain=example.ghe.com` we
can pick that up and put it in a cookie so that the user's content, when
they view it, can say `curl https://example.ghe.com/api/v1` instead
of the stock `curl https://HOSTNAME/api/v1`.
## How it works
For a certain number of query strings, we "snatch them up" and redirect
to the same URL as you were on but with the query string key removed.
And in the 302 Found response, we might include a `set-cookie`.
## Notes
none

View File

@@ -0,0 +1,68 @@
import statsd from '#src/observability/lib/statsd.js'
import { noCacheControl } from '#src/frame/middleware/cache-control.js'
const STATSD_KEY = 'middleware.handle_tracking_querystrings'
// Exported for the sake of end-to-end tests
export const DOMAIN_QUERY_PARAM = 'ghdomain'
export const MAX_DOMAINS_SAVED = 3
const DOMAIN_COOKIE_AGE_MS = 365 * 24 * 3600 * 1000
export const DOMAIN_COOKIE_NAME = 'github_domains'
export default function handleTrackingQueryStrings(req, res, next) {
if (req.path.startsWith('/_next/')) {
return next()
}
if (req.query[DOMAIN_QUERY_PARAM] || req.query[DOMAIN_QUERY_PARAM] === '') {
if (Array.isArray(req.query[DOMAIN_QUERY_PARAM])) {
res.status(400).send('can only be one')
const tags = [`key:${DOMAIN_QUERY_PARAM}`, 'domain:_multiple_']
statsd.increment(STATSD_KEY, 1, tags)
return
}
const searchParams = new URLSearchParams(req.query)
const oldCookieValue = req.cookies[DOMAIN_COOKIE_NAME] || ''
const oldCookieValueParsed = oldCookieValue
.split(',')
.map((x) => x.trim().toLowerCase())
.filter(Boolean)
const domain = (searchParams.get(DOMAIN_QUERY_PARAM) || '').toLowerCase().trim()
if (!domain && !oldCookieValueParsed.length) return next()
if (domain) {
const newCookieValue = [domain, ...oldCookieValueParsed.filter((x) => x !== domain)]
.slice(0, MAX_DOMAINS_SAVED)
.join(',')
res.cookie(DOMAIN_COOKIE_NAME, newCookieValue, {
maxAge: DOMAIN_COOKIE_AGE_MS,
httpOnly: false,
})
} else {
res.clearCookie(DOMAIN_COOKIE_NAME)
}
searchParams.delete(DOMAIN_QUERY_PARAM)
noCacheControl(res)
let newURL = req.path
if (searchParams.toString()) {
newURL += `?${searchParams.toString()}`
}
res.redirect(302, newURL)
const tags = [`key:${DOMAIN_QUERY_PARAM}`, `domain:${domain || '_empty_'}`]
statsd.increment(STATSD_KEY, 1, tags)
return
}
return next()
}

View File

@@ -0,0 +1,9 @@
import express from 'express'
import handleTrackingQueryStrings from './handle-query-strings.js'
const router = express.Router()
router.use(handleTrackingQueryStrings)
export default router

View File

@@ -0,0 +1,116 @@
import { get } from '#src/tests/helpers/e2etest.js'
import { expect } from '@jest/globals'
import {
DOMAIN_QUERY_PARAM,
DOMAIN_COOKIE_NAME,
MAX_DOMAINS_SAVED,
} from '../middleware/handle-query-strings.js'
describe('setting a cookie', () => {
test('on home page', async () => {
const res = await get(`/en?${DOMAIN_QUERY_PARAM}=acme.example.com`)
expect(res.statusCode).toBe(302)
const setCookie = res.headers['set-cookie'][0]
expect(setCookie).toMatch(/github_domains=acme.example.com/)
expect(res.headers.location).toBe('/en')
expect(res.headers['cache-control']).toMatch(/private/)
expect(res.headers['cache-control']).toMatch(/max-age=0/)
})
test('with other query string things', async () => {
const res = await get(`/en?${DOMAIN_QUERY_PARAM}=acme.example.com&foo=bar`)
expect(res.statusCode).toBe(302)
const setCookie = res.headers['set-cookie'][0]
expect(setCookie).toMatch(/github_domains=acme.example.com/)
expect(res.headers.location).toBe('/en?foo=bar')
})
test('always lowercase', async () => {
const res = await get(`/en?${DOMAIN_QUERY_PARAM}=Acme.example.COM`)
expect(res.statusCode).toBe(302)
const setCookie = res.headers['set-cookie'][0]
expect(setCookie).toMatch(/github_domains=acme.example.com/)
})
test('on root page', async () => {
const res = await get(`/?${DOMAIN_QUERY_PARAM}=acme.example.com`)
expect(res.statusCode).toBe(302)
const setCookie = res.headers['set-cookie'][0]
expect(setCookie).toMatch(/github_domains=acme.example.com/)
expect(res.headers.location).toBe('/')
})
test('empty value does nothing if nothing previous', async () => {
const res = await get(`/?${DOMAIN_QUERY_PARAM}=`)
expect(res.statusCode).toBe(302)
expect(res.headers['set-cookie']).toBeUndefined()
})
test('empty value, when trimmed, does nothing if nothing previous', async () => {
const res = await get(`/?${DOMAIN_QUERY_PARAM}=%20`)
expect(res.statusCode).toBe(302)
expect(res.headers['set-cookie']).toBeUndefined()
})
test('empty value resets previous cookie', async () => {
const res = await get(`/?${DOMAIN_QUERY_PARAM}=`, {
headers: {
cookie: `${DOMAIN_COOKIE_NAME}=acme.example.com`,
},
})
expect(res.statusCode).toBe(302)
const setCookie = res.headers['set-cookie'][0]
expect(setCookie).toMatch(/github_domains=;/)
})
test('append with previous', async () => {
const res = await get(`/?${DOMAIN_QUERY_PARAM}=next.example.com`, {
headers: {
cookie: `${DOMAIN_COOKIE_NAME}=previous.example.com`,
},
})
expect(res.statusCode).toBe(302)
const setCookie = res.headers['set-cookie'][0]
// %2C is a comma
expect(setCookie).toMatch(/github_domains=next.example.com%2Cprevious.example.com;/)
})
test('append with too many', async () => {
let cookie = ''
for (const letter of Array.from('abcdef')) {
const next = `${letter}.example.com`
const res = await get(`/?${DOMAIN_QUERY_PARAM}=${next}`, {
headers: { cookie },
})
const setCookie = res.headers['set-cookie'][0]
cookie = setCookie.split(';').filter((x) => x.startsWith(DOMAIN_COOKIE_NAME))[0]
if (letter === 'a') {
// first
expect(cookie).toBe(`${DOMAIN_COOKIE_NAME}=a.example.com`)
} else if (letter === 'f') {
// last
expect(cookie.split('%2C').length).toBe(MAX_DOMAINS_SAVED)
expect(cookie.startsWith(`${DOMAIN_COOKIE_NAME}=f.example.com`)).toBe(true)
}
}
})
test('append with same as before', async () => {
const res = await get(`/?${DOMAIN_QUERY_PARAM}=Acme.example.com`, {
headers: {
cookie: `${DOMAIN_COOKIE_NAME}=acme.example.com`,
},
})
expect(res.statusCode).toBe(302)
const setCookie = res.headers['set-cookie'][0]
expect(setCookie).toMatch(/github_domains=acme.example.com;/)
})
test('trying to set multiple', async () => {
const res = await get(
`/?${DOMAIN_QUERY_PARAM}=a.example.com&${DOMAIN_QUERY_PARAM}=b.example.com`,
)
expect(res.statusCode).toBe(400)
expect(res.body).toMatch(/can only be one/)
})
})