# -*- coding: utf-8 -*- # GOST 文件分发源 v4 最终版: 列表 + 查看 + 编辑(保存) + 上传 + 分页 + 文件夹 + serverip # 单文件, 阿里云(relly同理可部署) os, urllib.parse, html, json, time, re http.server BaseHTTPRequestHandler, ThreadingHTTPServer os _os PUB_DIR = _os.environ.get("MIRROR_PUB_DIR", "/tmp/pub") AUTH_USER = _os.environ.get("MIRROR_USER", "admin") AUTH_PASS = _os.environ.get("MIRROR_PASS", "oyh159159oiw.@") UPSTREAM = _os.environ.get("MIRROR_UPSTREAM", "") DESCRIPTIONS = { "gost-deploy.sh": ("🚀 GOST 三件套一键部署/管理", "安装 · 菜单管理 · 启停重启 · 升级 · 卸载(内嵌完整面板,自包含)"), "npm.sh": ("🟦 Nginx Proxy Manager 一键部署", "在线拉取 / 离线 docker load 双模式 · 启停 · 升级 · 备份 · 卸载"), "npm.tar": ("📦 NPM 离线镜像包", "docker load -i npm.tar 导入后配合 npm.sh 离线安装"), "panel.html": ("🖥️ GOST 面板页面 (v2)", "多API切换 · 启停开关 · 探活 · 限速 · yaml查看 · 操作日志 · 主题切换"), "gost-panel-server.py": ("⚙️ GOST 面板后端服务", "同源代理 · 状态监控 · 端口冲突检测 · 探活 · 操作日志接口"), "gost-save-server.py": ("💾 GOST 规则保存服务", "/save 持久化 · /yaml /rawyaml · 60秒自动同步守护"), "frp单版本": ("🔀 FRP 单版本脚本集", "FRP 服务端/客户端部署脚本"), "nps单版本": ("🔀 NPS 单版本脚本集", "NPS 服务端/客户端部署脚本"), } auto_desc(fp, base): : open(fp, "rb") f: head = f.read().decode("utf-", errors="replace") Exception: head = "" lines = head.split("\n")[:] title = "" subs = [] re _re ln lines: s = ln.strip().lstrip("#").strip() s s.startswith("!"): # 跳过纯分隔线 _re.fullmatch(r"[=\-*_]+", s): key ("描述:", "说明:", "简介:", "功能:"): s.startswith(key): title = s[len(key):].strip() title: s.startswith("用法") s.startswith("Usage") s.startswith("子命令"): ":" s len(s) > : subs.append(s) # 带内容的说明行 s.startswith("bash ") s.startswith("./"): subs.append(s) # 命令示例 len(s) >= title subs len(s) <= _re.search(r"[=\-]{,}", s): title = s title = title.strip("=").strip("-").strip(" ").strip("*").strip() title: title = "📄 " + base sub_text = " · ".join(subs[:]) subs "脚本文件" (title[:], sub_text[:]) check_auth(headers): a = headers.get("Authorization", "") a.startswith("Basic "): base64 : u, p = base64.b64decode(a[:]).decode().split(":", ) u == AUTH_USER p == AUTH_PASS Exception: # 登录session存储(文件持久化, 重启不丢失) secrets _secrets _SESS_FILE = "/tmp/pub_real/.sessions" : open(_SESS_FILE) _f: SESSIONS = set(l.strip() l _f l.strip()) Exception: SESSIONS = set() _save_sessions(): : open(_SESS_FILE, "w") f: f.write('\n'.join(SESSIONS)) Exception: _new_token(): _secrets.token_hex() _get_cookie(self): """从请求头解析cookie, 返回session token或None""" ck = self.headers.get("Cookie", "") part ck.split(";"): part = part.strip() part.startswith("gsess="): part[:] _is_logged_in(self): """检查请求是否已登录(session cookie 或 Basic Auth)""" tok = _get_cookie(self) tok tok SESSIONS: check_auth(self.headers) safe_path(rel): fpath = os.path.realpath(os.path.join(PUB_DIR, rel)) root, dirs, files os.walk(PUB_DIR): f files: : rp = os.path.realpath(os.path.join(root, f)) fpath == rp: fpath Exception: d dirs: : rd = os.path.realpath(os.path.join(root, d)) fpath == rd: fpath Exception: get_real_files(): result = [] root, dirs, files os.walk(PUB_DIR): rel_root = os.path.relpath(root, PUB_DIR) d dirs: rel = os.path.join(rel_root, d) rel_root != "." d rel [r[] r result]: result.append(("dir", rel, )) f files: f.startswith(".") f.endswith(".bak") f == "desc.json" : # 隐藏文件/备份不显示 rel = os.path.join(rel_root, f) rel_root != "." f fp = os.path.join(root, f) os.path.isfile(fp) os.path.islink(fp): : sz = os.path.getsize(os.path.realpath(fp)) Exception: sz = result.append(("file", rel, sz)) sorted(result, key= x: (x[] == "file", x[])) fsize_h(n): f = float(n); i = u ["B","KB","MB","GB"]: f < i == : f /= ; i += f"{f:.1f}{u}" i f"{int(n)}B" detect_ip(): urllib.request u src ("http://members.org/dyndns/getip", "http://ip.net", "https://api.ipify.org"): : v = u.urlopen(src, timeout=).read().decode().strip() v: v Exception: : socket s sk = s.socket(s.AF_INET, s.SOCK_DGRAM); sk.connect((".", )) ip = sk.getsockname()[]; sk.close(); ip Exception: "." CSS = ''':root{--bg1:#0f0c29;--bg2:#302b63;--bg3:#24243e;--pink:#ff8ac9;--purple:#b39dff;--cyan:#7af0ff;--bg:rgba(,,,.);--panel:rgba(,,,.);--panel2:rgba(,,,.);--line:#3d3868;--text:#e8e4ff;--muted:#9a94c9;--accent:#7af0ff;--green:#6ee7b7;--red:#fda4af;--amber:#fbbf24} *{box-sizing:border-box} body{font-family:"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:linear-gradient(rgba(,,,.),rgba(,,,.)),url("https://t.alcy.cc/moez") center/cover no-repeat fixed;margin:;padding:40px 24px;color:(--text);min-height:100vh;position:relative;overflow-x:hidden} .stars,.stars2,.stars3{position:fixed;inset:;pointer-events:none;z-index:} .stars{background-image:radial-gradient(5px 5px at % %,#fff %,transparent %),radial-gradient(1px 1px at % %,#ffe9f7 %,transparent %),radial-gradient(2px 2px at % %,#d0e9ff %,transparent %),radial-gradient(1px 1px at % %,#fff %,transparent %),radial-gradient(5px 5px at % %,#fff %,transparent %),radial-gradient(1px 1px at % %,#ffe9f7 %,transparent %),radial-gradient(5px 5px at % %,#fff %,transparent %),radial-gradient(1px 1px at % %,#d0e9ff %,transparent %);animation:twinkle 4s ease--out infinite alternate} .stars2{background-image:radial-gradient(1px 1px at % %,#fff %,transparent %),radial-gradient(5px 5px at % %,#e0c3ff %,transparent %),radial-gradient(1px 1px at % %,#fff %,transparent %),radial-gradient(2px 2px at % %,#fff %,transparent %),radial-gradient(1px 1px at % %,#ffe9f7 %,transparent %);animation:twinkle 6s ease--out infinite alternate-reverse;opacity:.} .stars3{background-image:radial-gradient(1px 1px at % %,#fff %,transparent %),radial-gradient(5px 5px at % %,#7af0ff55 %,transparent %),radial-gradient(1px 1px at % %,#fff %,transparent %),radial-gradient(1px 1px at % %,#ffc9e055 %,transparent %);animation:twinkle 8s ease--out infinite alternate;opacity:.} @keyframes twinkle{%{opacity:.}%{opacity:.}} .wrap,.card,.top,.uploadform,.cmd,.vpage,.vpage *{position:relative;z-index:} .wrap{max-width:900px;margin: auto} header{text-align:center;margin-bottom:30px} header h1{font-size:24px;margin: 8px} header .sub{color:(--muted);font-size:13px} header .sub code{background:(--panel2);border:1px solid (--line);padding:2px 10px;border-radius:99px;color:(--accent)} .card{background:(--panel);border:1px solid (--line);border-radius:16px;overflow:hidden;box-shadow: 8px 32px rgba(,,,.);backdrop-filter:blur(20px) saturate()} .fitem{display:flex;align-items:center;gap:16px;padding:16px 22px;border-bottom:1px solid (--line);transition:background .15s} .fitem:last-child{border-bottom:none} .fitem:hover{background:#ffffff08} .ficon{font-size:26px;width:36px;text-align:center;flex-shrink:} .finfo{flex:;min-width:} .fname{font-size:15px;font-weight:} .fname a{color:(--accent);text-decoration:none} .fname a:hover{text-decoration:underline} .fsize{color:(--muted);font-size:12px;margin-left:8px} .fdesc{color:(--muted);font-size:5px;margin-top:3px;line-height:} .fbtn{flex-shrink:;display:flex;gap:8px} .fbtn a{color:#fff;text-decoration:none;padding:7px 14px;border-radius:9px;font-size:13px;font-weight:;display:inline-block} .fbtn a:hover{filter:brightness();box-shadow: 16px rgba(,,,.)} .fbtn a.edit{background:linear-gradient(135deg,#10b981,#)} .fbtn a.view{background:linear-gradient(135deg,#38bdf8,#0284c7)} .fbtn a.dl{background:linear-gradient(135deg,#0ea5e9,#2563eb)} .fbtn a.copy{background:linear-gradient(135deg,#8b5cf6,#6d28d9)} .fbtn a.bash{background:linear-gradient(135deg,#,#)} .fbtn a.dir{background:linear-gradient(135deg,#6366f1,#4338ca)} .cmd{background:(--panel2);border:1px solid (--line);border-radius:10px;padding:14px 16px;margin-top:26px;font-size:5px;line-height:;color:(--muted)} .cmd code{background:(--bg);border:1px solid (--line);border-radius:6px;padding:3px 10px;color:#7dd3fc;font-size:12px;user-select:all} footer{text-align:center;color:#64748b;font-size:12px;margin-top:26px} .badge{display:inline-block;background:#10b98122;border:1px solid #10b98155;color:(--green);border-radius:99px;padding:2px 12px;font-size:12px;margin-left:8px} input#search{background:(--panel2);border:1px solid (--line);color:(--text);border-radius:9px;padding:8px 14px;font-size:13px;width:%;margin-bottom:16px} .uploadform input[type=file]{background:(--panel2);border:1px solid (--line);color:(--text);border-radius:8px;padding:8px 12px;font-size:13px} .uploadform button{background:linear-gradient(135deg,#10b981,#);color:#fff;border:none;border-radius:8px;padding:8px 18px;font-size:13px;font-weight:;cursor:pointer} .uploadform{padding: 22px 18px} .uploadform b{font-size:14px;color:(--text)} #pager{display:flex;gap:8px;justify-content:center;padding:14px;align-items:center;font-size:13px;color:var(--muted)} #pager button{background:var(--panel2);border:1px solid var(--line);color:var(--text);border-radius:7px;padding:5px 12px;cursor:pointer} .msg{display:none;padding:8px 16px;border-radius:8px;font-size:13px;position:fixed;top:16px;right:16px;z-index:} .msg.ok{background:#10b98122;border:1px solid #10b98155;color:#34d399;display:block} .msg.bad{background:#f8717122;border:1px solid #f8717155;color:#f87171;display:block} .vpage textarea{width:%;box-sizing:border-box;min-height:66vh;background:rgba(,,,.);border:1px solid (--line);border-radius:14px;padding:18px;font-size:13px;line-height:;color:#d5f0ff;font-family:Consolas,"JetBrains Mono",monospace;white-space:pre;overflow:auto;tab-size:;outline:none;resize:vertical;backdrop-filter:blur(18px) saturate();box-shadow: 8px 32px rgba(,,,.)} .vpage textarea:focus{border-color:(--purple);box-shadow: 3px rgba(,,,.)} .vpage .top{margin-bottom:14px;display:flex;align-items:center;gap:12px;flex-wrap:wrap} .vpage .top b{font-size:15px} .vpage .top a,.vpage .top button{color:#38bdf8;text-decoration:none;cursor:pointer;background:none;border:1px solid #;font-size:13px;padding:6px 14px;border-radius:8px} .vpage .top button.save{background:linear-gradient(135deg,#10b981,#);color:#fff;border:none;font-weight:} .vpage .top button.save:hover{filter:brightness()} /* 语法高亮 */ .kw{color:#7af0ff;font-weight:} .str{color:#ff8ac9} .cmt{color:#6b6699;font-style:italic} .num{color:#fbbf77} .fn{color:#ffd479} .op{color:#b39dff} .shellcmd{color:#6ee7b7} ''' icon_for(base): "gost-deploy" base: "🚀" base == "npm.sh": "🟦" base == "panel.html": "🖥" "panel-server" base: "⚙" "save-server" base: "💾" "📄" build_list_page(): items = [] typ, rel, sz get_real_files(): fn = html.escape(rel) base = os.path.basename(rel) ic = icon_for(base) typ == "dir": desc, sub = DESCRIPTIONS.get(rel, ("📁 文件夹", "子目录")) items.append('<div ="fitem">' '<div ="ficon">' + ic + '</div>' '<div ="finfo"><div ="fname"><a href="' + rel + '/">' + fn + '</a></div>' '<div ="fdesc"><b style="color:(--text)">' + desc + '</b><br>' + sub + '</div></div>' '<div ="fbtn"><a ="dir" href="' + rel + '/">打开</a></div></div>') rel DESCRIPTIONS: desc, sub = DESCRIPTIONS[rel] : desc, sub = auto_desc(safe_path(rel) (PUB_DIR + "/" + rel), base) : json _J2 open("/tmp/pub_real/desc.json") _f: _cd = _J2.load(_f).get(rel) _cd _cd[]: desc = _cd[] _cd[]: sub = _cd[] Exception: qrel = urllib.parse.quote(rel) szs = fsize_h(sz) items.append('<div ="fitem">' '<div ="ficon">' + ic + '</div>' '<div ="finfo"><div ="fname"><a href="/edit/' + qrel + '">' + fn + '</a><span ="fsize">' + szs + '</span></div>' '<div ="fdesc" style="cursor:pointer" onclick="editDesc(\'' + rel + '\')" title="点击修改描述"><b style="color:(--text)">' + desc + '</b> <span style="font-size:11px;opacity:">✏️</span><br>' + sub + '</div></div>' '<div ="fbtn">' '<a ="edit" href="/edit/' + qrel + '">📝 编辑</a> ' '<a ="view" href="/view/' + qrel + '">👁 查看</a> ' '<a ="dl" href="/dl/' + qrel + '" download>⬇ 下载</a> ' '<a ="rn" style="background:linear-gradient(135deg,#f59e0b,#d97706)" href="javascript:void()" onclick="renameItem(\'' + rel + '\')">✏️ 重命名</a> ' '<a ="del" style="background:linear-gradient(135deg,#f87171,#dc2626)" href="javascript:void()" onclick="delItem(\'' + rel + '\')">🗑 删除</a>' '</div></div>') CSS_TOP = CSS + """ input#search{background:(--panel2);border:1px solid (--line);color:(--text);border-radius:9px;padding:8px 14px;font-size:13px;width:%;margin-bottom:16px} .uploadform input[type=file]{background:(--panel2);border:1px solid (--line);color:(--text);border-radius:8px;padding:8px 12px;font-size:13px} .uploadform button{background:linear-gradient(135deg,#10b981,#);color:#fff;border:none;border-radius:8px;padding:8px 18px;font-size:13px;font-weight:;cursor:pointer} .uploadform{padding: 22px 18px} .uploadform b{font-size:14px;color:(--text)} #pager{display:flex;gap:8px;justify-content:center;padding:14px;align-items:center;font-size:13px;color:var(--muted)} #pager button{background:var(--panel2);border:1px solid var(--line);color:var(--text);border-radius:7px;padding:5px 12px;cursor:pointer} .fbtn a.rn{background:linear-gradient(135deg,#f59e0b,#d97706)} .fbtn a.del{background:linear-gradient(135deg,#f87171,#dc2626)}""" page = "<!doctype html>\n<html lang=\"zh\">\n<head>\n" page += "<meta charset=\"utf-\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=\">\n" page += "<title>GOST 分发源</title>\n<style>" + CSS_TOP + "</style>\n</head>\n<body>\n<div =\"stars\"></div><div =\"stars2\"></div><div =\"stars3\"></div>\n" page += "<div =\"wrap\">\n" page += " <header>\n" page += " <h1>\U0001F4E6 GOST / NPM 分发源 <span =\"badge\">\u25cf 在线</span></h1>\n" page += " <div =\"sub\">所有文件实时同步最新版 · 使用: <code id=\"baseurl\">bash <(curl -Ls http://当前地址/文件名)</code></div>\n" page += " </header>\n" page += " <input id=\"search\" placeholder=\"🔍 搜索…\" oninput=\"filterList()\">\n" page += " <div =\"card\" id=\"list\">\n" page += "\n".join(items) page += "\n </div>\n" page += """ <div ="card" style="margin-top:16px"> <div style="padding:16px 22px "><b style="font-size:14px;color:(--text)">📤 上传文件到分发源</b></div> <div ="uploadform"> <form action="/upload" method="post" enctype="multipart/form-data" style="margin-top:10px;display:flex;gap:10px;align-items:center;flex-wrap:wrap"> <input type="file" name="file" required> <button type="submit">上传</button> </form> <div style="font-size:11px;color:(--muted);margin-top:6px">上传后自动出现在列表 · 单文件最大 2GB</div> </div> </div> <div ="cmd"> <b style="color:(--text)">快速使用:</b><br> GOST 三件套: <code ="dynurl">bash <(curl -Ls http://当前地址/gost-deploy.sh)</code><br> NPM 面板: <code ="dynurl">bash <(curl -Ls http://当前地址/npm.sh)</code><br> 已装机器升级: <code>bash gost-deploy.sh update</code> (源默认指向本服务) </div> <footer>GOST Files Distribution · 阿里云 · 实时同步</footer> </div> <script> PER_PAGE=; CUR_PAGE=; SERVER_IP=''; fetch('/serverip').then((r){ r.text()}).then((ip){SERVER_IP=ip.trim()}).catch((){}); filterList(){ q=(document.getElementById('search')?document.getElementById('search').value:'').toLowerCase(); items=Array.(document.querySelectorAll('.fitem')); matched=items.filter((el){ el.textContent.toLowerCase().indexOf(q)>=}); totalPages=Math.max(,Math.ceil(matched.length/PER_PAGE)); (CUR_PAGE>totalPages)CUR_PAGE=totalPages; items.forEach((el){el.style.display='none'}); start=(CUR_PAGE-)*PER_PAGE; matched.slice(start,start+PER_PAGE).forEach((el){el.style.display='flex'}); pc=document.getElementById('pager'); (!pc){ pc=document.createElement('div'); pc.id='pager'; document.getElementById('list').after(pc); } (matched.length<=PER_PAGE){pc.innerHTML='共 '+matched.length+' 条';} pc.innerHTML='<button onclick="pageGo(-)">‹ 上一页</button> 第 '+CUR_PAGE+' / '+totalPages+' 页 (共'+matched.length+'条) <button onclick="pageGo()">下一页 ›</button>'; } pageGo(d){CUR_PAGE+=d;filterList()} ((){ host = location.host; // 含端口 base = 'http://' + host; b = document.getElementById('baseurl'); (b) b.textContent = 'bash <(curl -Ls ' + base + '/文件名)'; document.querySelectorAll('.dynurl').forEach((el){ el.textContent = el.textContent.replace('http://当前地址', base); }); })(); renameItem(rel){ m=showModal({title:'✏️ 重命名',sub:rel,fields:[ {label:'新名称',value:rel,placeholder:'输入新名称'} ]}); bindModalSave(m,{onSave:(vals,done){ nn=(vals[]||'').trim(); (!nn||nn===rel){ done(); ; } xhr= XMLHttpRequest(); xhr.open('POST','/rename'); xhr.setRequestHeader('Content-Type','application/json'); a=''; { ( AUTH!=='undefined'&&AUTH) a=AUTH; }catch(e){} (a) xhr.setRequestHeader('Authorization',a); xhr.onload=(){ (xhr.status===){ done(); location.reload(); } { alert('失败: '+xhr.responseText.slice(,)); } }; xhr.send(JSON.stringify({old: rel, : nn})); }}); } delItem(rel){ (!confirm('确定删除 ' + rel + ' ?')) ; xhr = XMLHttpRequest(); xhr.open('POST', '/delete'); xhr.onload = () { (xhr.status === ) location.reload(); }; xhr.send(JSON.stringify({path: rel})); } /* 美化编辑模态 */ showModal(cfg){ mask=document.getElementById('emask'); (!mask){ css=document.createElement('style'); css.textContent='#emask{position:fixed;inset:;background:rgba(,,,.);backdrop-filter:blur(6px);z-index:;display:none;align-items:center;justify-content:center}'+ '#emodal{background:#1c1839;border:1px solid #4a4378;border-radius:14px;width:420px;max-width:92vw;padding:26px 28px;box-shadow: 16px 48px rgba(,,,.)}'+ '#emodal h3{margin: 6px;font-size:16px}'+ '#emodal .esub{color:(--muted);font-size:5px;margin-bottom:16px}'+ '#emodal label{display:block;font-size:5px;color:(--muted);margin:12px 6px}'+ '#emodal input{width:%;box-sizing:border-box;background:#;border:1px solid #4a4378;color:(--text);border-radius:9px;padding:10px 14px;font-size:14px;outline:none}'+ '#emodal input:focus{border-color:(--accent)}'+ '#emodal .row{display:flex;gap:10px;justify-content:flex-end;margin-top:20px}'+ '#emodal .btn{padding:9px 20px;border-radius:9px;font-size:13px;font-weight:;cursor:pointer;border:none}'+ '#emodal .bc{background:(--panel2);border:1px solid (--line);color:(--text)}'+ '#emodal .bs{background:linear-gradient(135deg,#10b981,#);color:#fff}'; document.head.appendChild(css); mask=document.createElement('div'); mask.id='emask'; mask.innerHTML='<div id="emodal"><h3 id="etitle"></h3><div ="esub" id="esub"></div><div id="efields"></div><div ="row"><button ="btn bc" id="ecancel">取消</button><button ="btn bs" id="esave">保存</button></div></div>'; document.body.appendChild(mask); mask.addEventListener('click',(e){ (e.target===mask) mask.style.display='none'; }); } document.getElementById('etitle').textContent=cfg.title; document.getElementById('esub').textContent=cfg.sub||''; f=document.getElementById('efields'); f.innerHTML=''; inputs=[]; cfg.fields.forEach((fd){ l=document.createElement('label'); l.textContent=fd.label; i=document.createElement('input'); i.value=fd.value||''; i.placeholder=fd.placeholder||''; f.appendChild(l); f.appendChild(i); inputs.push(i); }); document.getElementById('esave').onclick=(){ vals=inputs.map((i){ i.value}); cfg.onSave(vals, (){ mask.style.display='none'; }); }; document.getElementById('ecancel').onclick=(){ mask.style.display='none'; }; mask.style.display='flex'; setTimeout((){ (inputs[]) inputs[].focus(); },); {inputs:inputs, close:(){mask.style.display='none';}}; } bindModalSave(m, cfg){ document.getElementById('esave').onclick=(){ cfg.onSave(m.inputs.map((i){ i.value}), (){ m.close(); }); }; document.getElementById('ecancel').onclick=(){ m.close(); }; } editDesc(rel){ ga=''; { ( AUTH!=='undefined'&&AUTH) ga=AUTH; }catch(e){} gh={}; (ga) gh['Authorization']=ga; fetch('/filedesc/'+encodeURIComponent(rel),{headers:gh}).then(r=>r.json()).then(d=>{ m=showModal({title:'✏️ 编辑描述',sub:rel,fields:[ {label:'标题',value:d.title||'',placeholder:'显示在文件名下方的粗体行'}, {label:'副说明',value:d.sub||'',placeholder:'灰色说明行'} ]}); bindModalSave(m,{onSave:(vals,done){ xhr= XMLHttpRequest(); xhr.open('POST','/filedesc/'+encodeURIComponent(rel)); xhr.setRequestHeader('Content-Type','application/json'); (ga) xhr.setRequestHeader('Authorization',ga); xhr.onload=(){ (xhr.status===){ done(); location.reload(); } { alert('保存失败(需要登录)'); } }; xhr.send(JSON.stringify({title:vals[],sub:vals[]})); }}); }); } document.addEventListener('DOMContentLoaded',(){filterList()}); </script> </body> </html>""" page.encode("utf-") LOGIN_PAGE = """<!doctype html> <html lang="zh"> <head> <meta charset="utf-"> <meta name="viewport" content="width=device-width, initial-scale="> <title>登录 - GOST 分发源</title> <style> :root{--bg:#0f172a;--panel:#1e293b;--panel2:#;--line:#;--text:#e2e8f0;--muted:#94a3b8;--accent:#38bdf8;--green:#34d399;--red:#f87171} *{box-sizing:border-box} body{font-family:"Segoe UI",system-ui,sans-serif;background:linear-gradient(rgba(,,,.),rgba(,,,.)),url("https://t.alcy.cc/moez") center/cover no-repeat fixed;margin:;padding:40px 24px;color:(--text);display:flex;align-items:center;justify-content:center;min-height:90vh} .login-card{background:(--panel);border:1px solid (--line);border-radius:16px;padding:36px 32px;width:360px;max-width:92vw;box-shadow: 12px 40px rgba(,,,.)} h1{font-size:20px;text-align:center;margin: 6px} .sub{color:(--muted);font-size:5px;text-align:center;margin-bottom:24px} label{display:block;font-size:5px;color:(--muted);margin:12px 6px} input{width:%;box-sizing:border-box;background:(--panel2);border:1px solid (--line);color:(--text);border-radius:9px;padding:10px 14px;font-size:14px;outline:none} input:focus{border-color:(--accent)} button{width:%;margin-top:20px;background:linear-gradient(135deg,#0ea5e9,#2563eb);color:#fff;border:none;border-radius:9px;padding:11px;font-size:14px;font-weight:;cursor:pointer} button:hover{filter:brightness()} .err{display:none;background:#f8717122;border:1px solid #f8717155;color:(--red);border-radius:8px;padding:8px 12px;font-size:5px;margin-top:14px;text-align:center} .back{text-align:center;margin-top:16px} .back a{color:(--accent);text-decoration:none;font-size:5px} </style> </head> <body> <div ="login-card"> <h1>🔐 GOST 分发源</h1> <div ="sub">登录后可使用编辑 · 重命名 · 删除</div> <form id="f" onsubmit=" doLogin()"> <label>用户名</label> <input id="username" autocomplete="username" required> <label>密码</label> <input id="password" type="password" autocomplete="current-password" required> <button type="submit">登 录</button> <div ="err" id="err">用户名或密码错误</div> </form> <div ="back"><a href="/">← 返回列表</a></div> </div> <script> doLogin(){ u=document.getElementById('username').value; p=document.getElementById('password').value; xhr= XMLHttpRequest(); xhr.open('POST','/login'); xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); xhr.onload=(){ (xhr.status===){ location.href='/'; } { document.getElementById('err').style.display='block'; } }; xhr.send('username='+encodeURIComponent(u)+'&password='+encodeURIComponent(p)); false; } </script> </body> </html> """ _hl(code_text): """轻量语法高亮: python/bash/js 通用""" re _re esc = html.escape(code_text) # . 注释整行先提取为占位符(避免内部被其他规则二次处理) comments = [] _stash_cmt(m): comments.append('<span ="cmt">' + m.group() + '</span>') m.group() + "\x00CMT" + str(len(comments)-) + "\x00" esc = _re.sub(r'(^|\n)(#[^\n]*)', _stash_cmt, esc) # . 关键字 kws = ("||||||||||||||||||||||||||||||||||") esc = _re.sub(r'\b(' + kws + r')\b', '<span ="kw">\</span>', esc) # . 数字 esc = _re.sub(r'\b(\d+\.?\d*)\b', '<span ="num">\</span>', esc) # . 还原注释占位 esc = _re.sub(r"\x00CMT(\d+)\x00", m: comments[int(m.group())], esc) esc H(BaseHTTPRequestHandler): do_GET(self): path = urllib.parse.unquote(self.path) path.startswith("/filedesc/"): fn = path[len("/filedesc/"):] DJ = "/tmp/pub_real/desc.json" json _J self.command == "GET": : open(DJ) f: data = _J.load(f).get(fn, ["",""]) Exception: data = ["",""] body = _J.dumps({"title": data[], "sub": data[]}).encode() self.send_response() self.send_header("Content-Type","application/json") self.send_header("Content-Length",str(len(body))) self.end_headers(); self.wfile.write(body); self.command == "POST": n = int(self.headers.get("Content-Length","") ) : d = _J.loads(self.rfile.read(n)) : open(DJ) f: all_d = _J.load(f) Exception: all_d = {} all_d[fn] = [str(d.get("title",""))[:], str(d.get("sub",""))[:]] open(DJ,"w") f: f.write(_J.dumps(all_d, ensure_ascii=, indent=)) r = b'{"ok":true}' self.send_response() Exception e: r = str(e).encode() self.send_response() self.send_header("Content-Type","application/json") self.send_header("Content-Length",str(len(r))) self.end_headers(); self.wfile.write(r); self.send_response(); self.end_headers(); path == "/serverip": ip = detect_ip() self.send_response() self.send_header("Content-Type","text/plain") self.send_header("Access-Control-Allow-Origin","*") self.end_headers(); self.wfile.write(ip.encode()); # 编辑页 path == "/login": # 已登录则跳首页 tok = _get_cookie(self) tok tok SESSIONS: self.send_response(); self.send_header("Location","/"); self.end_headers(); page = LOGIN_PAGE body = page.encode("utf-") self.send_response() self.send_header("Content-Type","text/html; charset=utf-") self.send_header("Cache-Control","no-cache") self.send_header("Content-Length",str(len(body))) self.end_headers(); self.wfile.write(body); path.startswith("/edit/"): fn = path[len("/edit/"):] fpath = safe_path(fn) fpath os.path.isfile(fpath): self.send_response(); self.end_headers(); _is_logged_in(self): self.send_response() self.send_header("Location","/login") self.end_headers(); open(fpath, "rb") f: content = f.read() : text = content.decode("utf-") Exception: text = content.decode("utf-", errors="replace") esc = html.escape(text, quote=) qfn = urllib.parse.quote(fn) page = (f'''<!doctype html><html lang="zh"><head><meta charset="utf-"> <title>编辑 {html.escape(fn)}</title><style>{CSS} .vpage textarea{{width:%;box-sizing:border-box;min-height:70vh;background:#1e293b;border:1px solid #;border-radius:10px;padding:16px;font-size:13px;line-height:;color:#7dd3fc;font-family:Consolas,monospace;white-space:pre;overflow:auto;tab-size:;outline:none;resize:vertical}} .vpage textarea:focus{{border-color:#38bdf8}} .vpage .top{{margin-bottom:14px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}} .vpage .top b{{font-size:15px}} .vpage .top a,.vpage .top button{{color:#38bdf8;text-decoration:none;cursor:pointer;background:none;border:1px solid #;font-size:13px;padding:6px 14px;border-radius:8px}} .vpage .top button.save{{background:linear-gradient(135deg,#10b981,#);color:#fff;border:none;font-weight:}} .msg{{display:none;padding:8px 16px;border-radius:8px;font-size:13px;position:fixed;top:16px;right:16px;z-index:}} .msg.ok{{background:#10b98122;border:1px solid #10b98155;color:#34d399;display:block}} .msg.bad{{background:#f8717122;border:1px solid #f8717155;color:#f87171;display:block}} </style></head><body ="vpage"> <div ="stars"></div><div ="stars2"></div><div ="stars3"></div> <div ="top"> <b>📝 编辑: {html.escape(fn)}</b> <button ="save" onclick="saveFile()">💾 保存 (Ctrl+S)</button> <a href="/view/{qfn}">👁 只读查看</a> <a href="/dl/{qfn}" download>⬇ 下载</a> <a href="/">← 返回列表</a> </div> <textarea id="editor" spellcheck="false">{esc}</textarea> <div ="msg" id="msg"></div> <script> FN={json.dumps(fn)}; saveFile(){{ t=document.getElementById('editor').value; xhr= XMLHttpRequest(); xhr.open('POST','/saveedit/'+encodeURIComponent(FN)); xhr.setRequestHeader('Content-Type','text/plain; charset=utf-'); xhr.setRequestHeader('Authorization','Basic '+btoa('{AUTH_USER}:{AUTH_PASS}')); xhr.onload=()=>{{ m=document.getElementById('msg'); (xhr.status===){{m.className='msg ok';m.textContent='✓ 已保存 ('+FN+')';}} {{m.className='msg bad';m.textContent='保存失败: '+xhr.status;}} m.style.display='block'; setTimeout(()=>m.style.display='none',); }}; xhr.onerror=()=>{{m.className='msg bad';m.textContent='网络错误';m.style.display='block';}}; xhr.send(t); }} document.addEventListener('keydown',e=>{{ ((e.ctrlKey||e.metaKey)&&e.key==='s'){{e.preventDefault();saveFile()}} }}); </script></body></html>''').encode("utf-") self.send_response() self.send_header("Content-Type","text/html; charset=utf-") self.send_header("Content-Length",str(len(page))) self.end_headers(); self.wfile.write(page); # 只读查看页 path.startswith("/view/"): fn = path[len("/view/"):] fpath = safe_path(fn) fpath os.path.isfile(fpath): self.send_response(); self.end_headers(); open(fpath, "rb") f: content = f.read() : text = content.decode("utf-") Exception: text = content.decode("utf-", errors="replace") esc = html.escape(text) qfn = urllib.parse.quote(fn) page = (f'''<!doctype html><html lang="zh"><head><meta charset="utf-"> <title>View {html.escape(fn)}</title><style>{CSS} .vpage pre{{background:rgba(,,,.);border:1px solid rgba(,,,.);border-radius:14px;padding:20px;overflow:auto;max-height:72vh;font-size:13px;line-height:;white-space:pre-wrap;word-:-all;margin:;color:#d5f0ff;font-family:Consolas,monospace;backdrop-filter:blur(18px) saturate();box-shadow: 8px 32px rgba(,,,.)}} .vpage .top{{margin-bottom:14px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}} .vpage .top b{{font-size:15px}} .vpage .top a{{color:#38bdf8;text-decoration:none;background:none;border:1px solid #;padding:6px 14px;border-radius:8px}} </style></head><body ="vpage"> <div ="stars"></div><div ="stars2"></div><div ="stars3"></div> <div ="top"><b>📄 {html.escape(fn)}</b> <a href="/edit/{qfn}">📝 编辑</a> <a href="/dl/{qfn}" download>⬇ 下载</a> <a href="/">← 返回列表</a></div> <pre>{_hl(text)}</pre></body></html>''').encode("utf-") self.send_response() self.send_header("Content-Type","text/html; charset=utf-") self.send_header("Content-Length",str(len(page))) self.end_headers(); self.wfile.write(page); # 目录页 path == "/" path == "/index.html": body = build_list_page() self.send_response() self.send_header("Content-Type","text/html; charset=utf-") self.send_header("Content-Length",str(len(body))) self.end_headers(); self.wfile.write(body); # 下载 rel = path.lstrip("/") rel.startswith("dl/"): rel = rel[:] fpath = safe_path(rel) fpath: self.send_response() self.send_header("Content-Type","text/plain; charset=utf-") self.end_headers(); self.wfile.write("".encode()); os.path.isdir(fpath): self.send_response(); self.send_header("Location","/"); self.end_headers(); os.path.isfile(fpath): open(fpath,"rb") f: body=f.read() ctype = "text/html; charset=utf-" fpath.endswith(".html") "text/plain; charset=utf-" self.send_response() self.send_header("Content-Type",ctype) self.send_header("Content-Length",str(len(body))) self.end_headers(); self.wfile.write(body); self.send_response(); self.end_headers(); self.wfile.write("".encode()) do_POST(self): path = urllib.parse.unquote(self.path) path == "/login": n = int(self.headers.get("Content-Length","") ) body = self.rfile.read(n).decode("utf-", errors="replace") urllib.parse UP params = UP.parse_qs(body) u = params.get("username",[""])[] pw = params.get("password",[""])[] u == AUTH_USER pw == AUTH_PASS: tok = _new_token() SESSIONS.add(tok) _save_sessions() body2 = b"OK" self.send_response() self.send_header("Set-Cookie","gsess=" + tok + "; Path=/; HttpOnly; SameSite=Lax; Max-Age=") self.send_header("Content-Type","text/plain; charset=utf-") self.send_header("Content-Length",str(len(body2))) self.end_headers(); self.wfile.write(body2) : body2 = b"BAD" self.send_response() self.send_header("Content-Type","text/plain; charset=utf-") self.send_header("Content-Length",str(len(body2))) self.end_headers(); self.wfile.write(body2) path.startswith("/filedesc/"): fn = path[len("/filedesc/"):] _is_logged_in(self): self.send_response(); self.end_headers(); DJ = "/tmp/pub_real/desc.json" json _J n = int(self.headers.get("Content-Length","") ) : d = _J.loads(self.rfile.read(n)) : open(DJ) f: all_d = _J.load(f) Exception: all_d = {} all_d[fn] = [str(d.get("title",""))[:], str(d.get("sub",""))[:]] open(DJ,"w") f: f.write(_J.dumps(all_d, ensure_ascii=, indent=)) r = b'{"ok":true}' self.send_response() Exception e: r = str(e).encode() self.send_response() self.send_header("Content-Type","application/json") self.send_header("Content-Length",str(len(r))) self.end_headers(); self.wfile.write(r); path.startswith("/saveedit/"): fn = path[len("/saveedit/"):] fpath = safe_path(fn) fpath os.path.isfile(fpath): self.send_response(); self.end_headers(); _is_logged_in(self): self.send_response(); self.end_headers(); n = int(self.headers.get("Content-Length","") ) new_content = self.rfile.read(n) open(fpath, "wb") f: f.write(new_content) self.send_response() self.send_header("Content-Type","text/plain; charset=utf-") self.end_headers() self.wfile.write(f"OK, saved {fn} ({len(new_content)}B)".encode()); path == "/rename": _is_logged_in(self): self.send_response(); self.end_headers(); n = int(self.headers.get("Content-Length","") ) : json J data = J.loads(self.rfile.read(n)) old_rel = data.get("old","") new_rel = os.path.basename(data.get("","").strip()) old_link = os.path.join(PUB_DIR, old_rel.strip("/")) old_fp = safe_path(old_rel) old_fp os.path.islink(old_link): self.send_response(); self.end_headers(); new_link = os.path.join(PUB_DIR, new_rel) os.path.exists(new_link) os.path.islink(new_link): self.send_response(); self.end_headers(); self.wfile.write(b"exists"); os.path.islink(old_link): # 软链接: 只重命名链接本身 os.rename(old_link, new_link) : os.rename(old_fp, new_link) self.send_response(); self.end_headers(); self.wfile.write(b"OK") Exception e: self.send_response(); self.end_headers(); self.wfile.write(str(e).encode()) path == "/delete": _is_logged_in(self): self.send_response(); self.end_headers(); n = int(self.headers.get("Content-Length","") ) : json J shutil data = J.loads(self.rfile.read(n)) rel = data.get("path","").strip("/") link_fp = os.path.join(PUB_DIR, rel) fp = safe_path(rel) fp os.path.islink(link_fp): self.send_response(); self.end_headers(); os.path.islink(link_fp): # 软链接: 只删链接本身, 不动源文件 os.unlink(link_fp) os.path.isdir(fp): shutil.rmtree(fp) : os.remove(fp) self.send_response(); self.end_headers(); self.wfile.write(b"OK") Exception e: self.send_response(); self.end_headers(); self.wfile.write(str(e).encode()) path == "/upload": ct = self.headers.get("Content-Type","") n = int(self.headers.get("Content-Length","") ) n > ***: self.send_response(); self.end_headers(); body = self.rfile.read(n) m = re.search(r'boundary="?([^";\s]+)"?', ct) m: self.send_response(); self.end_headers(); self.wfile.write(b"no boundary"); boundary = ("--"+m.group()).encode() parts = body.split(boundary) saved = [] part parts: b"filename=" part: fnm = re.search(r'filename="([^"]+)"', part.decode("utf-",errors="replace")) fnm: fname = os.path.basename(fnm.group()) fname = re.sub(r'[^\w.\-()]','_',fname) fname: didx = part.find(b"\r\n\r\n") didx < : data = part[didx+:] data.endswith(b"\r\n"): data = data[:-] dest = os.path.join(PUB_DIR, fname) open(dest,"wb") f: f.write(data) saved.append((fname,len(data))) saved: names = ", ".join(f"{html.escape(s[])}({s[]//}KB)" s saved) page = f'''<!doctype html><html><head><meta charset="utf-"><meta http-equiv="refresh" content=";url=/"><style>body{{font-family:sans-serif;background:#0f172a;color:#e2e8f0;display:flex;align-items:center;justify-content:center;height:100vh;margin:}}div{{text-align:center;background:#1e293b;padding:40px;border-radius:14px;border:1px solid #}}a{{color:#38bdf8}}h2{{color:#34d399}}</style></head><body><div><h2>✅ 上传成功</h2><p>{names}</p><p><a href="/">← 返回列表 (2秒后自动跳转)</a></p></div></body></html>''' : page = '''<!doctype html><html><head><meta charset="utf-"><meta http-equiv="refresh" content=";url=/"><style>body{font-family:sans-serif;background:#0f172a;color:#e2e8f0;display:flex;align-items:center;justify-content:center;height:100vh;margin:}div{text-align:center;background:#1e293b;padding:40px;border-radius:14px;border:1px solid #}a{color:#38bdf8}</style></head><body><div><h2>❌ 上传失败</h2><p><a href="/">返回</a></p></div></body></html>''' body = page.encode("utf-") self.send_response() self.send_header("Content-Type","text/html; charset=utf-") self.send_header("Content-Length",str(len(body))) self.end_headers(); self.wfile.write(body); self.send_response(); self.end_headers() log_message(self,*a): ThreadingHTTPServer((".",),H).serve_forever()