jordan-breton / uws-reverse-proxy

uWebSocket.js based reverse proxy. It allows to run uWebSocket.js on the same port than any other HTTP server. For example: making uWebSocket.js and express/ nestjs / koa / fastify / whatever on the same port possible.
GNU Lesser General Public License v3.0
7 stars 1 forks source link

Pathname support #3

Open mustafa519 opened 10 months ago

mustafa519 commented 10 months ago

Hello,

Thank you for providing this package, and your detailed efforts.

I came from the discussion topic from uws which was opened by you. However, I am pretty confused about usage of the library like reverse logic of the defining a proxy connection. And also, when you compare this package against http-proxy-middleware, I thought, I'll have kind of the same options what http-proxy package provides.

The things are which make me confused:

I feel like, I have no control of the proxy destination other than setting the target.

My purpose is I use a main dashboard service which is proxying all the private dashboards behind an authentication of the main dashboard between the docker containers, like Prometheus, Grafana, PhpMyAdmin, redis insight etc. There are a lot of different services which may require different options, path replication, origin replication etc.

Btw, I had implemented the same arch in the express server that was working fine but slow like a hell.

Mustafa, Thanks.

jordan-breton commented 10 months ago

Hello Mustafa and thank you for your interest in my work.

I'll answer to every question, do not hesitate to ask for more information if you find my answer unclear.

First of all, UWSProxy was intially created to map 1:1 with a backend server. This means that if your reach UWSProxy on route /dashboard/* it will currently proxy the request to /dashboard/* to the backend server.

How to set a pathname like /dashboard to proxy it from my uws wrapper instance?

For this one you can use the routes in UWSProxy options:

const proxy = new UWSProxy(
    createUWSConfig(
        uwsServer,
        { port } // Must be specified to avoid a warning
    ),
    createHTTPConfig(
        {
            port: httpPort, 
            host: httpHost
        }
    ),
       {
           routes: {
              any: '/dashboard/*'
           }
       }
);

This configuration will only route the traffic matching URL /dashboard/*

You can use any matching pattern supported by uWebSockets.js:

{
    routes: {
        post: '/dashboard/*', // will only route post requests for urls matching /dashboard/*,
        any: '/path2/*', // will root any request for urls matching /path2/*
   }
}

However as I said above: currently it's only mapping URLS 1:1 to one and only one backend server. That being said I'm willing to enhance this project, and your use case sounds to be an intresting challenge and would greatly improve UWSProxy use cases.

How to set cors settings for per proxy.

Currently by using UWSProxy options you can add additional headers to every requests before sending them to the backend server:

{
    routes: {
        any: '/dashboard/*', // will root any request for urls matching /path2/*
   },
   headers: {
       // your headers here
   }
}

Editing headers sent back to the client is not yet supported, but not that hard to implement.

What if I need to parse the request body?

The custom HTTP client I wrote for this project use a custom HTTP parser. I could expose its events to the public interface for you to read the response from proxied services. However, if you're willing to modify response body, it would be a very huge change. For headers it's insignificant and really easy to do.

Long story short: I initilay exposed every stream with a convenient API, but nodejs streams are painfully slow. That being said, I can think about an event system to expose request parsing, and some modifiers to change headers before sending the response back to the client.

What if I need to remove a header to not send to the proxied target, e.g.: Content-Security-Policy which is useless when serve it behind the proxy.

Currently not possible but not that hard to implement.

My purpose is I use a main dashboard service which is proxying all the private dashboards behind an authentication of the main dashboard between the docker containers, like Prometheus, Grafana, PhpMyAdmin, redis insight etc. There are a lot of different services which may require different options, path replication, origin replication etc.

As I said above UWSProxy where not meant to be used that way in first place, but I would be happy to enhance it to this point and push it further.

It will take a bit of time though, since I'm pretty busy.

mustafa519 commented 10 months ago

Thank you for your quick reply. Yeah, recent improvements of uws have made me excited about its future, ngl. I think, right now, node.js and uws are both ready to do a decent investment.

Oh, I missed the third option was the routes.

I haven't looked deeply into the code yet, however, an agent which uses keep alive connection of all the defined proxies can help in reducing the complexity of the future improvements and also gives you a top level control for every target. So, I think target based arch with the default options by the main agent controller sounds exciting for me.

Let me clarify the example arch/API which is in my mind:

import { uwsAgent as Agent } from 'uws-reverse-proxy';

const agent = new Agent({
  keepAlive: true,
  timeout: 10,
  changeOrigin: true, // can be defined here or in each target
  // or targetting the platform in the options like:
  // target: 'uws' | 'express' | 'fastify',
});

// Path + options
// Well, I don't think we need to set a specific method type rather than any(),
// if so, callbacks can be helpful to implement the specific use case
agent.addTarget('/services/grafana', {
  target: 'https://grafana:3000',
  ws: true, // default
  followRedirects: true,
  onProxyRequest: callback,
  onProxyResponse: (proxyRes, nativeReq, nativeRes)
  {
    proxyRes.headers['x-added'] = 'foobar'; // add new header to response
    delete proxyRes.headers['x-removed']; // remove header from response
  },
  onProxyError: callback,
  headers: // default headers
  {
    Authorization: `Basic '${btoa('hello:world')}`,
  },
  pathRewrite:
  {
    ['/^services/grafana': '/'],
  },
});

This is approximately kind of what http-proxy-middleware does. I think, that approach is going to reduce complexity for all of us while making it pretty flexible. And also surely it will be faster than now, IMHO.

Btw, proxying will never be as fast as the target as people expected, even so when you made the initial connection as fast as possible then using the callback must be the concern of the developer who needs to develop their specific use case. So providing the callbacks are more appropriate IMHO, instead of implementing every specific use cases.

Otherwise, they would be using nginx, Apache etc as proxy server rather than node.js itself.

jordan-breton commented 9 months ago

Sorry for the delay, as I said I'm pretty busy nowadays :)

Yes, an agent-like architecture is a well suited approach to support several targets.

You'll find below some clarification about your example:

const agent = new Agent({
/*
  It seems useless to me: since we use connection pipelining it's always the case.
  Is there any scenario in which we want it to be false ?
*/
  keepAlive: true,
});

agent.addTarget('/services/grafana', {
  /*
    I'm not willing to support proxying websocket trafic for now because it add a bunch of complexity. 
    That being said I may work on it later. If you need it, open a second issue for a feature request.
  */
  ws: true,
  /*
    Open a feature request for this one too. 

    I try to keep the proxy as dumb as possible: the philosophy until now is to let the proxy client handle
    as much as possible the result of a given request, especially for redirects, because the redirect
    could be on another domain, and then we have a problem because the HTTP client makes the
    assumption that the target is safe and well-known.

   Blindly following redirects could lead the proxy http client I wrote to call unkown/unsafe servers,
   opening a range of security vulnerabilities I'm not competent enough to anticipate. 
  */
  followRedirects: true,
  /* ... */
});

I'm starting to work on this ASAP, thanks for your time and feedback :)