campus-auth 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. campus_auth-1.0.0/LICENSE +21 -0
  2. campus_auth-1.0.0/PKG-INFO +228 -0
  3. campus_auth-1.0.0/README.md +197 -0
  4. campus_auth-1.0.0/pyproject.toml +53 -0
  5. campus_auth-1.0.0/setup.cfg +4 -0
  6. campus_auth-1.0.0/src/campus_auth/__init__.py +6 -0
  7. campus_auth-1.0.0/src/campus_auth/__main__.py +8 -0
  8. campus_auth-1.0.0/src/campus_auth/adapters/__init__.py +58 -0
  9. campus_auth-1.0.0/src/campus_auth/adapters/base.py +272 -0
  10. campus_auth-1.0.0/src/campus_auth/adapters/example_generic.py +37 -0
  11. campus_auth-1.0.0/src/campus_auth/adapters/huuc.py +92 -0
  12. campus_auth-1.0.0/src/campus_auth/cli.py +165 -0
  13. campus_auth-1.0.0/src/campus_auth/commands.py +553 -0
  14. campus_auth-1.0.0/src/campus_auth/config.py +176 -0
  15. campus_auth-1.0.0/src/campus_auth/credentials.py +71 -0
  16. campus_auth-1.0.0/src/campus_auth/crypto.py +40 -0
  17. campus_auth-1.0.0/src/campus_auth/errors.py +44 -0
  18. campus_auth-1.0.0/src/campus_auth/install.py +284 -0
  19. campus_auth-1.0.0/src/campus_auth/keepalive.py +102 -0
  20. campus_auth-1.0.0/src/campus_auth/logfile.py +92 -0
  21. campus_auth-1.0.0/src/campus_auth/net.py +111 -0
  22. campus_auth-1.0.0/src/campus_auth/portal.py +302 -0
  23. campus_auth-1.0.0/src/campus_auth/probe.py +342 -0
  24. campus_auth-1.0.0/src/campus_auth/runner.py +102 -0
  25. campus_auth-1.0.0/src/campus_auth.egg-info/PKG-INFO +228 -0
  26. campus_auth-1.0.0/src/campus_auth.egg-info/SOURCES.txt +38 -0
  27. campus_auth-1.0.0/src/campus_auth.egg-info/dependency_links.txt +1 -0
  28. campus_auth-1.0.0/src/campus_auth.egg-info/entry_points.txt +2 -0
  29. campus_auth-1.0.0/src/campus_auth.egg-info/requires.txt +9 -0
  30. campus_auth-1.0.0/src/campus_auth.egg-info/top_level.txt +1 -0
  31. campus_auth-1.0.0/tests/test_adapter.py +166 -0
  32. campus_auth-1.0.0/tests/test_cli.py +521 -0
  33. campus_auth-1.0.0/tests/test_config.py +145 -0
  34. campus_auth-1.0.0/tests/test_credentials.py +75 -0
  35. campus_auth-1.0.0/tests/test_crypto.py +50 -0
  36. campus_auth-1.0.0/tests/test_install.py +150 -0
  37. campus_auth-1.0.0/tests/test_logfile.py +80 -0
  38. campus_auth-1.0.0/tests/test_portal.py +355 -0
  39. campus_auth-1.0.0/tests/test_probe.py +215 -0
  40. campus_auth-1.0.0/tests/test_probe_against_fakes.py +48 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 young-ski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,228 @@
1
+ Metadata-Version: 2.4
2
+ Name: campus-auth
3
+ Version: 1.0.0
4
+ Summary: Dr.COM / eportal 校园网门户认证客户端,适配层按学校可插拔
5
+ Author: young-ski
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/young-ski/campus-auth
8
+ Project-URL: Issues, https://github.com/young-ski/campus-auth/issues
9
+ Project-URL: Changelog, https://github.com/young-ski/campus-auth/blob/main/CHANGELOG.md
10
+ Keywords: eportal,drcom,campus-network,captive-portal,cli
11
+ Classifier: Environment :: Console
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: System :: Networking
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: keyring
24
+ Requires-Dist: keyring>=24; extra == "keyring"
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8; extra == "dev"
27
+ Requires-Dist: ruff>=0.6; extra == "dev"
28
+ Requires-Dist: build>=1.2; extra == "dev"
29
+ Requires-Dist: twine>=5; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # campus-auth
33
+
34
+ **Dr.COM / eportal 校园网门户认证客户端。** 一行命令登录,断线自动重连。密钥算法、参数名、
35
+ 运营商后缀全部收在工具里,你不用再点一个 IP 一变就失效的快捷方式。
36
+
37
+ > A CLI client for Dr.COM/eportal campus captive portals (Chinese campus networks).
38
+
39
+ ```
40
+ $ campus-auth login -w cmcc
41
+ IP 10.138.14.27 MAC AABBCCDDEEFF 账号 ,0,081456789@cmcc
42
+ 服务端: 认证成功 ret_code=0
43
+ 结果: 已在线
44
+ ```
45
+
46
+ 文里的学号、MAC、IP 都是编的。
47
+
48
+ | 门户 | 适配层 | 状态 |
49
+ | --- | --- | --- |
50
+ | Dr.COM / eportal(`wlan_user_ip` + 异或) | `huuc` | **实机验证**(河南城建学院) |
51
+ | 其它 Dr.COM 门户 | `generic` | 模板,照它改 |
52
+
53
+ **范围只有 Dr.COM / eportal。** 锐捷(`InterFace.do`)和深澜(`srun_portal`)不随包提供:
54
+ 维护者拿不到真机,不想一直挂着“未实机”。`probe` 仍然能认出它们,认出来会直接告诉你换工具。
55
+
56
+ ---
57
+
58
+ ## 为什么还需要一个
59
+
60
+ 校园网客户端的老项目大多死在同一处:**把一所学校的协议写死在主流程里**,第二所学校只能 fork
61
+ 一份改烂,原仓库最后被几十个分支拖住。这里的约束反过来:**核心不认识任何学校**,一所学校的
62
+ 差异只写在适配层里,加学校不改 core。
63
+
64
+ ## 它解决什么
65
+
66
+ - **IP 一变就失效的快捷方式**:门户的密钥是 `wlan_user_ip` 的字符异或,DHCP 续租就废。
67
+ 客户端每次现场算。
68
+ - **断线重连**:`watch` 先查在线列表(一次请求),确认掉线才登录,失败指数退避,
69
+ 不会把网关点成 QPS 炸弹。
70
+ - **宿舍 / 教学楼的运营商差异**:SSID 和网段决定要不要 `@cmcc` 后缀,选错就是
71
+ `AC认证失败` —— 客户端会先提示,再把服务端的黑话翻译成人话。
72
+ - **给脚本用**:`--json` 加下面那张退出码表,塞进计划任务和监控都不用解析中文。
73
+
74
+ ## 三步上手
75
+
76
+ 必须在校园网内(手机热点不行)。
77
+
78
+ ```bash
79
+ campus-auth probe # 1. 先确认是 Dr.COM,不是别的门户
80
+ campus-auth init # 2. 生成配置模板
81
+ campus-auth password # 3. 存密码(不回显,进系统钥匙串)
82
+ ```
83
+
84
+ `init` 之后按提示改 `account`,然后:
85
+
86
+ ```bash
87
+ campus-auth status # 自检:没配好会直接指出来
88
+ campus-auth login # 宿舍网段加 -w cmcc,教学楼不加
89
+ campus-auth watch # 断线自动重连,前台跑
90
+ ```
91
+
92
+ `status` 会把“门户里有会话”和“会话是你自己的”分开说 —— 前者可能只是上一位用户还没掉线。
93
+
94
+ ## 第一步的 probe
95
+
96
+ `probe` 只请求匿名接口,**不要账号密码**;只从校园网内有效。
97
+
98
+ ```
99
+ $ campus-auth probe --url http://10.210.213.208:802
100
+ 判定: 是 Dr.COM(eportal) 置信度: 高(155 分)
101
+
102
+ ✓ 页面 / JS 里出现 dr1003 一类回调 (+35)
103
+ ✓ JS 里有 enc_pwd(异或加密函数) (+30)
104
+ ✓ JS 里有 getkey(密钥派生) (+25)
105
+ ✓ 出现 wlan_user_ip 参数 (+20)
106
+ ...
107
+ ```
108
+
109
+ 判定不是看一条信号:`dr1003` 只有**页面里出现过**才算数(Dr.COM 会回显你传的任何 callback),
110
+ 权重按“哪家独有”排,一家要过 35~40 分**并且领先第二名 10 分**才敢下结论,咬得紧就只说
111
+ “像”,并列出还缺哪些证据。
112
+
113
+ | probe 说 | 怎么办 |
114
+ | --- | --- |
115
+ | 是 Dr.COM | 用 `huuc` 或 `generic` 起配置,只改 `base_url` 与密钥/参数 |
116
+ | 像是 Dr.COM,证据不全 | 别急着配:F12 抓一次真登录请求核实参数名 |
117
+ | 是锐捷 / 深澜 | 本项目不覆盖,换对应工具 |
118
+ | 没认出来 | 可能不是网页认证:路由器自带页、802.1X 都长这样 |
119
+
120
+ 退出码:0 命中、1 未命中、2 地址格式错、3 探不到(多半不在校园网)。
121
+
122
+ ## 安装
123
+
124
+ ```bash
125
+ pip install campus-auth
126
+ pip install "campus-auth[keyring]" # 可选:把密码交给系统钥匙串
127
+ ```
128
+
129
+ 默认无第三方运行时依赖,`keyring` 是唯一的可选 extra。Python ≥ 3.11。
130
+ PyPI 上还没有发布时,用 git 的 tag 装同一条:
131
+
132
+ ```bash
133
+ pip install "campus-auth[keyring] @ git+https://github.com/young-ski/campus-auth@v1.0.0"
134
+ ```
135
+
136
+ 没有 pip 的机器用单文件:从 Releases 下 `campus-auth.pyz`,`python campus-auth.pyz status`。
137
+
138
+ ## 配置
139
+
140
+ 优先级:内置默认 < 配置文件 < 环境变量 < 命令行参数。配置文件依次找
141
+ `--config` → `$CAMPUS_AUTH_CONFIG` → `./campus-auth.toml` → 系统配置目录。
142
+
143
+ ```toml
144
+ adapter = "huuc" # 换成你自己学校的适配层
145
+ account = "081456789" # 学号,纯数字,别带 @ 后缀
146
+ password = "" # 留空则读 $CAMPUS_AUTH_PASSWORD,再不行读系统钥匙串
147
+ operator = "none" # none / cmcc / unicom / dx,宿舍才需要
148
+ base_url = "" # 留空用适配层里的地址
149
+ verify_tls = false # 校园门户基本是自签证书
150
+ timeout = 20
151
+ retries = 2
152
+ interval = 120
153
+ ```
154
+
155
+ **密码别写进仓库。** 优先级从高到低:`$CAMPUS_AUTH_PASSWORD` → 配置文件 → 系统钥匙串。
156
+ 推荐钥匙串(Windows 凭据管理器 / macOS Keychain / Linux Secret Service):
157
+
158
+ ```bash
159
+ campus-auth password # 交互式输入;管道里则读一行
160
+ campus-auth password --show # 只看有没有存
161
+ campus-auth password --forget # 删
162
+ campus-auth login --remember # 或者登录成功时顺手存
163
+ ```
164
+
165
+ 没装 `keyring` 或后端不可用(headless Linux 常见)时自动退回配置文件/环境变量。
166
+
167
+ ## 开机自启
168
+
169
+ ```bash
170
+ campus-auth install --dry-run # 先看它要写什么、跑什么
171
+ campus-auth install # Windows: HKCU\...\Run + .cmd;Linux: systemd --user;macOS: LaunchAgent
172
+ campus-auth uninstall # 停掉并删干净
173
+ ```
174
+
175
+ 自启进程不继承你当前 shell 的环境变量,密码得放进钥匙串或配置文件,install 会提醒。
176
+ 日志在系统 state 目录(Windows `%LOCALAPPDATA%\campus-auth\watch.log`),`watch` 自己按
177
+ 1 MiB 轮转;三条实现都带自动重启,崩了会自己回来。
178
+
179
+ ## 命令与退出码
180
+
181
+ | 命令 | 作用 |
182
+ | --- | --- |
183
+ | `probe` | 探测门户类型,不需要账号(`--url` 给地址) |
184
+ | `init` | 生成配置模板(`--force` 覆盖) |
185
+ | `adapters` | 列出内置适配层与验证状态 |
186
+ | `login` / `logout` / `unbind` | 登录 / 注销 / 解除 MAC 绑定 |
187
+ | `status` | 列出门户会话,标出哪条是你自己的,`--json` 给机器读 |
188
+ | `check` | 只判定在线与否,结果走退出码 |
189
+ | `watch` | 保活循环,`--once` 跑一轮,`--interval` 调间隔,`--log` 同时落盘(自动轮转) |
190
+ | `install` / `uninstall` | 把 `watch` 装成登录自启 / 卸掉(`--dry-run` 只打印) |
191
+ | `password` | 密码存进/删出系统钥匙串(`--show` 看状态,`--forget` 删) |
192
+ | `urls` | 按当前 IP 生成 `.url` / `.desktop` 快捷方式(内含可还原的密码) |
193
+
194
+ | 退出码 | 含义 |
195
+ | --- | --- |
196
+ | 0 | 成功 |
197
+ | 1 | 认证被拒 / 未在线 |
198
+ | 2 | 配置问题(缺字段、适配层不存在、URL 写错) |
199
+ | 3 | 环境问题(不在校园网、取不到本机 IP) |
200
+ | 4 | 传输问题(超时、连接被拒、重试耗尽) |
201
+
202
+ ## 加一所学校
203
+
204
+ `generic` 就是模板:复制成 `<学校代号>.py`,按 F12 抓到的真请求改 `base_url`、参数名、
205
+ 密钥函数,再到 `adapters/__init__.py` 登记。步骤和验收清单见
206
+ [`CONTRIBUTING.md`](CONTRIBUTING.md),那里也写了哪些东西不能提交(真实学号、密码、MAC)。
207
+
208
+ ## 已知限制
209
+
210
+ - 只覆盖 Dr.COM / eportal 的网页认证。锐捷、深澜、802.1X、专用客户端加密的学校都用不了。
211
+ - `probe` 是判断“哪一家”,不是抓包级证据;判成“证据不全”时仍需人工核实。
212
+ - **TLS 默认不校验证书**(校园门户清一色自签)。想校验就 `--verify-tls`,遇到自签会失败。
213
+ - `.url` / `.desktop` 快捷方式里的密码是 IP 异或的密文,而 IP 就在同一个 URL 里 ——
214
+ **等于明文**,分享前留意。报错里的 URL 默认把密码打成 `ab…yz`,要原文加
215
+ `--show-secrets`。
216
+ - `state.json` 只存学号、IP、会话索引,不含密码;POSIX 下权限 0600,Windows 靠目录 ACL。
217
+ - 门户升级会改参数名,适配层跟着改;`generic` 就是为此留的。
218
+
219
+ 变更历史见 [`CHANGELOG.md`](CHANGELOG.md)。
220
+
221
+ ## 免责
222
+
223
+ 非官方项目,与学校、Dr.COM 均无关系。协议来自公开前端代码和本机抓包,不含校内凭据。
224
+ 密码流经:内存、系统钥匙串(若启用)、配置文件(若你写了)、`.url` 文件与报错里的密文。
225
+
226
+ ## 许可证
227
+
228
+ MIT。
@@ -0,0 +1,197 @@
1
+ # campus-auth
2
+
3
+ **Dr.COM / eportal 校园网门户认证客户端。** 一行命令登录,断线自动重连。密钥算法、参数名、
4
+ 运营商后缀全部收在工具里,你不用再点一个 IP 一变就失效的快捷方式。
5
+
6
+ > A CLI client for Dr.COM/eportal campus captive portals (Chinese campus networks).
7
+
8
+ ```
9
+ $ campus-auth login -w cmcc
10
+ IP 10.138.14.27 MAC AABBCCDDEEFF 账号 ,0,081456789@cmcc
11
+ 服务端: 认证成功 ret_code=0
12
+ 结果: 已在线
13
+ ```
14
+
15
+ 文里的学号、MAC、IP 都是编的。
16
+
17
+ | 门户 | 适配层 | 状态 |
18
+ | --- | --- | --- |
19
+ | Dr.COM / eportal(`wlan_user_ip` + 异或) | `huuc` | **实机验证**(河南城建学院) |
20
+ | 其它 Dr.COM 门户 | `generic` | 模板,照它改 |
21
+
22
+ **范围只有 Dr.COM / eportal。** 锐捷(`InterFace.do`)和深澜(`srun_portal`)不随包提供:
23
+ 维护者拿不到真机,不想一直挂着“未实机”。`probe` 仍然能认出它们,认出来会直接告诉你换工具。
24
+
25
+ ---
26
+
27
+ ## 为什么还需要一个
28
+
29
+ 校园网客户端的老项目大多死在同一处:**把一所学校的协议写死在主流程里**,第二所学校只能 fork
30
+ 一份改烂,原仓库最后被几十个分支拖住。这里的约束反过来:**核心不认识任何学校**,一所学校的
31
+ 差异只写在适配层里,加学校不改 core。
32
+
33
+ ## 它解决什么
34
+
35
+ - **IP 一变就失效的快捷方式**:门户的密钥是 `wlan_user_ip` 的字符异或,DHCP 续租就废。
36
+ 客户端每次现场算。
37
+ - **断线重连**:`watch` 先查在线列表(一次请求),确认掉线才登录,失败指数退避,
38
+ 不会把网关点成 QPS 炸弹。
39
+ - **宿舍 / 教学楼的运营商差异**:SSID 和网段决定要不要 `@cmcc` 后缀,选错就是
40
+ `AC认证失败` —— 客户端会先提示,再把服务端的黑话翻译成人话。
41
+ - **给脚本用**:`--json` 加下面那张退出码表,塞进计划任务和监控都不用解析中文。
42
+
43
+ ## 三步上手
44
+
45
+ 必须在校园网内(手机热点不行)。
46
+
47
+ ```bash
48
+ campus-auth probe # 1. 先确认是 Dr.COM,不是别的门户
49
+ campus-auth init # 2. 生成配置模板
50
+ campus-auth password # 3. 存密码(不回显,进系统钥匙串)
51
+ ```
52
+
53
+ `init` 之后按提示改 `account`,然后:
54
+
55
+ ```bash
56
+ campus-auth status # 自检:没配好会直接指出来
57
+ campus-auth login # 宿舍网段加 -w cmcc,教学楼不加
58
+ campus-auth watch # 断线自动重连,前台跑
59
+ ```
60
+
61
+ `status` 会把“门户里有会话”和“会话是你自己的”分开说 —— 前者可能只是上一位用户还没掉线。
62
+
63
+ ## 第一步的 probe
64
+
65
+ `probe` 只请求匿名接口,**不要账号密码**;只从校园网内有效。
66
+
67
+ ```
68
+ $ campus-auth probe --url http://10.210.213.208:802
69
+ 判定: 是 Dr.COM(eportal) 置信度: 高(155 分)
70
+
71
+ ✓ 页面 / JS 里出现 dr1003 一类回调 (+35)
72
+ ✓ JS 里有 enc_pwd(异或加密函数) (+30)
73
+ ✓ JS 里有 getkey(密钥派生) (+25)
74
+ ✓ 出现 wlan_user_ip 参数 (+20)
75
+ ...
76
+ ```
77
+
78
+ 判定不是看一条信号:`dr1003` 只有**页面里出现过**才算数(Dr.COM 会回显你传的任何 callback),
79
+ 权重按“哪家独有”排,一家要过 35~40 分**并且领先第二名 10 分**才敢下结论,咬得紧就只说
80
+ “像”,并列出还缺哪些证据。
81
+
82
+ | probe 说 | 怎么办 |
83
+ | --- | --- |
84
+ | 是 Dr.COM | 用 `huuc` 或 `generic` 起配置,只改 `base_url` 与密钥/参数 |
85
+ | 像是 Dr.COM,证据不全 | 别急着配:F12 抓一次真登录请求核实参数名 |
86
+ | 是锐捷 / 深澜 | 本项目不覆盖,换对应工具 |
87
+ | 没认出来 | 可能不是网页认证:路由器自带页、802.1X 都长这样 |
88
+
89
+ 退出码:0 命中、1 未命中、2 地址格式错、3 探不到(多半不在校园网)。
90
+
91
+ ## 安装
92
+
93
+ ```bash
94
+ pip install campus-auth
95
+ pip install "campus-auth[keyring]" # 可选:把密码交给系统钥匙串
96
+ ```
97
+
98
+ 默认无第三方运行时依赖,`keyring` 是唯一的可选 extra。Python ≥ 3.11。
99
+ PyPI 上还没有发布时,用 git 的 tag 装同一条:
100
+
101
+ ```bash
102
+ pip install "campus-auth[keyring] @ git+https://github.com/young-ski/campus-auth@v1.0.0"
103
+ ```
104
+
105
+ 没有 pip 的机器用单文件:从 Releases 下 `campus-auth.pyz`,`python campus-auth.pyz status`。
106
+
107
+ ## 配置
108
+
109
+ 优先级:内置默认 < 配置文件 < 环境变量 < 命令行参数。配置文件依次找
110
+ `--config` → `$CAMPUS_AUTH_CONFIG` → `./campus-auth.toml` → 系统配置目录。
111
+
112
+ ```toml
113
+ adapter = "huuc" # 换成你自己学校的适配层
114
+ account = "081456789" # 学号,纯数字,别带 @ 后缀
115
+ password = "" # 留空则读 $CAMPUS_AUTH_PASSWORD,再不行读系统钥匙串
116
+ operator = "none" # none / cmcc / unicom / dx,宿舍才需要
117
+ base_url = "" # 留空用适配层里的地址
118
+ verify_tls = false # 校园门户基本是自签证书
119
+ timeout = 20
120
+ retries = 2
121
+ interval = 120
122
+ ```
123
+
124
+ **密码别写进仓库。** 优先级从高到低:`$CAMPUS_AUTH_PASSWORD` → 配置文件 → 系统钥匙串。
125
+ 推荐钥匙串(Windows 凭据管理器 / macOS Keychain / Linux Secret Service):
126
+
127
+ ```bash
128
+ campus-auth password # 交互式输入;管道里则读一行
129
+ campus-auth password --show # 只看有没有存
130
+ campus-auth password --forget # 删
131
+ campus-auth login --remember # 或者登录成功时顺手存
132
+ ```
133
+
134
+ 没装 `keyring` 或后端不可用(headless Linux 常见)时自动退回配置文件/环境变量。
135
+
136
+ ## 开机自启
137
+
138
+ ```bash
139
+ campus-auth install --dry-run # 先看它要写什么、跑什么
140
+ campus-auth install # Windows: HKCU\...\Run + .cmd;Linux: systemd --user;macOS: LaunchAgent
141
+ campus-auth uninstall # 停掉并删干净
142
+ ```
143
+
144
+ 自启进程不继承你当前 shell 的环境变量,密码得放进钥匙串或配置文件,install 会提醒。
145
+ 日志在系统 state 目录(Windows `%LOCALAPPDATA%\campus-auth\watch.log`),`watch` 自己按
146
+ 1 MiB 轮转;三条实现都带自动重启,崩了会自己回来。
147
+
148
+ ## 命令与退出码
149
+
150
+ | 命令 | 作用 |
151
+ | --- | --- |
152
+ | `probe` | 探测门户类型,不需要账号(`--url` 给地址) |
153
+ | `init` | 生成配置模板(`--force` 覆盖) |
154
+ | `adapters` | 列出内置适配层与验证状态 |
155
+ | `login` / `logout` / `unbind` | 登录 / 注销 / 解除 MAC 绑定 |
156
+ | `status` | 列出门户会话,标出哪条是你自己的,`--json` 给机器读 |
157
+ | `check` | 只判定在线与否,结果走退出码 |
158
+ | `watch` | 保活循环,`--once` 跑一轮,`--interval` 调间隔,`--log` 同时落盘(自动轮转) |
159
+ | `install` / `uninstall` | 把 `watch` 装成登录自启 / 卸掉(`--dry-run` 只打印) |
160
+ | `password` | 密码存进/删出系统钥匙串(`--show` 看状态,`--forget` 删) |
161
+ | `urls` | 按当前 IP 生成 `.url` / `.desktop` 快捷方式(内含可还原的密码) |
162
+
163
+ | 退出码 | 含义 |
164
+ | --- | --- |
165
+ | 0 | 成功 |
166
+ | 1 | 认证被拒 / 未在线 |
167
+ | 2 | 配置问题(缺字段、适配层不存在、URL 写错) |
168
+ | 3 | 环境问题(不在校园网、取不到本机 IP) |
169
+ | 4 | 传输问题(超时、连接被拒、重试耗尽) |
170
+
171
+ ## 加一所学校
172
+
173
+ `generic` 就是模板:复制成 `<学校代号>.py`,按 F12 抓到的真请求改 `base_url`、参数名、
174
+ 密钥函数,再到 `adapters/__init__.py` 登记。步骤和验收清单见
175
+ [`CONTRIBUTING.md`](CONTRIBUTING.md),那里也写了哪些东西不能提交(真实学号、密码、MAC)。
176
+
177
+ ## 已知限制
178
+
179
+ - 只覆盖 Dr.COM / eportal 的网页认证。锐捷、深澜、802.1X、专用客户端加密的学校都用不了。
180
+ - `probe` 是判断“哪一家”,不是抓包级证据;判成“证据不全”时仍需人工核实。
181
+ - **TLS 默认不校验证书**(校园门户清一色自签)。想校验就 `--verify-tls`,遇到自签会失败。
182
+ - `.url` / `.desktop` 快捷方式里的密码是 IP 异或的密文,而 IP 就在同一个 URL 里 ——
183
+ **等于明文**,分享前留意。报错里的 URL 默认把密码打成 `ab…yz`,要原文加
184
+ `--show-secrets`。
185
+ - `state.json` 只存学号、IP、会话索引,不含密码;POSIX 下权限 0600,Windows 靠目录 ACL。
186
+ - 门户升级会改参数名,适配层跟着改;`generic` 就是为此留的。
187
+
188
+ 变更历史见 [`CHANGELOG.md`](CHANGELOG.md)。
189
+
190
+ ## 免责
191
+
192
+ 非官方项目,与学校、Dr.COM 均无关系。协议来自公开前端代码和本机抓包,不含校内凭据。
193
+ 密码流经:内存、系统钥匙串(若启用)、配置文件(若你写了)、`.url` 文件与报错里的密文。
194
+
195
+ ## 许可证
196
+
197
+ MIT。
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "campus-auth"
7
+ version = "1.0.0"
8
+ description = "Dr.COM / eportal 校园网门户认证客户端,适配层按学校可插拔"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "young-ski" }]
14
+ keywords = ["eportal", "drcom", "campus-network", "captive-portal", "cli"]
15
+ classifiers = [
16
+ "Environment :: Console",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Topic :: System :: Networking",
24
+ "Topic :: Utilities",
25
+ ]
26
+ dependencies = []
27
+
28
+ [project.optional-dependencies]
29
+ keyring = ["keyring>=24"]
30
+ dev = ["pytest>=8", "ruff>=0.6", "build>=1.2", "twine>=5"]
31
+
32
+ [project.scripts]
33
+ campus-auth = "campus_auth.cli:main"
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/young-ski/campus-auth"
37
+ Issues = "https://github.com/young-ski/campus-auth/issues"
38
+ Changelog = "https://github.com/young-ski/campus-auth/blob/main/CHANGELOG.md"
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = ["tests"]
45
+ pythonpath = ["src"]
46
+ addopts = "-q"
47
+
48
+ [tool.ruff]
49
+ line-length = 100
50
+ target-version = "py311"
51
+
52
+ [tool.ruff.lint]
53
+ select = ["E", "F", "I", "UP", "B", "SIM"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """campus-auth —— Dr.COM / eportal 校园网门户认证客户端。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "1.0.0"
6
+ __all__ = ["__version__"]
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,58 @@
1
+ """适配层注册表。
2
+
3
+ 内置适配层在这里登记;也可以用 `--adapter module:attribute` 从任意
4
+ Python 文件加载(写学校适配层时不必改本文件,方便 PR 之外的自用)。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import importlib
10
+ from typing import Final
11
+
12
+ from ..errors import ConfigError
13
+ from .base import Adapter, LoginResult, Param
14
+
15
+ BUILTIN: Final[dict[str, str]] = {
16
+ "huuc": "campus_auth.adapters.huuc:ADAPTER",
17
+ "generic": "campus_auth.adapters.example_generic:ADAPTER",
18
+ }
19
+
20
+ _cache: dict[str, Adapter] = {}
21
+
22
+
23
+ def _load_target(target: str) -> Adapter:
24
+ module_name, _, attr = target.partition(":")
25
+ attr = attr or "ADAPTER"
26
+ try:
27
+ module = importlib.import_module(module_name)
28
+ except ImportError as exc:
29
+ raise ConfigError(f"导出适配层失败({target}): {exc}") from exc
30
+ try:
31
+ adapter = getattr(module, attr)
32
+ except AttributeError as exc:
33
+ raise ConfigError(f"{module_name} 里没有 {attr!r}") from exc
34
+ if not isinstance(adapter, Adapter):
35
+ raise ConfigError(f"{target} 是 {type(adapter).__name__},不是 Adapter 实例")
36
+ adapter.check()
37
+ return adapter
38
+
39
+
40
+ def get_adapter(name: str) -> Adapter:
41
+ """按名字取适配层,`a:b` 形式走动态加载。"""
42
+ if not name:
43
+ raise ConfigError("没有指定适配层(adapter),跑 `campus-auth init` 会生成模板")
44
+ if name in _cache:
45
+ return _cache[name]
46
+ target = name if ":" in name else BUILTIN.get(name, "")
47
+ if not target:
48
+ raise ConfigError(f"没有适配层 {name!r},内置的有: {', '.join(BUILTIN)}")
49
+ adapter = _load_target(target)
50
+ _cache[name] = adapter
51
+ return adapter
52
+
53
+
54
+ def names() -> list[str]:
55
+ return sorted(BUILTIN)
56
+
57
+
58
+ __all__ = ["Adapter", "LoginResult", "Param", "BUILTIN", "get_adapter", "names"]