From 11b722545fb22637bdafc147d76ecc40b4d4ee35 Mon Sep 17 00:00:00 2001 From: Roy Razon Date: Tue, 3 Oct 2023 10:45:22 +0300 Subject: [PATCH] tunnel server: fix headers - remove `transfer-encoding`, `content-length`, `connection` headers to use default node chunked encoding when possible - remove `accept-ranges` header because range requests are not compatible with injection - transform `etag` to indicate content changes due to injection. also transform `if-match` and `if-none-match` in outgoing requests --- tunnel-server/package.json | 1 + .../src/proxy/html-manipulation/index.ts | 33 +++++++++++++++++-- .../html-manipulation/inject-transform.ts | 11 ++++++- tunnel-server/src/proxy/index.ts | 3 +- tunnel-server/yarn.lock | 5 +++ 5 files changed, 48 insertions(+), 5 deletions(-) diff --git a/tunnel-server/package.json b/tunnel-server/package.json index 981e96a4..2b5d0ba7 100644 --- a/tunnel-server/package.json +++ b/tunnel-server/package.json @@ -6,6 +6,7 @@ "license": "Apache-2.0", "dependencies": { "@fastify/request-context": "^5.0.0", + "@sindresorhus/fnv1a": "^3.0.0", "content-type": "^1.0.5", "cookies": "^0.8.0", "fastify": "^4.22.2", diff --git a/tunnel-server/src/proxy/html-manipulation/index.ts b/tunnel-server/src/proxy/html-manipulation/index.ts index eb206ad4..89bfde59 100644 --- a/tunnel-server/src/proxy/html-manipulation/index.ts +++ b/tunnel-server/src/proxy/html-manipulation/index.ts @@ -1,4 +1,4 @@ -import { IncomingMessage, ServerResponse } from 'http' +import { IncomingMessage, ServerResponse, OutgoingHttpHeaders, IncomingHttpHeaders } from 'http' import zlib from 'zlib' import stream from 'stream' import { parse as parseContentType } from 'content-type' @@ -8,6 +8,22 @@ import { Logger } from 'pino' import { InjectHtmlScriptTransform } from './inject-transform' import { ScriptInjectionBase, ScriptInjection } from '../../tunnel-store' +const incomingEtagHeaders = ['if-none-match', 'if-match'] as const +export const removeEtagSuffix = (headers: IncomingHttpHeaders) => { + for (const header of incomingEtagHeaders) { + const value = headers[header] + if (value) { + headers[header] = value.replace(/\+preevy:[^"]+/, '') + } + } +} + +const addEtagSuffix = (headers: OutgoingHttpHeaders, added: string) => { + if (headers.etag && typeof headers.etag === 'string') { + headers.etag = headers.etag.replace(/"$/, `+preevy:${added}"`) + } +} + const compressionsForContentEncoding = ( contentEncoding: string | undefined, ): [stream.Transform, stream.Transform] | undefined => { @@ -45,11 +61,22 @@ const proxyWithInjection = ( injects: Omit[], charset = 'utf-8', ) => { - res.writeHead(proxyRes.statusCode as number, { ...proxyRes.headers, 'transfer-encoding': '' }) + const resHeaders = { ...proxyRes.headers } - const [input, output] = streamsForContentEncoding(proxyRes.headers['content-encoding'], proxyRes, res) + // enable default node chunked encoding when possible + delete resHeaders['transfer-encoding'] + delete resHeaders['content-length'] + delete resHeaders.connection + + // ranges not compatible with injection + delete resHeaders['accept-ranges'] const transform = new InjectHtmlScriptTransform(injects) + addEtagSuffix(resHeaders, transform.scriptTagsEtag()) + + res.writeHead(proxyRes.statusCode as number, resHeaders) + + const [input, output] = streamsForContentEncoding(proxyRes.headers['content-encoding'], proxyRes, res) input .pipe(iconv.decodeStream(charset)) diff --git a/tunnel-server/src/proxy/html-manipulation/inject-transform.ts b/tunnel-server/src/proxy/html-manipulation/inject-transform.ts index 59ff60db..ccb5c6b2 100644 --- a/tunnel-server/src/proxy/html-manipulation/inject-transform.ts +++ b/tunnel-server/src/proxy/html-manipulation/inject-transform.ts @@ -1,5 +1,6 @@ /* eslint-disable no-underscore-dangle */ import stream from 'stream' +import fnv1a from '@sindresorhus/fnv1a' import { Parser } from 'htmlparser2' import { ScriptInjection } from '../../tunnel-store' @@ -49,6 +50,7 @@ export class InjectHtmlScriptTransform extends stream.Transform { stringSoFar = '' currentChunkOffset = 0 injected = false + private cachedScriptTags: string | undefined constructor(readonly injects: Omit[]) { super({ decodeStrings: false, encoding: 'utf-8' }) @@ -61,8 +63,15 @@ export class InjectHtmlScriptTransform extends stream.Transform { } } + public scriptTagsEtag() { + return fnv1a(this.scriptTags(), { size: 32 }).toString(36) + } + private scriptTags() { - return this.injects.map(scriptTag).join('') + if (this.cachedScriptTags === undefined) { + this.cachedScriptTags = this.injects.map(scriptTag).join('') + } + return this.cachedScriptTags } private headWithScriptTags() { diff --git a/tunnel-server/src/proxy/index.ts b/tunnel-server/src/proxy/index.ts index 833580db..1fb39205 100644 --- a/tunnel-server/src/proxy/index.ts +++ b/tunnel-server/src/proxy/index.ts @@ -11,7 +11,7 @@ import { Claims, jwtAuthenticator, AuthenticationResult, AuthError, saasIdentity import { SessionStore } from '../session' import { BadGatewayError, BadRequestError, BasicAuthUnauthorizedError, RedirectError, UnauthorizedError, errorHandler, errorUpgradeHandler, tryHandler, tryUpgradeHandler } from '../http-server-helpers' import { TunnelFinder, proxyRouter } from './router' -import { proxyResHandler } from './html-manipulation' +import { proxyResHandler, removeEtagSuffix } from './html-manipulation' const loginRedirectUrl = (loginUrl: string) => ({ env, returnPath }: { env: string; returnPath?: string }) => { const url = new URL(loginUrl) @@ -142,6 +142,7 @@ export const proxy = ({ if (injects?.length) { injectsMap.set(mutatedReq, injects) + removeEtagSuffix(mutatedReq.headers) } return theProxy.web( diff --git a/tunnel-server/yarn.lock b/tunnel-server/yarn.lock index 1fa6a9d3..fa380cf2 100644 --- a/tunnel-server/yarn.lock +++ b/tunnel-server/yarn.lock @@ -672,6 +672,11 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== +"@sindresorhus/fnv1a@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/fnv1a/-/fnv1a-3.0.0.tgz#e8ce2e7c7738ec8c354867d38e3bfcde622b87ca" + integrity sha512-M6pmbdZqAryzjZ4ELAzrdCMoMZk5lH/fshKrapfSeXdf2W+GDqZvPmfXaNTZp43//FVbSwkTPwpEMnehSyskkQ== + "@sinonjs/commons@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72"