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.
Related
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)
}
}
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.
I have a simple go web server which serves on port localhost:8080 an public folder containing both an html file as well as a client script with websocket logic.
in my main.go file
listener, err := net.listen("tcp", "localhost:8080")
if err != nil {
log.Fatal(err)
}
//full code in gist https://gist.github.com/Kielan/98706aaf5dc0be9d6fbe
then in my client script
try {
var sock = new WebSocket("ws://127.0.0.1:8080");
console.log("Websocket - status: " + sock.readyState);
sock.onopen = function(message) {
console.log("CONNECTION opened..." + this.readyState);
//onmessage, onerr, onclose, ect...
}
I get the error in chrome
WebSocket connection to 'ws://127.0.0.1:8080/' failed: Error during WebSocket handshake: Unexpected response code: 200
and Firefox
Firefox can't establish a connection to the server at ws://127.0.0.1:8080/.
I found this article referring to node.js indicating to add /websocket to my client websocket string, though it did not solve the problem and resulted in a 404
I thought response code 200 is good, do I need to convert the request to a websocket somehow and maybe it is defaulting to http? If so how can I do this?
Like JimB pointed out, you are not handling http nor websocket connections yet.
You can do websocket handling with the package github.com/gorilla/websocket
This is how a simple setup could look like:
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
// wsHandler implements the Handler Interface
type wsHandler struct{}
func main() {
router := http.NewServeMux()
router.Handle("/", http.FileServer(http.Dir("./webroot"))) //handles static html / css etc. under ./webroot
router.Handle("/ws", wsHandler{}) //handels websocket connections
//serving
log.Fatal(http.ListenAndServe("localhost:8080", router))
}
func (wsh wsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// upgrader is needed to upgrade the HTTP Connection to a websocket Connection
upgrader := &websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
//Upgrading HTTP Connection to websocket connection
wsConn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("error upgrading %s", err)
return
}
//handle your websockets with wsConn
}
In your Javascript you then need var sock = new WebSocket("ws://localhost/ws:8080"); obviously.
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.
I'm doing a simple UDP "send" using Node's inbuilt datagram UDP socket :
http://nodejs.org/docs/v0.3.1/api/dgram.html
The destination of the message is a domain name that has to be resolved by DNS before transmission.. node.js handles this.
In the event that DNS resolution fails dgram throws a "ENOTFOUND Domain Not Found" error and passes it to the callback that I've registered.
My code is like this:
client = dgram.createSocket("udp4");
client.send(message,
0,
message.length,
this.port,
this.address,
function(err, bytes) {
if (err) {
//get rid of error??
}
}
);
client.close();
I'm not particularly interested in the error.. if it fails, it fails, its not important to the business rules of the application. I'll log it to console for completeness.. BUT I cant stop this exception walking back up the stack and bringing down the application. How do I handle this error?
I dont wish to put a global uhandled exception handler in place just for this. I've tried rethrowing the error inside the callback within a Try/Except handler.. that didn't work.
Any thoughts?
Thanks for reading.
You need to listen for an error event from the socket. If you don't, then node will convert this to an exception. Do not try to handle this with uncaughtException, because the only safe thing to do from uncaughtException is log then exit.
Here is an example of listening for error and causing an intentional DNS error:
var dgram = require('dgram');
var message = new Buffer("Some bytes");
var client = dgram.createSocket("udp4");
client.on("error", function (err) {
console.log("Socket error: " + err);
});
client.send(message, 0, message.length, 41234, "1.2.3.4.5");
This should print:
Socket error: Error: ENOTFOUND, Domain name not found
And the program will continue running.