#!/usr/bin/env node

const crypto = require('crypto');
var settings = {};
const args = require('minimist')(process.argv.slice(2));
const possibleCommands = ['listusers', 'listdevicegroups', 'listdevices', 'listusersofdevicegroup', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'broadcast', 'addusertodevicegroup', 'removeuserfromdevicegroup', 'sendinviteemail', 'generateinvitelink', 'config'];
//console.log(args);

if (args['_'].length == 0) {
    console.log("MeshCtrl performs command line actions on a MeshCentral server.");
    console.log("Information at: https://meshcommander.com/meshcentral");
    console.log("No action specified, use MeshCtrl like this:\r\n\r\n  meshctrl [action] [arguments]\r\n");
    console.log("Supported actions:");
    console.log("  Help [action]             - Get help on an action.");
    console.log("  ServerInfo                - Show server information.");
    console.log("  UserInfo                  - Show user information.");
    console.log("  ListUsers                 - List user accounts.");
    console.log("  ListDevices               - List devices.");
    console.log("  ListDeviceGroups          - List device groups.");
    console.log("  ListUsersOfDeviceGroup    - List the users in a device group.");
    console.log("  Config                    - Perform operation on config.json file.");
    console.log("  AddUser                   - Create a new user account.");
    console.log("  RemoveUser                - Delete a user account.");
    console.log("  AddDeviceGroup            - Create a new device group.");
    console.log("  RemoveDeviceGroup         - Delete a device group.");
    console.log("  AddUserToDeviceGroup      - Add a user to a device group.");
    console.log("  RemoveUserFromDeviceGroup - Remove a user from a device group.");
    console.log("  SendInviteEmail           - Send an agent install invitation email.");
    console.log("  GenerateInviteLink        - Create an invitation link.");
    console.log("  Broadcast                 - Display a message to all online users.");
    console.log("\r\nSupported login arguments:");
    console.log("  --url [wss://server]      - Server url, wss://localhost:443 is default.");
    console.log("  --loginuser [username]    - Login username, admin is default.");
    console.log("  --loginpass [password]    - Login password.");
    console.log("  --token [number]          - 2nd factor authentication token.");
    console.log("  --loginkey [hex]          - Server login key in hex.");
    console.log("  --loginkeyfile [file]     - File containing server login key in hex.");
    console.log("  --domain [domainid]       - Domain id, default is empty.");
    return;
} else {
    settings.cmd = args['_'][0].toLowerCase();
    if ((possibleCommands.indexOf(settings.cmd) == -1) && (settings.cmd != 'help')) { console.log("Invalid command. Possible commands are: " + possibleCommands.join(', ') + '.'); return; }
    //console.log(settings.cmd);

    var ok = false;
    switch (settings.cmd) {
        case 'config': { performConfigOperations(args); return; }
        case 'serverinfo': { ok = true; break; }
        case 'userinfo': { ok = true; break; }
        case 'listusers': { ok = true; break; }
        case 'listdevicegroups': { ok = true; break; }
        case 'listdevices': { ok = true; break; }
        case 'listusersofdevicegroup': {
            if (args.id == null) { console.log("Missing group id, use --id [groupid]"); }
            else { ok = true; }
            break;
        }
        case 'addusertodevicegroup': {
            if (args.userid == null) { console.log("Add user to group missing useid, use --userid [userid]"); }
            else if (args.id == null) { console.log("Add user to group missing group id, use --id [groupid]"); }
            else { ok = true; }
            break;
        }
        case 'removeuserfromdevicegroup': {
            if (args.userid == null) { console.log("Remove user from group missing useid, use --userid [userid]"); }
            else if (args.id == null) { console.log("Remove user from group missing group id, use --id [groupid]"); }
            else { ok = true; }
            break;
        }
        case 'adddevicegroup': {
            if (args.name == null) { console.log("Message group name, use --name [name]"); }
            else { ok = true; }
            break;
        }
        case 'removedevicegroup': {
            if (args.id == null) { console.log("Message group identifier, use --id [identifier]"); }
            else { ok = true; }
            break;
        }
        case 'broadcast': {
            if (args.msg == null) { console.log("Message missing, use --msg [message]"); }
            else { ok = true; }
            break;
        }
        case 'adduser': {
            if (args.user == null) { console.log("New account name missing, use --user [name]"); }
            else if ((args.pass == null) && (args.randompass == null)) { console.log("New account password missing, use --pass [password] or --randompass"); }
            else { ok = true; }
            break;
        }
        case 'removeuser': {
            if (args.userid == null) { console.log("Remove account userid missing, use --userid [id]"); }
            else { ok = true; }
            break;
        }
        case 'sendinviteemail': {
            if (args.id == null) { console.log("Device group identifier id missing, use --id [groupid]"); }
            else if (args.email == null) { console.log("Device email is missing, use --email [email]"); }
            else { ok = true; }
            break;
        }
        case 'generateinvitelink': {
            if (args.id == null) { console.log("Device group identifier id missing, use --id [groupid]"); }
            else if (args.hours == null) { console.log("Invitation validity period missing, use --hours [hours]"); }
            else { ok = true; }
            break;
        }
        case 'help': {
            if (args['_'].length < 2) {
                console.log("Get help on an action. Type:\r\n\r\n  help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.');
            } else {
                switch (args['_'][1].toLowerCase()) {
                    case 'config': {
                        displayConfigHelp();
                        break;
                    }
                    case 'sendinviteemail': {
                        console.log("Send invitation email with instructions on how to install the mesh agent for a specific device group. Example usage:\r\n");
                        console.log("  MeshCtrl SendInviteEmail --id devicegroupid --email user@sample.com");
                        console.log("\r\nRequired arguments:\r\n");
                        console.log("  --id [groupid]         - Device group identifier.");
                        console.log("  --email [email]        - Email address.");
                        break;
                    }
                    case 'generateinvitelink': {
                        console.log("Generate a agent invitation URL for a given group. Example usage:\r\n");
                        console.log("  MeshCtrl GenerateInviteLink --id devicegroupid --hours 24");
                        console.log("  MeshCtrl GenerateInviteLink --id devicegroupid --hours 0");
                        console.log("\r\nRequired arguments:\r\n");
                        console.log("  --id [groupid]         - Device group identifier.");
                        console.log("  --hours [hours]        - Validity period in hours or 0 for infinit.");
                        break;
                    }
                    case 'serverinfo': {
                        console.log("Get information on the MeshCentral server, Example usages:\r\n");
                        console.log("  MeshCtrl ServerInfo --loginuser myaccountname --loginpass mypassword");
                        console.log("  MeshCtrl ServerInfo --loginuser myaccountname --loginkeyfile key.txt");
                        console.log("\r\nOptional arguments:\r\n");
                        console.log("  --json                 - Show result as JSON.");
                        break;
                    }
                    case 'userinfo': {
                        console.log("Get account information for the login account, Example usages:\r\n");
                        console.log("  MeshCtrl UserInfo --loginuser myaccountname --loginpass mypassword");
                        console.log("  MeshCtrl UserInfo --loginuser myaccountname --loginkeyfile key.txt");
                        console.log("\r\nOptional arguments:\r\n");
                        console.log("  --json                 - Show result as JSON.");
                        break;
                    }
                    case 'listusers': {
                        console.log("List the account on the MeshCentral server, Example usages:\r\n");
                        console.log("  MeshCtrl ListUsers");
                        console.log("  MeshCtrl ListUsers --json");
                        console.log("  MeshCtrl ListUsers --nameexists \"bob\"");
                        console.log("  MeshCtrl ListUsers --filter 2fa");
                        console.log("\r\nOptional arguments:\r\n");
                        console.log("  --idexists [id]        - Return 1 if id exists, 0 if not.");
                        console.log("  --nameexists [name]    - Return id if name exists.");
                        console.log("  --filter [filter1,...] - Filter user names: 2FA, NO2FA.");
                        console.log("  --json                 - Show result as JSON.");
                        break;
                    }
                    case 'listdevicegroups': {
                        console.log("List the device groups for this account, Example usages:\r\n");
                        console.log("  MeshCtrl ListDeviceGroups ");
                        console.log("  MeshCtrl ListDeviceGroups --json");
                        console.log("\r\nOptional arguments:\r\n");
                        console.log("  --idexists [id]        - Return 1 if id exists, 0 if not.");
                        console.log("  --nameexists [name]    - Return id if name exists.");
                        console.log("  --emailexists [email]  - Return id if email exists.");
                        console.log("  --json                 - Show result as JSON.");
                        break;
                    }
                    case 'listdevices': {
                        console.log("List devices, Example usages:\r\n");
                        console.log("  MeshCtrl ListDevices");
                        console.log("  MeshCtrl ListDevices -id [groupid] --json");
                        console.log("\r\nOptional arguments:\r\n");
                        console.log("  --id [groupid]         - Filter by device group identifier.");
                        console.log("  --count                - Only return the device count.");
                        console.log("  --json                 - Show result as JSON.");
                        break;
                    }
                    case 'listusersofdevicegroup': {
                        console.log("List users that have permissions for a given device group, Example usage:\r\n");
                        console.log("  MeshCtrl ListUserOfDeviceGroup ");
                        console.log("\r\nRequired arguments:\r\n");
                        console.log("  --id [groupid]         - Device group identifier.");
                        console.log("\r\nOptional arguments:\r\n");
                        console.log("  --json                 - Show result as JSON.");
                        break;
                    }
                    case 'adduser': {
                        console.log("Add a new user account, Example usages:\r\n");
                        console.log("  MeshCtrl AddUser --user newaccountname --pass newpassword");
                        console.log("\r\nRequired arguments:\r\n");
                        console.log("  --user [name]          - New account name.");
                        console.log("  --pass [password]      - New account password.");
                        console.log("  --randompass           - Create account with a random password.");
                        console.log("\r\nOptional arguments:\r\n");
                        console.log("  --email [email]        - New account email address.");
                        console.log("  --emailverified        - New account email is verified.");
                        console.log("  --resetpass            - Request password reset on next login.");
                        console.log("  --siteadmin            - Create the account as full site administrator.");
                        console.log("  --manageusers          - Allow this account to manage server users.");
                        console.log("  --fileaccess           - Allow this account to store server files.");
                        console.log("  --serverupdate         - Allow this account to update the server.");
                        console.log("  --locked               - This account will be locked.");
                        console.log("  --nonewgroups          - Account will not be allowed to create device groups.");
                        console.log("  --notools              - Account not see MeshCMD download links.");
                        break;
                    }
                    case 'removeuser': {
                        console.log("Delete a user account, Example usages:\r\n");
                        console.log("  MeshCtrl RemoveUser --userid accountid");
                        console.log("\r\nRequired arguments:\r\n");
                        console.log("  --userid [id]          - Account identifier.");
                        break;
                    }
                    case 'adddevicegroup': {
                        console.log("Add a device group, Example usages:\r\n");
                        console.log("  MeshCtrl AddDeviceGroup --name newgroupname");
                        console.log("  MeshCtrl AddDeviceGroup --name newgroupname --desc description --amtonly");
                        console.log("\r\nRequired arguments:\r\n");
                        console.log("  --name [name]          - Name of the new group.");
                        console.log("\r\nOptional arguments:\r\n");
                        console.log("  --desc [description]   - New group description.");
                        console.log("  --amtonly              - New group is agent-less, Intel AMT only.");
                        break;
                    }
                    case 'removedevicegroup': {
                        console.log("Remove a device group, Example usages:\r\n");
                        console.log("  MeshCtrl RemoveDeviceGroup --id groupid");
                        console.log("\r\nRequired arguments:\r\n");
                        console.log("  --id [groupid]         - The group identifier.");
                        break;
                    }
                    case 'addusertodevicegroup': {
                        console.log("Add a user to a device group, Example usages:\r\n");
                        console.log("  MeshCtrl AddUserToDeviceGroup --id groupid --userid userid --fullrights");
                        console.log("  MeshCtrl AddUserToDeviceGroup --id groupid --userid userid --editgroup --manageusers");
                        console.log("\r\nRequired arguments:\r\n");
                        console.log("  --id [groupid]         - The group identifier.");
                        console.log("  --userid [userid]      - The user identifier.");
                        console.log("\r\nOptional arguments:\r\n");
                        console.log("  --fullrights           - Allow full rights over this device group.");
                        console.log("  --editgroup            - Allow the user to edit group information.");
                        console.log("  --manageusers          - Allow the user to add/remove users.");
                        console.log("  --managedevices        - Allow the user to edit device information.");
                        console.log("  --remotecontrol        - Allow device remote control operations.");
                        console.log("  --agentconsole         - Allow agent console operations.");
                        console.log("  --serverfiles          - Allow access to group server files.");
                        console.log("  --wakedevices          - Allow device wake operation.");
                        console.log("  --notes                - Allow editing of device notes.");
                        console.log("  --desktopviewonly      - Restrict user to view-only remote desktop.");
                        console.log("  --limiteddesktop       - Limit remote desktop keys.");
                        console.log("  --noterminal           - Hide the terminal tab from this user.");
                        console.log("  --nofiles              - Hide the files tab from this user.");
                        console.log("  --noamt                - Hide the Intel AMT tab from this user.");
                        break;
                    }
                    case 'removeuserfromdevicegroup': {
                        console.log("Remove a user from a device group, Example usages:\r\n");
                        console.log("  MeshCtrl RemoveuserFromDeviceGroup --id groupid --userid userid");
                        console.log("\r\nRequired arguments:\r\n");
                        console.log("  --id [groupid]         - The group identifier.");
                        console.log("  --userid [userid]      - The user identifier.");
                        break;
                    }
                    case 'broadcast': {
                        console.log("Display a message to all logged in users, Example usages:\r\n");
                        console.log("  MeshCtrl Broadcast --msg \"This is a test\"");
                        console.log("\r\nRequired arguments:\r\n");
                        console.log("  --msg [message]         - Message to display.");
                        break;
                    }
                    default: {
                        console.log("Get help on an action. Type:\r\n\r\n  help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.');
                    }
                }
            }
            break;
        }
    }

    if (ok) { serverConnect(); }
}

function displayConfigHelp() {
    console.log("Perform operations on the config.json file. Example usage:\r\n");
    console.log("  MeshCtrl config --show");
    console.log("\r\nOptional arguments:\r\n");
    console.log("  --show                        - Display the config.json file.");
    console.log("  --listdomains                 - Display non-default domains.");
    console.log("  --adddomain [domain]          - Add a domain.");
    console.log("  --removedomain [domain]       - Remove a domain.");
    console.log("  --settodomain [domain]        - Set values to the domain.");
    console.log("  --removefromdomain [domain]   - Remove values from the domain.");
    console.log("\r\nWith adddomain, removedomain, settodomain and removefromdomain you can add the key and value pair. For example:\r\n");
    console.log("  --adddomain \"MyDomain\" --title \"My Server Name\" --newAccounts false");
    console.log("  --settodomain \"MyDomain\" --title \"My Server Name\"");
    console.log("  --removefromdomain \"MyDomain\" --title");
}

function performConfigOperations(args) {
    var domainValues = ['title', 'title2', 'titlepicture', 'trustedcert', 'welcomepicture', 'welcometext', 'userquota', 'meshquota', 'newaccounts', 'usernameisemail', 'newaccountemaildomains', 'newaccountspass', 'newaccountsrights', 'geolocation', 'lockagentdownload', 'userconsentflags', 'Usersessionidletimeout', 'auth', 'ldapoptions', 'ldapusername', 'ldapuserbinarykey', 'ldapoptions', 'footer', 'certurl', 'loginKey', 'userallowedip', 'agentallowedip', 'agentnoproxy', 'agentconfig', 'orphanagentuser', 'httpheaders', 'yubikey', 'passwordrequirements', 'limits', 'amtacmactivation', 'redirects', 'sessionrecording', 'hide', 'loginkey'];
    var domainObjectValues = [ 'ldapoptions', 'httpheaders', 'yubikey', 'passwordrequirements', 'limits', 'amtacmactivation', 'redirects', 'sessionrecording' ];
    var domainArrayValues = [ 'newaccountemaildomains', 'newaccountsrights', 'loginkey', 'agentconfig' ];
    var configChange = false;
    var fs = require('fs');
    var path = require('path');
    var configFile = path.join(__dirname, 'config.json');
    var didSomething = 0;
    if (fs.existsSync(configFile) == false) { configFile = path.join('meshcentral-data', 'config.json'); }
    if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, 'meshcentral-data', 'config.json'); }
    if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, '..', 'meshcentral-data', 'config.json'); }
    if (fs.existsSync(configFile) == false) { console.log("Unable to find config.json."); return; }
    var config = null;
    try { config = fs.readFileSync(configFile); } catch (ex) { console.log("Error: Unable to read config.json"); return; }
    try { config = JSON.parse(config); } catch (ex) { console.log("Error: Unable to parse config.json"); return; }
    if (args.adddomain != null) {
        didSomething++;
        if (config.domains == null) { config.domains = {}; }
        if (config.domains[args.adddomain] != null) { console.log("Error: Domain \"" + args.adddomain + "\" already exists"); }
        else {
            configChange = true;
            config.domains[args.adddomain] = {};
            for (var i in args) {
                if (domainValues.indexOf(i.toLowerCase()) >= 0) {
                    if (args[i] == 'true') { args[i] = true; } else if (args[i] == 'false') { args[i] = false; } else if (parseInt(args[i]) == args[i]) { args[i] = parseInt(args[i]); }
                    config.domains[args.adddomain][i] = args[i];
                    configChange = true;
                }
            }
        }
    }
    if (args.removedomain != null) {
        didSomething++;
        if (config.domains == null) { config.domains = {}; }
        if (config.domains[args.removedomain] == null) { console.log("Error: Domain \"" + args.removedomain + "\" does not exist"); }
        else { delete config.domains[args.removedomain]; configChange = true; }
    }
    if (args.settodomain != null) {
        didSomething++;
        if (config.domains == null) { config.domains = {}; }
        if (args.settodomain == true) { args.settodomain = ''; }
        if (config.domains[args.settodomain] == null) { console.log("Error: Domain \"" + args.settodomain + "\" does not exist"); }
        else {
            for (var i in args) {
                if ((i == '_') || (i == 'settodomain')) continue;
                if (domainValues.indexOf(i.toLowerCase()) >= 0) {
                    var isObj = (domainObjectValues.indexOf(i.toLowerCase()) >= 0);
                    var isArr = (domainArrayValues.indexOf(i.toLowerCase()) >= 0);
                    if ((isObj == false) && (isArr == false)) {
                        // Simple value set
                        if (args[i] == '') { delete config.domains[args.settodomain][i]; configChange = true; } else {
                            if (args[i] == 'true') { args[i] = true; } else if (args[i] == 'false') { args[i] = false; } else if (parseInt(args[i]) == args[i]) { args[i] = parseInt(args[i]); }
                            config.domains[args.settodomain][i] = args[i];
                            configChange = true;
                        }
                    } else if (isObj || isArr) {
                        // Set an object/array value
                        if (args[i] == '') { delete config.domains[args.settodomain][i]; configChange = true; } else {
                            var x = null;
                            try { x = JSON.parse(args[i]); } catch (ex) { }
                            if ((x == null) || (typeof x != 'object')) { console.log("Unable to parse JSON for " + i + "."); } else {
                                if (isArr && Array.isArray(x) == false) {
                                    console.log("Value " + i + " must be an array.");
                                } else if (!isArr && Array.isArray(x) == true) {
                                    console.log("Value " + i + " must be an object.");
                                } else {
                                    config.domains[args.settodomain][i] = x;
                                    configChange = true;
                                }
                            }
                        }
                    }
                } else {
                    console.log('Invalid configuration value: ' + i);
                }
            }
        }
    }
    if (args.removefromdomain != null) {
        didSomething++;
        if (config.domains == null) { config.domains = {}; }
        if (config.domains[args.removefromdomain] == null) { console.log("Error: Domain \"" + args.removefromdomain + "\" does not exist"); }
        else { for (var i in args) { if (domainValues.indexOf(i.toLowerCase()) >= 0) { delete config.domains[args.removefromdomain][i]; configChange = true; } } }
    }
    if (configChange) {
        try { fs.writeFileSync(configFile, JSON.stringify(config, null, 2)); } catch (ex) { console.log("Error: Unable to read config.json"); return; }
    }
    if (args.show == 1) {
        console.log(JSON.stringify(config, null, 2)); return;
    } else if (args.listdomains == 1) {
        if (config.domains == null) {
            console.log('No domains found.'); return;
        } else {
            // Show the list of active domains, skip the default one.
            for (var i in config.domains) { if ((i != '') && (i[0] != '_')) { console.log(i); } } return;
        }
    } else {
        if (didSomething == 0) {
            displayConfigHelp();
        } else {
            console.log("Done.");
        }
    }
}

function onVerifyServer(clientName, certs) { return null; }

function serverConnect() {
    const WebSocket = require('ws');

    var url = 'wss://localhost/control.ashx';
    if (args.url) {
        url = args.url;
        if (url.length < 5) { console.log("Invalid url."); process.exit(); return; }
        if ((url.startsWith('wss://') == false) && (url.startsWith('ws://') == false)) { console.log("Invalid url."); process.exit(); return; }
        if (url.endsWith('/') == false) { url += '/'; }
        url += 'control.ashx';
    }

    // TODO: checkServerIdentity does not work???
    var options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }

    // Password authentication
    if (args.loginpass != null) {
        var username = 'admin';
        if (args.loginuser != null) { username = args.loginuser; }
        var token = '';
        if (args.token != null) { token = ',' + Buffer.from('' + args.token).toString('base64'); }
        options.headers = { 'x-meshauth': Buffer.from(username).toString('base64') + ',' + Buffer.from(args.loginpass).toString('base64') + token }
    }

    // Cookie authentication
    var ckey = null;
    if (args.loginkey != null) {
        // User key passed in a argument hex
        if (args.loginkey.length != 160) { console.log("Invalid login key."); process.exit(); return; }
        ckey = Buffer.from(args.loginkey, 'hex');
        if (ckey != 80) { console.log("Invalid login key."); process.exit(); return; }
    } else if (args.loginkeyfile != null) {
        // Load key from hex file
        var fs = require('fs');
        try {
            var keydata = fs.readFileSync(args.loginkeyfile, 'utf8').split(' ').join('').split('\r').join('').split('\n').join('');
            ckey = Buffer.from(keydata, 'hex');
            if (ckey.length != 80) { console.log("Invalid login key file."); process.exit(); return; }
        } catch (ex) { console.log(ex); process.exit(); return; }
    }

    if (ckey != null) {
        var domainid = '', username = 'admin';
        if (args.domain != null) { domainid = args.domain; }
        if (args.loginuser != null) { username = args.loginuser; }
        url += '?auth=' + encodeCookie({ userid: 'user/' + domainid + '/' + username, domainid: domainid }, ckey);
    }

    const ws = new WebSocket(url, options);
    //console.log('Connecting...');

    ws.on('open', function open() {
        //console.log('Connected.');
        switch (settings.cmd) {
            case 'serverinfo': { break; }
            case 'userinfo': { break; }
            case 'listusers': { ws.send(JSON.stringify({ action: 'users' })); break; }
            case 'listdevicegroups': { ws.send(JSON.stringify({ action: 'meshes' })); break; }
            case 'listusersofdevicegroup': { ws.send(JSON.stringify({ action: 'meshes' })); break; }
            case 'listdevices': {
                if (args.id) {
                    ws.send(JSON.stringify({ action: 'nodes', meshid: args.id, responseid: 'meshctrl' })); break;
                } else {
                    ws.send(JSON.stringify({ action: 'meshes' }));
                    ws.send(JSON.stringify({ action: 'nodes', responseid: 'meshctrl' })); break;
                }
            }
            case 'adduser': {
                var siteadmin = 0;
                if (args.siteadmin) { siteadmin = 0xFFFFFFFF; }
                if (args.manageusers) { siteadmin |= 2; }
                if (args.fileaccess) { siteadmin |= 8; }
                if (args.serverupdate) { siteadmin |= 16; }
                if (args.locked) { siteadmin |= 32; }
                if (args.nonewgroups) { siteadmin |= 64; }
                if (args.notools) { siteadmin |= 128; }
                if (args.randompass) { args.pass = getRandomAmtPassword(); }
                var op = { action: 'adduser', username: args.user, pass: args.pass, responseid: 'meshctrl' };
                if (args.email) { op.email = args.email; if (args.emailverified) { op.emailVerified = true; } }
                if (args.resetpass) { op.resetNextLogin = true; }
                if (siteadmin != 0) { op.siteadmin = siteadmin; }
                ws.send(JSON.stringify(op));
                break;
            }
            case 'removeuser': {
                var op = { action: 'deleteuser', userid: args.userid, responseid: 'meshctrl' };
                ws.send(JSON.stringify(op));
                break;
            }
            case 'adddevicegroup': {
                var op = { action: 'createmesh', meshname: args.name, meshtype: 2, responseid: 'meshctrl' };
                if (args.desc) { op.desc = args.desc; }
                if (args.amtonly) { op.meshtype = 1; }
                ws.send(JSON.stringify(op));
                break;
            }
            case 'removedevicegroup': {
                var op = { action: 'deletemesh', meshid: args.id, responseid: 'meshctrl' };
                ws.send(JSON.stringify(op));
                break;
            }
            case 'addusertodevicegroup': {
                var meshrights = 0;
                if (args.fullrights) { meshrights = 0xFFFFFFFF; }
                if (args.editgroup) { meshrights |= 1; }
                if (args.manageusers) { meshrights |= 2; }
                if (args.managedevices) { meshrights |= 4; }
                if (args.remotecontrol) { meshrights |= 8; }
                if (args.agentconsole) { meshrights |= 16; }
                if (args.serverfiles) { meshrights |= 32; }
                if (args.wakedevices) { meshrights |= 64; }
                if (args.notes) { meshrights |= 128; }
                if (args.desktopviewonly) { meshrights |= 256; }
                if (args.noterminal) { meshrights |= 512; }
                if (args.nofiles) { meshrights |= 1024; }
                if (args.noamt) { meshrights |= 2048; }
                if (args.limiteddesktop) { meshrights |= 4096; }
                var op = { action: 'addmeshuser', meshid: args.id, usernames: [args.userid], meshadmin: meshrights, responseid: 'meshctrl' };
                ws.send(JSON.stringify(op));
                break;
            }
            case 'removeuserfromdevicegroup': {
                var op = { action: 'removemeshuser', meshid: args.id, userid: args.userid, responseid: 'meshctrl' };
                ws.send(JSON.stringify(op));
                break;
            }
            case 'sendinviteemail': {
                var op = { action: "inviteAgent", meshid: args.id, email: args.email, name: "", os: "0", responseid: 'meshctrl' }
                ws.send(JSON.stringify(op));
                break;
            }
            case 'generateinvitelink': {
                var op = { action: "createInviteLink", meshid: args.id, expire: args.hours, flags: 0, responseid: 'meshctrl' }
                ws.send(JSON.stringify(op));
                break;
            }
            case 'broadcast': {
                var op = { action: 'userbroadcast', msg: args.msg, responseid: 'meshctrl' };
                ws.send(JSON.stringify(op));
                break;
            }
        }
    });

    ws.on('close', function close() { process.exit(); });

    ws.on('message', function incoming(rawdata) {
        //console.log(rawdata);
        var data = null;
        try { data = JSON.parse(rawdata); } catch (ex) { }
        if (data == null) { console.log('Unable to parse data: ' + rawdata); }
        switch (data.action) {
            case 'serverinfo': { // SERVERINFO
                if (settings.cmd == 'serverinfo') {
                    if (args.json) {
                        console.log(JSON.stringify(data.serverinfo, ' ', 2));
                    } else {
                        for (var i in data.serverinfo) { console.log(i + ':', data.serverinfo[i]); }
                    }
                    process.exit();
                }
                break;
            }
            case 'userinfo': { // USERINFO
                if (settings.cmd == 'userinfo') {
                    if (args.json) {
                        console.log(JSON.stringify(data.userinfo, ' ', 2));
                    } else {
                        for (var i in data.userinfo) { console.log(i + ':', data.userinfo[i]); }
                    }
                    process.exit();
                }
                break;
            }
            case 'adduser': // ADDUSER
            case 'deleteuser': // REMOVEUSER
            case 'createmesh': // ADDDEVICEGROUP
            case 'deletemesh': // REMOVEDEVICEGROUP
            case 'addmeshuser': //
            case 'removemeshuser': //
            case 'inviteAgent': //
            case 'userbroadcast': { // BROADCAST
                if (data.responseid == 'meshctrl') {
                    if (data.meshid) { console.log(data.result, data.meshid); }
                    else if (data.userid) { console.log(data.result, data.userid); }
                    else console.log(data.result);
                    process.exit();
                }
                break;
            }
            case 'createInviteLink':
                if (data.responseid == 'meshctrl') {
                    if (data.url) { console.log(data.url); }
                    else console.log(data.result);
                    process.exit();
                }
                break;
            case 'users': { // LISTUSERS
                if (args.filter) {
                    // Filter the list of users
                    var filters = args.filter.toLowerCase().split(',');
                    var filteredusers = [];
                    for (var i in data.users) {
                        var ok = false;
                        if ((filters.indexOf('2fa') >= 0) && ((data.users[i].otphkeys != null) || (data.users[i].otpkeys != null) || (data.users[i].otpsecret != null))) { ok = true; }
                        if ((filters.indexOf('no2fa') >= 0) && ((data.users[i].otphkeys == null) && (data.users[i].otpkeys == null) && (data.users[i].otpsecret == null))) { ok = true; }
                        if (ok == true) { filteredusers.push(data.users[i]); }
                    }
                    data.users = filteredusers;
                }
                if (args.json) {
                    console.log(JSON.stringify(data.users, ' ', 2));
                } else {
                    if (args.idexists) { for (var i in data.users) { const u = data.users[i]; if ((u._id == args.idexists) || (u._id.split('/')[2] == args.idexists)) { console.log('1'); process.exit(); return; } } console.log('0'); process.exit(); return; }
                    if (args.nameexists) { for (var i in data.users) { const u = data.users[i]; if (u.name == args.nameexists) { console.log(u._id); process.exit(); return; } } process.exit(); return; }

                    console.log('id, name, email\r\n---------------');
                    for (var i in data.users) {
                        const u = data.users[i];
                        var t = "\"" + u._id.split('/')[2] + "\", \"" + u.name + "\"";
                        if (u.email != null) { t += ", \"" + u.email + "\""; }
                        console.log(t);
                    }
                }
                process.exit();
                break;
            }
            case 'nodes': {
                if ((settings.cmd == 'listdevices') && (data.responseid == 'meshctrl')) {
                    if ((data.result != null) && (data.result != 'ok')) {
                        console.log(data.result);
                    } else {
                        if (args.count) {
                            // Return how many devices are in this group
                            var nodes = [];
                            for (var i in data.nodes) { var devicesInMesh = data.nodes[i]; for (var j in devicesInMesh) { nodes.push(devicesInMesh[j]); } }
                            console.log(nodes.length);
                        } else if (args.json) {
                            // Return all devices in JSON format
                            var nodes = [];
                            for (var i in data.nodes) { var devicesInMesh = data.nodes[i]; for (var j in devicesInMesh) { nodes.push(devicesInMesh[j]); } }
                            console.log(JSON.stringify(nodes, ' ', 2));
                        } else {
                            // Display the list of nodes in text format
                            for (var i in data.nodes) {
                                var devicesInMesh = data.nodes[i];
                                if (settings.xmeshes) { console.log('\r\nDevice group: \"' + settings.xmeshes[i].name + '\"'); }
                                console.log('id, name, icon, conn, pwr, ip\r\n-----------------------------');
                                for (var j in devicesInMesh) {
                                    var n = devicesInMesh[j];
                                    console.log(n._id.split('/')[2] + ', \"' + n.name + '\", ' + (n.icon ? n.icon : 0) + ', ' + (n.conn ? n.conn : 0) + ', ' + (n.pwr ? n.pwr : 0));
                                }
                            }
                        }
                    }
                    process.exit();
                }
                break;
            }
            case 'meshes': { // LISTDEVICEGROUPS
                if (settings.cmd == 'listdevices') {
                    // Store the list of device groups for later use
                    settings.xmeshes = {}
                    for (var i in data.meshes) { settings.xmeshes[data.meshes[i]._id] = data.meshes[i]; }
                } else if (settings.cmd == 'listdevicegroups') {
                    if (args.json) {
                        console.log(JSON.stringify(data.meshes, ' ', 2));
                    } else {
                        if (args.idexists) { for (var i in data.meshes) { const u = data.meshes[i]; if ((u._id == args.idexists) || (u._id.split('/')[2] == args.idexists)) { console.log('1'); process.exit(); return; } } console.log('0'); process.exit(); return; }
                        if (args.nameexists) { for (var i in data.meshes) { const u = data.meshes[i]; if (u.name == args.nameexists) { console.log(u._id); process.exit(); return; } } process.exit(); return; }

                        console.log('id, name\r\n---------------');
                        for (var i in data.meshes) {
                            const m = data.meshes[i];
                            var t = "\"" + m._id.split('/')[2] + "\", \"" + m.name + "\"";
                            console.log(t);
                        }
                    }
                    process.exit();
                } else if (settings.cmd == 'listusersofdevicegroup') {
                    for (var i in data.meshes) {
                        const m = data.meshes[i];
                        var mid = m._id.split('/')[2];
                        if (mid == args.id) {
                            if (args.json) {
                                console.log(JSON.stringify(m.links, ' ', 2));
                            } else {
                                console.log('userid, rights\r\n---------------');
                                for (var l in m.links) {
                                    var rights = m.links[l].rights;
                                    var rightsstr = [];
                                    if (rights == 4294967295) { rightsstr = ['FullAdministrator']; } else {
                                        if (rights & 1) { rightsstr.push('EditMesh'); }
                                        if (rights & 2) { rightsstr.push('ManageUsers'); }
                                        if (rights & 4) { rightsstr.push('ManageComputers'); }
                                        if (rights & 8) { rightsstr.push('RemoteControl'); }
                                        if (rights & 16) { rightsstr.push('AgentConsole'); }
                                        if (rights & 32) { rightsstr.push('ServerFiles'); }
                                        if (rights & 64) { rightsstr.push('WakeDevice'); }
                                        if (rights & 128) { rightsstr.push('SetNotes'); }
                                        if (rights & 256) { rightsstr.push('RemoteViewOnly'); }
                                        if (rights & 512) { rightsstr.push('NoTerminal'); }
                                        if (rights & 1024) { rightsstr.push('NoFiles'); }
                                        if (rights & 2048) { rightsstr.push('NoAMT'); }
                                        if (rights & 4096) { rightsstr.push('DesktopLimitedInput'); }
                                    }
                                    console.log(l.split('/')[2] + ', ' + rightsstr.join(', '));
                                }
                            }
                            process.exit();
                            return;
                        }
                    }
                    console.log('Group id not found');
                    process.exit();
                }
                break;
            }
            case 'close': {
                if (data.cause == 'noauth') {
                    if (data.msg == 'tokenrequired') {
                        console.log('Authentication token required, use --token [number].');
                    } else {
                        console.log('Invalid login.');
                    }
                }
                process.exit();
                break;
            }
            default: { break; }
        }
        //console.log('Data', data);
        //setTimeout(function timeout() { ws.send(Date.now()); }, 500);
    });
}

// Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more)
function encodeCookie(o, key) {
    try {
        if (key == null) { return null; }
        o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time
        const iv = Buffer.from(crypto.randomBytes(12), 'binary'), cipher = crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
        const crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]);
        return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
    } catch (e) { return null; }
}

// Generate a random Intel AMT password
function checkAmtPassword(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
function getRandomAmtPassword() { var p; do { p = Buffer.from(crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } while (checkAmtPassword(p) == false); return p; }
