Skip to content

Commit

Permalink
feat: allow overriding or modifying the queryString for websockets (#349
Browse files Browse the repository at this point in the history
)

* feat: allow overriding or modifying the queryString for websockets

Allow overriding or modifying the queryString when proxying websockets.

Fixes: #348

* test: add type test for queryString

Add type test for queryString
  • Loading branch information
ttshivers authored Apr 22, 2024
1 parent 8a48934 commit 04e2c14
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 2 deletions.
19 changes: 19 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ class WebSocketProxy {

handleConnection (source, request, dest) {
const url = this.findUpstream(request, dest)
const queryString = getQueryString(url.search, request.url, this.wsClientOptions, request)
url.search = queryString

const rewriteRequestHeaders = this.wsClientOptions.rewriteRequestHeaders
const headersToRewrite = this.wsClientOptions.headers

Expand All @@ -192,6 +195,22 @@ class WebSocketProxy {
}
}

function getQueryString (search, reqUrl, opts, request) {
if (typeof opts.queryString === 'function') {
return '?' + opts.queryString(search, reqUrl, request)
}

if (opts.queryString) {
return '?' + qs.stringify(opts.queryString)
}

if (search.length > 0) {
return search
}

return ''
}

function defaultWsHeadersRewrite (headers, request) {
if (request.headers.cookie) {
return { ...headers, cookie: request.headers.cookie }
Expand Down
126 changes: 126 additions & 0 deletions test/websocket-querystring.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
'use strict'

const { test } = require('tap')
const Fastify = require('fastify')
const proxy = require('../')
const WebSocket = require('ws')
const { createServer } = require('node:http')
const { promisify } = require('node:util')
const { once } = require('node:events')
const qs = require('fast-querystring')

const subprotocolValue = 'foo-subprotocol'

test('websocket proxy with object queryString', async (t) => {
t.plan(7)

const origin = createServer()
const wss = new WebSocket.Server({ server: origin })
t.teardown(wss.close.bind(wss))
t.teardown(origin.close.bind(origin))

const serverMessages = []
wss.on('connection', (ws, request) => {
t.equal(ws.protocol, subprotocolValue)
t.equal(request.url, '/?q=test')
ws.on('message', (message, binary) => {
serverMessages.push([message.toString(), binary])
// echo
ws.send(message, { binary })
})
})

await promisify(origin.listen.bind(origin))({ port: 0, host: '127.0.0.1' })

const server = Fastify()
server.register(proxy, {
upstream: `ws://127.0.0.1:${origin.address().port}`,
websocket: true,
wsClientOptions: {
queryString: { q: 'test' }
}
})

await server.listen({ port: 0, host: '127.0.0.1' })
t.teardown(server.close.bind(server))

const ws = new WebSocket(`ws://127.0.0.1:${server.server.address().port}`, [subprotocolValue])
await once(ws, 'open')

ws.send('hello', { binary: false })
const [reply0, binary0] = await once(ws, 'message')
t.equal(reply0.toString(), 'hello')
t.equal(binary0, false)

ws.send(Buffer.from('fastify'), { binary: true })
const [reply1, binary1] = await once(ws, 'message')
t.equal(reply1.toString(), 'fastify')
t.equal(binary1, true)

t.strictSame(serverMessages, [
['hello', false],
['fastify', true]
])

await Promise.all([
once(ws, 'close'),
server.close()
])
})

test('websocket proxy with function queryString', async (t) => {
t.plan(7)

const origin = createServer()
const wss = new WebSocket.Server({ server: origin })
t.teardown(wss.close.bind(wss))
t.teardown(origin.close.bind(origin))

const serverMessages = []
wss.on('connection', (ws, request) => {
t.equal(ws.protocol, subprotocolValue)
t.equal(request.url, '/?q=test')
ws.on('message', (message, binary) => {
serverMessages.push([message.toString(), binary])
// echo
ws.send(message, { binary })
})
})

await promisify(origin.listen.bind(origin))({ port: 0, host: '127.0.0.1' })

const server = Fastify()
server.register(proxy, {
upstream: `ws://127.0.0.1:${origin.address().port}`,
websocket: true,
wsClientOptions: {
queryString: () => qs.stringify({ q: 'test' })
}
})

await server.listen({ port: 0, host: '127.0.0.1' })
t.teardown(server.close.bind(server))

const ws = new WebSocket(`ws://127.0.0.1:${server.server.address().port}`, [subprotocolValue])
await once(ws, 'open')

ws.send('hello', { binary: false })
const [reply0, binary0] = await once(ws, 'message')
t.equal(reply0.toString(), 'hello')
t.equal(binary0, false)

ws.send(Buffer.from('fastify'), { binary: true })
const [reply1, binary1] = await once(ws, 'message')
t.equal(reply1.toString(), 'fastify')
t.equal(binary1, true)

t.strictSame(serverMessages, [
['hello', false],
['fastify', true]
])

await Promise.all([
once(ws, 'close'),
server.close()
])
})
17 changes: 15 additions & 2 deletions types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
/// <reference types='node' />

import { FastifyPluginCallback, preHandlerHookHandler, preValidationHookHandler } from 'fastify';
import {
FastifyPluginCallback,
FastifyRequest,
preHandlerHookHandler,
preValidationHookHandler,
RawServerBase,
RequestGenericInterface,
} from 'fastify';

import {
FastifyReplyFromOptions,
Expand All @@ -24,6 +31,12 @@ type FastifyHttpProxy = FastifyPluginCallback<
>;

declare namespace fastifyHttpProxy {
type QueryStringFunction = (
search: string | undefined,
reqUrl: string,
request: FastifyRequest<RequestGenericInterface, RawServerBase>
) => string;

export interface FastifyHttpProxyOptions extends FastifyReplyFromOptions {
upstream: string;
prefix?: string;
Expand All @@ -34,7 +47,7 @@ declare namespace fastifyHttpProxy {
preValidation?: preValidationHookHandler;
config?: Object;
replyOptions?: FastifyReplyFromHooks;
wsClientOptions?: ClientOptions;
wsClientOptions?: ClientOptions & { queryString?: { [key: string]: unknown } | QueryStringFunction; };
wsServerOptions?: ServerOptions;
httpMethods?: string[];
constraints?: { [name: string]: any };
Expand Down
11 changes: 11 additions & 0 deletions types/index.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import fastify, {
RawReplyDefaultExpression,
RawRequestDefaultExpression,
type FastifyRequest,
type RawServerBase,
type RequestGenericInterface,
} from 'fastify';
import { expectError, expectType } from 'tsd';
import fastifyHttpProxy from '..';
Expand Down Expand Up @@ -52,6 +55,14 @@ app.register(fastifyHttpProxy, {
constraints: { version: '1.0.2' },
websocket: true,
wsUpstream: 'ws://origin.asd/connection',
wsClientOptions: {
queryString(search, reqUrl, request) {
expectType<string | undefined>(search);
expectType<string>(reqUrl);
expectType<FastifyRequest<RequestGenericInterface, RawServerBase>>(request);
return '';
},
},
internalRewriteLocationHeader: true,
});

Expand Down

0 comments on commit 04e2c14

Please sign in to comment.