c
This commit is contained in:
@@ -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).
|
||||
@@ -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 "—"
|
||||
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 "—"
|
||||
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 — thereisnospoonadu.com</h1>
|
||||
<span class="refresh-note">Last updated: {now} | 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()
|
||||
@@ -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;
|
||||
@@ -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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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------
|
||||
Reference in New Issue
Block a user