using System; using System.Collections; using System.Collections.Generic; using System.Configuration; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Web.Script.Serialization; namespace HanvonF710XAttendanceService { /// /// F710X / AI Biometric web API client (POST http://{ip}/api). /// Auth: each request includes the device web UI password. /// internal sealed class HanvonHttpApiClient { private readonly string _baseUrl; private readonly string _password; private readonly int _timeoutMs; private readonly JavaScriptSerializer _json = new JavaScriptSerializer { MaxJsonLength = int.MaxValue }; public HanvonHttpApiClient(string machineIp, int httpPort = 80, string password = null, int timeoutMs = 60000) { if (string.IsNullOrWhiteSpace(machineIp)) throw new ArgumentException("machineIp required"); int port = httpPort > 0 ? httpPort : 80; _baseUrl = "http://" + machineIp.Trim() + (port == 80 ? "" : ":" + port) + "/api"; _password = password ?? MachineScope.GetDevicePassword() ?? ""; _timeoutMs = timeoutMs > 0 ? timeoutMs : 60000; } public static HanvonHttpApiClient ForMachine(AttendanceMachine machine) { int httpPort = GetIntSetting("DEVICE_HTTP_PORT", 80); int timeoutMs = MachineUserDeleteSettings.GetHttpTimeoutMs(); return new HanvonHttpApiClient(machine.MachineIp, httpPort, MachineScope.GetDevicePassword(), timeoutMs); } public bool TryLogin(out string error) { error = null; try { var resp = Post(new Dictionary { { "cmd", "login" }, { "username", MachineScope.GetDeviceUsername() }, { "password", _password }, { "rememberMe", false } }); if (IsSuccess(resp)) return true; error = GetMsg(resp) ?? "login failed"; return false; } catch (Exception ex) { error = ex.Message; return false; } } public List GetLogs(DateTime? from, DateTime? to, out string error) { error = null; var all = new List(); try { int index = 0; for (int guard = 0; guard < 10000; guard++) { var req = new Dictionary { { "cmd", "getlog" }, { "password", _password }, { "index", index } }; if (from.HasValue) req["from"] = from.Value.ToString("yyyy-MM-dd HH:mm:ss"); if (to.HasValue) req["to"] = to.Value.ToString("yyyy-MM-dd HH:mm:ss"); var resp = Post(req); if (!IsSuccess(resp)) { error = GetMsg(resp) ?? "getlog failed"; return all; } var batch = ParseLogRecords(resp); all.AddRange(batch); int count = GetInt(resp, "count"); int toIdx = GetInt(resp, "to"); if (count <= 0 || toIdx <= 0 || count <= toIdx || batch.Count == 0) { break; } index = toIdx + 1; } return all; } catch (Exception ex) { error = ex.Message; return all; } } public List GetUserList(out string error) { error = null; var all = new List(); try { int stn = 1; for (int guard = 0; guard < 10000; guard++) { var resp = Post(new Dictionary { { "cmd", "getuserlist" }, { "password", _password }, { "stn", stn } }); if (!IsSuccess(resp)) { error = GetMsg(resp) ?? "getuserlist failed"; return all; } var batch = ParseUserRecords(resp); all.AddRange(batch); int count = GetInt(resp, "count"); int toIdx = GetInt(resp, "to"); if (count <= 0 || batch.Count == 0 || (toIdx > 0 && toIdx >= count)) { break; } stn = 0; } return all; } catch (Exception ex) { error = ex.Message; return all; } } public bool DeleteUsers(IEnumerable enrollIds, out string error) { error = null; try { var list = new List(); foreach (var id in enrollIds) { if (string.IsNullOrWhiteSpace(id)) continue; if (int.TryParse(id.Trim(), out int n)) list.Add(n); else list.Add(id.Trim()); } if (list.Count == 0) return true; var resp = Post(new Dictionary { { "cmd", "deleteusers" }, { "password", _password }, { "list", list } }); if (IsSuccess(resp)) return true; error = GetMsg(resp) ?? "deleteusers failed"; return false; } catch (Exception ex) { error = ex.Message; return false; } } public bool CleanLog(out string error) { error = null; try { var resp = Post(new Dictionary { { "cmd", "cleanlog" }, { "password", _password } }); if (IsSuccess(resp)) return true; error = GetMsg(resp) ?? "cleanlog failed"; return false; } catch (Exception ex) { error = ex.Message; return false; } } public static string NormalizeFaceTemplatePayload(byte[] blob, out string pushMode) { pushMode = "record"; if (blob == null || blob.Length == 0) return null; if (blob.Length >= 3 && blob[0] == 0xFF && blob[1] == 0xD8 && blob[2] == 0xFF) { pushMode = "face"; return Convert.ToBase64String(blob); } string text = Encoding.UTF8.GetString(blob).Trim(); if (string.IsNullOrWhiteSpace(text)) return null; if (text.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) { int comma = text.IndexOf(','); if (comma >= 0) text = text.Substring(comma + 1); } text = text.Replace("\r", "").Replace("\n", "").Trim(); if (string.IsNullOrWhiteSpace(text)) return null; try { byte[] decoded = Convert.FromBase64String(text); if (decoded.Length >= 3 && decoded[0] == 0xFF && decoded[1] == 0xD8 && decoded[2] == 0xFF) { pushMode = "face"; } } catch { // Biometric templates from getuserinfo may not decode as standard base64. } return text; } public bool TryEnsureUser(int enrollId, string name, out string error) { error = null; try { var resp = Post(new Dictionary { { "cmd", "setuserinfo" }, { "password", _password }, { "enrollid", enrollId }, { "name", name ?? enrollId.ToString() }, { "department", "" }, { "shiftid", 1 }, { "admin", 0 }, { "pwd", 0 }, { "card", 0 }, { "zoneid", 0 }, { "groupid", 0 }, { "access_times", 0 }, { "verifymode", 0 }, { "birthday", "" }, { "starttime", "" }, { "endtime", "" }, { "userprofile", "" } }); if (IsSuccess(resp)) return true; error = GetMsg(resp) ?? "setuserinfo failed"; return false; } catch (Exception ex) { error = ex.Message; return false; } } public bool TrySetFaceTemplateRecord(int enrollId, string templatePayload, string pushMode, out string error) { error = null; if (string.IsNullOrWhiteSpace(templatePayload)) { error = "template record is empty"; return false; } try { var payload = new Dictionary { { "cmd", "setuserinfo" }, { "password", _password }, { "enrollid", enrollId } }; if (string.Equals(pushMode, "face", StringComparison.OrdinalIgnoreCase)) { payload["face"] = templatePayload; } else { payload["backupnum"] = GetIntSetting("DEVICE_FACE_BACKUPNUM", 50); payload["record"] = templatePayload; } var resp = Post(payload); if (IsSuccess(resp)) return true; error = GetMsg(resp) ?? "setuserinfo template push failed"; return false; } catch (Exception ex) { error = ex.Message; return false; } } public bool TrySetFacePhotoOnly(int enrollId, string faceBase64, out string error) { error = null; if (string.IsNullOrWhiteSpace(faceBase64)) { error = "face photo is empty"; return false; } try { var resp = Post(new Dictionary { { "cmd", "setuserinfo" }, { "password", _password }, { "enrollid", enrollId }, { "face", faceBase64 } }); if (IsSuccess(resp)) return true; error = GetMsg(resp) ?? "setuserinfo face push failed"; return false; } catch (Exception ex) { error = ex.Message; return false; } } public bool TrySetUserWithFacePhoto(int enrollId, string name, string faceBase64, out string error) { error = null; if (string.IsNullOrWhiteSpace(faceBase64)) { error = "face photo is empty"; return false; } try { var resp = Post(new Dictionary { { "cmd", "setuserinfo" }, { "password", _password }, { "enrollid", enrollId }, { "name", name ?? enrollId.ToString() }, { "department", "" }, { "shiftid", 1 }, { "admin", 0 }, { "pwd", 0 }, { "card", 0 }, { "zoneid", 0 }, { "groupid", 0 }, { "access_times", 0 }, { "verifymode", 8 }, { "birthday", "" }, { "starttime", "" }, { "endtime", "" }, { "userprofile", "" }, { "face", faceBase64 } }); if (IsSuccess(resp)) return true; error = GetMsg(resp) ?? "setuserinfo face push failed"; return false; } catch (Exception ex) { error = ex.Message; return false; } } public bool TryGetFaceRecord(int enrollId, out string recordBase64, out string name, out string error) { recordBase64 = null; name = null; error = null; try { var resp = Post(new Dictionary { { "cmd", "getuserinfo" }, { "password", _password }, { "enrollid", enrollId }, { "backupnum", GetIntSetting("DEVICE_FACE_BACKUPNUM", 50) } }); if (!IsSuccess(resp)) { error = GetMsg(resp) ?? "getuserinfo failed"; return false; } name = GetString(resp, "name"); recordBase64 = GetString(resp, "record"); if (string.IsNullOrWhiteSpace(recordBase64)) { error = "no face/record payload"; return false; } return true; } catch (Exception ex) { error = ex.Message; return false; } } public bool TryGetUserProfile(int enrollId, out string faceflag, out string photourl, out string name, out string error) { faceflag = null; photourl = null; name = null; error = null; try { var resp = Post(new Dictionary { { "cmd", "getuserinfo" }, { "password", _password }, { "enrollid", enrollId } }); if (!IsSuccess(resp)) { error = GetMsg(resp) ?? "getuserinfo failed"; return false; } name = GetString(resp, "name"); faceflag = GetString(resp, "faceflag"); photourl = GetString(resp, "photourl"); return true; } catch (Exception ex) { error = ex.Message; return false; } } public static bool ProfileIndicatesFaceEnrolled(string faceflag, string photourl) { if (!string.IsNullOrWhiteSpace(photourl)) { return true; } if (string.IsNullOrWhiteSpace(faceflag)) { return false; } if (string.Equals(faceflag, "0", StringComparison.OrdinalIgnoreCase) || string.Equals(faceflag, "false", StringComparison.OrdinalIgnoreCase)) { return false; } if (int.TryParse(faceflag.Trim(), out int flagValue)) { return flagValue > 0; } return true; } public bool UserHasFacePhoto(int enrollId, out string faceflag, out string photourl, out string error) { faceflag = null; photourl = null; error = null; if (!TryGetUserProfile(enrollId, out faceflag, out photourl, out _, out error)) { return false; } if (ProfileIndicatesFaceEnrolled(faceflag, photourl)) { return true; } error = "no face enrolled"; return false; } public bool TryUploadDbToDeviceTemplate(int enrollId, string name, DbToDeviceUploadPlan plan, out string error) { error = null; if (plan == null || !plan.HasUploadPayload) { error = "template payload is empty"; return false; } int delayMs = GetIntSetting("TEMPLATE_DEVICE_DELAY_MS", 300); if (string.Equals(plan.PushMode, "face", StringComparison.OrdinalIgnoreCase)) { if (!TryEnsureUser(enrollId, name, out error)) { return false; } if (delayMs > 0) { System.Threading.Thread.Sleep(delayMs); } return TrySetFacePhotoOnly(enrollId, plan.Payload, out error); } if (!TryEnsureUser(enrollId, name, out error)) { return false; } if (delayMs > 0) { System.Threading.Thread.Sleep(delayMs); } if (!TrySetFaceTemplateRecord(enrollId, plan.Payload, plan.PushMode, out error)) { return false; } return true; } public bool VerifyDbToDeviceTemplate(int enrollId, DbToDeviceUploadPlan plan, out string faceflag, out string photourl, out string error) { faceflag = null; photourl = null; error = null; if (plan != null && plan.UseProfileVerification) { return UserHasFacePhoto(enrollId, out faceflag, out photourl, out error); } if (UserHasFaceTemplate(enrollId, out error)) { return true; } return false; } public bool TryRestoreUserWithFaceTemplate(int enrollId, string name, string templatePayload, string pushMode, out string error) { error = null; if (string.IsNullOrWhiteSpace(templatePayload)) { error = "template record is empty"; return false; } if (!TryEnsureUser(enrollId, name, out error)) { return false; } int delayMs = GetIntSetting("TEMPLATE_DEVICE_DELAY_MS", 300); if (delayMs > 0) { System.Threading.Thread.Sleep(delayMs); } if (!TrySetFaceTemplateRecord(enrollId, templatePayload, pushMode, out error)) { string altMode = string.Equals(pushMode, "face", StringComparison.OrdinalIgnoreCase) ? "record" : "face"; if (!TrySetFaceTemplateRecord(enrollId, templatePayload, altMode, out error)) { return false; } } return true; } public bool UserHasFaceTemplate(int enrollId, out string error) { return TryGetFaceRecord(enrollId, out _, out _, out error); } public HttpExchangeResult PostDetailed(Dictionary payload) { var result = new HttpExchangeResult { Url = _baseUrl, Method = "POST", RequestContentType = "application/json; charset=utf-8" }; string body = _json.Serialize(payload ?? new Dictionary()); result.RequestBodySanitized = HanvonHttpDiagnostics.SanitizeJsonBody(body); try { var req = (HttpWebRequest)WebRequest.Create(_baseUrl); req.Method = "POST"; req.ContentType = result.RequestContentType; req.Timeout = _timeoutMs; req.ReadWriteTimeout = _timeoutMs; req.KeepAlive = false; byte[] bytes = Encoding.UTF8.GetBytes(body); req.ContentLength = bytes.Length; using (var stream = req.GetRequestStream()) { stream.Write(bytes, 0, bytes.Length); } using (var resp = (HttpWebResponse)req.GetResponse()) using (var reader = new StreamReader(resp.GetResponseStream(), Encoding.UTF8)) { result.StatusCode = (int)resp.StatusCode; result.ResponseHeaders = FormatResponseHeaders(resp); string text = reader.ReadToEnd(); result.ResponseBody = HanvonHttpDiagnostics.SanitizeJsonBody(text); result.ParsedResponse = ParseResponseBody(text); } } catch (WebException wex) { result.Error = wex.Message; if (wex.Response is HttpWebResponse errResp) { result.StatusCode = (int)errResp.StatusCode; result.ResponseHeaders = FormatResponseHeaders(errResp); using (var reader = new StreamReader(errResp.GetResponseStream() ?? Stream.Null, Encoding.UTF8)) { string text = reader.ReadToEnd(); result.ResponseBody = HanvonHttpDiagnostics.SanitizeJsonBody(text); result.ParsedResponse = ParseResponseBody(text); } } } catch (Exception ex) { result.Error = ex.Message; } return result; } public void DiagnoseDbToDeviceEmployee( int enrollId, string empId, string displayName, string templatePayload, string pushMode, TextWriter logWriter) { if (logWriter == null) throw new ArgumentNullException(nameof(logWriter)); logWriter.WriteLine($"[DB_TO_DEVICE] DIAG emp={empId} enrollid={enrollId} target={_baseUrl}"); logWriter.WriteLine($"[DB_TO_DEVICE] DIAG template {HanvonHttpDiagnostics.DescribeTemplatePayload(templatePayload, pushMode)}"); var login = PostDetailed(new Dictionary { { "cmd", "login" }, { "username", MachineScope.GetDeviceUsername() }, { "password", _password }, { "rememberMe", false } }); HanvonHttpDiagnostics.WriteExchange(logWriter, "LOGIN", empId, login); var listBefore = PostDetailed(new Dictionary { { "cmd", "getuserlist" }, { "password", _password }, { "stn", 1 } }); HanvonHttpDiagnostics.WriteExchange(logWriter, "USERLIST_BEFORE", empId, listBefore); var profileBefore = PostDetailed(new Dictionary { { "cmd", "getuserinfo" }, { "password", _password }, { "enrollid", enrollId } }); HanvonHttpDiagnostics.WriteExchange(logWriter, "VERIFY_PROFILE_BEFORE", empId, profileBefore); var recordBefore = PostDetailed(new Dictionary { { "cmd", "getuserinfo" }, { "password", _password }, { "enrollid", enrollId }, { "backupnum", GetIntSetting("DEVICE_FACE_BACKUPNUM", 50) } }); HanvonHttpDiagnostics.WriteExchange(logWriter, "VERIFY_RECORD_BEFORE", empId, recordBefore); var ensurePayload = new Dictionary { { "cmd", "setuserinfo" }, { "password", _password }, { "enrollid", enrollId }, { "name", displayName ?? enrollId.ToString() }, { "department", "" }, { "shiftid", 1 }, { "admin", 0 }, { "pwd", 0 }, { "card", 0 }, { "zoneid", 0 }, { "groupid", 0 }, { "access_times", 0 }, { "verifymode", 0 }, { "birthday", "" }, { "starttime", "" }, { "endtime", "" }, { "userprofile", "" } }; var ensure = PostDetailed(ensurePayload); HanvonHttpDiagnostics.WriteExchange(logWriter, "SEND ensure_user setuserinfo", empId, ensure); var templatePayloadReq = new Dictionary { { "cmd", "setuserinfo" }, { "password", _password }, { "enrollid", enrollId } }; string templateField; if (string.Equals(pushMode, "face", StringComparison.OrdinalIgnoreCase)) { templatePayloadReq["face"] = templatePayload; templateField = "face"; } else { templatePayloadReq["backupnum"] = GetIntSetting("DEVICE_FACE_BACKUPNUM", 50); templatePayloadReq["record"] = templatePayload; templateField = "record"; } logWriter.WriteLine($"[DB_TO_DEVICE] emp={empId} SEND template_field={templateField} enrollid={enrollId} employee_number={displayName}"); var push = PostDetailed(templatePayloadReq); HanvonHttpDiagnostics.WriteExchange(logWriter, "SEND template setuserinfo", empId, push); int delayMs = GetIntSetting("TEMPLATE_DEVICE_DELAY_MS", 300); if (delayMs > 0) System.Threading.Thread.Sleep(delayMs); var profileAfter = PostDetailed(new Dictionary { { "cmd", "getuserinfo" }, { "password", _password }, { "enrollid", enrollId } }); HanvonHttpDiagnostics.WriteExchange(logWriter, "VERIFY profile getuserinfo (no backupnum)", empId, profileAfter); var recordAfter = PostDetailed(new Dictionary { { "cmd", "getuserinfo" }, { "password", _password }, { "enrollid", enrollId }, { "backupnum", GetIntSetting("DEVICE_FACE_BACKUPNUM", 50) } }); HanvonHttpDiagnostics.WriteExchange(logWriter, "VERIFY record getuserinfo backupnum=50", empId, recordAfter); var listAfter = PostDetailed(new Dictionary { { "cmd", "getuserlist" }, { "password", _password }, { "stn", 1 } }); HanvonHttpDiagnostics.WriteExchange(logWriter, "USERLIST_AFTER", empId, listAfter); var users = GetUserList(out string listErr); var match = users.FirstOrDefault(u => string.Equals(u.DeviceUserId, empId, StringComparison.OrdinalIgnoreCase) || string.Equals(u.SerialNumber, empId, StringComparison.OrdinalIgnoreCase) || string.Equals(u.Name, empId, StringComparison.OrdinalIgnoreCase)); string recordValue = GetString(recordAfter.ParsedResponse, "record"); HanvonHttpDiagnostics.WriteResultSummary( logWriter, empId, match != null, match?.DeviceUserId, match?.Name, null, IsSuccess(profileAfter.ParsedResponse), GetString(profileAfter.ParsedResponse, "faceflag"), GetString(profileAfter.ParsedResponse, "photourl"), !string.IsNullOrWhiteSpace(recordValue), recordValue?.Length ?? 0, HanvonHttpDiagnostics.DescribeTemplatePayload(templatePayload, pushMode)); logWriter.WriteLine($"[DB_TO_DEVICE] emp={empId} SEND_OK_MEANS ensure_result={IsSuccess(ensure.ParsedResponse)} template_result={IsSuccess(push.ParsedResponse)} (Hanvon JSON result=true only, not proof of stored face)"); logWriter.WriteLine($"[DB_TO_DEVICE] emp={empId} VERIFY_FAIL_REASON={(string.IsNullOrWhiteSpace(recordValue) ? "getuserinfo backupnum=50 returned empty record field" : "record present")}"); if (!string.IsNullOrWhiteSpace(listErr)) { logWriter.WriteLine($"[DB_TO_DEVICE] getuserlist_err={listErr}"); } } private Dictionary Post(Dictionary payload) { var detailed = PostDetailed(payload); if (detailed.ParsedResponse != null && detailed.ParsedResponse.Count > 0) { return detailed.ParsedResponse; } if (!string.IsNullOrWhiteSpace(detailed.Error) && detailed.StatusCode == 0) { throw new IOException(detailed.Error); } return detailed.ParsedResponse ?? new Dictionary(); } private static Dictionary ParseResponseBody(string text) { if (string.IsNullOrWhiteSpace(text)) { return new Dictionary(); } try { return new JavaScriptSerializer { MaxJsonLength = int.MaxValue } .Deserialize>(text) ?? new Dictionary(); } catch { return new Dictionary(); } } private static string FormatResponseHeaders(HttpWebResponse resp) { if (resp?.Headers == null) return ""; var pairs = new List(); foreach (string key in resp.Headers.AllKeys) { pairs.Add(key + "=" + resp.Headers[key]); } return string.Join("; ", pairs); } private static bool IsSuccess(Dictionary resp) { if (resp == null) return false; if (!resp.TryGetValue("result", out object r) || r == null) return false; if (r is bool b) return b; return string.Equals(r.ToString(), "true", StringComparison.OrdinalIgnoreCase); } private static string GetMsg(Dictionary resp) { return GetString(resp, "msg") ?? GetString(resp, "reason"); } private static string GetString(Dictionary resp, string key) { if (resp == null || !resp.TryGetValue(key, out object v) || v == null) return null; return v.ToString(); } private static int GetInt(Dictionary resp, string key) { if (resp == null || !resp.TryGetValue(key, out object v) || v == null) return 0; if (v is int i) return i; if (v is long l) return (int)l; if (v is decimal d) return (int)d; int.TryParse(v.ToString(), out int n); return n; } private static List ParseLogRecords(Dictionary resp) { var list = new List(); if (resp == null || !resp.TryGetValue("record", out object rec) || rec == null) return list; if (rec is ArrayList arr) { foreach (var item in arr) AddLog(list, item as Dictionary); } else if (rec is object[] oa) { foreach (var item in oa) AddLog(list, item as Dictionary); } return list; } private static void AddLog(List list, Dictionary row) { if (row == null) return; string enroll = GetString(row, "enrollid") ?? ""; string name = GetString(row, "name") ?? ""; string timeStr = GetString(row, "time") ?? ""; if (!DateTime.TryParse(timeStr, out DateTime time)) return; string acNo = enroll.Trim(); if (string.IsNullOrWhiteSpace(acNo)) return; list.Add(new HttpLogRecord { EnrollId = enroll.Trim(), Name = name.Trim(), AcNo = acNo, CheckTime = time, Mode = GetInt(row, "mode"), InOut = GetInt(row, "inout"), Event = GetInt(row, "event") }); } private static List ParseUserRecords(Dictionary resp) { var list = new List(); if (resp == null || !resp.TryGetValue("record", out object rec) || rec == null) return list; IEnumerable items = null; if (rec is ArrayList arr) items = arr; else if (rec is object[] oa) items = oa; if (items == null) return list; foreach (var item in items) { var row = item as Dictionary; if (row == null) continue; string id = GetString(row, "id") ?? ""; string name = GetString(row, "name") ?? ""; string serial = !string.IsNullOrWhiteSpace(name) ? name.Trim() : id.Trim(); if (string.IsNullOrWhiteSpace(serial)) continue; list.Add(new HttpUserRecord { DeviceUserId = id.Trim(), Name = name.Trim(), SerialNumber = serial }); } return list; } private static int GetIntSetting(string key, int defaultValue) { try { var raw = ConfigurationManager.AppSettings[key]; if (string.IsNullOrWhiteSpace(raw)) return defaultValue; return int.TryParse(raw.Trim(), out var v) && v > 0 ? v : defaultValue; } catch { return defaultValue; } } } internal sealed class HttpLogRecord { public string EnrollId { get; set; } public string Name { get; set; } public string AcNo { get; set; } public DateTime CheckTime { get; set; } public int Mode { get; set; } public int InOut { get; set; } public int Event { get; set; } } internal sealed class HttpUserRecord { public string DeviceUserId { get; set; } public string Name { get; set; } public string SerialNumber { get; set; } } internal static class DeviceProtocol { public static bool UseHttp() { var p = ConfigurationManager.AppSettings["DEVICE_PROTOCOL"] ?? "HTTP"; return string.Equals(p.Trim(), "HTTP", StringComparison.OrdinalIgnoreCase) || string.Equals(p.Trim(), "WEB", StringComparison.OrdinalIgnoreCase) || string.Equals(p.Trim(), "API", StringComparison.OrdinalIgnoreCase); } } }