惠尔顿上网行为管理器 windows 服务 开机自动登录方法

Windows 网络认证保活程序。登录认证后通过 WebSocket 保持连接,默认每小时重新认证一次。

编译

需要 MinGW(MSYS2):

1
gcc wholeton_login.c -o wholeton_login.exe -lws2_32

用法

1
2
3
4
5
6
7
8
9
10
wholeton_login [选项]

--install 安装并启动系统服务(开机自动运行,需要管理员权限)
--uninstall 停止并卸载系统服务(需要管理员权限)
--user <用户名> HTTP 登录用户名(必填)
--pass <密码> HTTP 登录密码(必填)
--host <IP> 服务器地址(默认:10.100.100.3)
--port <端口> 服务器端口(默认:80)
--timeout <秒> 重新认证间隔(默认:3600 秒)
--help 显示帮助并退出

不带参数直接运行进入控制台模式,Ctrl+C 退出。

运行逻辑

1
2
3
4
5
6
7
8
登录 POST → 提取 session cookie

WebSocket 升级 /go-ws/user-auth(携带 cookie)

接收服务器心跳(OS 自动 ACK,无需回复)

超时到期 → 重新认证
服务端断开 → 等 10 秒 → 重新认证

安装为系统服务

程序可放在任意目录运行。执行 --install 时会自动将自身复制到用户家目录,服务指向该副本,原始 exe 删除后服务不受影响。

在普通命令行运行以下命令,若当前无管理员权限会自动弹出 UAC 提权:

1
wholeton_login --install --user 张三 --pass MyPass123

自定义重新认证间隔(单位秒):

1
wholeton_login --install --user 张三 --pass MyPass123 --timeout 1800

安装完成后开机自动启动,无需登录即可运行。

卸载:

1
wholeton_login --uninstall

文件位置

内容 路径
安装目录(exe + 日志) %USERPROFILE%\wholeton_login\
日志文件(服务模式) %USERPROFILE%\wholeton_login\log.txt
日志文件(控制台模式) exe 所在目录下的 log.txt
注册表凭据 HKLM\SYSTEM\CurrentControlSet\Services\wholeton_login\Parameters

日志每个认证周期覆盖写入,只保留当前周期的完整记录。

日志示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[时间] ===== 认证周期 #1 =====
--- 请求 ---
POST /user-login-auth?id=&url=&user=&mac= HTTP/1.1
Host: 10.100.100.3
Connection: close
Content-Length: 91
...
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8

param%5BUserName%5D=%E5%BC%A0%E4%B8%89&param%5BUserPswd%5D=MyPass123&uri=id%3D%26url%3D%26user%3D%26mac%3D&force=0
--- 响应 ---
HTTP/1.1 200 OK
Server: nginx/1.26.2
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
Set-Cookie: fms_session=eyJ...; path=/; HttpOnly

1a
{"status":"1","msg":"success"}
0

[时间] WebSocket 握手成功
[时间] 保持连接,3600 秒后重新认证
[时间] 心跳 #1 {"type": "ping", "data":"Heartbeat data package"}
[时间] 心跳 #2 {"type": "ping", "data":"Heartbeat data package"}
...
[时间] WebSocket 结束,共收到 N 个心跳
[时间] 3600s 到期,重新认证 (运行 3601s)

异常断开时:

1
2
3
[时间] ERROR: ws recv: WSA 10054
[时间] WebSocket 结束,共收到 42 个心跳
[时间] 连接异常断开 (运行 187s),10秒后重新认证

卸载清理内容

  • 系统服务条目
  • 注册表凭据(UserName / Password / Host / Port)
  • 日志文件 log.txt
  • 安装目录下的 exe 副本
  • 安装目录本身

注册表检查

1
reg query "HKLM\SYSTEM\CurrentControlSet\Services\wholeton_login\Parameters"

源码说明

编译宏

1
2
3
4
5
6
7
#define SERVER_HOST            "10.100.100.3"
#define SERVER_PORT 80
#define POST_PATH "/user-login-auth?id=&url=&user=&mac="
#define WS_PATH "/go-ws/user-auth"
#define RECONNECT_SECS_DEFAULT 3600
#define SERVICE_NAME "wholeton_login"
#define REG_KEY "SYSTEM\\CurrentControlSet\\Services\\wholeton_login\\Parameters"

全局变量

1
2
3
4
5
6
static char g_user[512]        = "";     /* 必须通过 --user 或注册表提供 */
static char g_pass[512] = ""; /* 必须通过 --pass 或注册表提供 */
static char g_host[256] = SERVER_HOST;
static int g_port = SERVER_PORT;
static int g_reconnect_secs = RECONNECT_SECS_DEFAULT; /* --timeout 可覆盖 */
static char g_cookie[2048] = {0}; /* 登录后从 Set-Cookie 提取 */

安装目录

1
2
3
4
5
6
static void get_install_dir(char *buf, size_t sz) {
char home[MAX_PATH] = {0};
if (!GetEnvironmentVariableA("USERPROFILE", home, sizeof(home)))
strncpy(home, "C:\\Users\\Default", sizeof(home) - 1);
snprintf(buf, sz, "%s\\wholeton_login\\", home);
}

日志目录(跟随运行中的 exe)

1
2
3
4
5
6
7
8
9
static void init_log_dir(void) {
char exe[MAX_PATH] = {0};
GetModuleFileNameA(NULL, exe, sizeof(exe));
char *slash = strrchr(exe, '\\');
if (slash)
*(slash + 1) = '\0';
strncpy(g_log_dir, exe, sizeof(g_log_dir) - 1);
CreateDirectoryA(g_log_dir, NULL);
}

服务从安装目录运行,因此 GetModuleFileNameA 返回安装目录,服务日志自然落在 %USERPROFILE%\wholeton_login\log.txt

注册表存储(REG_BINARY 防止编码损坏)

1
2
3
4
5
/* REG_BINARY avoids ANSI↔UTF-16 codepage conversion that corrupts UTF-8 bytes */
RegSetValueExA(hk, "UserName", 0, REG_BINARY, (const BYTE *)user, (DWORD)(strlen(user) + 1));
RegSetValueExA(hk, "Password", 0, REG_BINARY, (const BYTE *)pass, (DWORD)(strlen(pass) + 1));
RegSetValueExA(hk, "Host", 0, REG_BINARY, (const BYTE *)host, (DWORD)(strlen(host) + 1));
RegSetValueExA(hk, "Port", 0, REG_DWORD, (const BYTE *)&dport, sizeof(dport));

REG_SZ 经由系统 ANSI 代码页(GBK)与 UTF-16 互转,UTF-8 多字节序列中的字节(如 \xA9)会被破坏。REG_BINARY 原样存取,不做任何转换。

服务启动时读取注册表凭据

1
2
3
4
5
6
7
8
static VOID WINAPI ServiceMain(DWORD argc, LPSTR *argv) {
...
g_stop_event = CreateEvent(NULL, TRUE, FALSE, NULL);
/* load credentials written by --install --user/--pass; fall back to embedded defaults on failure */
reg_read(g_user, sizeof(g_user), g_pass, sizeof(g_pass), g_host, sizeof(g_host), &g_port);
main_loop();
...
}

WebSocket 心跳帧解包

服务器帧带 RSV1=1(permessage-deflate),实际使用 BTYPE=00(deflate stored block,无压缩):

1
[00][LEN lo][LEN hi][NLEN lo][NLEN hi][明文 JSON...]

跳过前 5 字节即得到可读内容:{"type": "ping", "data":"Heartbeat data package"}

卸载等待服务真正停止

1
2
3
4
5
6
7
ControlService(svc, SERVICE_CONTROL_STOP, &st);
for (int i = 0; i < 10; i++) {
Sleep(1000);
if (QueryServiceStatus(svc, &st) && st.dwCurrentState == SERVICE_STOPPED)
break;
}
/* 此时 exe 文件锁已释放,可以安全删除 */

UAC 提权

1
2
3
4
5
6
7
8
9
10
static void elevate_and_exit(void) {
wchar_t exe[MAX_PATH];
GetModuleFileNameW(NULL, exe, MAX_PATH);
SHELLEXECUTEINFOW sei = {0};
sei.lpVerb = L"runas";
sei.lpFile = exe;
sei.lpParameters = cmdline_args_only(); /* 去掉 exe 路径,只传参数部分 */
ShellExecuteExW(&sei);
exit(0);
}

命令行中文参数处理

1
2
3
// 使用 GetCommandLineW + CommandLineToArgvW 取得 UTF-16 参数,
// 再通过 WideCharToMultiByte(CP_UTF8) 转为 UTF-8,
// 避免 GBK 编码的 argv[] 破坏中文字符。

完整源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <stdint.h>
#include <time.h>

#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "advapi32.lib")

#define SERVER_HOST "10.100.100.3"
#define SERVER_PORT 80
#define POST_PATH "/user-login-auth?id=&url=&user=&mac="
#define WS_PATH "/go-ws/user-auth"
#define RECONNECT_SECS_DEFAULT 3600
#define SERVICE_NAME "wholeton_login"
#define SERVICE_DISPLAY "wholeton_login"
#define REG_KEY "SYSTEM\\CurrentControlSet\\Services\\wholeton_login\\Parameters"

static FILE *g_log = NULL;
static HANDLE g_stop_event = NULL;
static char g_log_dir[MAX_PATH];
static char g_user[512] = "";
static char g_pass[512] = "";
static char g_host[256] = SERVER_HOST;
static int g_port = SERVER_PORT;
static char g_cookie[2048] = {0};
static int g_reconnect_secs = RECONNECT_SECS_DEFAULT;

static SERVICE_STATUS g_svc_status;
static SERVICE_STATUS_HANDLE g_svc_handle;

/* ── 日志 ──────────────────────────────────────── */

static void tee(const char *s, size_t len) {
fwrite(s, 1, len, stdout);
if (g_log)
fwrite(s, 1, len, g_log);
}

static void log_vfmt(const char *prefix, const char *fmt, va_list ap) {
char msg[1024];
vsnprintf(msg, sizeof(msg), fmt, ap);
time_t t = time(NULL);
struct tm *tm = localtime(&t);
char line[1200];
int n = snprintf(line, sizeof(line),
"[%04d-%02d-%02d %02d:%02d:%02d] %s%s\r\n",
tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
tm->tm_hour, tm->tm_min, tm->tm_sec,
prefix, msg);
if (n > 0)
tee(line, (size_t)n);
fflush(stdout);
if (g_log)
fflush(g_log);
}

static void LOG_fn(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
log_vfmt("", fmt, ap);
va_end(ap);
}

static void ERR_fn(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
log_vfmt("ERROR: ", fmt, ap);
va_end(ap);
}

#define LOG(fmt, ...) LOG_fn(fmt, ##__VA_ARGS__)
#define ERR(fmt, ...) ERR_fn(fmt, ##__VA_ARGS__)

/* ── 日志目录 ──────────────────────────────────── */

static void get_install_dir(char *buf, size_t sz) {
char home[MAX_PATH] = {0};
if (!GetEnvironmentVariableA("USERPROFILE", home, sizeof(home)))
strncpy(home, "C:\\Users\\Default", sizeof(home) - 1);
snprintf(buf, sz, "%s\\wholeton_login\\", home);
}

static void init_log_dir(void) {
char exe[MAX_PATH] = {0};
GetModuleFileNameA(NULL, exe, sizeof(exe));
char *slash = strrchr(exe, '\\');
if (slash)
*(slash + 1) = '\0';
strncpy(g_log_dir, exe, sizeof(g_log_dir) - 1);
CreateDirectoryA(g_log_dir, NULL);
}

static void open_log(void) {
char p[MAX_PATH];
snprintf(p, sizeof(p), "%slog.txt", g_log_dir);
g_log = fopen(p, "wb");
}

static void close_log(void) {
if (g_log) {
fclose(g_log);
g_log = NULL;
}
}

/* ── 注册表 ────────────────────────────────────── */

static void reg_write(const char *user, const char *pass, const char *host, int port) {
HKEY hk;
DWORD dport = (DWORD)port;
if (RegCreateKeyExA(HKEY_LOCAL_MACHINE, REG_KEY, 0, NULL, 0, KEY_WRITE, NULL, &hk, NULL) != ERROR_SUCCESS)
return;
/* REG_BINARY avoids ANSI↔UTF-16 codepage conversion that corrupts UTF-8 bytes */
RegSetValueExA(hk, "UserName", 0, REG_BINARY, (const BYTE *)user, (DWORD)(strlen(user) + 1));
RegSetValueExA(hk, "Password", 0, REG_BINARY, (const BYTE *)pass, (DWORD)(strlen(pass) + 1));
RegSetValueExA(hk, "Host", 0, REG_BINARY, (const BYTE *)host, (DWORD)(strlen(host) + 1));
RegSetValueExA(hk, "Port", 0, REG_DWORD, (const BYTE *)&dport, sizeof(dport));
RegCloseKey(hk);
}

static int reg_read(char *user, DWORD user_sz, char *pass, DWORD pass_sz,
char *host, DWORD host_sz, int *port) {
HKEY hk;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, REG_KEY, 0, KEY_READ, &hk) != ERROR_SUCCESS)
return 0;

DWORD type = REG_SZ;
int ok = (RegQueryValueExA(hk, "UserName", NULL, &type, (BYTE *)user, &user_sz) == ERROR_SUCCESS)
&& (RegQueryValueExA(hk, "Password", NULL, &type, (BYTE *)pass, &pass_sz) == ERROR_SUCCESS);

type = REG_SZ;
RegQueryValueExA(hk, "Host", NULL, &type, (BYTE *)host, &host_sz);

DWORD dport = 0;
DWORD sz = sizeof(dport);
type = REG_DWORD;
if (RegQueryValueExA(hk, "Port", NULL, &type, (BYTE *)&dport, &sz) == ERROR_SUCCESS && dport)
*port = (int)dport;

RegCloseKey(hk);
return ok;
}

/* ── URL 编码 ──────────────────────────────────── */

static void url_encode(const char *src, char *dst, size_t dst_sz) {
static const char hex[] = "0123456789ABCDEF";
size_t j = 0;
for (size_t i = 0; src[i] && j + 4 < dst_sz; i++) {
unsigned char c = (unsigned char)src[i];
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') ||
c == '-' || c == '_' || c == '.' || c == '~') {
dst[j++] = (char)c;
} else {
dst[j++] = '%';
dst[j++] = hex[c >> 4];
dst[j++] = hex[c & 0xF];
}
}
dst[j] = '\0';
}

/* ── TCP 连接 ──────────────────────────────────── */

static SOCKET tcp_connect(void) {
struct addrinfo hints = {0}, *res = NULL;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
char port_str[8];
snprintf(port_str, sizeof(port_str), "%d", g_port);
if (getaddrinfo(g_host, port_str, &hints, &res) != 0) {
ERR("getaddrinfo: WSA %d", WSAGetLastError());
return INVALID_SOCKET;
}
SOCKET s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (s != INVALID_SOCKET && connect(s, res->ai_addr, (int)res->ai_addrlen) == SOCKET_ERROR) {
ERR("connect %s:%d WSA %d", g_host, g_port, WSAGetLastError());
closesocket(s);
s = INVALID_SOCKET;
}
freeaddrinfo(res);
return s;
}

/* ── 逐行读取 HTTP 头 ──────────────────────────── */

static int recv_line(SOCKET s, char *buf, int bufsz) {
int n = 0;
while (n < bufsz - 1) {
char c;
if (recv(s, &c, 1, 0) <= 0)
return -1;
buf[n++] = c;
if (n >= 2 && buf[n-2] == '\r' && buf[n-1] == '\n') {
buf[n-2] = '\0';
return n - 2;
}
}
buf[n] = '\0';
return n;
}

/* ── 精确读取 N 字节 ───────────────────────────── */

static int recv_exact(SOCKET s, void *buf, int len) {
char *p = (char *)buf;
while (len > 0) {
int n = recv(s, p, len, 0);
if (n <= 0)
return -1;
p += n;
len -= n;
}
return 0;
}

/* ── 发送全部 ───────────────────────────────────── */

static int send_all(SOCKET s, const char *buf, int len) {
while (len > 0) {
int n = send(s, buf, len, 0);
if (n == SOCKET_ERROR)
return -1;
buf += n;
len -= n;
}
return 0;
}

/* ── 登录,提取 session cookie ─────────────────── */

static int do_login(void) {
g_cookie[0] = '\0';

char user_raw[512] = {0};
char pass[512] = {0};

if (g_user[0] && g_pass[0]) {
strncpy(user_raw, g_user, sizeof(user_raw) - 1);
strncpy(pass, g_pass, sizeof(pass) - 1);
} else if (!reg_read(user_raw, sizeof(user_raw),
pass, sizeof(pass),
g_host, sizeof(g_host),
&g_port)) {
ERR("未找到凭据");
return 0;
}

char user_enc[768] = {0};
url_encode(user_raw, user_enc, sizeof(user_enc));

char body[1024];
int body_len = snprintf(body, sizeof(body),
"param%%5BUserName%%5D=%s"
"&param%%5BUserPswd%%5D=%s"
"&uri=id%%3D%%26url%%3D%%26user%%3D%%26mac%%3D"
"&force=0",
user_enc, pass);

char req[4096];
int req_len = snprintf(req, sizeof(req),
"POST " POST_PATH " HTTP/1.1\r\n"
"Host: %s\r\n"
"Connection: close\r\n"
"Content-Length: %d\r\n"
"Accept: application/json, text/javascript, */*; q=0.01\r\n"
"X-Requested-With: XMLHttpRequest\r\n"
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/149.0.0.0 Safari/537.36\r\n"
"Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n"
"Origin: http://%s\r\n"
"Referer: http://%s/login\r\n"
"Accept-Encoding: gzip, deflate\r\n"
"Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n"
"\r\n"
"%s",
g_host, body_len, g_host, g_host, body);

SOCKET s = tcp_connect();
if (s == INVALID_SOCKET)
return 0;

DWORD tmo = 10000;
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tmo, sizeof(tmo));

if (send_all(s, req, req_len) != 0) {
ERR("login send: WSA %d", WSAGetLastError());
closesocket(s);
return 0;
}

{ const char *hdr = "--- 请求 ---\n"; tee(hdr, (int)strlen(hdr)); }
tee(req, req_len);
tee("\n", 1);

char response[8192] = {0};
int rlen = 0, n;
while (rlen < (int)sizeof(response) - 1 &&
(n = recv(s, response + rlen, sizeof(response) - 1 - rlen, 0)) > 0)
rlen += n;
response[rlen] = '\0';

{ const char *hdr = "--- 响应 ---\n"; tee(hdr, (int)strlen(hdr)); }
tee(response, rlen);
if (rlen == 0 || (response[rlen-1] != '\n' && response[rlen-1] != '\r'))
tee("\n", 1);

closesocket(s);

int status = 0;
sscanf(response, "HTTP/%*s %d", &status);

int found = 0;
char *ck = strstr(response, "Set-Cookie: fms_session=");
if (ck) {
char *p = ck + 12;
char *end = strchr(p, ';');
if (!end) end = strchr(p, '\r');
if (!end) end = p + strlen(p);
int vlen = (int)(end - p);
if (vlen > 0 && vlen < (int)sizeof(g_cookie) - 1) {
memcpy(g_cookie, p, vlen);
g_cookie[vlen] = '\0';
found = 1;
}
}

if (!found)
ERR("未找到 Set-Cookie,登录失败");
return status == 200 && found;
}

/* ── Base64 编码 ──────────────────────────────── */

static void base64_encode(const unsigned char *in, int inlen, char *out) {
static const char t[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
int j = 0;
for (int i = 0; i < inlen; i += 3) {
unsigned int a = (unsigned char)in[i];
unsigned int b = (i+1 < inlen) ? (unsigned char)in[i+1] : 0;
unsigned int c = (i+2 < inlen) ? (unsigned char)in[i+2] : 0;
out[j++] = t[a >> 2];
out[j++] = t[((a & 3) << 4) | (b >> 4)];
out[j++] = (i+1 < inlen) ? t[((b & 15) << 2) | (c >> 6)] : '=';
out[j++] = (i+2 < inlen) ? t[c & 63] : '=';
}
out[j] = '\0';
}

/* ── WebSocket 握手 ────────────────────────────── */

static SOCKET ws_connect(void) {
unsigned char raw[16];
srand((unsigned int)(time(NULL) ^ GetTickCount()));
for (int i = 0; i < 16; i++)
raw[i] = (unsigned char)(rand() & 0xFF);
char ws_key[25];
base64_encode(raw, 16, ws_key);

char req[4096];
int req_len = snprintf(req, sizeof(req),
"GET " WS_PATH " HTTP/1.1\r\n"
"Host: %s\r\n"
"Connection: Upgrade\r\n"
"Pragma: no-cache\r\n"
"Cache-Control: no-cache\r\n"
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/149.0.0.0 Safari/537.36\r\n"
"Upgrade: websocket\r\n"
"Origin: http://%s\r\n"
"Sec-WebSocket-Version: 13\r\n"
"Accept-Encoding: gzip, deflate\r\n"
"Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n"
"Cookie: %s\r\n"
"Sec-WebSocket-Key: %s\r\n"
"Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n"
"\r\n",
g_host, g_host, g_cookie, ws_key);

SOCKET s = tcp_connect();
if (s == INVALID_SOCKET)
return INVALID_SOCKET;

DWORD tmo = 10000;
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tmo, sizeof(tmo));

if (send_all(s, req, req_len) != 0) {
ERR("ws send: WSA %d", WSAGetLastError());
closesocket(s);
return INVALID_SOCKET;
}

char line[1024];
int status = 0;
recv_line(s, line, sizeof(line));
sscanf(line, "HTTP/%*s %d", &status);
while (recv_line(s, line, sizeof(line)) > 0);

if (status != 101) {
ERR("WebSocket 升级失败 HTTP %d", status);
closesocket(s);
return INVALID_SOCKET;
}

DWORD zero = 0;
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char *)&zero, sizeof(zero));

LOG("WebSocket 握手成功");
return s;
}

/* ── WebSocket 帧接收,解出明文 payload ─────────── */

static int ws_recv_frame(SOCKET s, char *out, int outsz) {
unsigned char hdr[2];
if (recv_exact(s, hdr, 2) != 0)
return -1;

int opcode = hdr[0] & 0x0F;
int rsv1 = (hdr[0] & 0x40) != 0;
int masked = (hdr[1] & 0x80) != 0;
uint64_t plen = hdr[1] & 0x7F;

if (plen == 126) {
unsigned char ext[2];
if (recv_exact(s, ext, 2) != 0) return -1;
plen = ((uint64_t)ext[0] << 8) | ext[1];
} else if (plen == 127) {
unsigned char ext[8];
if (recv_exact(s, ext, 8) != 0) return -1;
plen = 0;
for (int i = 0; i < 8; i++)
plen = (plen << 8) | ext[i];
}

if (masked) {
unsigned char mask[4];
if (recv_exact(s, mask, 4) != 0) return -1;
}

char raw[4096] = {0};
int raw_len = (plen < (uint64_t)(sizeof(raw) - 1)) ? (int)plen : (int)(sizeof(raw) - 1);
if (recv_exact(s, raw, raw_len) != 0) return -1;
uint64_t rem = plen - raw_len;
char drain[256];
while (rem > 0) {
int chunk = (rem > sizeof(drain)) ? (int)sizeof(drain) : (int)rem;
if (recv_exact(s, drain, chunk) != 0) return -1;
rem -= chunk;
}

if (out && outsz > 0) {
/* RSV1=1: permessage-deflate stored block (BTYPE=00)
* 格式: [00/01][LEN lo][LEN hi][NLEN lo][NLEN hi][明文 JSON...] */
if (rsv1 && raw_len >= 5 && (raw[0] & 0x06) == 0x00) {
int dlen = (unsigned char)raw[1] | ((unsigned char)raw[2] << 8);
if (dlen > raw_len - 5) dlen = raw_len - 5;
int copy = (dlen < outsz - 1) ? dlen : outsz - 1;
memcpy(out, raw + 5, copy);
out[copy] = '\0';
} else {
int copy = (raw_len < outsz - 1) ? raw_len : outsz - 1;
memcpy(out, raw, copy);
out[copy] = '\0';
}
}
return opcode;
}

/* ── sleep / stop ──────────────────────────────── */

static void wait_sleep(DWORD ms) {
if (g_stop_event)
WaitForSingleObject(g_stop_event, ms);
else
Sleep(ms);
}

static int should_stop(void) {
return g_stop_event &&
WaitForSingleObject(g_stop_event, 0) == WAIT_OBJECT_0;
}

/* ── WebSocket 接收循环(OS 自动 ACK) ──────────── */

/* 返回 1 = 正常到期,0 = 异常断开 */
static int ws_recv_loop(SOCKET s, time_t deadline) {
int count = 0;
while (!should_stop() && time(NULL) < deadline) {
fd_set fds;
FD_ZERO(&fds);
FD_SET(s, &fds);
struct timeval tv = {1, 0};
int r = select(0, &fds, NULL, NULL, &tv);
if (r < 0) {
ERR("select: WSA %d", WSAGetLastError());
break;
}
if (r == 0)
continue;

char payload[512] = {0};
int op = ws_recv_frame(s, payload, sizeof(payload));
if (op < 0) {
int err = WSAGetLastError();
if (err)
ERR("ws recv: WSA %d", err);
else
LOG("WebSocket 连接关闭");
LOG("WebSocket 结束,共收到 %d 个心跳", count);
return 0;
}
if (op == 8) {
LOG("服务器发送 Close 帧");
LOG("WebSocket 结束,共收到 %d 个心跳", count);
return 0;
}
count++;
LOG("心跳 #%d %s", count, payload);
}
LOG("WebSocket 结束,共收到 %d 个心跳", count);
return 1;
}

/* ── 主循环 ────────────────────────────────────── */

static void main_loop(void) {
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);

int cycle = 0;
while (!should_stop()) {
open_log();
LOG("===== 认证周期 #%d =====", ++cycle);

if (!do_login()) {
ERR("登录失败,30秒后重试");
close_log();
wait_sleep(30 * 1000);
continue;
}

SOCKET ws = ws_connect();
if (ws == INVALID_SOCKET) {
ERR("WebSocket 连接失败,30秒后重试");
close_log();
wait_sleep(30 * 1000);
continue;
}

time_t start = time(NULL);
time_t deadline = start + g_reconnect_secs;
LOG("保持连接,%d 秒后重新认证", g_reconnect_secs);

int normal = ws_recv_loop(ws, deadline);
closesocket(ws);

long elapsed = (long)(time(NULL) - start);
if (normal)
LOG("%ds 到期,重新认证 (运行 %lds)", g_reconnect_secs, elapsed);
else
LOG("连接异常断开 (运行 %lds),10秒后重新认证", elapsed);
close_log();

if (!should_stop() && !normal)
wait_sleep(10 * 1000);
}

WSACleanup();
}

/* ── 服务 ──────────────────────────────────────── */

static void svc_report(DWORD state) {
g_svc_status.dwCurrentState = state;
SetServiceStatus(g_svc_handle, &g_svc_status);
}

static VOID WINAPI svc_ctrl(DWORD ctrl) {
if (ctrl == SERVICE_CONTROL_STOP) {
svc_report(SERVICE_STOP_PENDING);
SetEvent(g_stop_event);
}
}

static VOID WINAPI ServiceMain(DWORD argc, LPSTR *argv) {
(void)argc;
(void)argv;
g_svc_handle = RegisterServiceCtrlHandlerA(SERVICE_NAME, svc_ctrl);
g_svc_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
g_svc_status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
svc_report(SERVICE_RUNNING);
g_stop_event = CreateEvent(NULL, TRUE, FALSE, NULL);
/* load credentials written by --install --user/--pass; fall back to embedded defaults on failure */
reg_read(g_user, sizeof(g_user), g_pass, sizeof(g_pass), g_host, sizeof(g_host), &g_port);
main_loop();
CloseHandle(g_stop_event);
svc_report(SERVICE_STOPPED);
}

/* ── 安装 ──────────────────────────────────────── */

static int do_install(const char *user, const char *pass) {
if (!user || !pass) {
fprintf(stderr, "用法: --install --user <用户名> --pass <密码>\n");
return 1;
}

reg_write(user, pass, g_host, g_port);
printf("凭据已保存到注册表\n");

char install_dir[MAX_PATH];
get_install_dir(install_dir, sizeof(install_dir));
CreateDirectoryA(install_dir, NULL);

char src_exe[MAX_PATH];
GetModuleFileNameA(NULL, src_exe, MAX_PATH);

char install_exe[MAX_PATH];
snprintf(install_exe, sizeof(install_exe), "%swholeton_login.exe", install_dir);

if (!CopyFileA(src_exe, install_exe, FALSE)) {
fprintf(stderr, "复制 exe 失败: %lu\n", GetLastError());
return 1;
}
printf("程序已复制到: %s\n", install_exe);

SC_HANDLE scm = OpenSCManagerA(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
if (!scm) {
fprintf(stderr, "OpenSCManager 失败: %lu (需要管理员权限)\n", GetLastError());
return 1;
}

SC_HANDLE svc = CreateServiceA(
scm, SERVICE_NAME, SERVICE_DISPLAY,
SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
install_exe, NULL, NULL, NULL, NULL, NULL);

if (!svc) {
DWORD e = GetLastError();
if (e == ERROR_SERVICE_EXISTS)
fprintf(stderr, "服务已存在: %s\n", SERVICE_NAME);
else
fprintf(stderr, "CreateService 失败: %lu\n", e);
CloseServiceHandle(scm);
return 1;
}

SERVICE_DESCRIPTIONA desc = { (LPSTR)"Network authentication keepalive" };
ChangeServiceConfig2A(svc, SERVICE_CONFIG_DESCRIPTION, &desc);
printf("服务已安装: %s\n", SERVICE_NAME);

if (StartServiceA(svc, 0, NULL))
printf("服务已启动: %s\n", SERVICE_NAME);
else
fprintf(stderr, "启动失败: %lu\n", GetLastError());

printf("\n卸载命令: wholeton_login --uninstall\n");

CloseServiceHandle(svc);
CloseServiceHandle(scm);
return 0;
}

/* ── 卸载 ──────────────────────────────────────── */

static int do_uninstall(void) {
SC_HANDLE scm = OpenSCManagerA(NULL, NULL, SC_MANAGER_CONNECT);
if (!scm) {
fprintf(stderr, "OpenSCManager 失败: %lu\n", GetLastError());
return 1;
}

SC_HANDLE svc = OpenServiceA(scm, SERVICE_NAME, SERVICE_STOP | DELETE);
if (!svc) {
fprintf(stderr, "找不到服务: %s\n", SERVICE_NAME);
CloseServiceHandle(scm);
return 1;
}

SERVICE_STATUS st;
ControlService(svc, SERVICE_CONTROL_STOP, &st);

/* 等待服务进程真正退出(最多 10 秒),否则 exe 文件仍被锁住无法删除 */
for (int i = 0; i < 10; i++) {
Sleep(1000);
if (QueryServiceStatus(svc, &st) && st.dwCurrentState == SERVICE_STOPPED)
break;
}

if (DeleteService(svc))
printf("服务已卸载: %s\n", SERVICE_NAME);
else
fprintf(stderr, "DeleteService 失败: %lu\n", GetLastError());

CloseServiceHandle(svc);
CloseServiceHandle(scm);

RegDeleteKeyA(HKEY_LOCAL_MACHINE, REG_KEY);
printf("注册表凭据已清除\n");

char install_dir[MAX_PATH];
get_install_dir(install_dir, sizeof(install_dir));

char log_file[MAX_PATH];
snprintf(log_file, sizeof(log_file), "%slog.txt", install_dir);
DeleteFileA(log_file);

char install_exe[MAX_PATH];
snprintf(install_exe, sizeof(install_exe), "%swholeton_login.exe", install_dir);
DeleteFileA(install_exe);

RemoveDirectoryA(install_dir);
printf("已清除安装目录: %s\n", install_dir);

return 0;
}

/* ── 宽字符转 UTF-8 ────────────────────────────── */

static char *wcs_to_utf8(const wchar_t *w) {
int n = WideCharToMultiByte(CP_UTF8, 0, w, -1, NULL, 0, NULL, NULL);
if (n <= 0)
return NULL;
char *s = (char *)malloc(n);
WideCharToMultiByte(CP_UTF8, 0, w, -1, s, n, NULL, NULL);
return s;
}

/* ── 解析命令行参数 ─────────────────────────────── */

static void parse_args(int wargc, LPWSTR *wargv) {
for (int i = 1; i < wargc - 1; i++) {
if (wcscmp(wargv[i], L"--user") == 0) {
char *v = wcs_to_utf8(wargv[i + 1]);
if (v) { strncpy(g_user, v, sizeof(g_user) - 1); free(v); }
} else if (wcscmp(wargv[i], L"--pass") == 0) {
char *v = wcs_to_utf8(wargv[i + 1]);
if (v) { strncpy(g_pass, v, sizeof(g_pass) - 1); free(v); }
} else if (wcscmp(wargv[i], L"--host") == 0) {
char *v = wcs_to_utf8(wargv[i + 1]);
if (v) { strncpy(g_host, v, sizeof(g_host) - 1); free(v); }
} else if (wcscmp(wargv[i], L"--port") == 0) {
g_port = _wtoi(wargv[i + 1]);
} else if (wcscmp(wargv[i], L"--timeout") == 0) {
g_reconnect_secs = _wtoi(wargv[i + 1]);
}
}
}

/* ── UAC 提权重启 ──────────────────────────────── */

static const wchar_t *cmdline_args_only(void) {
const wchar_t *p = GetCommandLineW();
if (!p) return L"";
while (*p == L' ') p++;
if (*p == L'"') {
p++;
while (*p && *p != L'"') p++;
if (*p == L'"') p++;
} else {
while (*p && *p != L' ') p++;
}
while (*p == L' ') p++;
return p;
}

static void elevate_and_exit(void) {
wchar_t exe[MAX_PATH];
GetModuleFileNameW(NULL, exe, MAX_PATH);
SHELLEXECUTEINFOW sei = {0};
sei.cbSize = sizeof(sei);
sei.lpVerb = L"runas";
sei.lpFile = exe;
sei.lpParameters = cmdline_args_only();
sei.nShow = SW_SHOWNORMAL;
ShellExecuteExW(&sei);
exit(0);
}

static int is_admin(void) {
BOOL result = FALSE;
PSID admins_group = NULL;
SID_IDENTIFIER_AUTHORITY nt_authority = SECURITY_NT_AUTHORITY;
if (AllocateAndInitializeSid(&nt_authority, 2,
SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0, &admins_group)) {
CheckTokenMembership(NULL, admins_group, &result);
FreeSid(admins_group);
}
return result;
}

/* ── 入口 ──────────────────────────────────────── */

int main(void) {
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
init_log_dir();

int wargc = 0;
LPWSTR *wargv = CommandLineToArgvW(GetCommandLineW(), &wargc);

int do_inst = 0, do_uninst = 0;
for (int i = 1; i < wargc; i++) {
if (wcscmp(wargv[i], L"--install") == 0) do_inst = 1;
if (wcscmp(wargv[i], L"--uninstall") == 0) do_uninst = 1;
if (wcscmp(wargv[i], L"--help") == 0) {
printf(
"用法: wholeton_login [选项]\n"
"\n"
" --install 安装并启动系统服务(需要管理员权限)\n"
" --uninstall 停止并卸载系统服务(需要管理员权限)\n"
" --user <名字> HTTP 登录用户名(必填)\n"
" --pass <密码> HTTP 登录密码(必填)\n"
" --host <IP> 服务器地址(默认: %s)\n"
" --port <端口> 服务器端口(默认: %d)\n"
" --timeout <秒> 重新认证间隔(默认: %d 秒)\n"
" --help 显示此帮助\n"
"\n"
"运行逻辑: 登录认证 → 建立 WebSocket 保持连接 → 超时后重新认证。\n"
"服务日志: %%USERPROFILE%%\\wholeton_login\\log.txt\n"
"控制台日志: 程序所在目录\\log.txt\n",
SERVER_HOST, SERVER_PORT, RECONNECT_SECS_DEFAULT);
LocalFree(wargv);
return 0;
}
}

if ((do_inst || do_uninst) && !is_admin()) {
LocalFree(wargv);
elevate_and_exit();
}

if (do_inst) {
parse_args(wargc, wargv);
int ret = do_install(g_user[0] ? g_user : NULL,
g_pass[0] ? g_pass : NULL);
LocalFree(wargv);
printf("\n按任意键退出...\n");
getchar();
return ret;
}

if (do_uninst) {
LocalFree(wargv);
int ret = do_uninstall();
printf("\n按任意键退出...\n");
getchar();
return ret;
}

parse_args(wargc, wargv);
LocalFree(wargv);

SERVICE_TABLE_ENTRYA table[] = {
{ (LPSTR)SERVICE_NAME, ServiceMain },
{ NULL, NULL }
};
if (!StartServiceCtrlDispatcherA(table)) {
if (GetLastError() == ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) {
printf("控制台模式 (Ctrl+C 退出)\nlog: %slog.txt\n", g_log_dir);
main_loop();
}
}
return 0;
}