yzwwxm commited on
Commit
890931a
·
verified ·
1 Parent(s): c4a7303

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +7 -2
  2. auth.py +108 -0
  3. entrypoint.sh +20 -0
Dockerfile CHANGED
@@ -1,3 +1,8 @@
1
  FROM agentscope/copaw:latest
2
- ENV COPAW_PORT=7860
3
- EXPOSE 7860
 
 
 
 
 
 
1
  FROM agentscope/copaw:latest
2
+ RUN pip install --no-cache-dir python-multipart
3
+ COPY auth.py /app/auth.py
4
+ COPY entrypoint.sh /entrypoint.sh
5
+ RUN chmod +x /entrypoint.sh
6
+ ENV COPAW_PASSWORD="" COPAW_SESSION_SECRET="" COPAW_SESSION_MAX_AGE="86400"
7
+ EXPOSE 8088
8
+ CMD ["/entrypoint.sh"]
auth.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, secrets, hashlib, hmac, time
2
+ from fastapi import FastAPI, Form, Request
3
+ from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
4
+ from starlette.middleware.base import BaseHTTPMiddleware
5
+
6
+ PWD = os.environ.get("COPAW_PASSWORD", "")
7
+ SEC = os.environ.get("COPAW_SESSION_SECRET", "") or (secrets.token_hex(32) if PWD else "")
8
+ MAX = int(os.environ.get("COPAW_SESSION_MAX_AGE", "86400"))
9
+ ON = bool(PWD)
10
+
11
+ SKIP = {"/auth/login", "/auth/logout", "/health"}
12
+ EXTS = (".js", ".css", ".png", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".jpg", ".gif", ".wasm")
13
+
14
+
15
+ def _mk():
16
+ t = str(int(time.time()))
17
+ k = secrets.token_urlsafe(32)
18
+ s = hmac.new(SEC.encode(), f"{k}:{t}".encode(), hashlib.sha256).hexdigest()
19
+ return f"{k}:{t}:{s}"
20
+
21
+
22
+ def _ok(v):
23
+ if not v:
24
+ return False
25
+ try:
26
+ k, t, s = v.split(":")
27
+ expected = hmac.new(SEC.encode(), f"{k}:{t}".encode(), hashlib.sha256).hexdigest()
28
+ return hmac.compare_digest(s, expected) and time.time() - int(t) <= MAX
29
+ except:
30
+ return False
31
+
32
+
33
+ LOGIN_HTML = """<!DOCTYPE html>
34
+ <html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Login</title>
35
+ <style>
36
+ *{box-sizing:border-box}
37
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:linear-gradient(135deg,#667eea,#764ba2);min-height:100vh;display:flex;align-items:center;justify-content:center;margin:0}
38
+ .b{background:#fff;padding:40px;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,.2);width:100%;max-width:360px;text-align:center}
39
+ h1{margin:0 0 24px;color:#333}input{width:100%;padding:12px;margin:8px 0;border:1px solid #ddd;border-radius:6px;font-size:16px}
40
+ button{width:100%;padding:14px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border:none;border-radius:6px;font-size:16px;cursor:pointer}
41
+ .e{color:#c00;margin:12px 0;display:none}
42
+ </style></head>
43
+ <body><div class="b"><h1>CoPaw</h1><form id="f"><input type="password" id="p" placeholder="Password" required autofocus><button>Sign In</button></form><p class="e" id="e"></p></div>
44
+ <script>
45
+ document.getElementById('f').onsubmit=async function(ev){
46
+ ev.preventDefault();
47
+ var r = await fetch('/auth/login',{
48
+ method:'POST',
49
+ headers:{'Content-Type':'application/x-www-form-urlencoded'},
50
+ body:'password='+encodeURIComponent(document.getElementById('p').value)
51
+ });
52
+ if(r.ok){location='/';}
53
+ else{document.getElementById('e').textContent='Invalid password';document.getElementById('e').style.display='block';}
54
+ };
55
+ </script></body></html>
56
+ """
57
+
58
+
59
+ class AuthMiddleware(BaseHTTPMiddleware):
60
+ async def dispatch(self, req, call_next):
61
+ path = req.url.path
62
+
63
+ # 直接在中间件处理 auth 路由(避免被 catch-all 拦截)
64
+ if path == "/auth/login":
65
+ if req.method == "GET":
66
+ return HTMLResponse(content=LOGIN_HTML)
67
+ elif req.method == "POST":
68
+ # 解析 form 数据
69
+ form = await req.form()
70
+ password = form.get("password", "")
71
+ if ON and secrets.compare_digest(password, PWD):
72
+ r = JSONResponse({"ok": True, "redirect": "/"})
73
+ r.set_cookie("copaw_session", _mk(), max_age=MAX, httponly=True, samesite="lax")
74
+ return r
75
+ return JSONResponse({"error": "Invalid password"}, status_code=401)
76
+
77
+ if path == "/auth/logout":
78
+ r = RedirectResponse(url="/auth/login", status_code=302)
79
+ r.delete_cookie("copaw_session")
80
+ return r
81
+
82
+ if path == "/auth/status":
83
+ return JSONResponse({"authenticated": _ok(req.cookies.get("copaw_session")), "auth_required": ON})
84
+
85
+ # 未启用鉴权则直接放行
86
+ if not ON:
87
+ return await call_next(req)
88
+
89
+ # 静态资源放行
90
+ if any(path.endswith(e) for e in EXTS):
91
+ return await call_next(req)
92
+
93
+ # 已登录则放行
94
+ if _ok(req.cookies.get("copaw_session")):
95
+ return await call_next(req)
96
+
97
+ # API 返回 401,其他跳转登录
98
+ if path.startswith("/api"):
99
+ return JSONResponse({"error": "Unauthorized"}, status_code=401)
100
+ return RedirectResponse(f"/auth/login?next={path}", status_code=302)
101
+
102
+
103
+ def setup_auth(app: FastAPI):
104
+ if ON:
105
+ print(f"[Auth] ENABLED - password protected")
106
+ else:
107
+ print(f"[Auth] DISABLED - no COPAW_PASSWORD set")
108
+ app.add_middleware(AuthMiddleware)
entrypoint.sh ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ set -e
3
+ export COPAW_PORT="${COPAW_PORT:-8088}"
4
+
5
+ if [ -n "$COPAW_PASSWORD" ]; then
6
+ echo "=== CoPaw Auth ENABLED ==="
7
+ else
8
+ echo "=== CoPaw Auth DISABLED ==="
9
+ fi
10
+
11
+ cd /app
12
+ exec python3 -c "
13
+ import sys
14
+ sys.path.insert(0, '/app')
15
+ from copaw.app._app import app
16
+ from auth import setup_auth
17
+ setup_auth(app)
18
+ import uvicorn
19
+ uvicorn.run(app, host='0.0.0.0', port=int('${COPAW_PORT}'), log_level='info')
20
+ "