Comparing version 3.2.0 to 3.3.0
@@ -5,2 +5,8 @@ # Changelog | ||
## [3.3.0][] - 2021-02-19 | ||
- Change library file structure | ||
- Move Semaphore from metacom with fixes | ||
- Move timeout function from metacom | ||
## [3.2.0][] - 2021-02-03 | ||
@@ -32,3 +38,4 @@ | ||
[unreleased]: https://github.com/metarhia/metautil/compare/v3.2.0...HEAD | ||
[unreleased]: https://github.com/metarhia/metautil/compare/v3.3.0...HEAD | ||
[3.3.0]: https://github.com/metarhia/metautil/compare/v3.2.0...v3.3.0 | ||
[3.2.0]: https://github.com/metarhia/metautil/compare/v3.1.0...v3.2.0 | ||
@@ -35,0 +42,0 @@ [3.1.0]: https://github.com/metarhia/metautil/compare/v3.0.1...v3.1.0 |
282
metautil.js
'use strict'; | ||
const path = require('path'); | ||
const crypto = require('crypto'); | ||
const utilities = require('./lib/utilities.js'); | ||
const secutity = require('./lib/security.js'); | ||
const semaphore = require('./lib/semaphore.js'); | ||
const random = (min, max) => { | ||
if (max === undefined) { | ||
max = min; | ||
min = 0; | ||
} | ||
return min + Math.floor(Math.random() * (max - min + 1)); | ||
}; | ||
const UINT32_MAX = 0xffffffff; | ||
const BUF_LEN = 1024; | ||
const BUF_SIZE = BUF_LEN * Uint32Array.BYTES_PER_ELEMENT; | ||
const randomPrefetcher = { | ||
buf: crypto.randomBytes(BUF_SIZE), | ||
pos: 0, | ||
}; | ||
const cryptoRandom = () => { | ||
if (randomPrefetcher.pos === randomPrefetcher.buf.length) { | ||
randomPrefetcher.pos = 0; | ||
crypto.randomFillSync(randomPrefetcher.buf); | ||
} | ||
const val = randomPrefetcher.buf.readUInt32LE(randomPrefetcher.pos); | ||
randomPrefetcher.pos += Uint32Array.BYTES_PER_ELEMENT; | ||
return val / (UINT32_MAX + 1); | ||
}; | ||
const sample = (arr) => { | ||
const index = Math.floor(Math.random() * arr.length); | ||
return arr[index]; | ||
}; | ||
const ipToInt = (ip = '127.0.0.1') => { | ||
if (ip === '') return 0; | ||
const bytes = ip.split('.'); | ||
let res = 0; | ||
for (const byte of bytes) { | ||
res = (res << 8) + parseInt(byte, 10); | ||
} | ||
return res; | ||
}; | ||
const parseHost = (host) => { | ||
if (!host) { | ||
return 'no-host-name-in-http-headers'; | ||
} | ||
const portOffset = host.indexOf(':'); | ||
if (portOffset > -1) host = host.substr(0, portOffset); | ||
return host; | ||
}; | ||
const replace = (str, substr, newstr) => { | ||
if (substr === '') return str; | ||
let src = str; | ||
let res = ''; | ||
do { | ||
const index = src.indexOf(substr); | ||
if (index === -1) return res + src; | ||
const start = src.substring(0, index); | ||
src = src.substring(index + substr.length, src.length); | ||
res += start + newstr; | ||
} while (true); | ||
}; | ||
const fileExt = (fileName) => { | ||
const ext = path.extname(fileName).toLowerCase(); | ||
return replace(ext, '.', ''); | ||
}; | ||
const between = (s, prefix, suffix) => { | ||
let i = s.indexOf(prefix); | ||
if (i === -1) return ''; | ||
s = s.substring(i + prefix.length); | ||
if (suffix) { | ||
i = s.indexOf(suffix); | ||
if (i === -1) return ''; | ||
s = s.substring(0, i); | ||
} | ||
return s; | ||
}; | ||
const isFirstUpper = (s) => !!s && s[0] === s[0].toUpperCase(); | ||
const isConstant = (s) => s === s.toUpperCase(); | ||
const twoDigit = (n) => { | ||
const s = n.toString(); | ||
if (n < 10) return '0' + s; | ||
return s; | ||
}; | ||
const nowDate = (date) => { | ||
if (!date) date = new Date(); | ||
const yyyy = date.getUTCFullYear().toString(); | ||
const mm = twoDigit(date.getUTCMonth() + 1); | ||
const dd = twoDigit(date.getUTCDate()); | ||
return `${yyyy}-${mm}-${dd}`; | ||
}; | ||
const DURATION_UNITS = { | ||
d: 86400, // days | ||
h: 3600, // hours | ||
m: 60, // minutes | ||
s: 1, // seconds | ||
}; | ||
const duration = (s) => { | ||
if (typeof s === 'number') return s; | ||
if (typeof s !== 'string') return 0; | ||
let result = 0; | ||
const parts = s.split(' '); | ||
for (const part of parts) { | ||
const unit = part.slice(-1); | ||
const value = parseInt(part.slice(0, -1)); | ||
const mult = DURATION_UNITS[unit]; | ||
if (!isNaN(value) && mult) result += value * mult; | ||
} | ||
return result * 1000; | ||
}; | ||
const generateKey = (length, possible) => { | ||
const base = possible.length; | ||
let key = ''; | ||
for (let i = 0; i < length; i++) { | ||
const index = Math.floor(cryptoRandom() * base); | ||
key += possible[index]; | ||
} | ||
return key; | ||
}; | ||
const crcToken = (secret, key) => { | ||
const md5 = crypto.createHash('md5').update(key + secret); | ||
return md5.digest('hex').substring(0, 4); | ||
}; | ||
const generateToken = (secret, characters, length) => { | ||
const key = generateKey(length - 4, characters); | ||
return key + crcToken(secret, key); | ||
}; | ||
const validateToken = (secret, token) => { | ||
if (!token) return false; | ||
const len = token.length; | ||
const crc = token.slice(len - 4); | ||
const key = token.slice(0, -4); | ||
return crcToken(secret, key) === crc; | ||
}; | ||
const makePrivate = (instance) => { | ||
const iface = {}; | ||
const fields = Object.keys(instance); | ||
for (const fieldName of fields) { | ||
const field = instance[fieldName]; | ||
if (isConstant(fieldName)) { | ||
iface[fieldName] = field; | ||
} else if (typeof field === 'function') { | ||
const bindedMethod = field.bind(instance); | ||
iface[fieldName] = bindedMethod; | ||
instance[fieldName] = bindedMethod; | ||
} | ||
} | ||
return iface; | ||
}; | ||
const protect = (allowMixins, ...namespaces) => { | ||
for (const namespace of namespaces) { | ||
const names = Object.keys(namespace); | ||
for (const name of names) { | ||
const target = namespace[name]; | ||
if (!allowMixins.includes(name)) Object.freeze(target); | ||
} | ||
} | ||
}; | ||
const parseCookies = (cookie) => { | ||
const values = {}; | ||
const items = cookie.split(';'); | ||
for (const item of items) { | ||
const parts = item.split('='); | ||
const key = parts[0].trim(); | ||
const val = parts[1] || ''; | ||
values[key] = val.trim(); | ||
} | ||
return values; | ||
}; | ||
// Only change these if you know what you're doing | ||
const SCRYPT_PARAMS = { N: 32768, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }; | ||
const SCRYPT_PREFIX = '$scrypt$N=32768,r=8,p=1,maxmem=67108864$'; | ||
const serializeHash = (hash, salt) => { | ||
const saltString = salt.toString('base64').split('=')[0]; | ||
const hashString = hash.toString('base64').split('=')[0]; | ||
return `${SCRYPT_PREFIX}${saltString}$${hashString}`; | ||
}; | ||
const deserializeHash = (phcString) => { | ||
const parsed = phcString.split('$'); | ||
parsed.shift(); | ||
if (parsed[0] !== 'scrypt') { | ||
throw new Error('Node.js crypto module only supports scrypt'); | ||
} | ||
const params = Object.fromEntries( | ||
parsed[1].split(',').map((p) => { | ||
const kv = p.split('='); | ||
kv[1] = Number(kv[1]); | ||
return kv; | ||
}) | ||
); | ||
const salt = Buffer.from(parsed[2], 'base64'); | ||
const hash = Buffer.from(parsed[3], 'base64'); | ||
return { params, salt, hash }; | ||
}; | ||
const SALT_LEN = 32; | ||
const KEY_LEN = 64; | ||
const hashPassword = (password) => | ||
new Promise((resolve, reject) => { | ||
crypto.randomBytes(SALT_LEN, (err, salt) => { | ||
if (err) { | ||
reject(err); | ||
return; | ||
} | ||
crypto.scrypt(password, salt, KEY_LEN, SCRYPT_PARAMS, (err, hash) => { | ||
if (err) { | ||
reject(err); | ||
return; | ||
} | ||
resolve(serializeHash(hash, salt)); | ||
}); | ||
}); | ||
}); | ||
let defaultHash; | ||
hashPassword('').then((hash) => { | ||
defaultHash = hash; | ||
}); | ||
const validatePassword = (password, serHash = defaultHash) => { | ||
const { params, salt, hash } = deserializeHash(serHash); | ||
return new Promise((resolve, reject) => { | ||
const callback = (err, hashedPassword) => { | ||
if (err) { | ||
reject(err); | ||
return; | ||
} | ||
resolve(crypto.timingSafeEqual(hashedPassword, hash)); | ||
}; | ||
crypto.scrypt(password, salt, hash.length, params, callback); | ||
}); | ||
}; | ||
module.exports = { | ||
sample, | ||
ipToInt, | ||
parseHost, | ||
replace, | ||
fileExt, | ||
between, | ||
isFirstUpper, | ||
isConstant, | ||
nowDate, | ||
duration, | ||
generateKey, | ||
generateToken, | ||
crcToken, | ||
validateToken, | ||
random, | ||
cryptoRandom, | ||
makePrivate, | ||
protect, | ||
parseCookies, | ||
hashPassword, | ||
validatePassword, | ||
}; | ||
module.exports = { ...utilities, ...secutity, ...semaphore }; |
{ | ||
"name": "metautil", | ||
"version": "3.2.0", | ||
"version": "3.3.0", | ||
"author": "Timur Shemsedinov <timur.shemsedinov@gmail.com>", | ||
@@ -13,3 +13,5 @@ "license": "MIT", | ||
"main": "metautil.js", | ||
"files": [], | ||
"files": [ | ||
"lib/" | ||
], | ||
"engines": { | ||
@@ -37,3 +39,3 @@ "node": "^12.9 || 14 || 15" | ||
"devDependencies": { | ||
"eslint": "^7.19.0", | ||
"eslint": "^7.20.0", | ||
"eslint-config-metarhia": "^7.0.1", | ||
@@ -40,0 +42,0 @@ "eslint-config-prettier": "^7.2.0", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
12748
8
300
1