diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index f510064..23fbd22 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -18,3 +18,4 @@ services: - LDAP_ADMIN_PASSWORD=password - LDAP_USERS=gauss,einstein - LDAP_PASSWORDS=password,password + - LDAP_GROUP=科学A部 diff --git a/index.js b/index.js index 20279ae..4661a07 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,46 @@ const assert = require('assert') const ldap = require('ldapjs') +// convert an escaped utf8 string returned from ldapjs +function _isHex(c) { + return ( + (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') + ) +} +function _parseEscapedHexToUtf8(s) { + // convert 'cn=\\e7\\a0\\94\\e5\\8f\\91A\\e9\\83\\a8,ou=users,dc=example,dc=com' + // to 'cn=研发A部,ou=users,dc=example,dc=com' + let ret = Buffer.alloc(0) + let len = s.length + for (let i = 0; i < len; i++) { + let c = s[i] + let item + if (c == '\\' && i < len - 2 && _isHex(s[i + 1]) && _isHex(s[i + 2])) { + item = Buffer.from(s.substring(i + 1, i + 3), 'hex') + i += 2 + } else { + item = Buffer.from(c) + } + ret = Buffer.concat([ret, item]) + } + return ret.toString() +} + +function _recursiveParseHexString(obj) { + if (Array.isArray(obj)) { + return obj.map((ele) => _recursiveParseHexString(ele)) + } + if (typeof obj == 'string') { + return _parseEscapedHexToUtf8(obj) + } + if (typeof obj == 'object') { + for (let key in obj) { + obj[key] = _recursiveParseHexString(obj[key]) + } + return obj + } + return obj +} // convert a SearchResultEntry object in ldapjs 3.0 // to a user object to maintain backward compatibility @@ -11,7 +51,7 @@ function _searchResultToUser(pojo) { user[attribute.type] = attribute.values.length == 1 ? attribute.values[0] : attribute.values }) - return user + return _recursiveParseHexString(user) } // bind and return the ldap client function _ldapBind(dn, password, starttls, ldapOpts) { @@ -142,7 +182,7 @@ async function _searchUserGroups( return } res.on('searchEntry', function (entry) { - groups.push(entry.pojo) + groups.push(_recursiveParseHexString(entry.pojo)) }) res.on('searchReference', function (referral) {}) res.on('error', function (err) { @@ -422,3 +462,9 @@ class LdapAuthenticationError extends Error { module.exports.authenticate = authenticate module.exports.LdapAuthenticationError = LdapAuthenticationError + +module.exports.exportForTesting = { + _isHex, + _parseEscapedHexToUtf8, + _recursiveParseHexString, +} diff --git a/package-lock.json b/package-lock.json index 35b14f4..d6d6972 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ldap-authentication", - "version": "3.1.0", + "version": "3.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ldap-authentication", - "version": "3.1.0", + "version": "3.2.1", "license": "BSD-2-Clause", "dependencies": { "ldapjs": "^3.0.7" diff --git a/package.json b/package.json index 7312ea8..897d973 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ldap-authentication", - "version": "3.2.1", + "version": "3.2.2", "description": "A simple async nodejs library for LDAP user authentication", "main": "index.js", "types": "./index.d.ts", diff --git a/test/string.spec.js b/test/string.spec.js new file mode 100644 index 0000000..60ac706 --- /dev/null +++ b/test/string.spec.js @@ -0,0 +1,43 @@ +const { exportForTesting } = require('../index.js') +const { _isHex, _parseEscapedHexToUtf8, _recursiveParseHexString } = + exportForTesting + +describe('string conversion test', () => { + it('unescape string test', () => { + let s = + 'cn=\\e7\\a0\\94\\e5\\8f\\91A\\e9\\83\\a8,ou=users,dc=example,dc=com' + let us = _parseEscapedHexToUtf8(s) + expect(us).toEqual('cn=研发A部,ou=users,dc=example,dc=com') + }) + it('unescape string test2', () => { + let s = + 'cn=\\e7\\a0\\94\\e5\\8f\\91A\\e9\\83\\a8\\c2\\a9,ou=users,dc=example,dc=com' + let us = _parseEscapedHexToUtf8(s) + expect(us).toEqual('cn=研发A部©,ou=users,dc=example,dc=com') + }) + it('unescape string test3', () => { + let s = 'cn=ABC,ou=users,dc=example,dc=com' + let us = _parseEscapedHexToUtf8(s) + expect(us).toEqual('cn=ABC,ou=users,dc=example,dc=com') + }) + it('convert obj', () => { + let target = { + a: ['研发A部©', 'abc'], + b: 'xyz', + c: true, + d: null, + e: '研发A部©', + f: 1000, + } + let obj = { + a: ['\\e7\\a0\\94\\e5\\8f\\91A\\e9\\83\\a8\\c2\\a9', 'abc'], + b: 'xyz', + c: true, + d: null, + e: '\\e7\\a0\\94\\e5\\8f\\91A\\e9\\83\\a8\\c2\\a9', + f: 1000, + } + let converted = _recursiveParseHexString(obj) + expect(converted).toEqual(target) + }) +}) diff --git a/test/test.spec.js b/test/test.spec.js index 22d32e0..f8c28f8 100644 --- a/test/test.spec.js +++ b/test/test.spec.js @@ -124,6 +124,9 @@ describe('ldap-authentication test', () => { let user = await authenticate(options) expect(user).toBeTruthy() expect(user.groups.length).toBeGreaterThan(0) + expect(user.groups[0].objectName).toEqual( + 'cn=科学A部,ou=users,dc=example,dc=com' + ) }) it('Use regular user to authenticate and fetch user group information', async () => { let options = { @@ -144,6 +147,9 @@ describe('ldap-authentication test', () => { let user = await authenticate(options) expect(user).toBeTruthy() expect(user.groups.length).toBeGreaterThan(0) + expect(user.groups[0].objectName).toEqual( + 'cn=科学A部,ou=users,dc=example,dc=com' + ) }) it('Not specifying groupMemberAttribute or groupMemberUserAttribute should not cause an error and fallback to default values', async () => { let options = {