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 3 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
4 changes: 3 additions & 1 deletion packages/standalone-proxy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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'

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

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

const app = createApp({ envStore, sshPublicKey })

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

const tunnelName = (clientId: string, remotePath: string) => {
Expand Down
47 changes: 23 additions & 24 deletions packages/standalone-proxy/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,38 @@
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'

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 }: {
sshPublicKey: string
isProxyRequest: (req: http.IncomingMessage) => boolean
proxyHandlers: { wsHandler: (req: http.IncomingMessage, socket: internal.Duplex, head: Buffer) => void, handler: (req: http.IncomingMessage, res: http.ServerResponse) => void }
}) =>
Fastify({
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 {
socket.end()
Yshayy marked this conversation as resolved.
Show resolved Hide resolved
}
})
return server;
},
logger: appLoggerFromEnv(),
rewriteUrl,
})

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



Expand Down
101 changes: 69 additions & 32 deletions packages/standalone-proxy/src/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,54 @@
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'

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
Yshayy marked this conversation as resolved.
Show resolved Hide resolved
}

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: PreviewEnvStore, log=console){
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) {
log.warn('no env for %j', { targetHost, url })
log.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,
log.info('proxying to %j', { target: env.target, url: req.url })
Yshayy marked this conversation as resolved.
Show resolved Hide resolved
return proxy.web(
req,
res,
{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Expand All @@ -41,11 +57,32 @@ export const proxyRoutes: FastifyPluginAsync<{ envStore: PreviewEnvStore }> = as
},
},
(err) => {
req.log.warn('error in proxy %j', err, { targetHost, url })
log.warn('error in proxy %j', err, { targetHost: env.target, url: req.url })
Yshayy marked this conversation as resolved.
Show resolved Hide resolved
}
)
}, (err)=> log.error('error in proxy %j', 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) => {
log.warn('error in proxy %j', err, { targetHost: env.target, url: req.url })
Yshayy marked this conversation as resolved.
Show resolved Hide resolved
}
)

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