diff --git a/package.json b/package.json index 3c3863906..fc6dad26c 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "pkginfo": "^0.4.0", "request": "2.83.0", "semver": "^5.5.0", - "unix-crypt-td-js": "^1.0.0" + "unix-crypt-td-js": "^1.0.0", + "verdaccio-htpasswd": "0.1.2" }, "devDependencies": { "@commitlint/cli": "6.1.0", diff --git a/src/plugins/htpasswd/crypt3.js b/src/plugins/htpasswd/crypt3.js deleted file mode 100644 index 79a2a5380..000000000 --- a/src/plugins/htpasswd/crypt3.js +++ /dev/null @@ -1,59 +0,0 @@ -/* eslint require-jsdoc: off */ - -'use strict'; - -/** Node.js Crypt(3) Library - - Inspired by (and intended to be compatible with) sendanor/crypt3 - - see https://github.com/sendanor/node-crypt3 - - The key difference is the removal of the dependency on the unix crypt(3) function - which is not platform independent, and requires compilation. Instead, a pure - javascript version is used. - -*/ - -const crypt = require('unix-crypt-td-js'); -const crypto = require('crypto'); - -function createSalt(type) { - type = type || 'sha512'; - - switch (type) { - - case 'md5': - return '$1$' + crypto.randomBytes(10).toString('base64'); - - case 'blowfish': - return '$2a$' + crypto.randomBytes(10).toString('base64'); - - case 'sha256': - return '$5$' + crypto.randomBytes(10).toString('base64'); - - case 'sha512': - return '$6$' + crypto.randomBytes(10).toString('base64'); - - default: - throw new TypeError('Unknown salt type at crypt3.createSalt: ' + type); - } -} - -function crypt3(key, salt) { - salt = salt || createSalt(); - return crypt(key, salt); -} - -/** Crypt(3) password and data encryption. - * @param {string} key user's typed password - * @param {string} salt Optional salt, for example SHA-512 use "$6$salt$". - * @returns {string} A generated hash in format $id$salt$encrypted - * @see https://en.wikipedia.org/wiki/Crypt_(C) - */ -module.exports = crypt3; - -/** Create salt - * @param {string} type The type of salt: md5, blowfish (only some linux distros), sha256 or sha512. Default is sha512. - * @returns {string} Generated salt string - */ -module.exports.createSalt = createSalt; diff --git a/src/plugins/htpasswd/index.js b/src/plugins/htpasswd/index.js deleted file mode 100644 index 8b6c9ca1e..000000000 --- a/src/plugins/htpasswd/index.js +++ /dev/null @@ -1,137 +0,0 @@ -/* eslint require-jsdoc: off */ - -'use strict'; - -let fs = require('fs'); -let Path = require('path'); -let utils = require('./utils'); - -module.exports = HTPasswd; - -function HTPasswd(config, stuff) { - let self = Object.create(HTPasswd.prototype); - self._users = {}; - - // config for this module - self._config = config; - - // verdaccio logger - self._logger = stuff.logger; - - // verdaccio main config object - self._verdaccio_config = stuff.config; - - // all this "verdaccio_config" stuff is for b/w compatibility only - self._maxusers = self._config.max_users; - if (!self._maxusers) self._maxusers = self._verdaccio_config.max_users; - // set maxusers to Infinity if not specified - if (!self._maxusers) self._maxusers = Infinity; - - self._last_time = null; - let file = self._config.file; - if (!file) file = self._verdaccio_config.users_file; - if (!file) throw new Error('should specify "file" in config'); - self._path = Path.resolve(Path.dirname(self._verdaccio_config.self_path), file); - return self; -} - -HTPasswd.prototype.authenticate = function(user, password, cb) { - let self = this; - self._reload(function(err) { - if (err) return cb(err.code === 'ENOENT' ? null : err); - if (!self._users[user]) return cb(null, false); - if (!utils.verify_password(user, password, self._users[user])) return cb(null, false); - - // authentication succeeded! - // return all usergroups this user has access to; - // (this particular package has no concept of usergroups, so just return user herself) - return cb(null, [user]); - }); -}; - -// hopefully race-condition-free way to add users: -// 1. lock file for writing (other processes can still read) -// 2. reload .htpasswd -// 3. write new data into .htpasswd.tmp -// 4. move .htpasswd.tmp to .htpasswd -// 5. reload .htpasswd -// 6. unlock file -HTPasswd.prototype.adduser = function(user, password, real_cb) { - let self = this; - - function sanity_check() { - let err = null; - if (self._users[user]) { - err = Error('this user already exists'); - } else if (Object.keys(self._users).length >= self._maxusers) { - err = Error('maximum amount of users reached'); - } - if (err) err.status = 403; - return err; - } - - // preliminary checks, just to ensure that file won't be reloaded if it's not needed - let s_err = sanity_check(); - if (s_err) return real_cb(s_err, false); - - utils.lock_and_read(self._path, function(err, res) { - let locked = false; - - // callback that cleans up lock first - function cb(err) { - if (locked) { - utils.unlock_file(self._path, function() { - // ignore any error from the unlock - real_cb(err, !err); - }); - } else { - real_cb(err, !err); - } - } - - if (!err) { - locked = true; - } - - // ignore ENOENT errors, we'll just create .htpasswd in that case - if (err && err.code !== 'ENOENT') return cb(err); - - let body = (res || '').toString('utf8'); - self._users = utils.parse_htpasswd(body); - - // real checks, to prevent race conditions - let s_err = sanity_check(); - if (s_err) return cb(s_err); - - try { - body = utils.add_user_to_htpasswd(body, user, password); - } catch (err) { - return cb(err); - } - fs.writeFile(self._path, body, function(err) { - if (err) return cb(err); - self._reload(function() { - cb(null, true); - }); - }); - }); -}; - -HTPasswd.prototype._reload = function(_callback) { - let self = this; - - fs.stat(self._path, function(err, stats) { - if (err) return _callback(err); - - if (self._last_time === stats.mtime) return _callback(); - self._last_time = stats.mtime; - - fs.readFile(self._path, 'utf8', function(err, buffer) { - if (err) return _callback(err); - - self._users = utils.parse_htpasswd(buffer); - - _callback(); - }); - }); -}; diff --git a/src/plugins/htpasswd/utils.js b/src/plugins/htpasswd/utils.js deleted file mode 100644 index 3136f3a8f..000000000 --- a/src/plugins/htpasswd/utils.js +++ /dev/null @@ -1,72 +0,0 @@ -/* eslint require-jsdoc: off */ - -'use strict'; - -let crypto = require('crypto'); -let crypt3 = require('./crypt3'); -let md5 = require('apache-md5'); -let locker = require('@verdaccio/file-locking'); - -// this function neither unlocks file nor closes it -// it'll have to be done manually later -function lock_and_read(name, cb) { - locker.readFile(name, {lock: true}, function(err, res) { - if (err) { - return cb(err); - } - return cb(null, res); - }); -} - -// close and unlock file -function unlock_file(name, cb) { - locker.unlockFile(name, cb); -} - -function parse_htpasswd(input) { - let result = {}; - input.split('\n').forEach(function(line) { - let args = line.split(':', 3); - if (args.length > 1) result[args[0]] = args[1]; - }); - return result; -} - -function verify_password(user, passwd, hash) { - if (hash.indexOf('{PLAIN}') === 0) { - return passwd === hash.substr(7); - } else if (hash.indexOf('{SHA}') === 0) { - return crypto.createHash('sha1').update(passwd, 'binary').digest('base64') === hash.substr(5); - } else { - return ( - // for backwards compatibility, first check md5 then check crypt3 - md5(passwd, hash) === hash || - crypt3(passwd, hash) === hash - ); - } -} - -function add_user_to_htpasswd(body, user, passwd) { - if (user !== encodeURIComponent(user)) { - let err = Error('username should not contain non-uri-safe characters'); - err.status = 409; - throw err; - } - - if (crypt3) { - passwd = crypt3(passwd); - } else { - passwd = '{SHA}' + crypto.createHash('sha1').update(passwd, 'binary').digest('base64'); - } - let comment = 'autocreated ' + (new Date()).toJSON(); - - let newline = user + ':' + passwd + ':' + comment + '\n'; - if (body.length && body[body.length-1] !== '\n') newline = '\n' + newline; - return body + newline; -} - -module.exports.parse_htpasswd = parse_htpasswd; -module.exports.verify_password = verify_password; -module.exports.add_user_to_htpasswd = add_user_to_htpasswd; -module.exports.lock_and_read = lock_and_read; -module.exports.unlock_file = unlock_file; diff --git a/yarn.lock b/yarn.lock index 5dfc1a004..0325be487 100644 --- a/yarn.lock +++ b/yarn.lock @@ -182,7 +182,7 @@ version "9.4.0" resolved "https://registry.npmjs.org/@types/node/-/node-9.4.0.tgz#b85a0bcf1e1cc84eb4901b7e96966aedc6f078d1" -"@verdaccio/file-locking@0.0.5": +"@verdaccio/file-locking@0.0.5", "@verdaccio/file-locking@^0.0.5": version "0.0.5" resolved "https://registry.npmjs.org/@verdaccio/file-locking/-/file-locking-0.0.5.tgz#6172dfa2f7094a1da8e4c14906bfa33836b5713d" dependencies: @@ -2323,6 +2323,10 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" +crypto@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" + css-color-names@0.0.4: version "0.0.4" resolved "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" @@ -8821,6 +8825,15 @@ verdaccio-auth-memory@0.0.4: version "0.0.4" resolved "https://registry.npmjs.org/verdaccio-auth-memory/-/verdaccio-auth-memory-0.0.4.tgz#b44a65209778a8dc3c8d39478141a0bc22e04375" +verdaccio-htpasswd@0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/verdaccio-htpasswd/-/verdaccio-htpasswd-0.1.2.tgz#67aca5e9b0093be0fa2e018c524a7ba45f64f810" + dependencies: + "@verdaccio/file-locking" "^0.0.5" + apache-md5 "^1.1.2" + crypto "^1.0.1" + unix-crypt-td-js "^1.0.0" + verdaccio-memory@0.0.3: version "0.0.3" resolved "https://registry.npmjs.org/verdaccio-memory/-/verdaccio-memory-0.0.3.tgz#8ba6b51f2cf93d67958fedad5feb3f1e19933e48"