client-headers.ts 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
  1. /**
  2. * Request headers that tell UNA who the visitor is: the client address and the location a
  3. * CDN adds. UNA reads them from requests the web proxy sends with the API key, so only the
  4. * edge in front of NEO may set them. An edge sets its own headers and passes the rest
  5. * through: behind Vercel a client can still add `CF-IPCountry`, and with nothing in front
  6. * of Next (`next start` on a public port) a client's own `X-Forwarded-For` reaches UNA
  7. * unchanged, since Next adds the socket address only when there is none.
  8. *
  9. * `NEO_EDGE` names the edge:
  10. * - `vercel` (the default on Vercel): `X-Forwarded-For`, `X-Real-IP`, `X-Vercel-IP-*`
  11. * - `cloudflare`: `X-Forwarded-For`, `CF-Connecting-IP`, `CF-IP*`
  12. * - `cloudfront`: `X-Forwarded-For`, `CloudFront-Viewer-*`
  13. * - `proxy` (the default elsewhere): `X-Forwarded-For` and `X-Real-IP` from a reverse proxy
  14. * or load balancer that appends the address it got the request from (nginx with
  15. * `$proxy_add_x_forwarded_for`, Caddy, Railway); no location
  16. * - `none`: browsers reach Next directly, nothing is forwarded
  17. */
  18. const ADDRESS = ['x-forwarded-for', 'x-real-ip', 'cf-connecting-ip', 'true-client-ip'];
  19. const LOCATION = ['x-country-code', 'x-appengine-country'];
  20. const LOCATION_PREFIXES = ['cf-ip', 'x-vercel-ip-', 'cloudfront-viewer-'];
  21. type EdgeRules = { names: string[]; prefixes: string[] };
  22. const EDGES: Record<string, EdgeRules> = {
  23. vercel: { names: ['x-forwarded-for', 'x-real-ip'], prefixes: ['x-vercel-ip-'] },
  24. cloudflare: { names: ['x-forwarded-for', 'cf-connecting-ip'], prefixes: ['cf-ip'] },
  25. cloudfront: { names: ['x-forwarded-for'], prefixes: ['cloudfront-viewer-'] },
  26. proxy: { names: ['x-forwarded-for', 'x-real-ip'], prefixes: [] },
  27. none: { names: [], prefixes: [] },
  28. };
  29. /** The edge in front of NEO from the environment: `NEO_EDGE`, else `vercel` on Vercel, else `proxy`. */
  30. export function resolveEdge(env: Record<string, string | undefined> = {}): string {
  31. const edge = String(env.NEO_EDGE || '').trim().toLowerCase();
  32. if (Object.hasOwn(EDGES, edge)) return edge;
  33. return env.VERCEL ? 'vercel' : 'proxy';
  34. }
  35. function isClientHeader(name: string): boolean {
  36. return ADDRESS.includes(name) || LOCATION.includes(name) || LOCATION_PREFIXES.some((prefix) => name.startsWith(prefix));
  37. }
  38. function isEdgeHeader(name: string, edge: string): boolean {
  39. const rules = EDGES[edge] ?? { names: [], prefixes: [] };
  40. return rules.names.includes(name) || rules.prefixes.some((prefix) => name.startsWith(prefix));
  41. }
  42. /** Delete, in place, the address and location headers the edge didn't set. Returns the headers. */
  43. export function dropForeignClientHeaders(headers: Headers, edge: string): Headers {
  44. const names: string[] = [];
  45. headers.forEach((_value, name) => {
  46. const lower = name.toLowerCase();
  47. if (isClientHeader(lower) && !isEdgeHeader(lower, edge)) names.push(name);
  48. });
  49. for (const name of names) headers.delete(name);
  50. return headers;
  51. }