import os from 'os'
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 ensureBins from './ensureBins.mjs'
import getOutPlace from './getOutPlace.mjs'
import judgeResult from './judgeResult.mjs'
//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
*
* Net程式(connLDAP.net4.x.exe與其相鄰之Newtonsoft.Json.dll)不隨npm包發布:首次使用時經ensureBins依bin/manifest.json下載、逐檔校驗並整組放入快取,之後自快取取用;環境變數W_BIN_CACHE、W_BIN_MIRROR、W_BIN_OFFLINE見README〈External executables〉。輸出檔寫在作業系統暫存夾(跨磁碟機時寫在Net程式所在之快取夾),用畢即刪
*
* @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一律回傳錯誤訊息字串:輸入檢查失敗為說明字串;取得Net程式失敗(首次使用時下載、校驗或離線缺檔)以'ensureBins[錯誤碼]:'開頭;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`
//bundle, exe須與Newtonsoft.Json.dll相鄰, 故經ensureBins取得檔案組(校驗後整組放在同一資料夾), 失敗以字串拒絕
let bundle = null
try {
let bins = await ensureBins()
bundle = bins.bundles.connLDAP
}
catch (err) {
return Promise.reject(`ensureBins[${get(err, 'code', '')}]: ${get(err, 'message', '')}`)
}
//prog, fdExe
let prog = get(bundle, ['files', fnExe], null)
if (!isestr(prog)) {
return Promise.reject(`${fnExe} is not in the bundle of connLDAP`)
}
let fdExe = bundle.dir
// console.log('prog', prog)
//id
let id = genID()
//fnOut, fpOut, 輸出檔名由WLdap決定, 輸出檔落在作業系統暫存夾, 不寫入exe所在之快取夾
let { fnOut, fpOut } = getOutPlace(fdExe, os.tmpdir(), 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: '',
}
}
//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
}
//ensureBins, 預先取得並校驗外部執行檔組connLDAP(離線部署前於連網機器呼叫, 見README), 以靜態屬性匯出以維持單一default匯出
WLdap.ensureBins = ensureBins
export default WLdap