Skip to content

Commit

Permalink
fixes
Browse files Browse the repository at this point in the history
- injection does not rerender the page
- allow defer, async script tag attributes
- allow JSON/YAML configuration in label
  • Loading branch information
Roy Razon committed Sep 6, 2023
1 parent 2348c15 commit c2fc5e3
Show file tree
Hide file tree
Showing 17 changed files with 467 additions and 343 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ jobs:
- run: yarn

- run: yarn lint

- run: yarn lint
working-directory: tunnel-server
1 change: 1 addition & 0 deletions packages/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {
} from './src/files'
export { hasPropertyDefined, RequiredProperties } from './src/ts-utils'
export { tryParseJson, dateReplacer } from './src/json'
export { tryParseYaml } from './src/yaml'
export { Logger } from './src/log'
export { requiredEnv, numberFromEnv } from './src/env'
export { tunnelNameResolver, TunnelNameResolver } from './src/tunnel-name'
Expand Down
3 changes: 2 additions & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"lint-staged": "^13.1.2",
"ts-jest": "^29.1.0",
"tsx": "^3.12.3",
"typescript": "^5.0.4"
"typescript": "^5.0.4",
"yaml": "^2.3.1"
},
"scripts": {
"test": "yarn jest",
Expand Down
7 changes: 5 additions & 2 deletions packages/common/src/compose-tunnel-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ export const COMPOSE_TUNNEL_AGENT_SERVICE_LABELS = {
ENV_ID: 'preevy.env_id',
ACCESS: 'preevy.access',
EXPOSE: 'preevy.expose',
INJECT_SCRIPT_URL: 'preevy.inject_script_url',
INJECT_SCRIPT_SRC: 'preevy.inject_script_src',
INJECT_SCRIPTS: 'preevy.inject_scripts',
INJECT_SCRIPT_PATH_REGEX: 'preevy.inject_script_path_regex',
}

Expand All @@ -13,5 +14,7 @@ export const COMPOSE_TUNNEL_AGENT_PORT = 3000

export type ScriptInjection = {
pathRegex?: RegExp
url: string
src: string
defer?: boolean
async?: boolean
}
15 changes: 15 additions & 0 deletions packages/common/src/yaml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import yaml, { DocumentOptions, ParseOptions, SchemaOptions, ToJSOptions } from 'yaml'

type Reviver = (this: any, key: string, value: any) => any
type Options = ParseOptions & DocumentOptions & SchemaOptions & ToJSOptions

export function tryParseYaml(src: string, options?: Options): any
export function tryParseYaml(src: string, reviver: Reviver, options?: Options): any
export function tryParseYaml(src: string, ...rest: unknown[]) {
try {
return yaml.parse(src, ...rest as any[])
} catch (e) {
return undefined
}
}
27 changes: 17 additions & 10 deletions packages/compose-tunnel-agent/src/docker/events-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Docker from 'dockerode'
import { tryParseJson, Logger, COMPOSE_TUNNEL_AGENT_SERVICE_LABELS as LABELS, ScriptInjection } from '@preevy/common'
import { tryParseJson, Logger, COMPOSE_TUNNEL_AGENT_SERVICE_LABELS as LABELS, ScriptInjection, tryParseYaml } from '@preevy/common'
import { throttle } from 'lodash'
import { filters, portFilter } from './filters'
import { COMPOSE_PROJECT_LABEL, COMPOSE_SERVICE_LABEL } from './labels'
Expand All @@ -10,9 +10,23 @@ export type RunningService = {
networks: string[]
ports: number[]
access: 'private' | 'public'
inject?: ScriptInjection[]
inject: ScriptInjection[]
}

const reviveScriptInjection = ({ pathRegex, ...v }: ScriptInjection) => ({
...pathRegex && { pathRegex: new RegExp(pathRegex) },
...v,
})

const scriptInjectionFromLabels = ({
[LABELS.INJECT_SCRIPTS]: scriptsText,
[LABELS.INJECT_SCRIPT_SRC]: src,
[LABELS.INJECT_SCRIPT_PATH_REGEX]: pathRegex,
} : Record<string, string>): ScriptInjection[] => [
...(scriptsText ? (tryParseJson(scriptsText) || tryParseYaml(scriptsText) || []) : []),
...src ? [{ src, ...pathRegex && { pathRegex } }] : [],
].map(reviveScriptInjection)

export const eventsClient = ({
log,
docker,
Expand All @@ -35,14 +49,7 @@ export const eventsClient = ({
networks: Object.keys(c.NetworkSettings.Networks),
// ports may have both IPv6 and IPv4 addresses, ignoring
ports: [...new Set(c.Ports.filter(p => p.Type === 'tcp').filter(portFilter(c)).map(p => p.PrivatePort))],
inject: c.Labels[LABELS.INJECT_SCRIPT_URL]
? [{
url: c.Labels[LABELS.INJECT_SCRIPT_URL],
...c.Labels[LABELS.INJECT_SCRIPT_PATH_REGEX] && {
pathRegex: new RegExp(c.Labels[LABELS.INJECT_SCRIPT_PATH_REGEX]),
},
}]
: undefined,
inject: scriptInjectionFromLabels(c.Labels),
})

const getRunningServices = async (): Promise<RunningService[]> => (await listContainers()).map(containerToService)
Expand Down
2 changes: 1 addition & 1 deletion tunnel-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
"@fastify/request-context": "^5.0.0",
"cookies": "^0.8.0",
"content-type": "^1.0.5",
"fastify": "^4.22.2",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"htmlparser2": "^9.0.0",
"http-proxy": "^1.18.1",
"iconv-lite": "^0.6.3",
"jose": "^4.14.4",
"lodash": "^4.17.21",
"pino": "^8.11.0",
Expand Down
3 changes: 1 addition & 2 deletions tunnel-server/src/http-server-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Logger } from 'pino'
import http from 'node:http'
import stream from 'node:stream'
import { inspect } from 'node:util'
import internal from 'node:stream'

export const respond = (res: http.ServerResponse, content: string, type = 'text/plain', status = 200) => {
res.writeHead(status, { 'Content-Type': type })
Expand Down Expand Up @@ -86,7 +85,7 @@ export class RedirectError extends HttpError {
}
}

export type HttpUpgradeHandler = (req: http.IncomingMessage, socket: internal.Duplex, head: Buffer) => Promise<void>
export type HttpUpgradeHandler = (req: http.IncomingMessage, socket: stream.Duplex, head: Buffer) => Promise<void>
export type HttpHandler = (req: http.IncomingMessage, res: http.ServerResponse) => Promise<void>

export const errorHandler = (
Expand Down
92 changes: 0 additions & 92 deletions tunnel-server/src/proxy/html-manipulation.ts

This file was deleted.

64 changes: 64 additions & 0 deletions tunnel-server/src/proxy/html-manipulation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { IncomingMessage, ServerResponse } from 'http'
import zlib from 'zlib'
import stream from 'stream'
import { parse as parseContentType } from 'content-type'
import iconv from 'iconv-lite'
import { INJECT_SCRIPTS_HEADER } from '../common'
import { InjectHtmlScriptTransform } from './inject-transform'
import { ScriptInjection } from '../../tunnel-store'

const compressionsForContentEncoding = (contentEncoding: string) => {
if (contentEncoding === 'gzip') {
return [zlib.createGunzip(), zlib.createGzip()] as const
}
if (contentEncoding === 'deflate') {
return [zlib.createInflate(), zlib.createDeflate()] as const
}
if (contentEncoding === 'br') {
return [zlib.createBrotliDecompress(), zlib.createBrotliCompress()] as const
}
if (contentEncoding === 'identity') {
return undefined
}
throw new Error(`unsupported content encoding: "${contentEncoding}"`)
}

export const injectScripts = async (
proxyRes: stream.Readable & Pick<IncomingMessage, 'headers' | 'statusCode'>,
req: Pick<IncomingMessage, 'headers'>,
res: stream.Writable & Pick<ServerResponse<IncomingMessage>, 'writeHead'>,
) => {
const headerValue = req.headers[INJECT_SCRIPTS_HEADER] as string | undefined
if (!headerValue) {
return undefined
}

const injects = JSON.parse(headerValue) as Omit<ScriptInjection, 'pathRegex'>[]

const {
type: contentType,
parameters: { charset: reqCharset },
} = parseContentType(proxyRes)

if (!contentType.includes('text/html')) {
return undefined
}

res.writeHead(proxyRes.statusCode as number, proxyRes.headers)

const compress = compressionsForContentEncoding(proxyRes.headers['content-encoding'] || 'identity')

const [input, output] = compress
? [proxyRes.pipe(compress[0]), res.pipe(compress[1])]
: [proxyRes, res]

const transform = new InjectHtmlScriptTransform(injects)

input
.pipe(iconv.decodeStream(reqCharset || 'utf-8'))
.pipe(transform)
.pipe(iconv.encodeStream(reqCharset || 'utf-8'))
.pipe(output)

return undefined
}
Loading

0 comments on commit c2fc5e3

Please sign in to comment.