首次提交: OnebotCatalog 项目代码与文档(含 NX 按需生成服务二期、后台、一键启动)

This commit is contained in:
wangruiguo
2026-09-03 17:55:45 +08:00
commit fafa86d3a6
241 changed files with 78656 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
# 禁止直接访问加密模型文件双保险PHP 接口已校验令牌)
<FilesMatch "\.(stp|step|enc)$">
Require all denied
</FilesMatch>

View File

@@ -0,0 +1,66 @@
<?php
/**
* STEP 查看器 - 模型读取接口PHP 版)
* 校验令牌 → .enc 解密 / .stp 原样 → 流式返回
*/
$SECRET = '7175a8598ff7fa1c182f57f0c750e4f8';
// 与 upload.php 保持一致的私有目录配置:模型放 Web 目录外web 里根本没有文件可下载
$PRIVATE_DIR = '/volume1/stepviewer_private';
function json_out($code, $obj) {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($obj, JSON_UNESCAPED_UNICODE);
exit;
}
$f = $_GET['f'] ?? '';
$t = $_GET['t'] ?? '';
$e = (int)($_GET['e'] ?? 0);
if (!$f || !$t || !preg_match('/\.(stp|step|enc)$/i', $f)) json_out(400, ['error' => 'bad params']);
$sig = substr(hash_hmac('sha256', $f . '|' . $e, $SECRET), 0, 16);
if (!hash_equals($sig, $t) || time() > $e) json_out(403, ['error' => 'invalid or expired token']);
$root = realpath(__DIR__ . '/..');
if ($PRIVATE_DIR !== '') {
// 私有目录优先(嵌入链接写纯文件名即可);目录不存在时退回站内路径,
// 让留在 web 目录里的样例模型按原路径继续可用(这些文件本来就允许直链,
// 退回不新增暴露面)。要保护的文件必须移出 web 目录放进私有目录。
$full = realpath($PRIVATE_DIR . '/' . basename($f));
$ok = $full && strpos($full, realpath($PRIVATE_DIR)) === 0 && is_file($full);
if (!$ok) {
$full = realpath($root . '/' . $f);
$ok = $full && strpos($full, $root) === 0 && is_file($full);
}
} else {
$full = realpath($root . '/' . $f);
$ok = $full && strpos($full, $root) === 0 && is_file($full);
}
if (!$ok || !$full) json_out(404, ['error' => 'not found']);
$data = file_get_contents($full);
if (preg_match('/\.enc$/i', $f)) {
$nonce = substr($data, 0, 16);
$key = hash('sha256', $SECRET . $nonce, true);
$body = substr($data, 16);
$out = '';
for ($i = 0; $i < strlen($body); $i++) {
$out .= $body[$i] ^ $key[$i % strlen($key)];
}
$data = $out;
}
// 传输层 gzip新格式文件解密后即以 gzip 魔数开头,原样传输;
// 旧格式明文文件在线压缩。浏览器 fetch 自动解压(无需前端改动)
$send = $data;
if (substr($send, 0, 2) === "\x1f\x8b") {
header('Content-Encoding: gzip');
} elseif (!ini_get('zlib.output_compression') && strlen($data) > 1024) {
$gz = gzencode($data, 6);
if ($gz !== false) { $send = $gz; header('Content-Encoding: gzip'); }
}
header('Content-Type: application/octet-stream');
header('Content-Length: ' . strlen($send));
header('Cache-Control: no-store');
echo $send;

View File

@@ -0,0 +1,35 @@
<?php
/**
* STEP 查看器 - 令牌接口PHP 版)
* 同源校验 + HMAC 短时令牌5 分钟)
*/
header('Content-Type: application/json; charset=utf-8');
$SECRET = '7175a8598ff7fa1c182f57f0c750e4f8';
$TTL = 300;
function json_out($code, $obj) {
http_response_code($code);
echo json_encode($obj, JSON_UNESCAPED_UNICODE);
exit;
}
$f = $_GET['f'] ?? '';
if (!$f || !preg_match('/\.(stp|step|enc)$/i', $f)) json_out(400, ['error' => 'bad file']);
// 只允许本站页面来取令牌,挡掉直接爬取。
// 优先看 Sec-Fetch-Site由查看器页面发起的同源 fetch 恒为 same-origin跨站 iframe
// 嵌入也不受影响,且不会被父站点的 Referrer-Policy 剥掉);没有该头的旧浏览器
// 退回「Referer 含本站 host」的校验。直链/curl 两种都过不了。
$fetchSite = $_SERVER['HTTP_SEC_FETCH_SITE'] ?? '';
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$host = $_SERVER['HTTP_HOST'] ?? '';
if ($fetchSite !== '') {
if ($fetchSite !== 'same-origin' && $fetchSite !== 'same-site') json_out(403, ['error' => 'forbidden']);
} elseif (strpos($referer, $host) === false) {
json_out(403, ['error' => 'forbidden']);
}
$exp = time() + $TTL;
$sig = substr(hash_hmac('sha256', $f . '|' . $exp, $SECRET), 0, 16);
json_out(200, ['token' => $sig, 'expires' => $exp]);

View File

@@ -0,0 +1,98 @@
<?php
/**
* STEP 查看器 - 上传接口PHP 版,虚拟主机可用)
* 接收原始字节流 + X-Filename 头,加密存储为 uploads/xxx.enc
* 生产环境请修改 SECRET
*/
header('Content-Type: application/json; charset=utf-8');
$SECRET = '7175a8598ff7fa1c182f57f0c750e4f8';
$MAX_SIZE = 500 * 1024 * 1024;
// 私有目录Web 目录外的绝对路径模型只存这里web 目录里根本没有模型文件可下载。
// 群晖 File Station 在 web 目录外建 /volume1/stepviewer_private 并给 http 用户组读写权限;
// 路径不同就改成你的实际路径。
$PRIVATE_DIR = '/volume1/stepviewer_private';
function json_out($code, $obj) {
http_response_code($code);
echo json_encode($obj, JSON_UNESCAPED_UNICODE);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') json_out(405, ['error' => 'method not allowed']);
$len = (int)($_SERVER['CONTENT_LENGTH'] ?? 0);
if ($len <= 0) json_out(400, ['error' => 'empty body']);
if ($len > $MAX_SIZE) json_out(413, ['error' => 'file too large (max 500MB)']);
$raw = urldecode($_SERVER['HTTP_X_FILENAME'] ?? 'model.step');
$base = basename($raw);
$safe = preg_replace('/[^\w.\-\x{4e00}-\x{9fff}]/u', '_', $base);
if (!$safe || !preg_match('/\.(step|stp)$/i', $safe)) $safe .= '.stp';
$stem = pathinfo($safe, PATHINFO_FILENAME);
$ext = pathinfo($safe, PATHINFO_EXTENSION);
$name = $stem . '_' . substr(bin2hex(random_bytes(4)), 0, 8) . '.' . $ext;
$data = file_get_contents('php://input');
if ($data === false || strlen($data) !== $len) json_out(400, ['error' => 'incomplete upload']);
// 客户端可选 gzip 压缩上传STEP 文本约可压到 1/6节省带宽
if (($_SERVER['HTTP_X_ENCODING'] ?? '') === 'gzip') {
$dec = gzdecode($data);
if ($dec === false) json_out(400, ['error' => 'bad gzip body']);
if (strlen($dec) > $MAX_SIZE) json_out(413, ['error' => 'file too large (max 500MB)']);
$data = $dec;
}
// 存储层也压缩:先 gzip 再加密,磁盘占用约 1/6读取端靠 gzip 魔数自动识别)
$rawSize = strlen($data);
$data = gzencode($data, 6);
if ($data === false) json_out(500, ['error' => 'gzip failed']);
$dir = $PRIVATE_DIR !== '' ? $PRIVATE_DIR : (__DIR__ . '/../uploads');
if (!is_dir($dir)) @mkdir($dir, 0755, true);
if (!is_dir($dir)) json_out(500, ['error' => 'upload dir not writable']);
$nonce = random_bytes(16);
$key = hash('sha256', $SECRET . $nonce, true);
$enc = '';
for ($i = 0; $i < strlen($data); $i++) {
$enc .= $data[$i] ^ $key[$i % strlen($key)];
}
file_put_contents($dir . '/' . $name . '.enc', $nonce . $enc);
// 私有目录模式下返回裸文件名:嵌入链接就是 ?file=xxx.enc读取端按 basename 在私有目录找
$entryPath = ($PRIVATE_DIR !== '' ? '' : 'uploads/') . $name . '.enc';
// 记入首页数模库(入口目录的 library.json2D/3D 共享);库写失败不影响上传本身
try {
add_to_library($entryPath, $safe, '3d');
} catch (Throwable $e) { /* 忽略 */ }
json_out(200, ['path' => $entryPath, 'size' => $rawSize]);
/**
* 把刚上传的文件追加进首页数模库(../../library.json
* 同路径已有记录就刷新名称和时间,否则插到最前。
*/
function add_to_library($path, $name, $type) {
$libFile = dirname(__DIR__, 2) . '/library.json';
$records = [];
if (is_file($libFile)) {
$old = json_decode((string)file_get_contents($libFile), true);
if (is_array($old) && isset($old['records']) && is_array($old['records'])) $records = $old['records'];
}
$found = false;
foreach ($records as $i => $rec) {
if (isset($rec['path']) && $rec['path'] === $path) {
$records[$i]['name'] = $name;
$records[$i]['time'] = time();
$found = true;
break;
}
}
if (!$found) {
array_unshift($records, ['path' => $path, 'name' => $name, 'type' => $type, 'note' => '', 'time' => time()]);
}
@file_put_contents($libFile, json_encode(['records' => $records], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX);
}