Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to seamlessly redirect websockets?

I know that it is possible to pass requests through the reverse proxy (like Nginx, HAproxy and so on) but I need to redirect requests to another public server in the same domain suffix. I.e. from wss://example.com to wss://ws1.example.com.
Here is an example: enter image description here

I need to redirect requests from Nginx or Java. Is to possible to organize? Do I need to handle redirects on a client side or this code is enough?

var socket = new WebSocket("wss://example.com");

socket.onopen = function() {
  alert("Connection established.");
};

socket.onclose = function(event) {
  alert('Connection closed');
};

socket.onmessage = function(event) {
  alert("Data: " + event.data);
};

socket.onerror = function(error) {
  alert("Error " + error.message);
};
like image 549
Oleksandr Avatar asked Oct 26 '17 20:10

Oleksandr


People also ask

Can WebSockets be proxied?

WebSocket communication can take successfully take place in the presence of forward proxies, providing the client and proxy server have been configured properly to deal with it. This page explains how to configure a Universal Messaging JavaScript client and Apache serving as a forward proxy to permit WebSocket use.

Can WebSockets be spoofed?

If you build your websocket over HTTP, then yes, it is completely possible for a third party to spoof the connection (and also to eavesdrop). If your HTTPS/WSS system does not properly validate certificates, then that also can be spoofed.

Can WebSockets be restful?

REST requires a stateless protocol according to the statelessness constraint, websockets is a stateful protocol, so it is not possible.

Can WebSockets be load balanced?

Despite its obvious benefits, there remain some concerns with utilizing WebSockets for load balancing. For example, if a WebSocket-based app is running on two servers and the load balancer is distributing connections evenly across them, it won't distribute any of the existing traffic when a third server is added.


2 Answers

It's been four years since this question has been asked so here's how you'd do it today:

in npm ws package:

set followRedirects to true in your socket client constructor (source).

in wscat utility:

pass the -L flag (source).

in "most" browsers:

If you don't care about semantics, I wrote a package for this. otherwise it is possible to switch the URL in WebSocket's constructor by hooking into its constructor and eventually support a redirect response.

'use strict';

/* global window, WebSocket*/

(function (WebSocketNative = window.WebSocket) {
  const debug = console.log;
  debug('patching native WebSocket class to support HTTP redirects');
  window.WebSocket = class {
    constructor(url, ...args) {
      this.work = [];
      this.endpoint = url.replace(/^http/, 'ws');
      debug('ws constructed with url: "%s" and endpoint: "%s"', url, this.endpoint);
      return (
        (async () => {
          try {
            const resolver =
              window.WEBSOCKET_REDIRECT_RESOLVER || `${location.protocol}//${location.host}${location.pathname}`;
            debug('websocket address resolver: "%s"', resolver);
            const endpoint = `${resolver}?url=${encodeURIComponent(this.endpoint)}`;
            debug('resolver query endpoint: "%s"', endpoint);
            const response = await fetch(endpoint);
            debug('resolver query response: "%o"', response);
            const { dig, dug } = await response.json();
            debug('resolver dig: "%o" dug: "%o"', dig, dug);
            this.endpoint = dug;
          } finally {
            debug('websocket final endpoint: "%s"', this.endpoint);
            this.socket = new WebSocketNative(this.endpoint, ...args);
            debug('flushing %d queued operations', this.work.length);
            for (const op of this.work) op(this.socket);
          }
        })(),
        new Proxy(this, {
          get: function (target, prop) {
            debug('GETTER trace: "%s"', prop);
            switch (prop) {
              case 'construct':
              case 'endpoint':
              case 'socket':
              case 'work':
                debug('GETTER "%s" does not go through proxy', prop);
                return Reflect.get(...arguments);
              case 'OPEN':
              case 'CLOSED':
              case 'CLOSING':
              case 'CONNECTING':
                debug('GETTER "%s" is a static', prop);
                return Reflect.get(WebSocketNative, prop);
              case 'readyState':
              case 'bufferedAmount':
                return this.socket ? Reflect.get(this.socket, prop) : 0;
              case 'binaryType':
                return this.socket ? Reflect.get(this.socket, prop) : 'blob';
              case 'extensions':
              case 'protocol':
              case 'url':
                return this.socket ? Reflect.get(this.socket, prop) : '';
              default:
                return this.socket
                  ? Reflect.get(this.socket, prop)
                  : function (...args) {
                      if (target.socket) {
                        debug('socket is ready, skip queueing get "%s" with args "%o"', prop, args);
                        return Reflect.get(target.socket, prop).call(target.socket, ...args);
                      } else {
                        debug('socket is not ready yet. queueing get "%s" with args "%o"', prop, args);
                        target.work.push((socket) => Reflect.get(socket, prop).call(socket, ...args));
                      }
                    };
            }
          },
          set: function (target, prop, value) {
            debug('SETTER trace: "%s"="%o"', prop, value);
            switch (prop) {
              case 'endpoint':
              case 'socket':
              case 'work':
                return Reflect.set(...arguments);
              default:
                return this.socket
                  ? Reflect.set(this.socket, prop, value)
                  : (() => {
                      if (target.socket) {
                        debug('socket is ready, skip queueing set "%s" with value "%o"', prop, value);
                        Reflect.set(socket, prop, value);
                      } else {
                        debug('socket is not ready yet, queueing set "%s" with value "%o"', prop, value);
                        target.work.push((socket) => Reflect.set(socket, prop, value));
                      }
                      return true;
                    })();
            }
          },
        })
      );
    }
  };
})(window.WebSocket);

This snippet overrides browser's native WebSocket class with one that supports switching the URL to ALTERNATE_WEBSOCKET_ADDRESS on the fly. You need to define ALTERNATE_WEBSOCKET_ADDRESS somewhere as it's not possible to figure out a redirect response's eventual URL in a browser environment (neither with fetch, nor with XMLHttpRequest). fetch is only used to detect a redirect. You need to come up with a mechanism to have ALTERNATE_WEBSOCKET_ADDRESS ready when time comes to a redirect.

like image 195
Sepehr Avatar answered Sep 17 '22 13:09

Sepehr


Per the webSocket specification:

Once the client's opening handshake has been sent, the client MUST wait for a response from the server before sending any further data. The client MUST validate the server's response as follows:

  1. If the status code received from the server is not 101, the client handles the response per HTTP [RFC2616] procedures. In particular, the client might perform authentication if it receives a 401 status code; the server might redirect the client using a 3xx status code (but clients are not required to follow them), etc.

So, it's purely up to the client whether they want to support redirects or not and is clearly not something you can rely on unless you find in extensive testing that all relevant clients support it (which they apparently do not).

You will either have to go with something like a server-side proxy or a client-side scheme to manually move the connection to another server.

like image 28
jfriend00 Avatar answered Sep 20 '22 13:09

jfriend00