代理将WebSocket请求从HTTPS转发到HTTP在Node.js中

huangapple go评论84阅读模式
英文:

Proxying Websocket requests from https to http in Node.js

问题

以下是您提供的代码的翻译部分:

我正在开发一个Node.js代理服务器,用于将来自网站的请求路由到本地主机端点。该网站可以在本地运行,也可以在云实例上运行,因此代理需要支持HTTP和HTTPS请求,并将它们路由到HTTP端点

下面的代码几乎适用于所有用例,除了当WebSocket请求来自HTTPS地址时。以下是代码:

import * as express from "express";
import { Server, createServer } from "http";
import { createProxyServer } from "http-proxy";
import { settings } from "./settings";

export let port = 3004;

let server: Server;

export function startServer() {
    const URL = settings.URL;
    const app = express();
    server = createServer(app);

    const proxy = createProxyServer({
        target: URL,
        changeOrigin: !URL.includes("localhost"),
        ws: true,
        headers: {
            // 附加请求头
        },
    });

    server.on("upgrade", function (req, socket, head) {
        proxy.ws(req, socket, head, {});
    });

    app.get("/public/*", function (req, res) {
        proxy.web(req, res, {});
    });

    app.get("/api/*", function (req, res) {
        proxy.web(req, res, {});
    });

    app.post("/api/query", function (req, res) {
        proxy.web(req, res, {});
    });

    server.listen(0, () => {
        port = server?.address()?.port;
        console.log("服务器已启动");
    });
}

export function stopServer() {
    if (server) {
        server.close();
    }
}

changeOrigin: !URL.includes("localhost") 这一行根据主机设置了 changeOrigin。对于本地主机请求,它是不必要的,但对于HTTPS请求是必需的。

然而,此代码对WebSocket请求失败,并返回以下错误:

WebSocket连接到 'ws://localhost:61958/api/ws' 失败:
//...
ERR read ECONNRESET: Error: read ECONNRESET
at TCP.onStreamRead (node:internal/stream_base_commons:217:20)
at TCP.callbackTrampoline (node:internal/async_hooks:130:17)

如果我不为WebSocket端点设置 changeOrigin,如下所示:

 server.on("upgrade", function (req, socket, head) {
    proxy.ws(req, socket, head, {changeOrigin: false});
  });

我会得到不同类型的错误:

ERR 客户端网络套接字在建立安全的TLS连接之前断开连接:
Error: 客户端网络套接字在建立安全的TLS连接之前断开连接
at connResetException (node:internal/errors:704:14)
at TLSSocket.onConnectEnd (node:_tls_wrap:1590:19)
at TLSSocket.emit (node:events:525:35)
at endReadableNT (node:internal/streams/readable:1358:12)
at process.processTicksAndRejections (node:internal/process/task_queues:83:21)

有关如何修复这个问题的想法吗?我觉得可能是我忽视了一些简单的东西,但无法找出来。

英文:

I am working on a Node.js proxy server that routes requests from a website to a localhost endpoint. The website can run either locally or on a cloud instance, so the proxy needs to support both HTTP and HTTPS requests and route them to the HTTP endpoint.

The code below works for almost all use cases, except when Websocket requests originate from an HTTPS address. Here is the code:

import * as express from "express";
import { Server, createServer } from "http";
import { createProxyServer } from "http-proxy";
import { settings } from "./settings"

export let port = 3004;

let server: Server;

export function startServer() {
    const URL = settings.URL;
    const app = express();
    server = createServer(app);

    const proxy = createProxyServer({
        target: URL,
        changeOrigin: !URL.includes("localhost"),
        ws: true,
        headers: {
            // Attach request headers
        },
    });

    server.on("upgrade", function (req, socket, head) {
        proxy.ws(req, socket, head, {});
    });

    app.get("/public/*", function (req, res) {
        proxy.web(req, res, {});
    });

    app.get("/api/*", function (req, res) {
        proxy.web(req, res, {});
    });

    app.post("/api/query", function (req, res) {
        proxy.web(req, res, {});
    });

    server.listen(0, () => {
        port = server?.address()?.port;
        console.log("Server started");
    });
}

export function stopServer() {
    if (server) {
        server.close();
    }
}

The line changeOrigin: !URL.includes("localhost") sets changeOrigin based on the host. It's unnecessary for localhost requests, but required for HTTPS requests.

This code, however, fails for the Websocket requests and returns the following error:

WebSocket connection to 'ws://localhost:61958/api/ws' failed:
//...
ERR read ECONNRESET: Error: read ECONNRESET
at TCP.onStreamRead (node:internal/stream_base_commons:217:20)
at TCP.callbackTrampoline (node:internal/async_hooks:130:17)

If I do not set changeOrigin for the Websocket endpoint via:

 server.on("upgrade", function (req, socket, head) {
proxy.ws(req, socket, head, {changeOrigin: false});
});

I get a different kind of error:

ERR Client network socket disconnected before secure TLS connection was established:
Error: Client network socket disconnected before secure TLS connection was established
at connResetException (node:internal/errors:704:14)
at TLSSocket.onConnectEnd (node:_tls_wrap:1590:19)
at TLSSocket.emit (node:events:525:35)
at endReadableNT (node:internal/streams/readable:1358:12)
at process.processTicksAndRejections (node:internal/process/task_queues:83:21)

Any ideas on how to fix this? I feel like I'm overlooking something simple, but can't figure it out.

答案1

得分: 1

Ruslan Zhomir答案 提到了一个解决方法:通过修改 WebSocket 升级请求中的 origin 标头,使用 proxyReq 事件 来实现,该事件用于 http-party/node-http-proxy

这可能适用于需要使用 origin 标头验证 WebSocket 连接的目标端点的情况,但这并不是常见的要求。如果您只需要处理简单的、未安全保护的 WebSocket 连接,那可能已经足够了。

但是,如果您需要处理安全和未安全 WebSocket 连接的混合,或者正在开发生产级应用程序,那么您需要采用不同的方法。

问题分析

目前,您正在尝试直接将来自 HTTPS 客户端的安全 WebSocket 升级请求传递给 HTTP 目标,而没有正确管理和解密这些安全请求。您需要在代理服务器中添加正确的 SSL/TLS 处理以修复这个问题。

在您的原始代码中,您正在管理 upgrade 事件,并使用 proxy.ws 方法,类似于 node-http-proxy README

server.on("upgrade", function (req, socket, head) {
    proxy.ws(req, socket, head, {});
});

当客户端建立 WebSocket 连接时,它首先发送普通的 HTTP 请求,然后请求将连接升级为 WebSocket。在您的情况下,如果此初始请求通过 HTTPS 发送,那么将连接升级为 WebSocket 的请求也将通过 HTTPS 发送。

然而,在上面的代码中,您试图直接将此升级请求传递给代理目标。如果目标是一个 HTTP 端点,它将无法处理来自 HTTPS 客户端的加密请求,导致您看到的 ECONNRESET 错误。

当前的代理设置也没有正确处理 HTTPS WebSocket 连接:

const proxy = createProxyServer({
    target: URL,
    changeOrigin: !URL.includes("localhost"),
    ws: true,
    headers: {
        // 附加请求标头
    },
});

在这里,您将 ws 选项设置为 true,表示代理应该能够处理 WebSocket 请求。但是,由于您没有提供任何处理 HTTPS 的 SSL/TLS 选项,代理无法正确地建立安全的 WebSocket 连接。这就是第二个错误(在建立安全 TLS 连接之前断开连接)的原因:您试图将来自 HTTPS 客户端的 WebSocket 请求代理到一个 HTTP 服务器。

当来自 HTTPS 客户端的 WebSocket 请求时,客户端希望建立一个安全的 WebSocket 连接(wss://)。为了正确接受并将此安全连接降级为 HTTP 目标的非安全连接,您的代理服务器需要设置为处理 HTTPS。如果没有设置,它将无法正确建立客户端期望的安全 WebSocket 连接。

您的代理服务器需要先正确建立一个 HTTPS WebSocket 连接,然后将其降级为目标的非安全 WebSocket 连接。

可能的解决方案

由于您的目标始终是一个 HTTP 端点,并且您的服务器可以接收 HTTP 或 HTTPS,您应该分别处理 HTTPS 连接,然后将其传递给代理。

这意味着除了 HTTP 服务器之外,还需要创建一个 HTTPS 服务器。您的 HTTPS 服务器应该具有所有必要的 SSL/TLS 配置,并且可以简单地将请求转发给 HTTP 服务器。可以为此创建一个简单的自签名证书。

类似于:

import { createServer, Server } from "http";
import { createServer as createHttpsServer } from "https";
import * as express from "express";
import { createProxyServer } from "http-proxy";
import * as fs from "fs";
import * as https from "https";
import { settings } from "./settings";

export let port = 3004;

let server: Server;

export function startServer() {
    const URL = settings.URL;
    const app = express();
    server = createServer(app);
    
    const options = {
        key: fs.readFileSync('key.pem'),
        cert: fs.readFileSync('cert.pem')
    };

    const httpsServer = createHttpsServer(options, app);

    const proxy = createProxyServer({
        target: URL,
        changeOrigin: !URL.includes("localhost"),
        ws: true,
        headers: {
            // 附加请求标头
        },
    });

    httpsServer.on("upgrade", function (req, socket, head) {
        proxy.ws(req, socket, head, {});
    });

    server.on("upgrade", function (req, socket, head) {
        proxy.ws(req, socket, head, {});
    });

    app.get("/public/*", function (req, res) {
        proxy.web(req, res, {});
    });

    app.get("/api/*", function (req, res) {
        proxy.web(req, res, {});
    });

    app.post("/api/query", function (req, res) {
        proxy.web(req, res, {});
    });

    server.listen(0, () => {
        port = server?.address()?.port;
        console.log("HTTP 服务器已启动");
    });

    httpsServer.listen(8443, () => {
        console.log("HTTPS 服务器已在端口 8443 上启动");
    });
}

export function stopServer() {
    if (server) {
        server.close();
    }
}

该代码创建了一个监听不同端口(3004/8443)的 HTTP 服务器和 HTTPS 服务器。HTTPS 服务器使用 key.pemcert.pem 进行 SSL/T

英文:

Ruslan Zhomir's answer alluded to a workaround: modifying the origin header in the WebSocket upgrade request, using the proxyReq event used in http-party/node-http-proxy.
That may be suitable in scenarios where the origin header is being used by the target endpoint to validate WebSocket connections, but this is not a common requirement. If you only need to handle simple, unsecured WebSocket connections, that might be enough.

But, if you need to handle a mixture of secured and unsecured WebSocket connections, or if you are developing a production-grade application, you need a different approach.

Problem analysis

Right now, you are attempting to directly pass secure WebSocket upgrade requests from HTTPS clients to an HTTP target without properly managing and decrypting these secure requests. You need to add proper SSL/TLS handling to your proxy server to fix that.

In your original code, you are managing the upgrade event and using the proxy.ws method, similar to the node-http-proxy README.

server.on("upgrade", function (req, socket, head) {
    proxy.ws(req, socket, head, {});
});

When a client makes a WebSocket connection, it first sends a normal HTTP request and then requests to upgrade the connection to a WebSocket. In your case, if this initial request is sent over HTTPS, the request to upgrade the connection to a WebSocket is also sent over HTTPS.

However, in the code above, you are trying to directly pass this upgrade request to the proxy target. If the target is an HTTP endpoint, it will not be able to handle the encrypted request coming from an HTTPS client, resulting in the ECONNRESET error you are seeing.

The current proxy settings also do not handle HTTPS WebSocket connections correctly:

const proxy = createProxyServer({
    target: URL,
    changeOrigin: !URL.includes("localhost"),
    ws: true,
    headers: {
        // Attach request headers
    },
});

Here, you are setting the ws option to true, indicating that the proxy should be able to handle WebSocket requests. But since you are not providing any SSL/TLS options for handling HTTPS, the proxy is not able to correctly establish secure WebSocket connections. That is where the second error (about disconnecting before a secure TLS connection was established) comes from: you try to proxy WebSocket requests from HTTPS clients to an HTTP server.

When a WebSocket request comes from an HTTPS client, the client expects to establish a secure WebSocket connection (wss://). In order to properly accept and downgrade this secure connection to a non-secure one for the HTTP target, your proxy server needs to be set up to handle HTTPS. If it is not, it will not be able to correctly establish the secure WebSocket connection that the client is expecting.

Your proxy server needs to correctly establish an HTTPS WebSocket connection first, then downgrade it to a non-secure WebSocket connection for the target.

Possible solution

Since your target is always an HTTP endpoint, and your server can receive either HTTP or HTTPS, you should handle the HTTPS connections separately and then pass it to the proxy.

That means creating an HTTPS server in addition to the HTTP one. Your HTTPS server should have all the necessary SSL/TLS configurations, and it can simply forward the requests to the HTTP server. A simple self-signed certificate can be created for this purpose.

Something like:

import { createServer, Server } from "http";
import { createServer as createHttpsServer } from "https";
import * as express from "express";
import { createProxyServer } from "http-proxy";
import * as fs from "fs";
import * as https from "https";
import { settings } from "./settings"

export let port = 3004;

let server: Server;

export function startServer() {
    const URL = settings.URL;
    const app = express();
    server = createServer(app);
    
    const options = {
        key: fs.readFileSync('key.pem'),
        cert: fs.readFileSync('cert.pem')
    };

    const httpsServer = createHttpsServer(options, app);

    const proxy = createProxyServer({
        target: URL,
        changeOrigin: !URL.includes("localhost"),
        ws: true,
        headers: {
            // Attach request headers
        },
    });

    httpsServer.on("upgrade", function (req, socket, head) {
        proxy.ws(req, socket, head, {});
    });

    server.on("upgrade", function (req, socket, head) {
        proxy.ws(req, socket, head, {});
    });

    app.get("/public/*", function (req, res) {
        proxy.web(req, res, {});
    });

    app.get("/api/*", function (req, res) {
        proxy.web(req, res, {});
    });

    app.post("/api/query", function (req, res) {
        proxy.web(req, res, {});
    });

    server.listen(0, () => {
        port = server?.address()?.port;
        console.log("HTTP Server started");
    });

    httpsServer.listen(8443, () => {
        console.log("HTTPS Server started on port 8443");
    });
}

export function stopServer() {
    if (server) {
        server.close();
    }
}

That code creates an HTTP server and an HTTPS server listening on different ports (3004/8443). The HTTPS server is using the key.pem and cert.pem for the SSL/TLS configuration. It handles the HTTPS WebSocket upgrade requests, while the HTTP server handles the HTTP WebSocket upgrade requests.

The HTTPS server does not explicitly decode incoming secure WebSocket upgrade requests. Instead, it listens for these secure WebSocket requests and proxies them, leveraging the node-http-proxy library's capability to handle WebSocket connections.

const httpsServer = createHttpsServer(options, app);
    
httpsServer.on("upgrade", function (req, socket, head) {
    proxy.ws(req, socket, head, {});
});

Your separate HTTPS server is created using the provided SSL/TLS options. That server listens for "upgrade" events, which occur when a client wants to initiate a WebSocket connection.
When an "upgrade" event is received, it invokes proxy.ws(), as illustrated in node-http-proxy / UPGRADING, and handles the process of proxying the WebSocket request to the target specified when the proxy was created.


> After some logging, it seems that the httpsServer's upgrade event is never called, only the one from the httpServer.
>
> Also I see this error in the console: transport_websocket.js:32 WebSocket connection to 'ws://localhost:8443' failed.

So the client is trying to establish a non-secure WebSocket connection (ws://) to the secure server (localhost:8443). That will not work because the localhost:8443 server is set up to only accept secure WebSocket (wss://) connections due to the presence of the SSL/TLS configuration.

You will need to modify the client side to initiate a secure WebSocket connection. If the client is a web browser, it should use wss://localhost:8443 instead of ws://localhost:8443 when creating a new WebSocket connection.

The correct protocol (ws:// for HTTP and wss:// for HTTPS) should be used depending on whether the connection needs to be secure or not.

huangapple
  • 本文由 发表于 2023年7月18日 14:04:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/76709903.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定