-
-
Save nginx-gists/85be3fdc8c9a55c446988bbbfdcf0a1d to your computer and use it in GitHub Desktop.
function parseCSKVpairs(cskvpairs, key) { | |
if ( cskvpairs.length ) { | |
var kvpairs = cskvpairs.split(','); | |
for ( var i = 0; i < kvpairs.length; i++ ) { | |
var kvpair = kvpairs[i].split('='); | |
if ( kvpair[0].toUpperCase() == key ) { | |
return kvpair[1]; | |
} | |
} | |
} | |
return ""; // Default condition | |
} | |
var client_messages = 1; | |
var client_id_str = "-"; | |
function getClientId(s) { | |
s.on('upload', function (data, flags) { | |
if ( data.length == 0 ) { // Initial calls may contain no data, so | |
s.log("No buffer yet"); // ask that we get called again | |
//s.done(1); // (supposing that code=1 means that) | |
return; | |
} else if ( client_messages == 1 ) { // Connect is first packet from the client | |
// Connect packet is 1, using upper 4 bits (00010000 to 00011111) | |
var packet_type_flags_byte = data.charCodeAt(0); | |
s.log("MQTT packet type+flags = " + packet_type_flags_byte.toString()); | |
if ( packet_type_flags_byte >= 16 && packet_type_flags_byte < 32 ) { | |
// Calculate remaining length with variable encoding scheme | |
var multiplier = 1; | |
var remaining_len_val = 0; | |
var remaining_len_byte; | |
for (var remaining_len_pos = 1; remaining_len_pos < 5; remaining_len_pos++ ) { | |
remaining_len_byte = data.charCodeAt(remaining_len_pos); | |
if ( remaining_len_byte == 0 ) break; // Stop decoding on 0 | |
remaining_len_val += (remaining_len_byte & 127) * multiplier; | |
multiplier *= 128; | |
} | |
// Extract ClientId based on length defined by 2-byte encoding | |
var payload_offset = remaining_len_pos + 12; // Skip fixed header | |
var client_id_len_msb = data.charCodeAt(payload_offset).toString(16); | |
var client_id_len_lsb = data.charCodeAt(payload_offset + 1).toString(16); | |
if ( client_id_len_lsb.length < 2 ) client_id_len_lsb = "0" + client_id_len_lsb; | |
var client_id_len_int = parseInt(client_id_len_msb + client_id_len_lsb, 16); | |
client_id_str = data.substr(payload_offset + 2, client_id_len_int); | |
s.log("ClientId value = " + client_id_str); | |
// If client authentication then check certificate CN matches ClientId | |
var client_cert_cn = parseCSKVpairs(s.variables.ssl_client_s_dn, "CN"); | |
if ( client_cert_cn.length && client_cert_cn != client_id_str ) { | |
s.log("Client certificate common name (" + client_cert_cn + ") does not match client ID"); | |
s.deny(); // Close the TCP connection (logged as 500) | |
} | |
} else { | |
s.log("Received unexpected MQTT packet type+flags: " + packet_type_flags_byte.toString()); | |
} | |
} | |
client_messages++; | |
s.allow(); | |
}); | |
} | |
function setClientId(s) { | |
return client_id_str; | |
} |
ssl_verify_client on; # Clients must supply certificate | |
ssl_verify_depth 2; # In case of intermediate CA | |
ssl_client_certificate /etc/nginx/certs/cafile.pem; # Issuer of client certificates | |
# vim: syntax=nginx |
server { | |
listen 8883 ssl; # MQTT secure port | |
preread_buffer_size 1k; | |
js_preread getClientId; | |
ssl_certificate /etc/nginx/certs/my_cert.crt; | |
ssl_certificate_key /etc/nginx/certs/my_cert.key; | |
ssl_ciphers HIGH:!aNULL:!MD5; | |
ssl_session_cache shared:SSL:128m; # 128MB ~= 500k sessions | |
ssl_session_tickets on; | |
ssl_session_timeout 8h; | |
proxy_pass hive_mq; | |
proxy_connect_timeout 1s; | |
access_log /var/log/nginx/mqtt_access.log mqtt; | |
error_log /var/log/nginx/mqtt_error.log info; # nginScript debug logging | |
} | |
# vim: syntax=nginx |
Regarding the comment made 27-Mar-2019 by MRobertEvers: the getClientId
function has now been updated to use the s
object as refactored in NGINX JavaScript 0.2.4. Our apologies for any confusion or inconvenience caused by the delayed update.
I had to make two changes to get this to work.
First, line 45:
client_id_str = data.substr(payload_offset + 2, client_id_len_int);
This no longer works because it strips the first two characters off of the client_id.
I modified it to:
client_id_str = data.substr(payload_offset, client_id_len_int);
Also, the parseCSKVpairs function is no longer working when a client certificate isn't passed. The cskvpairs.length fails because it is undefined. I modified line 2 to include the following:
if ( undefined !== cskvpairs && cskvpairs.length ) {
Hopefully this helps others. Note, I'm not a javascript developer so there may be better ways to handle this :)
Maybe I am doing something wrong, but this does not appear to work. When I connect with an MQTT client ID that does not match the certificate common name, I see this error message confirming the mismatched was detected:
Client certificate common name xxx does not match client ID
...which also means s.deny()
is called as well. However, return
is not called, so flow continues to eventually also call s.allow()
before returning (and after s.deny()
) is called, and this means the connection succeeds with the client ID / CN mismatch. If I am a return after s.deny()
, then it works because s.allow()
is not called.
Am I doing something wrong? Seems like this has worked for others, but it clearly does not for me.
To anyone wondering why it does not work in some scenarios: MQTT 5 has a different structure of the Connect
packet and some fields are dynamically populated.
This one works with MQTT 5: https://gist.github.com/iRevive/a441c7f880bba10ca9a3bb7ee686d222
The function
getClientId
was broken with the release of 0.2.4. (September 2018)For those of you wondering how to fix it, use