Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix proxy ws and other behavior #14

Merged
merged 4 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/standalone-proxy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { sshServer as createSshServer } from './src/ssh-server'
import { getSSHKeys } from './src/ssh-keys'
import url from 'url'
import path from 'path'
import { isProxyRequest, proxyHandlers } from './src/proxy'
import { appLoggerFromEnv } from './src/logging'
import pino from 'pino'

const __dirname = url.fileURLToPath(new URL('.', import.meta.url))

Expand All @@ -32,8 +35,9 @@ const envStore = inMemoryPreviewEnvStore({
},
})

const app = createApp({ envStore, sshPublicKey })
const sshLogger = app.log.child({ name: 'ssh_server' })
const logger = pino(appLoggerFromEnv())
const app = createApp({ sshPublicKey, isProxyRequest: isProxyRequest(BASE_URL), proxyHandlers: proxyHandlers({envStore, logger}), logger })
const sshLogger = logger.child({ name: 'ssh_server' })

const tunnelName = (clientId: string, remotePath: string) => {
const serviceName = remotePath.replace(/^\//, '')
Expand Down
53 changes: 27 additions & 26 deletions packages/standalone-proxy/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,40 @@
import Fastify, { RawRequestDefaultExpression } from 'fastify'
import Fastify from 'fastify'
import { fastifyRequestContext } from '@fastify/request-context'
import { InternalServerError } from './errors'
import { appLoggerFromEnv } from './logging'
import { proxyRoutes } from './proxy'
import { PreviewEnvStore } from './preview-env'
import http from 'http'
import internal from 'stream'
import {Logger} from 'pino'

const rewriteUrl = ({ url, headers: { host } }: RawRequestDefaultExpression): string => {
if (!url) {
throw new InternalServerError('no url in request')
}
if (!host) {
throw new InternalServerError('no host header in request')
}

const target = host.split('.', 1)[0]
if (!target.includes('-')) {
return url
}

return `/proxy/${target}${url}`
}

export const app = ({ envStore, sshPublicKey }: {
envStore: PreviewEnvStore
export const app = ({ sshPublicKey,isProxyRequest, proxyHandlers, logger }: {
sshPublicKey: string
isProxyRequest: (req: http.IncomingMessage) => boolean
logger: Logger
proxyHandlers: { wsHandler: (req: http.IncomingMessage, socket: internal.Duplex, head: Buffer) => void, handler: (req: http.IncomingMessage, res: http.ServerResponse) => void }
}) =>
Fastify({
logger: appLoggerFromEnv(),
rewriteUrl,
serverFactory: (handler) => {
const {wsHandler:proxyWsHandler, handler: proxyHandler } = proxyHandlers
const server = http.createServer((req, res) => {
if (isProxyRequest(req)){
return proxyHandler(req, res)
}
return handler(req, res)
})
server.on('upgrade', (req, socket, head) => {
Yshayy marked this conversation as resolved.
Show resolved Hide resolved
if (isProxyRequest(req)){
proxyWsHandler(req, socket, head)
} else {
logger.warn('unexpected upgrade request %j', {url: req.url, host: req.headers['host']})
socket.end()
Yshayy marked this conversation as resolved.
Show resolved Hide resolved
}
})
return server;
},
logger,
})

.register(fastifyRequestContext)
.get('/healthz', { logLevel: 'warn' }, async () => 'OK')
.get('/ssh-public-key', async () => sshPublicKey)
.register(proxyRoutes, { prefix: '/proxy/', envStore })



Expand Down
7 changes: 3 additions & 4 deletions packages/standalone-proxy/src/logging.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FastifyBaseLogger, FastifyLoggerOptions, PinoLoggerOptions } from 'fastify/types/logger'
import { PinoLoggerOptions } from 'fastify/types/logger'

const envToLogger: Record<string, (FastifyLoggerOptions & PinoLoggerOptions) | FastifyBaseLogger | false> = {
const envToLogger: Record<string, PinoLoggerOptions> = {
development: {
level: process.env.DEBUG ? 'debug' : 'info',
transport: {
Expand All @@ -13,8 +13,7 @@ const envToLogger: Record<string, (FastifyLoggerOptions & PinoLoggerOptions) | F
},
production: {
level: process.env.DEBUG ? 'debug' : 'info',
},
test: false,
}
}

export const appLoggerFromEnv = () => envToLogger[process.env.NODE_ENV || 'development']
108 changes: 76 additions & 32 deletions packages/standalone-proxy/src/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,61 @@
import { FastifyPluginAsync, HTTPMethods } from 'fastify'
import { PreviewEnvStore } from './preview-env'
import { NotFoundError } from './errors'
import httpProxy from 'http-proxy'
import { IncomingMessage, ServerResponse } from 'http'
import internal from 'stream'
import type { Logger } from 'pino'

const ALL_METHODS = Object.freeze(['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']) as HTTPMethods[]

export const proxyRoutes: FastifyPluginAsync<{ envStore: PreviewEnvStore }> = async (app, { envStore }) => {
const proxy = httpProxy.createProxy({})

app.addHook('onClose', () => proxy.close())

// prevent FST_ERR_CTP_INVALID_MEDIA_TYPE error
app.removeAllContentTypeParsers()
app.addContentTypeParser('*', function (_request, _payload, done) { done(null) })
export const isProxyRequest = (baseUrl: {hostname:string, port:string}) => (req: IncomingMessage)=> {
const host = req.headers["host"]
if (!host) return false
const {hostname: reqHostname, port: reqPort} = new URL(`http://${host}`)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optimization nit creating a URL for each request might be more expensive than doing regex

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Started that way (actually started with splits and joins), but it shouldn't matter that much and and it's nicer to read

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nodejs/node#30334
actually slower then I thought, might replace it later

if (reqPort !== baseUrl.port) return false
return reqHostname.endsWith(`.${baseUrl.hostname}`) && reqHostname !== baseUrl.hostname
}

app.route<{
Params: { targetHost: string; ['*']: string }
}>({
url: ':targetHost/*',
method: ALL_METHODS,
handler: async (req, res) => {
const { targetHost, ['*']: url } = req.params
req.log.debug('proxy request: %j', { targetHost, url, params: req.params })
const env = await envStore.get(targetHost)
function asyncHandler<TArgs extends unknown[]>(fn: (...args: TArgs) => Promise<void>, onError: (error: unknown, ...args: TArgs)=> void ) {
return async (...args: TArgs) => {
try {
await fn(...args)
} catch (err) {
onError(err, ...args)
}
}
}

export function proxyHandlers({
envStore,
logger
}: {
envStore: PreviewEnvStore
logger: Logger
} ){
const proxy = httpProxy.createProxy({})
const resolveTargetEnv = async (req: IncomingMessage)=>{
const {url} = req
const host = req.headers['host']
const targetHost = host?.split('.', 1)[0]
const env = await envStore.get(targetHost as string)
if (!env) {
logger.warn('no env for %j', { targetHost, url })
logger.warn('no host header in request')
return;
}
return env
}
return {
handler: asyncHandler(async (req: IncomingMessage, res: ServerResponse<IncomingMessage>) => {
const env = await resolveTargetEnv(req)
if (!env) {
throw new NotFoundError(`host ${targetHost}`)
res.statusCode = 502;
res.end();
Yshayy marked this conversation as resolved.
Show resolved Hide resolved
return;
}

req.raw.url = `/${url}`

proxy.web(
req.raw,
res.raw,
logger.info('proxying to %j', { target: env.target, url: req.url })
return proxy.web(
req,
res,
{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Expand All @@ -41,11 +64,32 @@ export const proxyRoutes: FastifyPluginAsync<{ envStore: PreviewEnvStore }> = as
},
},
(err) => {
req.log.warn('error in proxy %j', err, { targetHost, url })
logger.warn('error in proxy %j', { error:err, targetHost: env.target, url: req.url })
}
)
}, (err)=> logger.error('error forwarding traffic %j', {error:err}) ),
wsHandler: asyncHandler(async (req: IncomingMessage, socket: internal.Duplex, head: Buffer) => {
const env = await resolveTargetEnv(req)
if (!env) {
socket.end();
Yshayy marked this conversation as resolved.
Show resolved Hide resolved
return;
}
return proxy.ws(
req,
socket,
head,
{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
target: {
socketPath: env.target,
},
},
(err) => {
logger.warn('error in ws proxy %j', { error:err, targetHost: env.target, url: req.url })
}
)

return res
},
})
}
}, (err)=> logger.error('error forwarding ws traffic %j', {error: err}))
}
}