Sometimes I want to refuse a http client's request to upgrade connection to websocket.
Code
(using go's Gin and gorilla/websocket framework:)
To allow upgrade:
c, err := ctl.upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
err = c.WriteJSON(resp)
To refuse upgrade (due to invalid request params):
if contentId == "" || !exists {
// FIXME: provide response in a way that ws client can recognize & show tip?
ctx.String(http.StatusBadRequest, "invalid or non-existing contentId")
return
}
Explaination: Here to refuse the upgrade I just return a http 400 code, then terminate the connection, and didn't do the upgrade at all.
The issue
The problem to refuse websocket upgrade request with about code is that, the websocket client (e.g js), can't read data (text or json) in my response.
Code - client-side (js):
ws.onerror = function (evt) {
// TOOD: handle error, (e.g print error msg?),
print("ERROR");
}
It does print the "ERROR" on refuse, but after checking chrome developer tool about the evt object, can't find a way to get server response data, so I can't show tip to frontend UI with reason of refuse.
Questions
How to refuse websocket upgrade request properly, and let client be able to receive the returned reason/data ? (e.g client is js, server is go / gin / gorilla/websocket).
Is there a better way to refuse websocket upgrade request, other than return http code like 400?
To reject a websocket connection, do not upgrade the connection as described in the question.
The browser API does not provide information about why the connection was rejected because the information can violate the same-origin policy.
Do the following to send an error reason back to the client application or user:
Upgrade the connection.
Send a close message with the error reason.
Close the connection.
Here's an example:
c, err := ctl.upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
if err != nil {
// TODO: handle error
}
if contentId == "" || !exists {
c.WriteMessage(websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.ClosePolicyViolation,
"bad content id or not exist"))
c.Close()
return
}
// Continue with non-error case here.
Access the reason from the close handler in JS:
ws.onclose = function (evt) {
if (evt.code == 1008) { // 1008 is policy violation
console.log(evt.reason)
}
}
Related
I wrote a program in c++ to upgrade an HTTP session to a WebSocket. The server correctly receives the upgrade request, and responds with 101 switching protocols. Firefox receives the 101 response, but in the console claims it couldn't connect. Here's the full output in Firefox.
The interesting thing is, in the network tab you can see the 101 response come in so it did establish an HTTP connection, the server got it, accepted it, tried to upgrade, and sent an upgrade response, but it's at that point that firefox doesn't upgrade to a WebSocket and prints the error.
I tried in Google Chrome as well. Similar error, but a bit more detail. WebSocket connection to 'ws://localhost:3000/' failed: Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received
My code c++ hangs on the while loop, no errors thrown, which I think means it thinks the connection is still working?
I did notice it's sending Sec-Websocket-Protocol, even though I only asked for ws, not wss. Do chrome and firefox disallow non secure websockets? Or am I missing something in my code?
C++:
#define PORT (unsigned short) 3000
#define MAX_BUF 2048
int main()
{
Poco::Net::ServerSocket x(PORT);
Poco::Timespan timeout(25000000);
if (x.poll(timeout, 1)) {
Poco::Net::StreamSocket ss = x.acceptConnection();
Poco::AutoPtr<Poco::Net::HTTPServerParams> params = new Poco::Net::HTTPServerParams();
Poco::Net::HTTPServerSession sess(ss,params);
Poco::Net::HTTPServerResponseImpl res(sess);
Poco::Net::HTTPServerRequestImpl req(res, sess, params);
if (req.getMethod()=="GET" && req.get("Upgrade")=="websocket") {
Poco::Net::WebSocket webSock(req,res);
while (!webSock.available());
char buf[MAX_BUF];
memset(buf,0,MAX_BUF);
int flags = 0;
webSock.sendFrame("Hello, World!\n",15);
webSock.receiveFrame(buf,MAX_BUF,flags);
printf("received %s\n",buf);
}
}
else {
printf("Timeout!\n");
}
return 0;
}
Javascript:
$(document).ready(function() {
webSoc = new WebSocket("ws://localhost:3000");
webSoc.onopen = function (event) {
$("#c").click(function () {
console.log("Sending 'Hello, World!'");
webSoc.send("Hello, World!\n");
});
}
});
I've tried new WebSocket("ws://localhost:3000") with and without the echo-protocol parameter, because I read it in an answer to a question I didn't understand, which I can't find now.
Is it possible to programmatically check if a WebSocket connection failed with a 403 response? For instance, with the following server code:
package main
import (
"errors"
"io"
"log"
"net/http"
"golang.org/x/net/websocket"
)
func main() {
handler := websocket.Handler(func(ws *websocket.Conn) {
io.Copy(ws, ws)
})
handshake := func(conf *websocket.Config, req *http.Request) error {
if req.URL.Path == "/sekret" {
return nil
}
return errors.New("Oops!")
}
server := &websocket.Server{
Handshake: handshake,
Handler: handler,
}
log.Fatal(http.ListenAndServe(":8080", server))
}
And the following sample JS connection that triggers a 403 response:
var ws = new WebSocket("ws://localhost:8080/something");
ws.onerror = console.log;
The error response is an Event with type "error". On the other hand, the following JS code triggers a net::ERR_CONNECTION_REFUSED (at least on Chrome):
var ws = new WebSocket("ws://localhost:8081/something");
ws.onerror = console.log;
The error event object looks almost exactly the same. Is there some way to distinguish error types in WebSocket connections from the client side? (I'm specifically looking for 403 responses, so I can alert the user to take special action.)
Apparently it's deliberate:
My understanding is that for security reasons, WebSocket error statuses are fairly restricted to limit the ability of malicious JS to probe an internal network.
I've written a web application that uses web-sockets. The idea is that my app tries to auto-connect to recently connected to hosts when it starts up. If it can't establish a connection to any of them, then it directs the user to the connection part and asks them to establish a connection manually.
All of this works. In summary, I try each known host in order, and if 200ms later it hasn't connected (`readyState != 1), it tries the next one. All these hosts should be on the LAN so 200ms works pretty reliably. If the last one on the list fails too, then the web opens up a modal directing the user to manually type in a host.
The problem is, by trying to auto-connect, I have to create websockets to my attempted hosts, which outputs error messages like the following to the console:
WebSocket connection to 'ws://lightmate:8080/' failed: Error in
connection establishment: net::ERR_NAME_NOT_RESOLVED
WebSocket connection to 'ws://localhost:8080/' failed: Error in
connection establishment: net::ERR_CONNECTION_REFUSED
While not a fatal flaw by any means, it's unsightly and gets in the way of my debugging.
I've tried to remove it by surrounding the calls to new WebSocket(address) with a try/catch block, and the errors still get through, and I've also tried to set an onerror handler, hoping that would suppress the error messages. Nothing's worked.
connect: function(){
var fulladdr = completeServerAddress(address);
try {
connection = new WebSocket(fulladdr);
connection.suppressErrorsBecauseOfAutoConnection = suppressErrorsBecauseOfAutoConnection; //Store this module-scoped variable in connection, so if the module changes suppression state, this connection won't.
} catch (e){
//Make sure we don't try to send anything down this dead websocket
connection = false;
return false;
}
connection.binaryType = "arraybuffer";
connection.onerror = function(){
if (connection !== false && !connection.suppressErrorsBecauseOfAutoConnection){
Announce.announceMessage("Connection failed with server");
}
connection = false;
};
connection.onmessage = function(m){
rxMessage(ConnectionProtocol.BaseMessage.parseChunk(m.data));
};
connection.onclose = function(){
hooks.swing("disconnected", "", 0);
if (connection !== false && !connection.suppressErrorsBecauseOfAutoConnection){
Announce.announceMessage("Connection lost with server");
}
};
connection.onopen = function(){
sendMessages(ConnectionProtocol.HandshakeMessage.create(name, sources, sinks));
while (idlingmessages.length){
websocketConnection.send(idlingmessages.splice(0,1)[0]);
}
hooks.swing("connected", "", 0);
};
},
Dupl Disclaimer:
This question is similar to this StackOverflow question, but that question is out of date by a year, and the consensus there was "you can't". I'm hoping things have changed since then.
There is no way to trap that error message, which occurs asynchronously to the code where the WebSocket object is created.
More details here: Javascript doesn't catch error in WebSocket instantiation
Ok, I got this case. I got this app build on angular.js with a back-end build on node.js ... it works fine by all means.
The client got a crappy connection to the internet. It goes online and offline every now and then. What I need to do is a routine that pings my server to check for connection on it. If it fails, I need to ping to something else (lets say : google.com) so I can check two things:
1.- My server is online (or not)
2.- The client has no internet connection.
Ping to server works fine using this routine:
function hostReachable(host) { // Handle IE and more capable browsers
var xhr = new ( window.ActiveXObject || XMLHttpRequest )( "Microsoft.XMLHTTP" );
var status;
// Open new request as a HEAD to the root hostname with a random param to bust the cache
xhr.open( "GET", "//" + host + "/?rand=" + Math.floor((1 + Math.random()) * 0x10000), false);
// Issue request and handle response
try { xhr.send(); return (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304); }
catch (error) { return false; }
}
}
function check() {
if (!hostReachable(window.location.host)) {
if (!hostReachable('www.google.co.ve')) {
alert('No se pudo establecer conexión con el sistema. Revise su conexion a internet.');
} else {
alert('Existe un problema con el sistema. Por favor contacto a los administradores.');
}
} else {
console.log('Connected...');
}
}
check();
The deal is that checking google.com goes with cross-domains problems. so the question is very simple. Is there a way to check for internet connectivity like this ?
If you send a cross-domain request and the server you are trying to ping against doesn't accept it, they will send back an access-denied status. I think that is 403, but you would want to check firebug / chrome dev tools to see what the actual code is. Then, if you get that code, you know at the very least the request was sent and a response was received.
--edit--
Here is an example of what I mean on JSFiddle. Be sure to check the network requests on chrome dev tools / firebug. It is sending a request to www.google.com, and receiving a 404 status code in the response.
Sidenote: As it turns out, different servers send back different codes when denying cross-domain requests. It seems 401, 403, and 404 are the most popular ones. For anyone looking at this problem in the future, you will want to check which codes the site(s) you ping against are sending back.
You could try making a request for a javascript resource from google or some other highly reliable cdn. It would not be subjected to cross-domain restrictions.
I want to provide a meaningful error to the client when too many users are connected or when they're connecting from an unsupported domain, so...
I wrote some WebSocket server code:
var http = require('http');
var httpServer = http.createServer(function (request, response)
{
// i see this if i hit http://localhost:8001/
response.end('go away');
});
httpServer.listen(8001);
// https://github.com/Worlize/WebSocket-Node/wiki/Documentation
var webSocket = require('websocket');
var webSocketServer = new webSocket.server({ 'httpServer': httpServer });
webSocketServer.on('request', function (request)
{
var connection = request.reject(102, 'gtfo');
});
And some WebSocket client code:
var connection = new WebSocket('ws://127.0.0.1:8001');
connection.onopen = function (openEvent)
{
alert('onopen');
console.log(openEvent);
};
connection.onclose = function (closeEvent)
{
alert('onclose');
console.log(closeEvent);
}
connection.onerror = function (errorEvent)
{
alert('onerror');
console.log(errorEvent);
};
connection.onmessage = function (messageEvent)
{
alert('onmessage');
console.log(messageEvent);
};
All I get is alert('onclose'); with a CloseEvent object logged to the console without any status code or message that I can find. When I connect via ws://localhost:8001 the httpServer callback doesn't come into play, so I can't catch it there. The RFC suggests I should be able to send any status code other than 101 when there's a problem, but Chrome throws an error in the console Unexpected response code: 102. If I call request.reject(101, 'gtfo'), implying it was successful I get a handshake error, as I'd expect.
Not really sure what else I can do. Is it just not possible right now to get the server response in Chrome's WebSocket implementation?
ETA: Here's a really nasty hack in the mean time, I hope that's not what I have to end up doing.
var connection = request.accept(null, request.origin);
connection.sendUTF('gtfo');
connection.close();
I'm the author of WebSocket-Node and I've also posted this response to the corresponding issue on GitHub: https://github.com/Worlize/WebSocket-Node/issues/46
Unfortunately, the WebSocket protocol does not provide any specific mechanism for providing a close code or reason at this stage when rejecting a client connection. The rejection is in the form of an HTTP response with an HTTP status of something like 40x or 50x. The spec allows for this but does not define a specific way that the client should attempt to divine any specific error messaging from such a response.
In reality, connections should be rejected at this stage only when you are rejecting a user from a disallowed origin (i.e. someone from another website is trying to connect users to your websocket server without permission) or when a user otherwise does not have permission to connect (i.e. they are not logged in). The latter case should be handled by other code on your site: a user should not be able to attempt to connect the websocket connection if they are not logged in.
The code and reason that WebSocket-Node allow you to specify here are an HTTP Status code (e.g. 404, 500, etc.) and a reason to include as a non-standard "X-WebSocket-Reject-Reason" HTTP header in the response. It is mostly useful when analyzing the connection with a packet sniffer, such as WireShark. No browser has any facility for providing rejection codes or reasons to the client-side JavaScript code when a connection is rejected in this way, because it's not provided for in the WebSocket specification.