better-staridc-MNBT
1<?php
2/**
3 * 梦奈宝塔虚拟主机 — 魔方财务(idcsmart)server module
4 *
5 * 通过 MNBT 外部 API(api/api.php)对接宝塔虚拟主机分销。
6 * 开通即建站(FTP + MySQL + 配额),配额随产品配置选项落库。
7 *
8 * 关联:api/api.php(cfif/kt/zt/jc/tz/xf/czmm/zjmode/start/stop + ztcx)
9 */
10
11// ========================================================================
12// MetaData
13// ========================================================================
14
15function mnbthost_MetaData()
16{
17 return [
18 'DisplayName' => '梦奈宝塔虚拟主机',
19 'APIVersion' => '1.1',
20 'HelpDoc' => 'https://github.com/MNBT/API.md', // 替换为实际帮助文档地址
21 ];
22}
23
24// ========================================================================
25// ConfigOptions(产品级配额配置)
26// ========================================================================
27
28function mnbthost_ConfigOptions()
29{
30 return [
31 [
32 'type' => 'text',
33 'name' => '网站空间',
34 'description' => 'MB(开通时 webdx 参数)',
35 'default' => '500',
36 'key' => 'webdx',
37 ],
38 [
39 'type' => 'text',
40 'name' => '数据库空间',
41 'description' => 'MB(开通时 sqldx 参数)',
42 'default' => '100',
43 'key' => 'sqldx',
44 ],
45 [
46 'type' => 'text',
47 'name' => '流量',
48 'description' => 'MB(0=不限制,开通时 sizemax 参数)',
49 'default' => '0',
50 'key' => 'sizemax',
51 ],
52 [
53 'type' => 'text',
54 'name' => '域名绑定数',
55 'description' => '个(开通时 ymbds 参数)',
56 'default' => '1',
57 'key' => 'ymbds',
58 ],
59 [
60 'type' => 'text',
61 'name' => '控制台地址',
62 'description' => '虚拟主机用户控制台,如 https://mnbt.example.com/user/login.php',
63 'default' => '',
64 'key' => 'console_url',
65 ],
66 ];
67}
68
69// ========================================================================
70// 内部工具:参数解析 & API 调用
71// ========================================================================
72
73/**
74 * 解析参数(连接信息全部来自服务器设置)
75 * 服务器字段映射:
76 * server_ip / server_host → MNBT 主机
77 * port / secure → 端口 / HTTPS
78 * server_username → 节点编号(MN_bt.btdh 宝塔开通代号)
79 * server_password → 调用密钥 md5(ktmy.qmk),空则默认 md5('')
80 * accesshash → 系统 API 密钥($conf['api'])
81 *
82 * @return array [api_url, api_key, node_id, call_key, console_url]
83 */
84function _mnbthost_resolve_params($params)
85{
86 $scheme = ($params['secure'] ?? '') == '1' ? 'https' : 'http';
87 $host = $params['server_ip'] ?: ($params['server_host'] ?? '');
88 $port = $params['port'] ?? '';
89 $api_url = '';
90 if (!empty($host)) {
91 $api_url = $scheme . '://' . $host;
92 if (!empty($port) && $port != '80' && $port != '443') {
93 $api_url .= ':' . $port;
94 }
95 $api_url .= '/api/api.php';
96 }
97
98 $api_key = trim((string)($params['accesshash'] ?? ''));
99 $node_id = trim((string)($params['server_username'] ?? ''));
100 $call_key = trim((string)($params['server_password'] ?? ''));
101
102 // 魔方可能对服务器密码做了 AES 加密存储,模块收到的是密文,
103 // 参考 noKVM 的 aesPasswordDecode 处理;解密成功且为 32 位 hex(md5)才采用
104 if (!empty($call_key) && function_exists('aesPasswordDecode')) {
105 try {
106 $decoded = aesPasswordDecode($call_key);
107 if (is_string($decoded) && preg_match('/^[a-f0-9]{32}$/i', trim($decoded))) {
108 $call_key = trim($decoded);
109 }
110 } catch (\Exception $e) {
111 // 解密失败保持原值
112 }
113 }
114
115 // 控制台地址:优先 configoption5,兼容旧配置
116 $console_url = $params['configoption5'] ?? '';
117 if (empty($console_url) && !empty($host)) {
118 $console_url = $scheme . '://' . $host;
119 if (!empty($port) && $port != '80' && $port != '443') {
120 $console_url .= ':' . $port;
121 }
122 $console_url .= '/user/login.php';
123 }
124
125 return [$api_url, $api_key, $node_id, $call_key, $console_url];
126}
127
128/**
129 * 将魔方的到期时间统一转换为 'Y-m-d' 格式
130 * 魔方 nextduedate 可能是纯时间戳或 'Y-m-d H:i:s' 字符串;
131 * 空 / '0000-00-00' 返回 '0'(后端约定 0 = 永不到期)
132 */
133function _mnbthost_format_duedate($nextduedate)
134{
135 $nextduedate = trim((string)$nextduedate);
136 if ($nextduedate === '' || $nextduedate == '0000-00-00' || $nextduedate == '0000-00-00 00:00:00') {
137 return '0';
138 }
139 if (ctype_digit($nextduedate)) {
140 return date('Y-m-d', (int)$nextduedate);
141 }
142 $ts = strtotime($nextduedate);
143 return $ts ? date('Y-m-d', $ts) : '0';
144}
145
146/**
147 * 调用 MNBT 虚拟主机 API
148 *
149 * @param array $params 魔方传入的 $params
150 * @param string $gn 动作(cfif/kt/zt/jc/tz/xf/czmm/zjmode/start/stop/ztcx)
151 * @param array $extra 额外 POST 字段(业务参数)
152 * @param int $timeout cURL 超时秒数
153 * @return array [success, code, msg, data?, _debug?]
154 */
155function _mnbthost_api_call($params, $gn, $extra = [], $timeout = 30)
156{
157 list($api_url, $api_key, $node_id, $call_key) = _mnbthost_resolve_params($params);
158
159 if (empty($api_url)) return ['success' => false, 'code' => 0, 'msg' => '[mnbthost] 未配置服务器 IP/域名'];
160 if (empty($api_key)) return ['success' => false, 'code' => 0, 'msg' => '[mnbthost] 未配置 Access Hash(系统 API 密钥)'];
161 if (empty($node_id)) return ['success' => false, 'code' => 0, 'msg' => '[mnbthost] 未配置用户名(节点 btdh)'];
162 // 调用密钥为空时默认 md5(''),适配节点未设置 ktmy/qmk 的场景
163 if (empty($call_key)) $call_key = md5('');
164
165 // 账号名优先用 domain(开通时就是用它),username 可能是 root 等主机名
166 $username = $params['domain'] ?? ($params['username'] ?? '');
167
168 $post = array_merge([
169 'mn_bh' => $node_id,
170 'mn_key' => $api_key,
171 'mn_keye' => $call_key,
172 'mn_vs' => 15,
173 'username' => $username,
174 ], $extra);
175
176 // DEBUG:打印请求参数摘要(仅当定义了 MNBT_DEBUG 且为真时附加,避免生产环境泄露敏感信息)
177 $dbg = '';
178 if (defined('MNBT_DEBUG') && MNBT_DEBUG) {
179 $dbg = "gn={$gn} | mn_bh=[{$post['mn_bh']}] | mn_key_len=" . strlen($post['mn_key'])
180 . " | mn_keye=[" . substr($post['mn_keye'], 0, 6) . '***' . substr($post['mn_keye'], -4) . '](len=' . strlen($post['mn_keye']) . ')'
181 . " | username=[{$post['username']}]";
182 }
183
184 // SSL 证书校验开关:默认关闭以兼容自签名证书环境;
185 // 需要开启时在魔方入口文件定义 define('MNBT_SSL_VERIFY', true) 即可
186 $ssl_verify = defined('MNBT_SSL_VERIFY') ? MNBT_SSL_VERIFY : false;
187
188 $url = $api_url . '?gn=' . urlencode($gn);
189
190 $ch = curl_init();
191 curl_setopt_array($ch, [
192 CURLOPT_URL => $url,
193 CURLOPT_POST => true,
194 CURLOPT_POSTFIELDS => http_build_query($post),
195 CURLOPT_TIMEOUT => $timeout,
196 CURLOPT_RETURNTRANSFER => true,
197 CURLOPT_SSL_VERIFYPEER => (bool)$ssl_verify,
198 CURLOPT_SSL_VERIFYHOST => $ssl_verify ? 2 : 0,
199 CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded; charset=UTF-8'],
200 ]);
201 $resp = curl_exec($ch);
202 $errno = curl_errno($ch);
203 $error = curl_error($ch);
204 curl_close($ch);
205
206 if ($errno) {
207 return ['success' => false, 'code' => 0, 'msg' => '[mnbthost] cURL 错误(' . $errno . '):' . $error];
208 }
209
210 $decoded = json_decode($resp, true);
211 if ($decoded === null) {
212 return ['success' => false, 'code' => 0, 'msg' => '[mnbthost] 响应解析失败:' . substr($resp, 0, 200)];
213 }
214
215 // 附加调试摘要到响应(仅 MNBT_DEBUG 开启时)
216 if ($dbg !== '') {
217 $decoded['_debug'] = $dbg;
218 }
219 return $decoded;
220}
221
222/**
223 * 将 MNBT API 响应转为魔方模块返回值
224 */
225function _mnbthost_return($api_result)
226{
227 if (($api_result['success'] ?? false) && ($api_result['code'] ?? 0) == 200) {
228 return 'success';
229 }
230 return $api_result['msg'] ?? '未知错误';
231}
232
233// ========================================================================
234// 测试连接
235// ========================================================================
236
237function mnbthost_TestLink($params)
238{
239 list($api_url, $api_key, $node_id) = _mnbthost_resolve_params($params);
240
241 if (empty($api_url)) return ['status' => 200, 'data' => ['server_status' => 0, 'msg' => '未配置服务器 IP/域名']];
242 if (empty($api_key)) return ['status' => 200, 'data' => ['server_status' => 0, 'msg' => '未配置 Access Hash(系统 API 密钥)']];
243 if (empty($node_id)) return ['status' => 200, 'data' => ['server_status' => 0, 'msg' => '未配置用户名(节点 btdh)']];
244
245 // cfif 需要 username 非空,测试连接时无产品账户故传占位值
246 $r = _mnbthost_api_call($params, 'cfif', ['username' => 'test'], 15);
247 if (($r['success'] ?? false) && ($r['code'] ?? 0) == 200) {
248 return ['status' => 200, 'data' => ['server_status' => 1]];
249 }
250 $msg = $r['msg'] ?? '未知错误';
251 if (!empty($r['_debug'])) {
252 $msg .= ' [' . $r['_debug'] . ']';
253 }
254 return ['status' => 200, 'data' => ['server_status' => 0, 'msg' => $msg]];
255}
256
257// ========================================================================
258// 生命周期方法
259// ========================================================================
260
261/**
262 * 开通主机(开通即建站:FTP + 数据库 + 站点)
263 */
264function mnbthost_CreateAccount($params)
265{
266 $username = $params['domain'] ?? '';
267 if (empty($username)) {
268 $username = 'zh_' . $params['hostid'];
269 }
270
271 $password = $params['password'] ?? '';
272 if (empty($password)) {
273 $password = substr(md5(uniqid(mt_rand(), true)), 0, 12);
274 }
275
276 $dqtime = _mnbthost_format_duedate($params['nextduedate'] ?? '');
277
278 $co = $params['configoptions'] ?? [];
279 $extra = [
280 'username' => $username,
281 'password' => $password,
282 'dqtime' => $dqtime,
283 'webdx' => $co['webdx'] ?? 500,
284 'sqldx' => $co['sqldx'] ?? 100,
285 'sizemax' => $co['sizemax'] ?? 0,
286 'ymbds' => $co['ymbds'] ?? 1,
287 ];
288
289 $r = _mnbthost_api_call($params, 'kt', $extra);
290 return _mnbthost_return($r);
291}
292
293/** 暂停 */
294function mnbthost_SuspendAccount($params)
295{
296 $r = _mnbthost_api_call($params, 'zt');
297 return _mnbthost_return($r);
298}
299
300/** 解除暂停 */
301function mnbthost_UnsuspendAccount($params)
302{
303 $r = _mnbthost_api_call($params, 'jc');
304 return _mnbthost_return($r);
305}
306
307/** 删除(删站点 + 删行) */
308function mnbthost_TerminateAccount($params)
309{
310 $r = _mnbthost_api_call($params, 'tz');
311 return _mnbthost_return($r);
312}
313
314/** 续费 */
315function mnbthost_Renew($params)
316{
317 $dqtime = _mnbthost_format_duedate($params['nextduedate'] ?? '');
318 $r = _mnbthost_api_call($params, 'xf', ['setdate' => $dqtime]);
319 return _mnbthost_return($r);
320}
321
322/** 升降级(更新空间/数据库/流量配额,全量传三项配额,用现有配置值兜底) */
323function mnbthost_ChangePackage($params)
324{
325 $co = $params['configoptions'] ?? [];
326
327 // 后端 zjmode 按传参覆盖对应配额,为保持三项配额一致,这里全量传三项
328 $extra = [
329 'websize' => $co['webdx'] ?? 0,
330 'sqlsize' => $co['sqldx'] ?? 0,
331 'll' => $co['sizemax'] ?? 0,
332 ];
333 $r = _mnbthost_api_call($params, 'zjmode', $extra);
334 return _mnbthost_return($r);
335}
336
337/** 重置密码(idcsmart 将新密码作为第二参数传入,部分版本仅放在 $params['password']) */
338function mnbthost_CrackPassword($params, $new_pass = '')
339{
340 // 优先取第二参数,为空则回退到 $params['password'](兼容魔方部分版本)
341 if (empty($new_pass)) {
342 $new_pass = $params['password'] ?? '';
343 }
344 if (empty($new_pass)) return '缺少新密码';
345 $r = _mnbthost_api_call($params, 'czmm', ['password' => $new_pass]);
346 return _mnbthost_return($r);
347}
348
349// ========================================================================
350// 站点启停
351// ========================================================================
352
353/** 开机(启动站点) */
354function mnbthost_On($params)
355{
356 $r = _mnbthost_api_call($params, 'start');
357 return _mnbthost_return($r);
358}
359
360/** 关机(停止站点) */
361function mnbthost_Off($params)
362{
363 $r = _mnbthost_api_call($params, 'stop');
364 return _mnbthost_return($r);
365}
366
367// ========================================================================
368// 状态 & 同步
369// ========================================================================
370
371/**
372 * 获取主机状态(调 gn=ztcx:状态 + 配额用量)
373 */
374function mnbthost_Status($params)
375{
376 $r = _mnbthost_api_call($params, 'ztcx', [], 60);
377 if (!($r['success'] ?? false) || ($r['code'] ?? 0) != 200) {
378 return [
379 'status' => 'error',
380 'msg' => $r['msg'] ?? '状态查询失败',
381 ];
382 }
383
384 $data = $r['data'] ?? [];
385 $user = $data['user'] ?? [];
386 $qk = $user['qk'] ?? 'true';
387 $datae = $user['datae'] ?? '0000-00-00';
388
389 // 状态映射:qk=false → 暂停;到期 → 暂停;否则运行中
390 $today = date('Y-m-d');
391 if ($qk == 'false') {
392 $status = 'suspend';
393 $des = '已暂停';
394 } elseif ($datae != '0000-00-00' && strtotime($today) > strtotime($datae)) {
395 $status = 'suspend';
396 $des = '已到期';
397 } else {
398 $status = 'on';
399 $des = '运行中';
400 }
401
402 return [
403 'status' => 'success',
404 'data' => [
405 'status' => $status,
406 'des' => $des,
407 'quota' => $data['quota'] ?? null,
408 ],
409 ];
410}
411
412/** 同步 */
413function mnbthost_Sync($params)
414{
415 return mnbthost_Status($params);
416}
417
418// ========================================================================
419// ClientArea 前台自定义输出
420// ========================================================================
421
422function mnbthost_ClientArea($params)
423{
424 return [
425 'console' => ['name' => '主机信息'],
426 ];
427}
428
429function mnbthost_ClientAreaOutput($params, $key)
430{
431 if ($key !== 'console') {
432 return '';
433 }
434
435 // 查询最新状态
436 $status_data = mnbthost_Status($params);
437 $status_info = $status_data['data'] ?? ['status' => 'unknown', 'des' => '未知'];
438 $status_class = $status_info['status'] ?? 'unknown';
439 $quota = $status_info['quota'] ?? null;
440
441 // 预计算配额进度百分比(0-100)
442 $quota_pct = null;
443 if (is_array($quota)) {
444 $quota_pct = [];
445 foreach (['web_size', 'sql_size', 'flow'] as $k) {
446 $max = (int)($quota[$k . '_max'] ?? 0);
447 $used = (int)($quota[$k . '_used'] ?? 0);
448 $pct = $max > 0 ? min(100, (int)round($used / $max * 100)) : 0;
449 $cls = $max > 0 ? ($pct >= 90 ? 'danger' : ($pct >= 70 ? 'warn' : 'ok')) : 'ok';
450 $quota_pct[$k] = ['pct' => $pct, 'cls' => $cls];
451 }
452 }
453
454 list(, , , , $console_url) = _mnbthost_resolve_params($params);
455
456 return [
457 'template' => 'templates/console.html',
458 'vars' => [
459 'status_text' => htmlspecialchars($status_info['des'] ?? '', ENT_QUOTES),
460 'status_class' => htmlspecialchars($status_class, ENT_QUOTES),
461 'console_url' => htmlspecialchars($console_url ?? '', ENT_QUOTES),
462 'username' => htmlspecialchars($params['domain'] ?? ($params['username'] ?? ''), ENT_QUOTES),
463 'password' => htmlspecialchars($params['password'] ?? '', ENT_QUOTES),
464 // 密码默认展示掩码,点击"显示"后再展示明文
465 'password_mask' => !empty($params['password']) ? str_repeat('•', 8) : '',
466 'quota' => $quota,
467 'quota_pct' => $quota_pct,
468 ],
469 ];
470}
471
472// ========================================================================
473// ClientButton & AllowFunction
474// ========================================================================
475
476function mnbthost_ClientButton($params)
477{
478 return [
479 'console' => [
480 'place' => 'console',
481 'name' => '打开主机控制台',
482 ],
483 ];
484}
485
486function mnbthost_AllowFunction()
487{
488 return [
489 'client' => ['console'],
490 'admin' => [],
491 ];
492}
493
494/**
495 * 前台按钮:返回控制台 URL
496 */
497function mnbthost_console($params)
498{
499 list(, , , , $console_url) = _mnbthost_resolve_params($params);
500 return ['status' => 'success', 'msg' => '正在跳转到主机控制台', 'url' => $console_url];
501}