Skip to content

Commit

Permalink
Merge pull request #6983 from mook-as/fix/dashboard/websocket
Browse files Browse the repository at this point in the history
Dashboard: Fix websocket requests
  • Loading branch information
jandubois authored Jun 4, 2024
2 parents 2859219 + 685260e commit e93ccfd
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 58 deletions.
52 changes: 28 additions & 24 deletions pkg/rancher-desktop/main/dashboardServer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import net from 'net';
import path from 'path';

import express from 'express';
import { createProxyMiddleware, Options, RequestHandler } from 'http-proxy-middleware';
import { createProxyMiddleware, Options } from 'http-proxy-middleware';

import { proxyWsOpts, proxyOpts } from './proxyUtils';

Expand All @@ -30,21 +30,22 @@ export class DashboardServer {

private proxies = (() => {
const proxy: Record<ProxyKeys, Options> = {
'/k8s': proxyWsOpts(), // Straight to a remote cluster (/k8s/clusters/<id>/)
'/pp': proxyWsOpts(), // For (epinio) standalone API
'/api': proxyWsOpts(), // Management k8s API
'/apis': proxyWsOpts(), // Management k8s API
'/v1': proxyWsOpts(), // Management Steve API
'/v3': proxyWsOpts(), // Rancher API
'/api-ui': proxyOpts(), // Browser API UI
'/v3-public': proxyOpts(), // Rancher Unauthed API
'/meta': proxyOpts(), // Browser API UI
'/v1-*': proxyOpts(), // SAML, KDM, etc
'/k8s': proxyWsOpts, // Straight to a remote cluster (/k8s/clusters/<id>/)
'/pp': proxyWsOpts, // For (epinio) standalone API
'/api': proxyWsOpts, // Management k8s API
'/apis': proxyWsOpts, // Management k8s API
'/v1': proxyWsOpts, // Management Steve API
'/v3': proxyWsOpts, // Rancher API
'/api-ui': proxyOpts, // Browser API UI
'/v3-public': proxyOpts, // Rancher Unauthed API
'/meta': proxyOpts, // Browser API UI
'/v1-*': proxyOpts, // SAML, KDM, etc
};
const entries = Object.entries(proxy).map(([key, options]) => {
return [key, createProxyMiddleware({ ...options, target: this.api + key })] as const;
});

return Object.fromEntries(Object.entries(proxy).map(([key, options]) => {
return [key, createProxyMiddleware({ ...options, target: this.api + key })];
})) as unknown as Record<ProxyKeys, RequestHandler>;
return Object.fromEntries(entries);
})();

/**
Expand Down Expand Up @@ -89,16 +90,19 @@ export class DashboardServer {
);
})
.listen(this.port, this.host)
.on('upgrade', (incomingMessage, duplex, head) => {
const req = incomingMessage as express.Request;
const socket = duplex as net.Socket;

if (req?.url?.startsWith('/v1')) {
return this.proxies['/v1'].upgrade?.(req, socket, head);
} else if (req?.url?.startsWith('/v3')) {
return this.proxies['/v3'].upgrade?.(req, socket, head);
} else if (req?.url?.startsWith('/k8s/')) {
return this.proxies['/k8s'].upgrade?.(req, socket, head);
.on('upgrade', (req, socket, head) => {
if (!(socket instanceof net.Socket)) {
console.log(`Invalid upgrade for ${ req.url }`);

return;
}

if (req.url?.startsWith('/v1')) {
return this.proxies['/v1'].upgrade(req, socket, head);
} else if (req.url?.startsWith('/v3')) {
return this.proxies['/v3'].upgrade(req, socket, head);
} else if (req.url?.startsWith('/k8s/')) {
return this.proxies['/k8s'].upgrade(req, socket, head);
} else {
console.log(`Unknown Web socket upgrade request for ${ req.url }`);
}
Expand Down
92 changes: 58 additions & 34 deletions pkg/rancher-desktop/main/dashboardServer/proxyUtils.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,74 @@
import { ClientRequest } from 'http';
import { Socket } from 'net';

import { Options } from 'http-proxy-middleware';

import Logging from '@pkg/utils/logging';

import type { ErrorCallback, ProxyReqCallback, ProxyReqWsCallback } from 'http-proxy';

const console = Logging.dashboardServer;

export const proxyOpts = (): Omit<Options, 'target'> => {
return {
followRedirects: true,
secure: false,
logger: console,
on: {
proxyReq: onProxyReq,
proxyReqWs: onProxyReqWs,
error: onError,
},
};
};
const onProxyReq: ProxyReqCallback = (clientReq, req) => {
const actualClientReq: ClientRequest | undefined = (clientReq as any)._currentRequest;

export const proxyWsOpts = (): Omit<Options, 'target'> => {
return {
...proxyOpts(),
ws: false,
changeOrigin: true,
};
if (!actualClientReq || !actualClientReq.headersSent) {
if (req.headers.host) {
clientReq.setHeader('x-api-host', req.headers.host);
}
clientReq.setHeader('x-forwarded-proto', 'https');
}
};

const onProxyReq = (proxyReq: any, req: any) => {
if (!(proxyReq._currentRequest && proxyReq._currentRequest._headerSent)) {
proxyReq.setHeader('x-api-host', req.headers['host']);
proxyReq.setHeader('x-forwarded-proto', 'https');
const onProxyReqWs: ProxyReqWsCallback = (clientReq, req, socket, options) => {
const target = options?.target as Partial<URL> | undefined;

if (!target?.href) {
console.error(`onProxyReqWs: No target href, aborting`);
req.destroy(new Error(`onProxyReqWs: no target href`));

return;
}
if (target.pathname && clientReq.path.startsWith(target.pathname)) {
// `options.prependPath` is required for non-websocket requests to be routed
// correctly; this means that we end up with the prepended path here, but
// that does not work in this case. Therefore we need to manually strip off
// the prepended path here before passing it to the backend.
clientReq.path = clientReq.path.substring(target.pathname.length);
}
req.headers.origin = target.href;
clientReq.setHeader('origin', target.href);
if (req.headers.host) {
clientReq.setHeader('x-api-host', req.headers.host);
}
clientReq.setHeader('x-forwarded-proto', 'https');

socket.on('error', err => console.error('Proxy WS Error:', err));
};

const onProxyReqWs = (proxyReq: any, req: any, socket: any, options: any, _head: any) => {
req.headers.origin = options.target.href;
proxyReq.setHeader('origin', options.target.href);
proxyReq.setHeader('x-api-host', req.headers['host']);
proxyReq.setHeader('x-forwarded-proto', 'https');
const onError: ErrorCallback = (err, req, res) => {
console.error('Proxy Error:', err);
if (res instanceof Socket) {
res.destroy(err);
} else {
res.statusCode = 598; // (Informal) Network read timeout error
res.write(JSON.stringify(err));
}
};

socket.on('error', (err: any) => {
console.error('Proxy WS Error:', err);
});
export const proxyOpts: Omit<Options, 'target'> = {
followRedirects: true,
secure: false,
logger: console,
on: {
proxyReq: onProxyReq,
proxyReqWs: onProxyReqWs,
error: onError,
},
};

const onError = (err: any, req: any, res: any) => {
res.statusCode = 598;
console.error('Proxy Error:', err);
res.write(JSON.stringify(err));
export const proxyWsOpts: Omit<Options, 'target'> = {
...proxyOpts,
ws: false,
changeOrigin: true,
};

0 comments on commit e93ccfd

Please sign in to comment.