nginx / njs

A subset of JavaScript language to use in nginx
http://nginx.org/en/docs/njs/
BSD 2-Clause "Simplified" License
832 stars 116 forks source link

closed request followed by 400 response #714

Open dsl400 opened 1 month ago

dsl400 commented 1 month ago

The TLDR is ... for some unknow reason my websocket client was receiving the full 400 html response and I could not do anything about it.

While I do understand that this is a very wild experiment I thought "y'all" should know

the following is my attempt at sending a "known close status" to a websocket request that was not accompanied by the correct authentication headers

how I ended up in this mess ? well, according to this post a websocket client does not have access to the underlying http connection and can not read the status.

the goal is to accept the connection and send a known close status that can be received on the client and used as an indicator that the authentication failed.

`function websocket_reject(r) { var key = r.headersIn['Sec-WebSocket-Key']; r.log('Received WebSocket key: ' + key);

var crypto = require('crypto');
var accept = crypto.createHash('sha1')
                   .update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
                   .digest('base64');

r.headersOut['Upgrade'] = 'websocket';
r.headersOut['Sec-WebSocket-Accept'] = accept;
r.headersOut['Connection'] = 'Upgrade';
r.headersOut['Sec-WebSocket-Protocol'] = 'mqtt';
r.status = 101;
r.sendHeader();

var closeFrameBytes = [0x88, 0x02, 0x03, 0xe8];
var closeFrame = '';
for (var i = 0; i < closeFrameBytes.length; i++) {
    closeFrame += String.fromCharCode(closeFrameBytes[i]);
}

r.send(closeFrame);

r.finish();

}

export default { websocket_reject };`

jo-carter commented 1 month ago

@dsl400 The same behavior can be seen when using return directive and add_header in nginx, so this behavior isn't specific to njs.

Nginx only supports the proxying of websocket connections with proxy module, and not acting as a websocket server via other content handlers (such as js_content).

the goal is to accept the connection and send a known close status that can be received on the client and used as an indicator that the authentication failed.

Likely the best way to achieve this is is within the nginx configuration itself, without njs; simply proxy_pass to a different endpoint (an actual websocket server) if authentication fails - that endpoint should just complete websocket opening handshake and then send the closing frame.

You could of course fulfill the last part by writing a fake websocket server with stream js_filter if you aren't able to use anything other than nginx in your set up.