Initialize Clash Web source and deployment documentation

This commit is contained in:
lhc committed 2026-10-05 17:32:00 +08:00
commit 8e0fa00a73
66 files changed
+8532

No files matched your search

+235
View File
@@ -0,0 +1,235 @@
package app
import (
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"errors"
"io"
"net"
"net/http"
"time"
"golang.org/x/crypto/bcrypt"
)
type loginLimit struct {
Count int
Until time.Time
}
type credentials struct {
Password string `json:"password"`
Token string `json:"token"`
CurrentPassword string `json:"currentPassword"`
}
func decode(w http.ResponseWriter, r *http.Request, target any) error {
r.Body = http.MaxBytesReader(w, r.Body, 16<<20)
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(target); err != nil {
return errors.New("请求内容无效或过大")
}
var extra any
if err := decoder.Decode(&extra); err != io.EOF {
return errors.New("请求必须是单个 JSON 对象")
}
return nil
}
func jsonResponse(w http.ResponseWriter, status int, value any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(value)
}
func writeError(w http.ResponseWriter, status int, message string) {
jsonResponse(w, status, map[string]string{"error": message})
}
func hashToken(token string) string {
b := sha256.Sum256([]byte(token))
return hex.EncodeToString(b[:])
}
func validPassword(password string) bool { return len(password) >= 12 && len(password) <= 72 }
func (a *App) session(r *http.Request) (string, string, error) {
cookie, err := r.Cookie("cw_session")
if err != nil {
return "", "", err
}
hash := hashToken(cookie.Value)
var csrf string
var expires int64
err = a.store.db.QueryRow("SELECT csrf,expires FROM sessions WHERE token_hash=?", hash).Scan(&csrf, &expires)
if err != nil || expires <= time.Now().Unix() {
return "", "", errors.New("会话已过期")
}
return hash, csrf, nil
}
func (a *App) newSession(w http.ResponseWriter) error {
token, csrf := randomToken(), randomToken()
expires := time.Now().Add(24 * time.Hour)
if _, err := a.store.db.Exec("INSERT INTO sessions(token_hash,csrf,expires) VALUES(?,?,?)", hashToken(token), csrf, expires.Unix()); err != nil {
return err
}
http.SetCookie(w, &http.Cookie{Name: "cw_session", Value: token, Path: "/", HttpOnly: true, Secure: a.secureCookie, SameSite: http.SameSiteStrictMode, MaxAge: 86400, Expires: expires})
jsonResponse(w, 200, map[string]any{"authenticated": true, "csrf": csrf})
return nil
}
func (a *App) protect(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, csrf, err := a.session(r)
if err != nil {
writeError(w, 401, "请先登录")
return
}
if r.Method != "GET" && r.Method != "HEAD" {
if subtle.ConstantTimeCompare([]byte(r.Header.Get("X-CSRF-Token")), []byte(csrf)) != 1 {
writeError(w, 403, "会话校验失败,请重新登录")
return
}
}
next.ServeHTTP(w, r)
})
}
func (a *App) authStatus(w http.ResponseWriter, r *http.Request) {
password, err := a.store.Get("password")
if err != nil {
writeError(w, 500, "数据库读取失败")
return
}
_, csrf, err := a.session(r)
jsonResponse(w, 200, map[string]any{"initialized": password != "", "authenticated": err == nil, "csrf": csrf})
}
func (a *App) setup(w http.ResponseWriter, r *http.Request) {
a.authMu.Lock()
defer a.authMu.Unlock()
password, err := a.store.Get("password")
if err != nil {
writeError(w, 500, "数据库读取失败")
return
}
if password != "" {
writeError(w, 409, "管理员已经设置")
return
}
var input credentials
if err := decode(w, r, &input); err != nil {
writeError(w, 400, err.Error())
return
}
if subtle.ConstantTimeCompare([]byte(input.Token), []byte(a.setupToken)) != 1 {
writeError(w, 403, "首次设置令牌不正确,请查看服务启动日志")
return
}
if !validPassword(input.Password) {
writeError(w, 400, "密码需为 12–72 字节")
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
if err != nil {
writeError(w, 500, "密码处理失败")
return
}
if err := a.store.Set("password", string(hash)); err != nil {
writeError(w, 500, "保存失败")
return
}
_ = a.store.Set("setup_token", "")
a.setupToken = ""
if err := a.newSession(w); err != nil {
writeError(w, 500, "会话创建失败")
}
}
func (a *App) login(w http.ResponseWriter, r *http.Request) {
a.authMu.Lock()
defer a.authMu.Unlock()
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
limit := a.failures[ip]
if limit.Count >= 5 && time.Now().Before(limit.Until) {
writeError(w, 429, "登录尝试过多,请 5 分钟后重试")
return
}
if time.Now().After(limit.Until) {
limit = loginLimit{}
}
var input credentials
if err := decode(w, r, &input); err != nil {
writeError(w, 400, err.Error())
return
}
password, err := a.store.Get("password")
if err != nil {
writeError(w, 500, "读取失败")
return
}
if password == "" || bcrypt.CompareHashAndPassword([]byte(password), []byte(input.Password)) != nil {
if len(a.failures) > 1024 {
a.failures = map[string]loginLimit{}
}
limit.Count++
limit.Until = time.Now().Add(5 * time.Minute)
a.failures[ip] = limit
writeError(w, 401, "密码不正确")
return
}
delete(a.failures, ip)
if err := a.newSession(w); err != nil {
writeError(w, 500, "会话创建失败")
}
}
func (a *App) logout(w http.ResponseWriter, r *http.Request) {
hash, _, _ := a.session(r)
_, err := a.store.db.Exec("DELETE FROM sessions WHERE token_hash=?", hash)
if err != nil {
writeError(w, 500, "退出失败")
return
}
http.SetCookie(w, &http.Cookie{Name: "cw_session", Path: "/", HttpOnly: true, Secure: a.secureCookie, SameSite: http.SameSiteStrictMode, MaxAge: -1})
jsonResponse(w, 200, map[string]bool{"ok": true})
}
func (a *App) password(w http.ResponseWriter, r *http.Request) {
a.authMu.Lock()
defer a.authMu.Unlock()
var input credentials
if err := decode(w, r, &input); err != nil {
writeError(w, 400, err.Error())
return
}
old, err := a.store.Get("password")
if err != nil || bcrypt.CompareHashAndPassword([]byte(old), []byte(input.CurrentPassword)) != nil {
writeError(w, 400, "当前密码不正确")
return
}
if !validPassword(input.Password) {
writeError(w, 400, "新密码需为 12–72 字节")
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
if err != nil {
writeError(w, 500, "密码处理失败")
return
}
tx, err := a.store.db.Begin()
if err != nil {
writeError(w, 500, "保存失败")
return
}
defer tx.Rollback()
if _, err = tx.Exec("UPDATE kv SET value=? WHERE key='password'", string(hash)); err == nil {
_, err = tx.Exec("DELETE FROM sessions")
}
if err != nil || tx.Commit() != nil {
writeError(w, 500, "保存失败")
return
}
if err := a.newSession(w); err != nil {
writeError(w, 500, "会话创建失败")
}
}