neffos.js icon indicating copy to clipboard operation
neffos.js copied to clipboard

Can not we set the header in the dial?

Open majidbigdeli opened this issue 5 years ago • 8 comments

I know that There is no method in the JavaScript WebSockets API for specifying additional headers for the client/browser to send. The HTTP path ("GET /xyz") and protocol header ("Sec-WebSocket-Protocol") can be specified in the WebSocket constructor.

The Sec-WebSocket-Protocol header (which is sometimes extended to be used in websocket specific authentication) is generated from the optional second argument to the WebSocket constructor:

var ws = new WebSocket("ws://example.com/path", "protocol");
var ws = new WebSocket("ws://example.com/path", ["protocol1", "protocol2"]);

The above results in the following headers: Sec-WebSocket-Protocol: protocol and Sec-WebSocket-Protocol: protocol1, protocol2

but I have error

WebSocket connection to 'ws://localhost:3811/echo' failed: Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received
(anonymous) @ neffos.js:612
dial @ neffos.js:604
runExample @ (index):37
(anonymous) @ (index):59

my index.html


<input id="input" type="text" />

<button id="sendBtn" disabled>Send</button>

<pre id="output"></pre>

<script src="./neffos.js"></script>
<script>
    var scheme = document.location.protocol == "https:" ? "wss" : "ws";
    var port = ":3811"
    var wsURL = "ws://localhost:3811/echo" 
    var outputTxt = document.getElementById("output");
    function addMessage(msg) {
        outputTxt.innerHTML += msg + "\n";
    }
    function handleError(reason) {
        console.log(reason);
        window.alert(reason);
    }
    function handleNamespaceConnectedConn(nsConn) {
        let inputTxt = document.getElementById("input");
        let sendBtn = document.getElementById("sendBtn");
        sendBtn.disabled = false;
        sendBtn.onclick = function () {
            const input = inputTxt.value;
            inputTxt.value = "";
            nsConn.emit("send", input);
            addMessage("Me: " + input);
        };
    }
    async function runExample() {
        // You can omit the "default" and simply define only Events, the namespace will be an empty string"",
        // however if you decide to make any changes on this example make sure the changes are reflecting inside the ../server.go file as well.
        try {
            const conn = await neffos.dial(wsURL, {
                default: { // "default" namespace.
                    _OnNamespaceConnected: function (nsConn, msg) {
                        handleNamespaceConnectedConn(nsConn);
                    },
                    _OnNamespaceDisconnect: function (nsConn, msg) {
                        
                    },
                    chat: function (nsConn, msg) { // "chat" event.

                    }
                }
            },["123"]);
            // You can either wait to conenct or just conn.connect("connect")
            // and put the `handleNamespaceConnectedConn` inside `_OnNamespaceConnected` callback instead.
            // const nsConn = await conn.connect("default");
            // handleNamespaceConnectedConn(nsConn);
            conn.connect("default");
        } catch (err) {
            handleError(err);
        }
    }
    runExample();
  </script>

I used Query String to solve this problem.

change wsURL to ws://localhost:3811/echo?UserId=123

and remove ["123"] protocol in the dial method.

Can not we have a custom header on dial?

function dial(endpoint: string, connHandler: any, protocols?: string[]): Promise<Conn> {
    if (endpoint.indexOf("ws") == -1) {
        endpoint = "ws://" + endpoint;
    }

    return new Promise((resolve, reject) => {
        if (!WebSocket) {
            reject("WebSocket is not accessible through this browser.");
        }


        let namespaces = resolveNamespaces(connHandler, reject);
        if (isNull(namespaces)) {
            return;
        }

        let ws = new WebSocket(endpoint, protocols);
        let conn = new Conn(ws, namespaces);
        ws.binaryType = "arraybuffer";
        ws.onmessage = ((evt: MessageEvent) => {
            let err = conn.handle(evt);
            if (!isEmpty(err)) {
                reject(err);
                return;
            }

            if (conn.isAcknowledged()) {
                resolve(conn);
            }
        });
        ws.onopen = ((evt: Event) => {
            // let b = new Uint8Array(1)
            // b[0] = 1;
            // this.conn.send(b.buffer);
            ws.send(ackBinary);
        });
        ws.onerror = ((err: Event) => {
            conn.close();
            reject(err);
        });
    });
}

Like golang client

	client, err := neffos.Dial(
		// Optional context cancelation and deadline for dialing.
		nil,
		// The underline dialer, can be also a gobwas.Dialer/DefautlDialer or a gorilla.Dialer/DefaultDialer.
		// Here we wrap a custom gobwas dialer in order to send the username among, on the handshake state,
		// see `startServer().server.IDGenerator`.
		gobwas.Dialer(gobwas.Options{Header: gobwas.Header{"X-Username": []string{username}}}),
		// The endpoint, i.e ws://localhost:8080/path.
		endpoint,
		// The namespaces and events, can be optionally shared with the server's.
		serverAndClientEvents)

majidbigdeli avatar Jun 03 '19 18:06 majidbigdeli

Oh @majidbigdeli I just saw it. It's the only one missing part of the neffos repo's javascript-equivalent example. It's already ready on my local machine but I didn't have enough time to test it yet (I am "designing" the new website for Iris too), I will push that feature tomorrow :)

kataras avatar Jun 09 '19 20:06 kataras

Just to give some details:

For the browser:

As you said there is no method in the JavaScript WebSockets API for specifying additional headers for the browser to send. Therefore we have 3-4 solutions:

  1. queries on the endpoint (as your comment above uses as well) ?header1=value1 (I've choose to proceed with that one which is already working locally but I am waiting for your respond to push)
  2. all modern browsers send cookies among with the websocket connection, so use document.cookies under the hoods can work.
  3. If you need those headers for basic auth something like this can work: ws://username:password@domain/endpoint_path
  4. The protocols[] parameter of the neffos.dial (and raw's new WebSocket constructor) can be used to send headers but server must be able to parse them, but there is a limitation: it must not contain a comma ,

For the nodejs side, the ws library we use can support custom headers.

kataras avatar Jun 10 '19 07:06 kataras

Hello @kataras
Thank you for your answer . I agree with the first solution To push in the examples. And I use this solution. Please add an example for the custom header in nodejs. Thank You.

majidbigdeli avatar Jun 10 '19 09:06 majidbigdeli

Updated

For the nodejs: You can simply use the http.request.options literal, which can contain a headers: { 'key': 'value'} as documented at https://nodejs.org/api/http.html#http_http_request_options_callback:

For the browser: I made it to accept the same dictionary for headers, so the same { headers: {...}} dictionary must be used.

    const conn = await neffos.dial(wsURL, {...}, {
        headers: {
          'X-Username': 'kataras',
        }
      });

Just a note for the browser-side:

The neffos.dial function will add a prefix of X-Websocket-Header-$key and server-side will parse all url parameters that starts with X-Websocket-Header and set the url param to the http.Request.Headers field as $key, on nodejs and go client custom headers are possible by-default and are set directly.

Hope that helps @majidbigdeli

kataras avatar Jun 10 '19 09:06 kataras

@kataras . I saw the parseHeadersAsURLParameters method . Thank you for this solution.

Your solution is very clever

majidbigdeli avatar Jun 10 '19 16:06 majidbigdeli

I used this solution for myself already.

	server := neffos.New(gobwas.DefaultUpgrader, handler)
	server.IDGenerator = func(w http.ResponseWriter, r *http.Request) string {

		if userID := r.Header.Get("X-Username"); userID != "" {
			return userID
		} else if userID := r.FormValue("X-Username"); userID != "" {
			return userID
		}

		return neffos.DefaultIDGenerator(w, r)
	}

majidbigdeli avatar Jun 10 '19 16:06 majidbigdeli

Thank you a lot @majidbigdeli!

Yeah the solution of yours is fine and you can still use it but, you know, we needed a way to separate any user-specific url parameters to the endpoint that meant to be used as Request.URL.Query() and not as headers, that's why we have the prefix and the parse functions on both sides (I don't want to see future issues like "why my url parameters are parsed as headers!!").

kataras avatar Jun 10 '19 16:06 kataras

Yeah the solution of yours is fine and you can still use it but, you know, we needed a way to separate any user-specific url parameters to the endpoint that meant to be used as Request.URL.Query() and not as headers, that's why we have the prefix and the parse functions on both sides (I don't want to see future issues like "why my url parameters are parsed as headers!!").

Yes you are right . That's why I say that your solution is very clever.

majidbigdeli avatar Jun 10 '19 17:06 majidbigdeli