import path from 'path'
import fs from 'fs'
import process from 'process'
import get from 'lodash-es/get.js'
import genID from 'wsemi/src/genID.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import ispint from 'wsemi/src/ispint.mjs'
import arrHas from 'wsemi/src/arrHas.mjs'
import str2b64 from 'wsemi/src/str2b64.mjs'
import execCli from 'wsemi/src/execCli.mjs'
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
import judgeResult from './judgeResult.mjs'
let fdSrv = path.resolve()
//timeoutMax, 未給opt.timeout時之逾時上限, 取計時器上限(約24.8天)即實質不設限, 使行為與改用execCli前相同
let timeoutMax = 2147483647
function isWindows() {
return process.platform === 'win32'
}
//readOutput, 讀取exe之輸出檔, 不存在與讀取失敗分開回報
function readOutput(fp) {
try {
return { state: 'read', text: fs.readFileSync(fp, 'utf8') }
}
catch (err) {
if (get(err, 'code') === 'ENOENT') {
return { state: 'missing' }
}
return { state: 'unreadable', msg: `${get(err, 'code', '')} ${get(err, 'message', '')}`.trim() }
}
}
/**
* 呼叫LDAP服務進行登入或取得使用者清單,係呼叫Net程式呼叫LDAP取得資料,故會有Net Framework版本與作業系統限制
*
* 輸入(含帳密)經子程序之stdin交給Net程式,不出現在程序之命令列;只轉交mode、account、password、ldappath,不改動呼叫端之input
*
* @param {Object} input 輸入呼叫資料物件
* @param {String} input.mode 輸入呼叫方法字串,可有'logIn'與'listUsers'
* @param {String} input.account 輸入LDAP帳號字串
* @param {String} input.password 輸入LDAP帳號之密碼字串
* @param {String} input.ldappath 輸入LDAP伺服器位址字串
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {String} [opt.ver='4.8'] 輸入調用windows程序之Net Framework版本字串,可有'4.5'、'4.6'、'4.7.2'與'4.8',預設'4.8'
* @param {Number} [opt.timeout] 輸入逾時毫秒數正整數,逾時將強制結束Net程式並reject,未給或非正整數時不設限(實際上限為計時器上限約24.8天)。LDAP伺服器接受連線卻不回應時,Net程式約240秒後自行以錯誤結束
* @returns {Promise} 回傳Promise。resolve回傳成功資料:logIn為該帳號之LDAP條目,帳密驗證成功但以samaccountname查無該帳號條目時回傳{success:null};listUsers為使用者條目陣列。reject一律回傳錯誤訊息字串:輸入檢查失敗為說明字串;LDAP回報之錯誤為其原始訊息;Net程式啟動失敗以'spawn['開頭;Net程式以非0結束以'code['開頭;逾時以'timeout['開頭;Net程式結束但未產生輸出檔為'connLDAP exited without output file';輸出內容不合格式以'invalid output from connLDAP:'開頭
* @example
* import WLdap from 'w-ldap'
*
* async function test() {
* let inp
* let r
* let account = '{account}'
* let password = '{password}'
* let ldappath = 'LDAP://{PATH}'
*
* inp = {
* mode: 'logIn',
* account,
* password,
* ldappath,
* }
* r = await WLdap(inp)
* console.log('logIn', r)
* w.fsWriteText('logIn.txt', w.o2j(r, true))
*
* inp = {
* mode: 'listUsers',
* account,
* password,
* ldappath,
* }
* r = await WLdap(inp)
* w.fsWriteText('listUsers.txt', w.o2j(r, true))
*
* }
* test()
* .catch((err) => {
* console.log('catch', err)
* })
*
*/
async function WLdap(input = {}, opt = {}) {
//isWindows
if (!isWindows()) {
return Promise.reject('operating system is not windows')
}
//check
if (!iseobj(input)) {
return Promise.reject('invalid input')
}
//check
let mode = get(input, 'mode')
if (mode !== 'logIn' && mode !== 'listUsers') {
return Promise.reject(`input.mode needs to be 'logIn' or 'listUsers'`)
}
let account = get(input, 'account')
if (!isestr(account)) {
return Promise.reject('invalid input.account')
}
let password = get(input, 'password')
if (!isestr(password)) {
return Promise.reject('invalid input.password')
}
let ldappath = get(input, 'ldappath')
if (!isestr(ldappath)) {
return Promise.reject('invalid input.ldappath')
}
//ver
let ver = get(opt, 'ver', null)
if (!isestr(ver)) {
ver = '4.8'
}
//check
if (!arrHas(ver, ['4.5', '4.6', '4.7.2', '4.8'])) {
return Promise.reject('opt.ver needs to be one of 4.5, 4.6, 4.7.2 or 4.8')
}
//timeout, 未給或非正整數時不設限
let timeout = get(opt, 'timeout', null)
if (!ispint(timeout)) {
timeout = timeoutMax
}
//fnExe
let fnExe = `connLDAP.net${ver}.exe`
//fdExe
let fdExe = ''
if (true) {
let fdExeSrc = `${fdSrv}/src/`
let fdExeNM = `${fdSrv}/node_modules/w-ldap/src/`
if (fsIsFile(`${fdExeSrc}${fnExe}`)) {
fdExe = fdExeSrc
}
else if (fsIsFile(`${fdExeNM}${fnExe}`)) {
fdExe = fdExeNM
}
else {
return Promise.reject('can not find folder for connLDAP')
}
}
// console.log('fdExe', fdExe)
//prog
let prog = `${fdExe}${fnExe}`
// console.log('prog', prog)
//id
let id = genID()
//fnOut, 輸出檔名由WLdap決定
let fnOut = `_${id}`
//payload, 只轉交exe所需之欄位另建物件, 不改動呼叫端之input
let payload = {
mode,
account,
password,
ldappath,
'__System:OutputFilename': fnOut,
}
//b64Input
let b64Input = str2b64(JSON.stringify(payload))
//exec, 輸入經stdin交給exe, 不出現在程序之命令列
let exec = null
try {
exec = await execCli(prog, [], { input: b64Input, timeoutMs: timeout })
}
catch (err) {
//execCli於spawn同步失敗(如EPERM、UNKNOWN)時會拋錯, 轉為同格式之結果物件, 只取code與message
exec = {
ok: false,
code: null,
error: `${get(err, 'code') || 'UNKNOWN'}: ${get(err, 'message', '')}`,
stdout: '',
stderr: '',
}
}
//fpOut
let fpOut = `${fdExe}${fnOut}`
//file
let file = readOutput(fpOut)
//unlinkSync, 清理盡力而為, 不決定結局
try {
fs.unlinkSync(fpOut)
}
catch (err) {}
//judgeResult
let r = judgeResult({ exec, timeoutMs: timeout, file })
if (r.state === 'error') {
return Promise.reject(r.msg)
}
return r.msg
}
export default WLdap