This commit is contained in:
2026-06-12 17:19:26 +08:00
parent a3ebb28ce0
commit 8d2e18c5a4
20 changed files with 3011 additions and 411 deletions
+86
View File
@@ -0,0 +1,86 @@
# Server Information
**SSH Access:** `ssh root@39.102.124.161` (Alibaba Cloud)
**OS:** CentOS with Baota panel pre-configured
**Website:** https://www.thereisnospoonadu.com/
**Web Server:** Tengine (Alibaba's fork of Nginx) — use `tengine` commands, NOT `nginx`
## Gitea (self-hosted Git server)
**Web UI:** https://git.thereisnospoonadu.com
**Admin username:** beastgitea2026
**API token:** paste fresh when needed (not stored)
**SSH remote:** `git@git.thereisnospoonadu.com:beastgitea2026/<repo>.git`
**SSH port:** 222 — configured in `~/.ssh/config` on dev machine
**Gitea data:** `/var/lib/gitea/` | **Docker container name:** `gitea`
**Tengine config:** `/etc/tengine/conf.d/gitea.conf`
## Critical File Paths
**Web Root:** `/www/wwwroot/soft_download/`
**Tengine Config (ACTUAL):** `/etc/tengine/conf.d/thereisnospoonadu.conf`
- Site uses HTTPS (port 443), HTTP redirects to HTTPS
- Always test changes with: `tengine -t && tengine -s reload`
**Auth File:** `/etc/tengine/.htpasswd`
**Logs:** Check tengine log paths inside `/etc/tengine/conf.d/thereisnospoonadu.conf`
## User Management
Add a new end user (can login and download files):
```bash
# Add user (file already exists — do NOT use -c or it will overwrite all users)
htpasswd /etc/tengine/.htpasswd username
# Verify a user's password
htpasswd -v /etc/tengine/.htpasswd username
# Delete a user
htpasswd -D /etc/tengine/.htpasswd username
# List current users
cat /etc/tengine/.htpasswd
```
Note: `htpasswd` requires `httpd-tools` package (`yum install -y httpd-tools`).
## Architecture
**Download Portal System:**
- Login page: `/login.html` (public)
- File listing: `/list/` (requires HTTP Basic Auth via Authorization header)
- File downloads: Direct links to `.exe`, `.md`, `.pdf`, etc. (NO auth required)
- Security model: Must login to see file list, but downloads are public once you know the filename
**Key Files:**
- `/www/wwwroot/soft_download/index.html` - Main file browser
- `/www/wwwroot/soft_download/login.html` - Login page
- `/www/wwwroot/soft_download/api/validate` - Auth validation endpoint
## Important Notes
- When making Tengine changes, ALWAYS edit `/etc/tengine/conf.d/thereisnospoonadu.conf`
- Test against the actual domain: `curl -I https://www.thereisnospoonadu.com/filename`
- PHP is NOT configured/working on this server - use Tengine-only solutions
- Large files (170MB+ exe) need native browser downloads, not blob/XHR approach
## Verification Checklist (Before Making Changes)
```bash
# 1. Verify which Tengine config is active
tengine -T | grep -A 30 'server_name.*thereisnospoonadu'
# 2. Test current behavior against actual domain
curl -I https://www.thereisnospoonadu.com/版本升级说明(用记事本打开).md
# 3. After changes: test and reload
tengine -t && tengine -s reload
```
## Troubleshooting (Only When Issues Occur)
```bash
# Find log paths from config
grep 'access_log\|error_log' /etc/tengine/conf.d/thereisnospoonadu.conf
```
**Note:** Logs are automatically rotated daily, keeping 7 days of history (compressed).
+276
View File
@@ -0,0 +1,276 @@
#!/usr/bin/env python3
import re
import os
from http.server import HTTPServer, BaseHTTPRequestHandler
from datetime import datetime, timedelta
from collections import defaultdict
LOG_FILE = "/usr/local/nginx/logs/download_access.log"
LOG_RE = re.compile(
r'(?P<ip>\S+) - (?P<user>\S+) \[(?P<time>[^\]]+)\] "(?P<request>[^"]*)" '
r'(?P<status>\d+) (?P<bytes>\d+) "(?P<referer>[^"]*)" "(?P<ua>[^"]*)"'
)
DOWNLOAD_EXTS = re.compile(
r'\.(exe|md|pdf|zip|rar|7z|tar|gz|txt|doc|docx|xls|xlsx|ppt|pptx|deb)(\?|$)',
re.IGNORECASE
)
def fmt_bytes(b):
b = int(b)
for unit in ['B', 'KB', 'MB', 'GB']:
if b < 1024:
return "{:.1f} {}".format(b, unit)
b /= 1024
return "{:.1f} TB".format(b)
def parse_time(s):
try:
return datetime.strptime(s, "%d/%b/%Y:%H:%M:%S %z")
except Exception:
return None
def parse_logs():
entries = []
try:
with open(LOG_FILE, 'r', errors='replace') as f:
for line in f:
m = LOG_RE.match(line)
if not m:
continue
user = m.group('user')
entries.append({
'ip': m.group('ip'),
'user': user,
'time': parse_time(m.group('time')),
'request': m.group('request'),
'status': int(m.group('status')),
'bytes': int(m.group('bytes')),
})
except FileNotFoundError:
pass
return entries
def build_stats(entries):
users = defaultdict(lambda: {
'logins': [], 'downloads': [], 'total_bytes': 0, 'ips': set()
})
all_downloads = []
for e in entries:
u = e['user']
t = e['time']
req = e['request']
parts = req.split(' ')
path = parts[1] if len(parts) >= 2 else req
# For anonymous download entries, extract username from ?u= query param
if u == '-' and DOWNLOAD_EXTS.search(path) and e['status'] in (200, 206):
m = re.search(r'[?&]u=([^&\s]+)', path)
if m:
try:
from urllib.parse import unquote
u = unquote(m.group(1))
except Exception:
u = m.group(1)
else:
continue
if u == '-':
continue
users[u]['ips'].add(e['ip'])
# Count login: either /api/validate or /list/ success
if e['status'] == 200 and t:
if '/api/validate' in path or path.rstrip('/') == '/list':
users[u]['logins'].append(t)
if DOWNLOAD_EXTS.search(path) and e['status'] in (200, 206):
filename = path.split('/')[-1].split('?')[0]
try:
from urllib.parse import unquote
filename = unquote(filename)
# Tengine logs non-ASCII bytes as \xNN sequences; decode them as UTF-8
def _decode_nginx_escapes(s):
parts = re.split(r'((?:\\x[0-9A-Fa-f]{2})+)', s)
out = []
for part in parts:
if part.startswith('\\x'):
raw = bytes(int(h, 16) for h in re.findall(r'\\x([0-9A-Fa-f]{2})', part))
out.append(raw.decode('utf-8', errors='replace'))
else:
out.append(part)
return ''.join(out)
filename = _decode_nginx_escapes(filename)
except Exception:
pass
users[u]['downloads'].append({'time': t, 'file': filename, 'bytes': e['bytes']})
users[u]['total_bytes'] += e['bytes']
all_downloads.append({'user': u, 'time': t, 'file': filename, 'bytes': e['bytes']})
def sort_key(x):
t = x['time']
if t is None:
return datetime.min.replace(tzinfo=None)
try:
return t.replace(tzinfo=None)
except Exception:
return datetime.min.replace(tzinfo=None)
all_downloads.sort(key=sort_key, reverse=True)
return users, all_downloads
def render_html(users, all_downloads):
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
user_rows = ""
for uname in sorted(users.keys()):
data = users[uname]
logins = sorted(data['logins'], reverse=True)
last_login = logins[0].strftime("%Y-%m-%d %H:%M") if logins else "&mdash;"
login_count = len(logins)
dl_count = len(data['downloads'])
total_data = fmt_bytes(data['total_bytes'])
ips = ", ".join(sorted(data['ips']))
if logins:
delta = datetime.now(logins[0].tzinfo) - logins[0]
is_active = delta < timedelta(hours=24)
else:
is_active = False
status_class = "active" if is_active else "inactive"
status_label = "Active 24h" if is_active else "Inactive"
user_rows += (
"<tr>"
"<td><span class='username'>{}</span></td>"
"<td><span class='badge {}'>{}</span></td>"
"<td>{}</td>"
"<td class='num'>{}</td>"
"<td class='num'>{}</td>"
"<td class='num'>{}</td>"
"<td class='ip'>{}</td>"
"</tr>"
).format(uname, status_class, status_label, last_login,
login_count, dl_count, total_data, ips)
dl_rows = ""
for d in all_downloads[:100]:
t = d['time'].strftime("%Y-%m-%d %H:%M") if d['time'] else "&mdash;"
dl_rows += (
"<tr>"
"<td>{}</td>"
"<td><span class='username'>{}</span></td>"
"<td class='filename'>{}</td>"
"<td class='num'>{}</td>"
"</tr>"
).format(t, d['user'], d['file'], fmt_bytes(d['bytes']))
total_users = len(users)
total_dls = sum(len(v['downloads']) for v in users.values())
total_data_all = fmt_bytes(sum(v['total_bytes'] for v in users.values()))
no_users = "<tr><td colspan='7' class='empty'>No authenticated activity yet</td></tr>"
no_dls = "<tr><td colspan='4' class='empty'>No downloads recorded yet</td></tr>"
html = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="refresh" content="60">
<title>Admin Dashboard</title>
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f1f5f9; color: #1e293b; min-height: 100vh; }}
header {{ background: #ffffff; border-bottom: 1px solid #e2e8f0; padding: 20px 32px; display: flex; align-items: center; justify-content: space-between; }}
header h1 {{ font-size: 1.4rem; font-weight: 600; color: #7c3aed; }}
.refresh-note {{ font-size: 0.75rem; color: #94a3b8; }}
.stats-bar {{ display: flex; gap: 16px; padding: 24px 32px; }}
.stat-card {{ background: #ffffff; border: 1px solid #e2e8f0; border-radius: 10px; padding: 18px 24px; flex: 1; }}
.stat-card .label {{ font-size: 0.75rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em; }}
.stat-card .value {{ font-size: 2rem; font-weight: 700; color: #7c3aed; margin-top: 4px; }}
.section {{ padding: 0 32px 32px; }}
.section h2 {{ font-size: 1rem; font-weight: 600; color: #64748b; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.05em; }}
table {{ width: 100%; border-collapse: collapse; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 10px; overflow: hidden; }}
th {{ background: #f8fafc; padding: 12px 16px; text-align: left; font-size: 0.75rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }}
td {{ padding: 12px 16px; border-top: 1px solid #f1f5f9; font-size: 0.875rem; }}
tr:hover td {{ background: #f8fafc; }}
.username {{ color: #2563eb; font-weight: 500; }}
.filename {{ color: #059669; font-family: monospace; font-size: 0.8rem; }}
.ip {{ color: #64748b; font-size: 0.8rem; font-family: monospace; }}
.num {{ text-align: right; color: #1e293b; }}
.badge {{ padding: 2px 10px; border-radius: 999px; font-size: 0.7rem; font-weight: 600; }}
.badge.active {{ background: #d1fae5; color: #065f46; }}
.badge.inactive {{ background: #f1f5f9; color: #94a3b8; }}
.empty {{ text-align: center; color: #94a3b8; padding: 32px; }}
</style>
</head>
<body>
<header>
<h1>Admin Dashboard &mdash; thereisnospoonadu.com</h1>
<span class="refresh-note">Last updated: {now} &nbsp;|&nbsp; Auto-refresh every 60s</span>
</header>
<div class="stats-bar">
<div class="stat-card"><div class="label">Total Users</div><div class="value">{total_users}</div></div>
<div class="stat-card"><div class="label">Total Downloads</div><div class="value">{total_dls}</div></div>
<div class="stat-card"><div class="label">Total Data Served</div><div class="value">{total_data_all}</div></div>
</div>
<div class="section">
<h2>User Summary</h2>
<table>
<thead><tr>
<th>Username</th><th>Status</th><th>Last Login</th>
<th style="text-align:right">Logins</th>
<th style="text-align:right">Downloads</th>
<th style="text-align:right">Data Used</th>
<th>IP Address(es)</th>
</tr></thead>
<tbody>{user_rows}</tbody>
</table>
</div>
<div class="section">
<h2>Download History (last 100)</h2>
<table>
<thead><tr><th>Time</th><th>User</th><th>File</th><th style="text-align:right">Size</th></tr></thead>
<tbody>{dl_rows}</tbody>
</table>
</div>
</body>
</html>""".format(
now=now,
total_users=total_users,
total_dls=total_dls,
total_data_all=total_data_all,
user_rows=user_rows if user_rows else no_users,
dl_rows=dl_rows if dl_rows else no_dls,
)
return html
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
entries = parse_logs()
users, all_downloads = build_stats(entries)
html = render_html(users, all_downloads)
body = html.encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
pass
if __name__ == '__main__':
server = HTTPServer(('127.0.0.1', 8765), Handler)
print("Admin dashboard running on 127.0.0.1:8765")
server.serve_forever()
+14
View File
@@ -0,0 +1,14 @@
<?php
/**
* Used by Nginx auth_request. Returns 200 if session valid, 401 otherwise.
* Also returns X-Auth-User header so Tengine can log the authenticated username.
* Deploy to /www/wwwroot/soft_download/auth_check.php
*/
session_start();
if (!empty($_SESSION['soft_download_user'])) {
header('X-Auth-User: ' . $_SESSION['soft_download_user']);
http_response_code(200);
exit;
}
http_response_code(401);
exit;
+175
View File
@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>软件下载页面</title>
<style>
body{font-size:18px!important;line-height:2;font-family:sans-serif;margin:.5em 1.5em 2em}
h1{font-size:24px!important;margin-top:0;margin-bottom:.5em;border-bottom:2px solid #ccc;padding-bottom:.3em}
.breadcrumb{font-size:18px;margin-bottom:.75em}
.breadcrumb a{color:#0066cc;cursor:pointer}
.loading{font-size:20px;color:#666}
.error{color:#c00;font-size:18px}
.user-bar{margin-bottom:.4em;padding:.25em 0;display:flex;align-items:center;justify-content:flex-end;gap:1em}
.user-bar .logged-in{color:green}
.user-bar .logout-btn{padding:.3em .8em;font-size:16px;cursor:pointer;background:#0066cc;color:#fff;border:none;border-radius:4px}
.user-bar .logout-btn:hover{background:#0052a3}
.file-list{width:100%}
.file-table{width:100%;border-collapse:collapse;margin-top:.25em;table-layout:fixed}
.file-table th,.file-table td{padding:.45em .65em;border-bottom:1px solid #ddd;text-align:left;vertical-align:middle;word-break:break-word}
.file-table th{font-size:17px;font-weight:600;color:#333;background:#f5f5f5;border-bottom:2px solid #ccc}
.file-table td:nth-child(1){width:52%}
.file-table td:nth-child(2),.file-table td:nth-child(3){font-size:16px;color:#555;white-space:nowrap}
.file-table a{font-size:19px!important;color:#0066cc;text-decoration:none}
.file-table a:hover{text-decoration:underline}
.file-table .dir{font-weight:bold;cursor:pointer}
.file-table .empty-msg{text-align:center;color:#666;padding:1em}
</style>
</head>
<body>
<div class="user-bar">
<span class="logged-in">当前已登录: <span id="username-display"></span></span>
<button class="logout-btn" id="logout-btn">注销</button>
</div>
<h1>文件列表 <span id="path-display"></span></h1>
<div class="breadcrumb" id="breadcrumb"></div>
<div id="content">
<div class="loading" id="loading">正在加载...</div>
<div class="file-list" id="file-list" style="display:none"></div>
<div class="error" id="error" style="display:none"></div>
</div>
<script>
(function(){
var authHeader=sessionStorage.getItem("auth");
var username=sessionStorage.getItem("username");
if(!authHeader){
window.location.href="/login.html?return="+encodeURIComponent(window.location.pathname);
return;
}
document.getElementById("username-display").textContent=username||"";
loadFileList(getPath());
function getPath(){
var p=window.location.pathname;
if(p==="/"||p==="")return"";
if(p.endsWith("/"))p=p.slice(0,-1);
return p.replace(/^\//,"");
}
function shouldHideFile(filename){
var hiddenPatterns=[/\.php$/i,/\.html$/i,/\.backup$/i,/^api$/i,/^auth_check/i,/^login/i,/^logout/i,/^index\./i];
return hiddenPatterns.some(function(pattern){return pattern.test(filename)});
}
function escapeHtml(s){
if(s==null||s==="")return"";
return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
}
function formatSize(bytes){
if(!bytes)return"-";
if(bytes<1024)return bytes+" B";
if(bytes<1048576)return(bytes/1024).toFixed(1)+" K";
if(bytes<1073741824)return(bytes/1048576).toFixed(1)+" M";
return(bytes/1073741824).toFixed(1)+" G";
}
function formatMtime(mtime){
if(!mtime)return"";
try{
var d=new Date(mtime);
return isNaN(d)?mtime:d.toLocaleString();
}catch(e){
return mtime;
}
}
function renderBreadcrumb(path){
var parts=path?path.split("/").filter(Boolean):[];
if(parts.length===0){
document.getElementById("breadcrumb").innerHTML="";
return;
}
var html="";
var acc="";
for(var i=0;i<parts.length;i++){
acc+=(acc?"/":"")+parts[i];
if(i>0)html+=" / ";
html+='<a onclick="navigateTo(\'/'+acc+'/\')">'+ parts[i]+"</a>";
}
document.getElementById("breadcrumb").innerHTML=html;
}
window.navigateTo=function(path){
window.location.href=path;
};
function loadFileList(path){
var loadingEl=document.getElementById("loading");
var errorEl=document.getElementById("error");
var listEl=document.getElementById("file-list");
loadingEl.style.display="block";
errorEl.style.display="none";
listEl.style.display="none";
fetch("/list/"+(path?path+"/":""),{
headers:{Authorization:authHeader}
})
.then(function(r){
if(r.status===401){
sessionStorage.clear();
window.location.href="/login.html";
return;
}
if(!r.ok)throw new Error("加载失败");
return r.json();
})
.then(function(data){
if(!data)return;
loadingEl.style.display="none";
document.getElementById("path-display").textContent=path?"/"+path:"";
renderBreadcrumb(path);
var rows="";
var items=Array.isArray(data)?data:[];
items.forEach(function(item){
if(shouldHideFile(item.name))return;
var fullPath=(path?path+"/":"")+item.name;
var isDir=item.type==="directory";
var size=formatSize(item.size);
var mtime=formatMtime(item.mtime);
var nameEsc=escapeHtml(item.name);
if(isDir){
var navPath="/"+fullPath+"/";
rows+="<tr><td><a onclick=\"navigateTo("+JSON.stringify(navPath)+")\" class=\"dir\" href=\"javascript:void(0)\">"+nameEsc+"/</a></td><td>"+escapeHtml(size)+"</td><td>"+escapeHtml(mtime)+"</td></tr>";
}else{
rows+="<tr><td><a href=\"/"+fullPath+"?u="+encodeURIComponent(username)+"\" download>"+nameEsc+"</a></td><td>"+escapeHtml(size)+"</td><td>"+escapeHtml(mtime)+"</td></tr>";
}
});
var tableStart='<table class="file-table"><thead><tr><th>文件名称</th><th>文件大小</th><th>上传时间</th></tr></thead><tbody>';
var tableEnd="</tbody></table>";
if(!rows){
listEl.innerHTML=tableStart+'<tr><td class="empty-msg" colspan="3">目录为空</td></tr>'+tableEnd;
}else{
listEl.innerHTML=tableStart+rows+tableEnd;
}
listEl.style.display="block";
})
.catch(function(err){
loadingEl.style.display="none";
errorEl.textContent="加载失败: "+(err.message||"未知错误");
errorEl.style.display="block";
});
}
document.getElementById("logout-btn").addEventListener("click",function(){
sessionStorage.clear();
window.location.href="/login.html";
});
})();
</script>
</body>
</html>
+103
View File
@@ -0,0 +1,103 @@
<!DOCTYPE html>
<!-- Deploy: copy to /www/wwwroot/soft_download/login.html on the server. Must be excluded from auth_basic in Nginx. -->
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - 软件下载页面</title>
<style>
* { box-sizing: border-box; }
body { font-size: 18px; line-height: 1.6; font-family: sans-serif; margin: 0; padding: 2em; min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #f5f5f5; }
.login-box { background: #fff; padding: 2em 2.5em; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); max-width: 380px; width: 100%; }
h1 { font-size: 22px; margin: 0 0 1.2em 0; color: #333; border-bottom: 2px solid #ccc; padding-bottom: 0.4em; }
.form-group { margin-bottom: 1.2em; }
.form-group label { display: block; margin-bottom: 0.3em; color: #555; font-size: 16px; }
.form-group input { width: 100%; padding: 0.5em 0.8em; font-size: 16px; border: 1px solid #ccc; border-radius: 4px; }
.form-group input:focus { outline: none; border-color: #0066cc; }
.login-btn { width: 100%; padding: 0.6em 1em; font-size: 18px; background: #0066cc; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
.login-btn:hover { background: #0052a3; }
.error-msg { color: #c00; font-size: 14px; margin-top: 0.5em; display: none; }
</style>
</head>
<body>
<div class="login-box">
<h1>登录</h1>
<form id="login-form">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" required autocomplete="username">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<div class="error-msg" id="error-msg"></div>
<button type="submit" class="login-btn">登录</button>
</form>
</div>
<script>
(function() {
var form = document.getElementById('login-form');
var errorEl = document.getElementById('error-msg');
function getReturnPath() {
var params = new URLSearchParams(window.location.search);
var ret = params.get('return');
return ret && ret.length > 0 ? ret : '/';
}
form.addEventListener('submit', function(e) {
e.preventDefault();
var user = document.getElementById('username').value.trim();
var pass = document.getElementById('password').value;
if (!user || !pass) {
errorEl.textContent = '请输入用户名和密码';
errorEl.style.display = 'block';
return;
}
var returnPath = getReturnPath();
if (!returnPath.startsWith('/')) returnPath = '/' + returnPath;
// Submit to login.php via POST
var formData = new FormData();
formData.append('username', user);
formData.append('password', pass);
formData.append('return', returnPath);
fetch('/login.php', {
method: 'POST',
body: formData,
credentials: 'same-origin'
})
.then(function(response) {
if (response.redirected) {
window.location.href = response.url;
} else if (response.ok) {
window.location.href = returnPath;
} else {
return response.text();
}
})
.then(function(html) {
if (html) {
// Login failed, extract error message from response
var parser = new DOMParser();
var doc = parser.parseFromString(html, 'text/html');
var errorMsg = doc.querySelector('.error-msg');
if (errorMsg) {
errorEl.textContent = errorMsg.textContent;
} else {
errorEl.textContent = '登录失败,请重试';
}
errorEl.style.display = 'block';
}
})
.catch(function(err) {
errorEl.textContent = '网络错误,请重试';
errorEl.style.display = 'block';
});
});
})();
</script>
</body>
</html>
+91
View File
@@ -0,0 +1,91 @@
<?php
/**
* Session-based login. POST: validate against .htpasswd and set session. GET: show form.
* Deploy to /www/wwwroot/soft_download/login.php
* Requires: PHP with session support, htpasswd in PATH for validation.
*/
session_start();
$HTPASSWD_FILE = '/usr/local/nginx/conf/.htpasswd';
$HTPASSWD_CMD = '/usr/bin/htpasswd'; // or 'htpasswd' if in PATH
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$user = isset($_POST['username']) ? trim((string) $_POST['username']) : '';
$pass = isset($_POST['password']) ? (string) $_POST['password'] : '';
$return_path = isset($_POST['return']) ? trim((string) $_POST['return']) : '/';
if (!strlen($return_path) || $return_path[0] !== '/') {
$return_path = '/';
}
$error = '';
if ($user !== '' && $pass !== '' && is_readable($HTPASSWD_FILE)) {
$user_esc = escapeshellarg($user);
$pass_esc = escapeshellarg($pass);
$file_esc = escapeshellarg($HTPASSWD_FILE);
$cmd = sprintf('%s -vb %s %s %s 2>/dev/null', $HTPASSWD_CMD, $file_esc, $user_esc, $pass_esc);
exec($cmd, $out, $code);
if ($code === 0) {
$_SESSION['soft_download_user'] = $user;
header('Location: ' . $return_path);
exit;
}
$error = '用户名或密码错误';
} else {
if ($user === '' && $pass === '') {
$error = '请输入用户名和密码';
} else {
$error = '用户名或密码错误';
}
}
} else {
$error = '';
$return_path = isset($_GET['return']) ? trim((string) $_GET['return']) : '/';
if (!strlen($return_path) || $return_path[0] !== '/') {
$return_path = '/';
}
}
$return_esc = htmlspecialchars($return_path, ENT_QUOTES, 'UTF-8');
$error_esc = htmlspecialchars($error ?? '', ENT_QUOTES, 'UTF-8');
?>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - 软件下载页面</title>
<style>
* { box-sizing: border-box; }
body { font-size: 18px; line-height: 1.6; font-family: sans-serif; margin: 0; padding: 2em; min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #f5f5f5; }
.login-box { background: #fff; padding: 2em 2.5em; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); max-width: 380px; width: 100%; }
h1 { font-size: 22px; margin: 0 0 1.2em 0; color: #333; border-bottom: 2px solid #ccc; padding-bottom: 0.4em; }
.form-group { margin-bottom: 1.2em; }
.form-group label { display: block; margin-bottom: 0.3em; color: #555; font-size: 16px; }
.form-group input { width: 100%; padding: 0.5em 0.8em; font-size: 16px; border: 1px solid #ccc; border-radius: 4px; }
.form-group input:focus { outline: none; border-color: #0066cc; }
.login-btn { width: 100%; padding: 0.6em 1em; font-size: 18px; background: #0066cc; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
.login-btn:hover { background: #0052a3; }
.error-msg { color: #c00; font-size: 14px; margin-top: 0.5em; }
</style>
</head>
<body>
<div class="login-box">
<h1>登录</h1>
<form method="post" action="login.php">
<input type="hidden" name="return" value="<?php echo $return_esc; ?>">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" required autocomplete="username" value="<?php echo htmlspecialchars($_POST['username'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<?php if ($error_esc !== ''): ?>
<div class="error-msg"><?php echo $error_esc; ?></div>
<?php endif; ?>
<button type="submit" class="login-btn">登录</button>
</form>
</div>
</body>
</html>
+14
View File
@@ -0,0 +1,14 @@
<?php
/**
* Destroy session and redirect to login.
* Deploy to /www/wwwroot/soft_download/logout.php
*/
session_start();
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$p = session_get_cookie_params();
setcookie(session_name(), '', time() - 3600, $p['path'], $p['domain'], $p['secure'], $p['httponly']);
}
session_destroy();
header('Location: login.php');
exit;
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""
Email sender for user credentials
Sends username and password to all users in user_data.csv
"""
import csv
import smtplib
import time
import os
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
from datetime import datetime
# ============ CONFIGURATION ============
# TODO: Fill in your NetEase email credentials before running
SENDER_EMAIL = "sofomoupc@163.com" # Your NetEase email address
SMTP_PASSWORD = "FERFPaJFJ89ckKw5" # SMTP authorization code (NOT login password)
SMTP_SERVER = "smtp.163.com" # Use smtp.126.com for @126.com, smtp.yeah.net for @yeah.net
SMTP_PORT = 465 # SSL port
# Email settings
EMAIL_SUBJECT = "“海察”软件下载网址用户名、密码"
# Use absolute path to CSV file
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
CSV_FILE = os.path.join(SCRIPT_DIR, "user_data.csv")
# Delay between emails (seconds) to avoid being flagged as spam
DELAY_BETWEEN_EMAILS = 2
# =======================================
def create_email_body(username, password):
"""Create email body with user credentials"""
return f"""用户名:{username}
密码:{password}"""
def send_email(sender, password, recipient, subject, body):
"""Send email via NetEase SMTP server"""
try:
# Create message
msg = MIMEMultipart()
msg['From'] = Header(sender)
msg['To'] = Header(recipient)
msg['Subject'] = Header(subject, 'utf-8')
# Attach body
msg.attach(MIMEText(body, 'plain', 'utf-8'))
# Connect to SMTP server with SSL
server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT)
server.login(sender, password)
# Send email
server.sendmail(sender, recipient, msg.as_string())
server.quit()
return True, None
except Exception as e:
return False, str(e)
def read_users_from_csv(filename):
"""Read user data from CSV file"""
users = []
try:
with open(filename, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
users.append({
'username': row['username'],
'passwd': row['passwd'],
'email': row['email']
})
return users
except Exception as e:
print(f"Error reading CSV file: {e}")
return None
def main():
"""Main function to send emails to all users"""
print("=" * 60)
print("User Credentials Email Sender")
print("=" * 60)
# Validate configuration
if SENDER_EMAIL == "your_email@163.com" or SMTP_PASSWORD == "your_smtp_auth_code":
print("\n❌ ERROR: Please configure your email credentials first!")
print("Edit the CONFIGURATION section at the top of this script:")
print(" - SENDER_EMAIL: Your NetEase email address")
print(" - SMTP_PASSWORD: Your SMTP authorization code")
print(" - SMTP_SERVER: smtp.163.com (or smtp.126.com, smtp.yeah.net)")
return
# Read users from CSV
print(f"\n📂 Reading users from {CSV_FILE}...")
users = read_users_from_csv(CSV_FILE)
if not users:
print("❌ Failed to read users from CSV file")
return
print(f"✓ Found {len(users)} users")
# Confirm before sending
print(f"\n📧 Ready to send {len(users)} emails")
print(f" From: {SENDER_EMAIL}")
print(f" Subject: {EMAIL_SUBJECT}")
response = input("\nProceed? (yes/no): ")
if response.lower() not in ['yes', 'y']:
print("Cancelled.")
return
# Send emails
print(f"\n🚀 Sending emails...\n")
success_count = 0
failed_users = []
for i, user in enumerate(users, 1):
username = user['username']
passwd = user['passwd']
email = user['email']
print(f"[{i}/{len(users)}] Sending to {username} ({email})...", end=" ")
# Create email body
body = create_email_body(username, passwd)
# Send email
success, error = send_email(SENDER_EMAIL, SMTP_PASSWORD, email, EMAIL_SUBJECT, body)
if success:
print("")
success_count += 1
else:
print(f"✗ Failed: {error}")
failed_users.append({'user': username, 'email': email, 'error': error})
# Delay between emails
if i < len(users):
time.sleep(DELAY_BETWEEN_EMAILS)
# Summary
print("\n" + "=" * 60)
print(f"✓ Successfully sent: {success_count}/{len(users)}")
if failed_users:
print(f"✗ Failed: {len(failed_users)}")
print("\nFailed users:")
for failed in failed_users:
print(f" - {failed['user']} ({failed['email']}): {failed['error']}")
# Save failed users to log
log_file = f"failed_emails_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
with open(log_file, 'w', encoding='utf-8') as f:
for failed in failed_users:
f.write(f"{failed['user']},{failed['email']},{failed['error']}\n")
print(f"\n📝 Failed users saved to: {log_file}")
print("=" * 60)
if __name__ == "__main__":
main()
+158
View File
@@ -0,0 +1,158 @@
import paramiko
import sys
HOST = "39.102.124.161"
USER = "root"
PASS = "Beast2026"
VHOST_CONFIG = r"""# Redirect HTTP to HTTPS
server {
listen 80;
server_name www.thereisnospoonadu.com thereisnospoonadu.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
allow all;
}
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS Server
server {
listen 443 ssl;
server_name www.thereisnospoonadu.com thereisnospoonadu.com;
ssl_certificate /etc/letsencrypt/live/www.thereisnospoonadu.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.thereisnospoonadu.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
root /www/wwwroot/soft_download;
index index.html index.htm;
charset utf-8;
client_max_body_size 500M;
client_body_timeout 300s;
send_timeout 300s;
keepalive_timeout 300s;
proxy_read_timeout 300s;
location = / {
try_files /index.html =404;
add_header Access-Control-Allow-Origin *;
}
location = /index.html {
add_header Access-Control-Allow-Origin *;
}
location = /login.html {
add_header Access-Control-Allow-Origin *;
}
location = /api/validate {
auth_basic "App Download Portal";
auth_basic_user_file /etc/tengine/.htpasswd;
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
try_files /api/validate =404;
}
location ^~ /list/ {
index nothing_will_match;
auth_basic "App Download Portal";
auth_basic_user_file /etc/tengine/.htpasswd;
alias /www/wwwroot/soft_download/;
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
autoindex_format json;
add_header Access-Control-Allow-Origin *;
}
location ~ \.(exe|md|pdf|zip|rar|7z|tar|gz|txt|doc|docx|xls|xlsx|ppt|pptx|deb)$ {
add_header Content-Disposition "attachment";
add_header X-Content-Type-Options "nosniff";
add_header Access-Control-Allow-Origin *;
}
location / {
auth_basic "App Download Portal";
auth_basic_user_file /etc/tengine/.htpasswd;
try_files $uri $uri/ =404;
add_header Access-Control-Allow-Origin *;
}
access_log /var/log/tengine/download_access.log;
error_log /var/log/tengine/download_error.log warn;
server_tokens off;
}
"""
def run_cmd(ssh, cmd, desc=""):
if desc:
print(f"\n{'='*60}")
print(f">>> {desc}")
print(f"CMD: {cmd}")
print('='*60)
stdin, stdout, stderr = ssh.exec_command(cmd, timeout=30)
out = stdout.read().decode('utf-8', errors='replace')
err = stderr.read().decode('utf-8', errors='replace')
rc = stdout.channel.recv_exit_status()
if out:
print("STDOUT:", out)
if err:
print("STDERR:", err)
print(f"EXIT CODE: {rc}")
return rc, out, err
def main():
print(f"Connecting to {HOST}...")
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(HOST, username=USER, password=PASS, timeout=15)
print("Connected.\n")
# Step 1: Write the vhost config
print("="*60)
print(">>> STEP 1: Write vhost config")
print("="*60)
run_cmd(ssh, "mkdir -p /etc/tengine/conf.d", "Ensure conf.d dir exists")
sftp = ssh.open_sftp()
with sftp.open("/etc/tengine/conf.d/thereisnospoonadu.conf", "w") as f:
f.write(VHOST_CONFIG)
sftp.close()
print("File written: /etc/tengine/conf.d/thereisnospoonadu.conf")
# Verify write
run_cmd(ssh, "cat /etc/tengine/conf.d/thereisnospoonadu.conf", "Verify written file")
# Step 2: Test the config
run_cmd(ssh, "tengine -t", "STEP 2: tengine -t (config test)")
# Step 3: Enable and start tengine
run_cmd(ssh, "systemctl enable tengine && systemctl start tengine", "STEP 3: Enable and start tengine")
# Step 4: Verify running and listening
run_cmd(ssh, "systemctl status tengine --no-pager", "STEP 4a: systemctl status tengine")
run_cmd(ssh, "ss -tlnp | grep -E '80|443'", "STEP 4b: ss -tlnp ports 80/443")
# Step 5: Quick local test
run_cmd(ssh,
'curl -sk https://localhost/ -o /dev/null -w "%{http_code}" --resolve www.thereisnospoonadu.com:443:127.0.0.1 -H "Host: www.thereisnospoonadu.com" 2>/dev/null || echo "curl test done"',
"STEP 5: Quick local curl test"
)
ssh.close()
print("\nAll steps complete.")
if __name__ == "__main__":
main()
+91
View File
@@ -0,0 +1,91 @@
# Redirect all HTTP traffic to HTTPS (forces secure connections)
server {
listen 80;
server_name www.thereisnospoonadu.com thereisnospoonadu.com;
# Certbot ACME challenge - must be served over HTTP for Let's Encrypt validation
location /.well-known/acme-challenge/ {
root /var/www/certbot;
allow all;
}
location / {
return 301 https://$host$request_uri;
}
}
# Custom log format: $remote_user is populated by auth_basic, captures real username
log_format download '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent"';
# HTTPS Server Configuration (core SSL setup + Basic Auth download portal)
server {
listen 443 ssl;
server_name www.thereisnospoonadu.com thereisnospoonadu.com;
# Path to your Certbot SSL certificate (unchanged—valid path)
ssl_certificate /etc/letsencrypt/live/www.thereisnospoonadu.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.thereisnospoonadu.com/privkey.pem;
# Modern SSL security settings (A+ grade)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
root /www/wwwroot/soft_download;
index index.html index.htm;
charset utf-8;
# Entry points: no auth required
location = / {
try_files /index.html =404;
add_header Access-Control-Allow-Origin *;
}
location = /index.html {
add_header Access-Control-Allow-Origin *;
}
location = /login.html {
add_header Access-Control-Allow-Origin *;
}
# API validation endpoint: HTTP Basic Auth
location = /api/validate {
auth_basic "App Download Portal";
auth_basic_user_file /etc/tengine/.htpasswd;
default_type text/plain;
alias /www/wwwroot/soft_download/api/validate.txt;
}
# JSON file listing: HTTP Basic Auth
location /list/ {
auth_basic "App Download Portal";
auth_basic_user_file /etc/tengine/.htpasswd;
alias /www/wwwroot/soft_download/;
index nothing_will_match;
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
autoindex_format json;
add_header Access-Control-Allow-Origin *;
}
# File downloads: no auth required (username tracked via ?u= query param)
location ~ \.(exe|md|pdf|zip|rar|7z|tar|gz|txt|doc|docx|xls|xlsx|ppt|pptx|deb)$ {
add_header Access-Control-Allow-Origin *;
}
# All other paths: serve static files (no auth)
location / {
try_files $uri $uri/ =404;
error_page 403 = /index.html;
add_header Access-Control-Allow-Origin *;
}
access_log /usr/local/nginx/logs/download_access.log download;
error_log /usr/local/nginx/logs/download_error.log warn;
# Security: Hide Nginx version
server_tokens off;
}
+28
View File
@@ -0,0 +1,28 @@
username,passwd,email
huangjing,1090,179864000@qq.com
douzhn,1111,29867370@qq.com
zhenglu,0872,18622306979@163.com
zhangshh,1081,nkuzsh@sina.com
qiangdq,1165,eacone@sina.com
zhangwd,1381,zhangwendi0820@163.com
chengqi,1481,531740421@qq.com
lizj,1231,tjlizejun@163.com
sunyl,1270,39204996@qq.com
caichl,1296,cwtjwd@126.com
lijy,0818,lijiayan222@qq.com
chzhl,1295,46113395@qq.com
qinglu,1230,luqingsmiles@foxmail.com
renzw,1653,2734310562@qq.com
songht,1215,luck_0076@163.com
wangwx,1280,wenxingone@qq.com
yangbo,1837,yangbo_6@126.com
wangyi,0870,691511321@qq.com
wangzhr,1407,396400640@qq.com
weihq,1816,hailife@163.com
yushj,1126,122758312@qq.com
zhbl,1468,sdzbxiaolang@163.com
shxj,1120,13811022092@139.com
zhangl,1665,452838289@qq.com
heyb,1019,121867346@qq.com
dingpc,1164,pchding@163.com
chengyh,1026,359957622@qq.com
1 username passwd email
2 huangjing 1090 179864000@qq.com
3 douzhn 1111 29867370@qq.com
4 zhenglu 0872 18622306979@163.com
5 zhangshh 1081 nkuzsh@sina.com
6 qiangdq 1165 eacone@sina.com
7 zhangwd 1381 zhangwendi0820@163.com
8 chengqi 1481 531740421@qq.com
9 lizj 1231 tjlizejun@163.com
10 sunyl 1270 39204996@qq.com
11 caichl 1296 cwtjwd@126.com
12 lijy 0818 lijiayan222@qq.com
13 chzhl 1295 46113395@qq.com
14 qinglu 1230 luqingsmiles@foxmail.com
15 renzw 1653 2734310562@qq.com
16 songht 1215 luck_0076@163.com
17 wangwx 1280 wenxingone@qq.com
18 yangbo 1837 yangbo_6@126.com
19 wangyi 0870 691511321@qq.com
20 wangzhr 1407 396400640@qq.com
21 weihq 1816 hailife@163.com
22 yushj 1126 122758312@qq.com
23 zhbl 1468 sdzbxiaolang@163.com
24 shxj 1120 13811022092@139.com
25 zhangl 1665 452838289@qq.com
26 heyb 1019 121867346@qq.com
27 dingpc 1164 pchding@163.com
28 chengyh 1026 359957622@qq.com
+26
View File
@@ -0,0 +1,26 @@
username,passwd,email
douzhn,1111,29867370@qq.com
zhenglu,0872,18622306979@163.com
zhangshh,1081,nkuzsh@sina.com
qiangdq,1165,eacone@sina.com
zhangwd,1381,zhangwendi0820@163.com
chengqi,1481,531740421@qq.com
lizj,1231,tjlizejun@163.com
sunyl,1270,39204996@qq.com
caichl,1296,cwtjwd@126.com
lijy,0818,lijiayan222@qq.com
chzhl,1295,46113395@qq.com
qinglu,1230,luqingsmiles@foxmail.com
renzw,1653,2734310562@qq.com
songht,1215,luck_0076@163.com
wangwx,1280,wenxingone@qq.com
yangbo,1837,yangbo_6@126.com
wangyi,0870,691511321@qq.com
weihq,1816,hailife@163.com
yushj,1126,122758312@qq.com
zhbl,1468,sdzbxiaolang@163.com
shxj,1120,13811022092@139.com
zhangl,1665,452838289@qq.com
heyb,1019,121867346@qq.com
dingpc,1164,pchding@163.com
chengyh,1026,359957622@qq.com
1 username passwd email
2 douzhn 1111 29867370@qq.com
3 zhenglu 0872 18622306979@163.com
4 zhangshh 1081 nkuzsh@sina.com
5 qiangdq 1165 eacone@sina.com
6 zhangwd 1381 zhangwendi0820@163.com
7 chengqi 1481 531740421@qq.com
8 lizj 1231 tjlizejun@163.com
9 sunyl 1270 39204996@qq.com
10 caichl 1296 cwtjwd@126.com
11 lijy 0818 lijiayan222@qq.com
12 chzhl 1295 46113395@qq.com
13 qinglu 1230 luqingsmiles@foxmail.com
14 renzw 1653 2734310562@qq.com
15 songht 1215 luck_0076@163.com
16 wangwx 1280 wenxingone@qq.com
17 yangbo 1837 yangbo_6@126.com
18 wangyi 0870 691511321@qq.com
19 weihq 1816 hailife@163.com
20 yushj 1126 122758312@qq.com
21 zhbl 1468 sdzbxiaolang@163.com
22 shxj 1120 13811022092@139.com
23 zhangl 1665 452838289@qq.com
24 heyb 1019 121867346@qq.com
25 dingpc 1164 pchding@163.com
26 chengyh 1026 359957622@qq.com
+105
View File
@@ -0,0 +1,105 @@
From: <Saved by Blink>
Snapshot-Content-Location: https://www.thereisnospoonadu.com/
Subject: =?utf-8?Q?=E8=BD=AF=E4=BB=B6=E4=B8=8B=E8=BD=BD=E9=A1=B5=E9=9D=A2?=
Date: Fri, 3 Apr 2026 14:22:37 +0800
MIME-Version: 1.0
Content-Type: multipart/related;
type="text/html";
boundary="----MultipartBoundary--2C2rn7PyrLSMK9sISEgqNX2BSrI2xA1ugtSUwK34wU----"
------MultipartBoundary--2C2rn7PyrLSMK9sISEgqNX2BSrI2xA1ugtSUwK34wU----
Content-Type: text/html
Content-ID: <frame-86EA5B71750C5F07310888D40FAFED31@mhtml.blink>
Content-Transfer-Encoding: quoted-printable
Content-Location: https://www.thereisnospoonadu.com/
<!DOCTYPE html><html lang=3D"zh"><head><meta http-equiv=3D"Content-Type" co=
ntent=3D"text/html; charset=3DUTF-8"><link rel=3D"stylesheet" type=3D"text/=
css" href=3D"cid:css-37d7331d-d968-49c2-b3bd-7d9250c044e7@mhtml.blink" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=3D1.=
0">
<title>=E8=BD=AF=E4=BB=B6=E4=B8=8B=E8=BD=BD=E9=A1=B5=E9=9D=A2</title>
</head>
<body>
<div class=3D"user-bar">
<span class=3D"logged-in">=E5=BD=93=E5=89=8D=E5=B7=B2=E7=99=BB=E5=BD=95: <s=
pan id=3D"username-display">haoxp</span></span>
<button class=3D"logout-btn" id=3D"logout-btn">=E6=B3=A8=E9=94=80</button>
</div>
<h1>=E6=96=87=E4=BB=B6=E5=88=97=E8=A1=A8 <span id=3D"path-display"></span><=
/h1>
<div class=3D"breadcrumb" id=3D"breadcrumb"></div>
<div id=3D"content">
<div class=3D"loading" id=3D"loading" style=3D"display: none;">=E6=AD=A3=E5=
=9C=A8=E5=8A=A0=E8=BD=BD...</div>
<div class=3D"file-list" id=3D"file-list" style=3D"display: block;"><a href=
=3D"https://www.thereisnospoonadu.com/%E7%89%88%E6%9C%AC%E5%8D%87%E7%BA%A7%=
E8%AF%B4%E6%98%8E%EF%BC%88%E7%94%A8%E8%AE%B0%E4%BA%8B%E6%9C%AC%E6%89%93%E5%=
BC%80%EF%BC%89.md?u=3Dhaoxp" download=3D"">=E7=89=88=E6=9C=AC=E5=8D=87=E7=
=BA=A7=E8=AF=B4=E6=98=8E=EF=BC=88=E7=94=A8=E8=AE=B0=E4=BA=8B=E6=9C=AC=E6=89=
=93=E5=BC=80=EF=BC=89.md<span class=3D"size">13.0 K 2026/4/3 14:10:24</spa=
n></a><a href=3D"https://www.thereisnospoonadu.com/%E7%BA%AA%E6%A3%80%E7%9B=
%91%E5%AF%9F%E6%95%B0%E6%8D%AE%E6%B8%85%E6%B4%97%E5%88%86%E6%9E%90%E8%BD%AF=
%E4%BB%B6_v2026.03.19.deb?u=3Dhaoxp" download=3D"">=E7=BA=AA=E6=A3=80=E7=9B=
=91=E5=AF=9F=E6=95=B0=E6=8D=AE=E6=B8=85=E6=B4=97=E5=88=86=E6=9E=90=E8=BD=AF=
=E4=BB=B6_v2026.03.19.deb<span class=3D"size">108.1 M 2026/4/3 14:10:31</s=
pan></a><a href=3D"https://www.thereisnospoonadu.com/%E7%BA%AA%E6%A3%80%E7%=
9B%91%E5%AF%9F%E6%95%B0%E6%8D%AE%E6%B8%85%E6%B4%97%E5%88%86%E6%9E%90%E8%BD%=
AF%E4%BB%B6_v2026.04.03%EF%BC%88%E5%8F%B3%E9%94%AE-%E4%BB%A5%E7%AE%A1%E7%90=
%86%E5%91%98%E8%BA%AB%E4%BB%BD%E8%BF%90%E8%A1%8C%EF%BC%89.exe?u=3Dhaoxp" do=
wnload=3D"">=E7=BA=AA=E6=A3=80=E7=9B=91=E5=AF=9F=E6=95=B0=E6=8D=AE=E6=B8=85=
=E6=B4=97=E5=88=86=E6=9E=90=E8=BD=AF=E4=BB=B6_v2026.04.03=EF=BC=88=E5=8F=B3=
=E9=94=AE-=E4=BB=A5=E7=AE=A1=E7=90=86=E5=91=98=E8=BA=AB=E4=BB=BD=E8=BF=90=
=E8=A1=8C=EF=BC=89.exe<span class=3D"size">120.1 M 2026/4/3 14:10:37</span=
></a></div>
<div class=3D"error" id=3D"error" style=3D"display:none"></div>
</div>
</body></html>
------MultipartBoundary--2C2rn7PyrLSMK9sISEgqNX2BSrI2xA1ugtSUwK34wU----
Content-Type: text/css
Content-Transfer-Encoding: quoted-printable
Content-Location: cid:css-37d7331d-d968-49c2-b3bd-7d9250c044e7@mhtml.blink
@charset "utf-8";
body { line-height: 2; font-family: sans-serif; margin: 2em; font-size: 18p=
x !important; }
h1 { margin-bottom: 0.5em; border-bottom: 2px solid rgb(204, 204, 204); pad=
ding-bottom: 0.3em; font-size: 24px !important; }
.file-list a { color: rgb(0, 102, 204); text-decoration: none; display: blo=
ck; margin: 0.3em 0px; font-size: 20px !important; }
.file-list a:hover { text-decoration: underline; }
.file-list .dir { font-weight: bold; cursor: pointer; }
.file-list .size { font-size: 16px; color: rgb(102, 102, 102); margin-left:=
1em; }
.breadcrumb { font-size: 18px; margin-bottom: 1em; }
.breadcrumb a { color: rgb(0, 102, 204); cursor: pointer; }
.loading { font-size: 20px; color: rgb(102, 102, 102); }
.error { color: rgb(204, 0, 0); font-size: 18px; }
.user-bar { margin-bottom: 1em; padding: 0.5em 0px; display: flex; align-it=
ems: center; justify-content: flex-end; gap: 1em; }
.user-bar .logged-in { color: green; }
.user-bar .logout-btn { padding: 0.3em 0.8em; font-size: 16px; cursor: poin=
ter; background: rgb(0, 102, 204); color: rgb(255, 255, 255); border: none;=
border-radius: 4px; }
.user-bar .logout-btn:hover { background: rgb(0, 82, 163); }
------MultipartBoundary--2C2rn7PyrLSMK9sISEgqNX2BSrI2xA1ugtSUwK34wU------