UNPKG

net-user

Version:

The Windows NET USER command wrapped in JavaScript

295 lines (257 loc) 9.49 kB
if (process.platform !== 'win32') { console.error('netuser module is only for windows platforms') return } var exec = require('child_process').exec , assert = require('assert') , os = require('os') var RE_KEY_VAL_PAIR = /^(\S+(?: \S+)*)(?:\s{2,}(\S.*)?)?$/ , RE_TITLE = /^User accounts for / , RE_CLOSING = /^The command completed successfully./ , RE_HR = /^-+$/ // ANSI text-altering sequences var yellowSeq = "\u001b[33m" , brightSeq = "\u001b[1m" , resetSeq = "\u001b[0m" module.exports = { usernames: usernames_depr, netUsers: netUsers_depr, netUser: netUser, list: usernames, get: getUser, getAll: getAllUsers } function usernames_depr(cb) { console.warn(yellowSeq + brightSeq + "DEPRECATED: usernames() - use list() instead" + resetSeq) return usernames(cb) } function netUsers_depr(cb) { console.warn(yellowSeq + brightSeq + "DEPRECATED: netUsers() - use list() instead" + resetSeq) return usernames(cb) } // Fetch only the list of usernames on the local system function usernames(cb) { assert(typeof cb === 'function', 'Must provide callback') exec('net user', function(err, sout, serr) { if (err) cb(err) var lines = sout.split(os.EOL) , names = [] for (var i = 0; i < lines.length; i++) { if (lines[i] === '') continue if (RE_TITLE.test(lines[i]) || RE_HR.test(lines[i])) continue if (RE_CLOSING.test(lines[i])) break names = names.concat( lines[i].split(/\s+/).filter(function(v){ return v !== '' }) ) } return cb(null, names) }).once('error', function(err) { console.error('Child process is blocked') cb(err) }) } // Fetch the data of the named user, or fetch only the list of usernames // on the local system if no username given function netUser(userName, cb) { console.warn(yellowSeq + brightSeq + "DEPRECATED: netUser() - use get() or list() instead" + resetSeq) if (typeof cb !== 'function') { if (userName && typeof username === 'function') { cb = userName; userName = undefined } } if (!userName) return usernames(cb) return getUser(userName, cb) } // Fetch the data of the named user function getUser(userName, cb) { assert(typeof userName === 'string' || userName instanceof String, 'Must give username as a string') assert(typeof cb === 'function', 'Must provide callback') userName = userName.trim() assert(userName.length, 'Given username is empty') assert(userName.length <= 20, 'Given username is too long') // "The name of the user account can have as many as 20 characters." // - NET USER documentation // Guard against injection of change commands, and against illegal chars: // "User account names ... cannot be terminated by a period and they // cannot include..." (see regexp below) assert(userName.search(/[,"/\\\[\]:|<>+=;?*\x00-\x1F]/) == -1, 'Illegal character in username "' + userName + '"') assert(userName.slice(-1) !== '.', 'Invalid name "' + userName + '"') // But guess what: a username can have embedded spaces! // Therefore, quotemarks are necessary exec('net user "' + userName + '"', function(err, sout, serr) { if (err) { if (serr.indexOf('The user name could not be found.') != -1) return cb(null, null) return cb(err) } var data try { data = parseUserInfo(sout) } catch (exc) { return cb(exc) } return cb(null, data) }).once('error', function(err) { console.error('Child process is blocked') cb(err) }) } // Fetch data of all local user accounts function getAllUsers(cb) { assert(typeof cb === 'function', 'Must provide callback') var list = [] usernames(function(err, names) { if (err) return cb(err) return fetchNext() function fetchNext() { if (names.length == 0) return cb(null, list) exec('net user "' + names.shift() + '"', function(err, sout, serr) { if (err) return cb(err) try { list.push(parseUserInfo(sout)) } catch (exc) { return cb(exc) } return fetchNext() }).once('error', function(err) { console.error('Child process is blocked') cb(err) }) } }) } function parseUserInfo(text) { var lines = text.split(os.EOL) , info = {} , matches = null , j for (var i = 0; i < lines.length; i++) { if (lines[i] === '') continue if (RE_CLOSING.test(lines[i])) break matches = lines[i].match(RE_KEY_VAL_PAIR) //if (!matches) continue if (!matches) { console.warn('Unexpected line in output:', lines[i]) continue } //console.log('netUser: matches[1] is', '"' + matches[1] + '"') // For values, currently we're getting undefined for matches[2] when // there's no value set, but it's *always* preceded by 2 or more spaces switch (matches[1]) { case "User name": info.user_name = matches[2]; break case "Full Name": info.full_name = matches[2]; break case "Comment": info.comment = matches[2]; break case "User's comment": info.usr_comment = matches[2]; break case "Country code": info.country_code = ctryCode(matches[2]); break case "Account active": info.acct_active = xlateBool(matches[2]); break case "Account expires": info.acct_expires = xlateTimespec(matches[2]); break case "Password last set": info.password_set = xlateTimespec(matches[2]); break case "Password expires": info.password_expires = xlateTimespec(matches[2]); break case "Password changeable": info.password_changeable = xlateTimespec(matches[2]); break case "Password required": info.password_required = xlateBool(matches[2]); break case "User may change password": info.password_can_change = xlateBool(matches[2]); break case "Workstations allowed": info.workstations = parseWorkstnList(matches[2]); break case "Logon script": info.script_path = matches[2]; break; case "User profile": info.profile = matches[2]; break; case "Home directory": info.home_dir = matches[2]; break; case "Last logon": info.last_logon = xlateTimespec(matches[2]); break case "Logon hours allowed": // logon_hours can have values "All" and "None" if (matches[2] === 'All') { info.logon_hours = null; break } info.logon_hours = [] if (matches[2] === 'None') break info.logon_hours.push(matches[2]) // Can be followed by multiple lines of timespans for (j = i + 1; j < lines.length; j++) { // Blank line means end of timespan entries if (lines[j] === '') break var parts = lines[j].split(/\s\s+/) // There is nothing but padding space prefixed to each line of // logon hours after the first; so, after splitting on space padding, // if the 1st element is not an empty string, we have overshot. if (parts[0]) break info.logon_hours.push(lines[j].trim()) } i = j - 1; break // It seems that group names are always prefixed by '*'; this helps parse // them out, given that group names can be made up of words separated by // a space case "Local Group Memberships": info.local_groups = parseGroupList(matches[2]) if (!info.local_groups || info.local_groups.length == 0) break for (j = i + 1; j < lines.length; j++) { if (lines[j] === '') break if (/^\s*\*/.test(lines[j]) === false) break info.local_groups = info.local_groups.concat(parseGroupList(lines[j])) } i = j - 1; break case "Global Group memberships": info.global_groups = parseGroupList(matches[2]) if (!info.global_groups || info.global_groups.length == 0) break for (j = i + 1; j < lines.length; j++) { if (lines[j] === '') break if (/^\s*\*/.test(lines[j]) === false) break info.global_groups = info.local_global.concat(parseGroupList(lines[j])) } i = j - 1; break } } return info } function xlateBool(s) { switch (s) { case 'Yes': return true case 'No' : return false } // default: undefined } function xlateTimespec(s) { // Timespec format from 'net user' seen to follow 'm/d/yyy h:mm:ss' // when the value is not 'Never' if (s === 'Never') return null var t = new Date(s) if (t.toString() === 'Invalid Date') { console.warn("Invalid timespec in 'net user' output:", s) return null } return t } function ctryCode(s) { if (!s || s === '(null)') return null var matches = s.match(/^(\d{3})(?: .+)?$/) // Dev code - watch for unexpected errors if (!matches) { console.warn('Unexpected value for Country code:', s) return null } return matches[1] // Production code: //return matches ? matches[1] : null } function parseWorkstnList(s) { if (!s) return [] else if (s === 'All') return null else return s.split(',') } function parseGroupList(s) { // debug note: saw empty value for "Local Group Memberships" for user "HelpAssistant"; // also saw truncated names, and names jammed together (nothing but a '*' between // them), when several long-named groups were added to a user's info var l if (!s) l = [] else { l = s.split(/\s*\*/g).filter(function(v){ return v !== '' }) if (l.length) l[l.length - 1] = l[l.length - 1].trim() if (l[0] === 'None') l = [] } return l }