ninenines / gun

HTTP/1.1, HTTP/2, Websocket client (and more) for Erlang/OTP.
ISC License
891 stars 232 forks source link

Websocket without upgrade? #281

Closed ostinelli closed 2 years ago

ostinelli commented 2 years ago

Hello, Is it possible to use gun's WebSocket implementation without going through the upgrade exchange over an http connection?

My use case: I'm integrating with Janus WebRTC server which has a WebSocket gateway to perform server-to-server tasks. I'm unable to achieve a connection and my hypothesis is that gun goes through the whole http(s) Connection: upgrade mechanism, while Janus implements the WebSocket protocol directly.

I can successfully connect to Janus using other libraries that expose the WebSocket protocol without an upgrade mechanism, however I'd rather use gun if possible.

Any input is welcome!

essen commented 2 years ago

There's no such thing as an HTTP-less Websocket. You have to negotiate at least the Sec-Websocket-Key.

According to their docs it's just plain Websocket, except you have to set a specific subprotocol:

To interact with Janus using WebSockets you MUST specify a specific subprotocol, named janus-protocol, e.g.,

var websocket = new WebSocket('ws://1.2.3.4:8188', 'janus-protocol');

ostinelli commented 2 years ago

Thank you for your response Loïc. Yes, I do that already. Maybe I'm not understanding how the connection upgrade works then, indeed I'm not familiar with it.

For some reason though I'm unable to connect with gun:

 with {:ok, gun_pid} <- :gun.open(to_charlist(host()), port(), %{transport: :tcp}),
         {:ok, _protocol} <- :gun.await_up(gun_pid),
         stream_ref <- :gun.ws_upgrade(gun_pid, "/janus", [{"sec-websocket-protocol", "janus-protocol"}])
         do:
 [...]

I receive a {:gun_down, ^gun_pid, _protocol, reason, _killed_streams, _unprocessed_streams} with reason :normal, immediately after the upgrade call.

Does it look like I'm doing something wrong here?

For comparison, this works with another library (yet again, I'd prefer to use gun):

ws_url = "ws://#{host()}:#{port()}/janus"
opts = [extra_headers: [{"Sec-WebSocket-Protocol", "janus-protocol"}]]
{:ok, client_pid} = WebSockex.start_link(ws_url, __MODULE__, state, opts)
essen commented 2 years ago

Other libraries are just hiding the upgrade.

I'm not sure why you would get a normal here except under normal connection shutdown. Can you not use with and instead do hard matches so the code crashes where it's having a problem? You'll be able to more easily figure out what fails exactly.

essen commented 2 years ago

Also make sure that you match against the http protocol value in await_up return value.

ostinelli commented 2 years ago

Here's an Erlang example:

-module(guntest_janus).

-export([main/0]).

main() ->
    {ok, GunPid} = gun:open("localhost", 8188, #{transport => tcp}),
    {ok, http} = gun:await_up(GunPid),
    _StreamRef = gun:ws_upgrade(GunPid, "/janus", [{"sec-websocket-protocol", "janus-protocol"}]),
    receive
        Any ->
            gun:close(GunPid),
            Any
    end.

Running it:

1> guntest_janus:main().
{gun_down,<0.128.0>,http,normal,
          [#Ref<0.1435093131.708313092.205050>],
          []}

Anything you think I might try?

essen commented 2 years ago

Right I forgot how that worked. So you have to set the protocols option like [{<<"janus-protocol">>, gun_ws_h}] when you upgrade and not provide headers. Seems there's no test inside Gun for this, must be something developed for a customer. If that solves the problem please leave the ticket open so I can add a test and documentation if it's missing.

essen commented 2 years ago

gun:ws_upgrade(ConnPid, Path, Headers, #{protocols => [...]})

ostinelli commented 2 years ago

Thank you Loïc, this worked.

-module(guntest_janus).

-export([main/0]).

main() ->
    {ok, GunPid} = gun:open("localhost", 8188, #{transport => tcp}),
    {ok, http} = gun:await_up(GunPid),
    _StreamRef = gun:ws_upgrade(GunPid, "/janus", [], #{protocols => [{<<"janus-protocol">>, gun_ws_h}]}),

    receive
        Any ->
            gun:close(GunPid),
            Any
    end.

FTR, it also works if the sec-websocket-protocol is also set in the headers.

Leaving open per your request.

essen commented 2 years ago

Opened a new ticket for the followup.