Added MQTT authentication.
This commit is contained in:
parent
5b69657b11
commit
4f014fc218
|
@ -241,6 +241,21 @@ module.exports.CertificateOperations = function (parent) {
|
||||||
return obj.pki.getPublicKeyFingerprint(publickey, { encoding: "hex", md: obj.forge.md.sha384.create() });
|
return obj.pki.getPublicKeyFingerprint(publickey, { encoding: "hex", md: obj.forge.md.sha384.create() });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Return the SHA384 hash of the certificate, return hex
|
||||||
|
obj.getCertHashSha1 = function (cert) {
|
||||||
|
try {
|
||||||
|
var md = obj.forge.md.sha1.create();
|
||||||
|
md.update(obj.forge.asn1.toDer(obj.pki.certificateToAsn1(obj.pki.certificateFromPem(cert))).getBytes());
|
||||||
|
return md.digest().toHex();
|
||||||
|
} catch (ex) {
|
||||||
|
// If this is not an RSA certificate, hash the raw PKCS7 out of the PEM file
|
||||||
|
var x1 = cert.indexOf('-----BEGIN CERTIFICATE-----'), x2 = cert.indexOf('-----END CERTIFICATE-----');
|
||||||
|
if ((x1 >= 0) && (x2 > x1)) {
|
||||||
|
return obj.crypto.createHash('sha1').update(Buffer.from(cert.substring(x1 + 27, x2), 'base64')).digest('hex');
|
||||||
|
} else { console.log('ERROR: Unable to decode certificate.'); return null; }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Return the SHA384 hash of the certificate, return hex
|
// Return the SHA384 hash of the certificate, return hex
|
||||||
obj.getCertHash = function (cert) {
|
obj.getCertHash = function (cert) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -826,7 +826,7 @@ function CreateMeshCentralServer(config, args) {
|
||||||
obj.apfserver = require('./apfserver.js').CreateApfServer(obj, obj.db, obj.args);
|
obj.apfserver = require('./apfserver.js').CreateApfServer(obj, obj.db, obj.args);
|
||||||
|
|
||||||
// Create MQTT Broker to hook into webserver and mpsserver
|
// Create MQTT Broker to hook into webserver and mpsserver
|
||||||
if (obj.config.settings.mqtt != null) { obj.mqttbroker = require("./mqttbroker.js").CreateMQTTBroker(obj, obj.db, obj.args); }
|
if ((typeof obj.config.settings.mqtt == 'object') && (typeof obj.config.settings.mqtt.auth == 'object') && (typeof obj.config.settings.mqtt.auth.keyid == 'string') && (typeof obj.config.settings.mqtt.auth.key == 'string')) { obj.mqttbroker = require("./mqttbroker.js").CreateMQTTBroker(obj, obj.db, obj.args); }
|
||||||
|
|
||||||
// Start the web server and if needed, the redirection web server.
|
// Start the web server and if needed, the redirection web server.
|
||||||
obj.webserver = require('./webserver.js').CreateWebServer(obj, obj.db, obj.args, obj.certificates);
|
obj.webserver = require('./webserver.js').CreateWebServer(obj, obj.db, obj.args, obj.certificates);
|
||||||
|
|
48
meshuser.js
48
meshuser.js
|
@ -2876,6 +2876,54 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'getmqttlogin': {
|
||||||
|
var err = null;
|
||||||
|
if (parent.parent.mqttbroker == null) { err = 'MQTT not supported on this server'; }
|
||||||
|
if (common.validateString(command.nodeid, 1, 1024) == false) { err = 'Invalid nodeid'; } // Check the nodeid
|
||||||
|
|
||||||
|
// Handle any errors
|
||||||
|
if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'getmqttlogin', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
|
||||||
|
|
||||||
|
var nodeid = command.nodeid;
|
||||||
|
if ((nodeid.split('/').length == 3) && (nodeid.split('/')[1] == domain.id)) { // Validate the domain, operation only valid for current domain
|
||||||
|
// Get the device
|
||||||
|
db.Get(nodeid, function (err, nodes) {
|
||||||
|
if ((nodes == null) || (nodes.length != 1)) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'getmqttlogin', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } return; } }
|
||||||
|
var node = nodes[0];
|
||||||
|
|
||||||
|
// Get the device group for this node
|
||||||
|
var mesh = parent.meshes[node.meshid];
|
||||||
|
if (mesh) {
|
||||||
|
// Check if this user has rights to do this
|
||||||
|
if ((mesh.links[user._id] != null) && (mesh.links[user._id].rights == 0xFFFFFFFF)) {
|
||||||
|
var token = parent.parent.mqttbroker.generateLogin(mesh._id, node._id);
|
||||||
|
var r = { action: 'getmqttlogin', responseid: command.responseid, nodeid: node._id, user: token.user, pass: token.pass };
|
||||||
|
const serverName = parent.getWebServerName(domain);
|
||||||
|
|
||||||
|
// Add MPS URL
|
||||||
|
if (parent.parent.mpsserver != null) {
|
||||||
|
r.mpsCertHashSha384 = parent.parent.certificateOperations.getCertHash(parent.parent.mpsserver.certificates.mps.cert);
|
||||||
|
r.mpsCertHashSha1 = parent.parent.certificateOperations.getCertHashSha1(parent.parent.mpsserver.certificates.mps.cert);
|
||||||
|
r.mpsUrl = 'mqtts://' + serverName + ':' + ((args.mpsaliasport != null) ? args.mpsaliasport : args.mpsport) + '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add WS URL
|
||||||
|
var xdomain = (domain.dns == null) ? domain.id : '';
|
||||||
|
if (xdomain != '') xdomain += "/";
|
||||||
|
var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
|
||||||
|
r.wsUrl = "ws" + (args.notls ? '' : 's') + "://" + serverName + ":" + httpsPort + "/" + xdomain + "mqtt.ashx";
|
||||||
|
r.wsTrustedCert = parent.isTrustedCert(domain);
|
||||||
|
|
||||||
|
try { ws.send(JSON.stringify(r)); } catch (ex) { }
|
||||||
|
} else {
|
||||||
|
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'getmqttlogin', responseid: command.responseid, result: 'Unable to perform this operation' })); } catch (ex) { } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'amt': {
|
case 'amt': {
|
||||||
if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
|
if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
|
||||||
if (common.validateInt(command.mode, 0, 3) == false) break; // Check connection mode
|
if (common.validateInt(command.mode, 0, 3) == false) break; // Check connection mode
|
||||||
|
|
|
@ -16,26 +16,46 @@ module.exports.CreateMQTTBroker = function (parent, db, args) {
|
||||||
obj.handle = obj.aedes.handle;
|
obj.handle = obj.aedes.handle;
|
||||||
obj.connections = {}; // NodesID --> client array
|
obj.connections = {}; // NodesID --> client array
|
||||||
|
|
||||||
|
// Generate a username and password for MQTT login
|
||||||
|
obj.generateLogin = function (meshid, nodeid) {
|
||||||
|
const meshidsplit = meshid.split('/'), nodeidsplit = nodeid.split('/');
|
||||||
|
const xmeshid = meshidsplit[2], xnodeid = nodeidsplit[2], xdomainid = meshidsplit[1];
|
||||||
|
const username = 'MCAuth1:' + xnodeid + ':' + xmeshid + ':' + xdomainid;
|
||||||
|
const nonce = Buffer.from(parent.crypto.randomBytes(9), 'binary').toString('base64');
|
||||||
|
return { meshid: meshid, nodeid: nodeid, user: username, pass: parent.config.settings.mqtt.auth.keyid + ':' + nonce + ':' + parent.crypto.createHash('sha384').update(username + ':' + nonce + ':' + parent.config.settings.mqtt.auth.key).digest("base64") };
|
||||||
|
}
|
||||||
|
|
||||||
// Connection Authentication
|
// Connection Authentication
|
||||||
obj.aedes.authenticate = function (client, username, password, callback) {
|
obj.aedes.authenticate = function (client, username, password, callback) {
|
||||||
// TODO: add authentication handler
|
obj.parent.debug("mqtt", "Authentication User:" + username + ", Pass:" + password.toString() + ", ClientID:" + client.id + ", " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip));
|
||||||
obj.parent.debug("mqtt", "Authentication with " + username + ":" + password + ":" + client.id + ", " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip));
|
|
||||||
|
|
||||||
|
// Parse the username and password
|
||||||
var usersplit = username.split(':');
|
var usersplit = username.split(':');
|
||||||
if (usersplit.length != 5) { callback(null, false); return; }
|
var passsplit = password.toString().split(':');
|
||||||
|
if ((usersplit.length !== 4) || (passsplit.length !== 3)) { obj.parent.debug("mqtt", "Invalid user/pass format, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip)); callback(null, false); return; }
|
||||||
|
if (usersplit[0] !== 'MCAuth1') { obj.parent.debug("mqtt", "Invalid auth method, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip)); callback(null, false); return; }
|
||||||
|
|
||||||
|
// Check authentication
|
||||||
|
if (passsplit[0] !== parent.config.settings.mqtt.auth.keyid) { obj.parent.debug("mqtt", "Invalid auth keyid, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip)); callback(null, false); return; }
|
||||||
|
if (parent.crypto.createHash('sha384').update(username + ':' + passsplit[1] + ':' + parent.config.settings.mqtt.auth.key).digest("base64") !== passsplit[2]) { obj.parent.debug("mqtt", "Invalid password, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip)); callback(null, false); return; }
|
||||||
|
|
||||||
// Setup the identifiers
|
// Setup the identifiers
|
||||||
var xnodeid = usersplit[1];
|
const xnodeid = usersplit[1];
|
||||||
var xmeshid = usersplit[2];
|
var xmeshid = usersplit[2];
|
||||||
var xdomainid = usersplit[3];
|
const xdomainid = usersplit[3];
|
||||||
|
|
||||||
|
// Check the domain
|
||||||
|
if ((typeof client.conn.xdomain == 'object') && (xdomainid != client.conn.xdomain.id)) { obj.parent.debug("mqtt", "Invalid domain connection, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip)); callback(null, false); return; }
|
||||||
|
|
||||||
// Convert meshid from HEX to Base64 if needed
|
// Convert meshid from HEX to Base64 if needed
|
||||||
if (xmeshid.length == 96) { xmeshid = Buffer.from(xmeshid, 'hex').toString('base64'); }
|
if (xmeshid.length === 96) { xmeshid = Buffer.from(xmeshid, 'hex').toString('base64'); }
|
||||||
if ((xmeshid.length != 64) || (xnodeid.length != 64)) { callback(null, false); return; }
|
if ((xmeshid.length !== 64) || (xnodeid.length != 64)) { callback(null, false); return; }
|
||||||
|
|
||||||
client.xdbNodeKey = 'node/' + xdomainid + '/' + xnodeid;
|
client.xdbNodeKey = 'node/' + xdomainid + '/' + xnodeid;
|
||||||
client.xdbMeshKey = 'mesh/' + xdomainid + '/' + xmeshid;
|
client.xdbMeshKey = 'mesh/' + xdomainid + '/' + xmeshid;
|
||||||
|
|
||||||
|
//console.log(obj.generateLogin(client.xdbMeshKey, client.xdbNodeKey));
|
||||||
|
|
||||||
// Check if this node exists in the database
|
// Check if this node exists in the database
|
||||||
db.Get(client.xdbNodeKey, function (err, nodes) {
|
db.Get(client.xdbNodeKey, function (err, nodes) {
|
||||||
if ((nodes == null) || (nodes.length != 1)) { callback(null, false); return; } // Node does not exist
|
if ((nodes == null) || (nodes.length != 1)) { callback(null, false); return; } // Node does not exist
|
||||||
|
@ -75,6 +95,7 @@ module.exports.CreateMQTTBroker = function (parent, db, args) {
|
||||||
// Check if a client can publish a packet
|
// Check if a client can publish a packet
|
||||||
obj.aedes.authorizePublish = function (client, packet, callback) {
|
obj.aedes.authorizePublish = function (client, packet, callback) {
|
||||||
// TODO: add authorized publish control
|
// TODO: add authorized publish control
|
||||||
|
//console.log(packet);
|
||||||
obj.parent.debug("mqtt", "AuthorizePublish, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip));
|
obj.parent.debug("mqtt", "AuthorizePublish, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip));
|
||||||
callback(null);
|
callback(null);
|
||||||
}
|
}
|
||||||
|
@ -82,13 +103,14 @@ module.exports.CreateMQTTBroker = function (parent, db, args) {
|
||||||
// Check if a client can publish a packet
|
// Check if a client can publish a packet
|
||||||
obj.aedes.authorizeSubscribe = function (client, sub, callback) {
|
obj.aedes.authorizeSubscribe = function (client, sub, callback) {
|
||||||
// TODO: add subscription control here
|
// TODO: add subscription control here
|
||||||
obj.parent.debug("mqtt", "AuthorizeSubscribe, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip));
|
obj.parent.debug("mqtt", "AuthorizeSubscribe \"" + sub.topic + "\", " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip));
|
||||||
callback(null, sub);
|
callback(null, sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a client can publish a packet
|
// Check if a client can forward a packet
|
||||||
obj.aedes.authorizeForward = function (client, packet) {
|
obj.aedes.authorizeForward = function (client, packet) {
|
||||||
// TODO: add forwarding control
|
// TODO: add forwarding control
|
||||||
|
//console.log(packet);
|
||||||
obj.parent.debug("mqtt", "AuthorizeForward, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip));
|
obj.parent.debug("mqtt", "AuthorizeForward, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip));
|
||||||
//return packet;
|
//return packet;
|
||||||
return packet;
|
return packet;
|
||||||
|
|
|
@ -2274,6 +2274,24 @@
|
||||||
QV('agentInvitationLinkDiv', true);
|
QV('agentInvitationLinkDiv', true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'getmqttlogin': {
|
||||||
|
if ((currentNode == null) || (currentNode._id != message.nodeid) || (xxdialogMode != null)) return;
|
||||||
|
var x = "These settings can be used to connect MQTT for this device.<br /><br />";
|
||||||
|
delete message.action;
|
||||||
|
delete message.nodeid;
|
||||||
|
x += '<textarea readonly=readonly style=width:100%;resize:none;height:100px;overflow:auto;font-size:12px readonly>' + JSON.stringify(message) + '</textarea>';
|
||||||
|
/*
|
||||||
|
x += addHtmlValue('Username', '<input style=width:230px readonly value="' + message.user + '" />');
|
||||||
|
x += addHtmlValue('Password', '<input style=width:230px readonly value="' + message.pass + '" />');
|
||||||
|
x += addHtmlValue('WS URL', '<input style=width:230px readonly value="' + message.wsUrl + '" />');
|
||||||
|
if (message.mpsUrl && message.mpsCertHash) {
|
||||||
|
x += addHtmlValue('MPS URL', '<input style=width:230px readonly value="' + message.mpsUrl + '" />');
|
||||||
|
x += addHtmlValue('MPS Cert Hash', '<input style=width:230px readonly value="' + message.mpsCertHash + '" />');
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
setDialogMode(2, "MQTT Credentials", 1, null, x);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'stopped': { // Server is stopping.
|
case 'stopped': { // Server is stopping.
|
||||||
// Disconnect
|
// Disconnect
|
||||||
autoReconnect = false;
|
autoReconnect = false;
|
||||||
|
@ -4280,6 +4298,9 @@
|
||||||
x += '<a href=# onclick=p10clickOnce("' + node._id + '","WSCP",22) title="Requires Microsoft ClickOnce support in your browser.">WinSCP</a> ';
|
x += '<a href=# onclick=p10clickOnce("' + node._id + '","WSCP",22) title="Requires Microsoft ClickOnce support in your browser.">WinSCP</a> ';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MQTT options
|
||||||
|
if ((meshrights == 0xFFFFFFFF) && (features & 0x00400000)) { x += '<a href=# onclick=p10showMqttLoginDialog("' + node._id + '") title="Get MQTT login credentials for this device.">MQTT Login</a> '; }
|
||||||
x += '</div><br>'
|
x += '</div><br>'
|
||||||
|
|
||||||
QH('p10html3', x);
|
QH('p10html3', x);
|
||||||
|
@ -4664,6 +4685,9 @@
|
||||||
setDialogMode(2, "MeshCentral Router", 1, null, x, "fileDownload");
|
setDialogMode(2, "MeshCentral Router", 1, null, x, "fileDownload");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request MQTT login credentials
|
||||||
|
function p10showMqttLoginDialog(nodeid) { meshserver.send({ action: 'getmqttlogin', nodeid: nodeid }); }
|
||||||
|
|
||||||
// Show MeshCmd dialog
|
// Show MeshCmd dialog
|
||||||
function p10showMeshCmdDialog(mode, nodeid) {
|
function p10showMeshCmdDialog(mode, nodeid) {
|
||||||
if (xxdialogMode) return;
|
if (xxdialogMode) return;
|
||||||
|
|
|
@ -1505,6 +1505,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { features += 0x00080000; } // LDAP or SSPI in use, warn that users must login first before adding a user to a group.
|
if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { features += 0x00080000; } // LDAP or SSPI in use, warn that users must login first before adding a user to a group.
|
||||||
if (domain.amtacmactivation) { features += 0x00100000; } // Intel AMT ACM activation/upgrade is possible
|
if (domain.amtacmactivation) { features += 0x00100000; } // Intel AMT ACM activation/upgrade is possible
|
||||||
if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
|
if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
|
||||||
|
if (parent.mqttbroker != null) { features += 0x00400000; } // This server supports MQTT channels
|
||||||
|
|
||||||
// Create a authentication cookie
|
// Create a authentication cookie
|
||||||
const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: cleanRemoteAddr(req.ip) }, obj.parent.loginCookieEncryptionKey);
|
const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: cleanRemoteAddr(req.ip) }, obj.parent.loginCookieEncryptionKey);
|
||||||
|
@ -1617,7 +1618,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return true if it looks like we are using a real TLS certificate.
|
// Return true if it looks like we are using a real TLS certificate.
|
||||||
function isTrustedCert(domain) {
|
obj.isTrustedCert = function(domain) {
|
||||||
if (obj.args.notls == true) return false; // We are not using TLS, so not trusted cert.
|
if (obj.args.notls == true) return false; // We are not using TLS, so not trusted cert.
|
||||||
if ((domain != null) && (typeof domain.trustedcert == 'boolean')) return domain.trustedcert; // If the status of the cert specified, use that.
|
if ((domain != null) && (typeof domain.trustedcert == 'boolean')) return domain.trustedcert; // If the status of the cert specified, use that.
|
||||||
if (typeof obj.args.trustedcert == 'boolean') return obj.args.trustedcert; // If the status of the cert specified, use that.
|
if (typeof obj.args.trustedcert == 'boolean') return obj.args.trustedcert; // If the status of the cert specified, use that.
|
||||||
|
@ -2886,7 +2887,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Type': 'text/plain', 'Content-Disposition': 'attachment; filename="' + scriptInfo.rname + '"' });
|
res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Type': 'text/plain', 'Content-Disposition': 'attachment; filename="' + scriptInfo.rname + '"' });
|
||||||
var data = scriptInfo.data;
|
var data = scriptInfo.data;
|
||||||
var cmdoptions = { wgetoptionshttp: '', wgetoptionshttps: '', curloptionshttp: '-L ', curloptionshttps: '-L ' }
|
var cmdoptions = { wgetoptionshttp: '', wgetoptionshttps: '', curloptionshttp: '-L ', curloptionshttps: '-L ' }
|
||||||
if (isTrustedCert(domain) != true) {
|
if (obj.isTrustedCert(domain) != true) {
|
||||||
cmdoptions.wgetoptionshttps += '--no-check-certificate ';
|
cmdoptions.wgetoptionshttps += '--no-check-certificate ';
|
||||||
cmdoptions.curloptionshttps += '-k ';
|
cmdoptions.curloptionshttps += '-k ';
|
||||||
}
|
}
|
||||||
|
@ -3350,7 +3351,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
// For example: https://localhost/createLoginToken.ashx?user=admin&pass=admin&a=3
|
// For example: https://localhost/createLoginToken.ashx?user=admin&pass=admin&a=3
|
||||||
// It's not advised to use this to create login tokens since the URL is often logged and you got credentials in the URL.
|
// It's not advised to use this to create login tokens since the URL is often logged and you got credentials in the URL.
|
||||||
// Since it's bad, it's only offered when an untrusted certificate is used as a way to help developers get started.
|
// Since it's bad, it's only offered when an untrusted certificate is used as a way to help developers get started.
|
||||||
if (isTrustedCert() == false) {
|
if (obj.isTrustedCert() == false) {
|
||||||
obj.app.get(url + 'createLoginToken.ashx', function (req, res) {
|
obj.app.get(url + 'createLoginToken.ashx', function (req, res) {
|
||||||
// A web socket session can be authenticated in many ways (Default user, session, user/pass and cookie). Check authentication here.
|
// A web socket session can be authenticated in many ways (Default user, session, user/pass and cookie). Check authentication here.
|
||||||
if ((req.query.user != null) && (req.query.pass != null)) {
|
if ((req.query.user != null) && (req.query.pass != null)) {
|
||||||
|
|
Loading…
Reference in New Issue