OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
4
dev/DWGViewer/api/.htaccess
Normal file
4
dev/DWGViewer/api/.htaccess
Normal file
@@ -0,0 +1,4 @@
|
||||
# 禁止直接访问图纸文件(双保险:PHP 接口已校验令牌)
|
||||
<FilesMatch "\.(dwg|dxf|enc)$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
80
dev/DWGViewer/api/model.php
Normal file
80
dev/DWGViewer/api/model.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* DWG/DXF 查看器 - 图纸读取接口
|
||||
* 校验令牌 → .enc 解密 / 原文件原样 → 流式返回。
|
||||
* SECRET 必须与 token.php、upload.php 一致。
|
||||
*/
|
||||
$SECRET = 'be323d1b90bd2fda624083d4d1d716b7';
|
||||
// 与 upload.php 保持一致的私有目录配置:图纸放 Web 目录外,web 里根本没有文件可下载
|
||||
$PRIVATE_DIR = '/volume1/dwgviewer_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('/\.(dwg|dxf|enc)$/i', $f)) json_out(400, ['error' => 'bad params']);
|
||||
if (strpos($f, '..') !== false) json_out(400, ['error' => 'bad path']);
|
||||
|
||||
$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) json_out(404, ['error' => 'not found']);
|
||||
|
||||
$data = file_get_contents($full);
|
||||
$origName = basename($f);
|
||||
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)];
|
||||
}
|
||||
// 加密体头部藏着原文件名(落盘名是随机的,链接里看不出是哪张图)
|
||||
if (strlen($out) > 2) {
|
||||
$nl = unpack('n', substr($out, 0, 2))[1];
|
||||
if ($nl > 0 && $nl <= 255 && strlen($out) > 2 + $nl) {
|
||||
$origName = substr($out, 2, $nl);
|
||||
$out = substr($out, 2 + $nl);
|
||||
}
|
||||
}
|
||||
$data = $out;
|
||||
}
|
||||
header('X-Filename: ' . rawurlencode($origName));
|
||||
header('Access-Control-Expose-Headers: X-Filename');
|
||||
|
||||
// 上传时已经 gzip 过,解密后就是 gzip 流,直接标 Content-Encoding 让浏览器解;
|
||||
// 明文文件(如 dxf)在线压一次,DXF 文本能压到 1/8 左右
|
||||
$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');
|
||||
header('Content-Disposition: inline');
|
||||
echo $send;
|
||||
38
dev/DWGViewer/api/token.php
Normal file
38
dev/DWGViewer/api/token.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* DWG/DXF 查看器 - 令牌接口
|
||||
* 同源校验 + HMAC 短时令牌(5 分钟)。与 STEPViewer 的 api/token.php 同一套设计。
|
||||
* 部署前务必改掉 SECRET。
|
||||
*/
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
|
||||
$SECRET = 'be323d1b90bd2fda624083d4d1d716b7';
|
||||
$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('/\.(dwg|dxf|enc)$/i', $f)) json_out(400, ['error' => 'bad file']);
|
||||
if (strpos($f, '..') !== false) json_out(400, ['error' => 'bad path']);
|
||||
|
||||
// 只允许本站页面来取令牌,挡掉直接爬取。
|
||||
// 优先看 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 (!$host || 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]);
|
||||
109
dev/DWGViewer/api/upload.php
Normal file
109
dev/DWGViewer/api/upload.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* DWG/DXF 查看器 - 上传接口
|
||||
* 接收原始字节流 + X-Filename 头,gzip 后异或加密存成 uploads/xxx.enc。
|
||||
* 落盘就是密文,即使目录被翻到也拿不到可用的图纸。
|
||||
* SECRET 必须与 token.php、model.php 一致。
|
||||
*/
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$SECRET = 'be323d1b90bd2fda624083d4d1d716b7';
|
||||
$MAX_SIZE = 200 * 1024 * 1024;
|
||||
// 私有目录:Web 目录外的绝对路径,图纸只存这里,web 目录里根本没有图纸文件可下载。
|
||||
// 群晖 File Station 在 web 目录外建 /volume1/dwgviewer_private 并给 http 用户组读写权限;
|
||||
// 路径不同就改成你的实际路径。
|
||||
$PRIVATE_DIR = '/volume1/dwgviewer_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']);
|
||||
|
||||
// 只允许本站页面发起上传(与 token.php 同一策略:Sec-Fetch-Site 优先、Referer 兜底,
|
||||
// 父站点的 Referrer-Policy 剥不掉 Sec-Fetch-Site,跨站 iframe 嵌入也能上传)
|
||||
$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 (!$host || strpos($referer, $host) === false) {
|
||||
json_out(403, ['error' => 'forbidden']);
|
||||
}
|
||||
|
||||
$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 200MB)']);
|
||||
|
||||
$raw = urldecode($_SERVER['HTTP_X_FILENAME'] ?? 'drawing.dwg');
|
||||
$base = basename($raw);
|
||||
$safe = preg_replace('/[^\w.\-\x{4e00}-\x{9fff}]/u', '_', $base);
|
||||
if (!$safe || !preg_match('/\.(dwg|dxf)$/i', $safe)) json_out(400, ['error' => '只支持 .dwg / .dxf']);
|
||||
if (strlen($safe) > 200) $safe = substr($safe, 0, 200);
|
||||
|
||||
// 落盘名是纯随机的,不带原文件名也不带真实后缀:
|
||||
// 嵌入链接里就看不出这是哪张图、属于哪个项目,也没法按名字猜其它图纸。
|
||||
// 原文件名放进加密体里,读取时由 model.php 通过 X-Filename 头带回来显示。
|
||||
$name = bin2hex(random_bytes(12)) . '.enc';
|
||||
|
||||
$data = file_get_contents('php://input');
|
||||
if ($data === false || strlen($data) !== $len) json_out(400, ['error' => 'incomplete upload']);
|
||||
|
||||
// 存储层压缩:DWG 本身已压过,DXF 文本能压到 1/8
|
||||
$rawSize = strlen($data);
|
||||
$data = gzencode($data, 6);
|
||||
if ($data === false) json_out(500, ['error' => 'gzip failed']);
|
||||
// 加密体 = 2 字节名字长度 + 原文件名 + gzip 数据
|
||||
$data = pack('n', strlen($safe)) . $safe . $data;
|
||||
|
||||
$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)];
|
||||
}
|
||||
|
||||
if (file_put_contents($dir . '/' . $name, $nonce . $enc) === false) {
|
||||
json_out(500, ['error' => 'write failed']);
|
||||
}
|
||||
// 私有目录模式下返回裸文件名:嵌入链接就是 ?file=xxx.enc,读取端按 basename 在私有目录找
|
||||
$entryPath = ($PRIVATE_DIR !== '' ? '' : 'uploads/') . $name;
|
||||
|
||||
// 记入首页数模库(入口目录的 library.json,2D/3D 共享);库写失败不影响上传本身
|
||||
try {
|
||||
add_to_library($entryPath, $safe, '2d');
|
||||
} catch (Throwable $e) { /* 忽略 */ }
|
||||
|
||||
json_out(200, ['path' => $entryPath, 'name' => $safe, '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);
|
||||
}
|
||||
Reference in New Issue
Block a user