package main import ( "crypto/rand" "crypto/subtle" "encoding/hex" "encoding/json" "fmt" "io/fs" "net/http" "strconv" "strings" "sync" "time" ) type Server struct { engine *Engine mux *http.ServeMux password string tokenMu sync.Mutex tokens map[string]time.Time } func NewServer(e *Engine, static fs.FS, password string) *Server { s := &Server{ engine: e, mux: http.NewServeMux(), password: password, tokens: map[string]time.Time{}, } mux := s.mux mux.HandleFunc("GET /api/status", s.handleStatus) mux.HandleFunc("GET /api/events", s.handleEvents) mux.HandleFunc("POST /api/login", s.handleLogin) mux.HandleFunc("POST /api/logout", s.handleLogout) mux.HandleFunc("POST /api/repos", s.handleCreate) mux.HandleFunc("GET /api/repos/{name}", s.wrapRepo(s.handleGet)) mux.HandleFunc("PUT /api/repos/{name}", s.wrapRepo(s.handleUpdate)) mux.HandleFunc("DELETE /api/repos/{name}", s.wrapRepo(s.handleDelete)) mux.HandleFunc("POST /api/repos/{name}/start", s.wrapRepo(s.startRepo)) mux.HandleFunc("POST /api/repos/{name}/stop", s.wrapRepo(s.stopRepo)) mux.HandleFunc("POST /api/repos/{name}/sync", s.wrapRepo(s.syncRepo)) mux.HandleFunc("POST /api/repos/{name}/action", s.wrapRepo(s.actionRepo)) mux.HandleFunc("POST /api/repos/{name}/force-push", s.wrapRepo(s.forcePushRepo)) mux.HandleFunc("POST /api/repos/{name}/abort-push", s.wrapRepo(s.abortPushRepo)) mux.HandleFunc("GET /api/repos/{name}/conflicts", s.wrapRepo(s.conflictsRepo)) mux.HandleFunc("GET /api/repos/{name}/log", s.wrapRepo(s.logRepo)) mux.HandleFunc("GET /api/repos/{name}/stats", s.wrapRepo(s.statsRepo)) mux.HandleFunc("GET /api/repos/{name}/commits", s.wrapRepo(s.commitsRepo)) mux.HandleFunc("GET /api/repos/{name}/commits/{hash}", s.wrapRepo(s.commitDiffRepo)) mux.Handle("/", http.FileServer(http.FS(static))) return s } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 访问密码保护:静态资源开放,/api/* 需要会话(登录接口除外) if s.password != "" && strings.HasPrefix(r.URL.Path, "/api/") && r.URL.Path != "/api/login" && !s.validToken(r) { writeErr(w, 401, "需要登录") return } s.mux.ServeHTTP(w, r) } func (s *Server) validToken(r *http.Request) bool { c, err := r.Cookie("git_sync_token") if err != nil { return false } s.tokenMu.Lock() defer s.tokenMu.Unlock() exp, ok := s.tokens[c.Value] if !ok { return false } if time.Now().After(exp) { delete(s.tokens, c.Value) return false } return true } func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { var req struct { Password string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErr(w, 400, "请求体格式错误") return } if subtle.ConstantTimeCompare([]byte(req.Password), []byte(s.password)) != 1 { writeErr(w, 401, "密码错误") return } buf := make([]byte, 32) if _, err := rand.Read(buf); err != nil { writeErr(w, 500, "生成会话失败") return } token := hex.EncodeToString(buf) s.tokenMu.Lock() s.tokens[token] = time.Now().Add(30 * 24 * time.Hour) s.tokenMu.Unlock() http.SetCookie(w, &http.Cookie{ Name: "git_sync_token", Value: token, Path: "/", HttpOnly: true, MaxAge: 30 * 24 * 3600, }) writeJSON(w, 200, map[string]any{"ok": true}) } func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { if c, err := r.Cookie("git_sync_token"); err == nil { s.tokenMu.Lock() delete(s.tokens, c.Value) s.tokenMu.Unlock() } http.SetCookie(w, &http.Cookie{Name: "git_sync_token", Value: "", Path: "/", MaxAge: -1}) writeJSON(w, 200, map[string]any{"ok": true}) } func writeJSON(w http.ResponseWriter, code int, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(code) _ = json.NewEncoder(w).Encode(v) } func writeErr(w http.ResponseWriter, code int, msg string) { writeJSON(w, code, map[string]string{"error": msg}) } func (s *Server) wrapRepo(h func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") if s.engine.get(name) == nil { writeErr(w, 404, "配置不存在: "+name) return } h(w, r, name) } } func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { writeJSON(w, 200, map[string]any{"repos": s.engine.Snapshots(true)}) } func (s *Server) handleGet(w http.ResponseWriter, r *http.Request, name string) { if st := s.engine.get(name); st != nil { writeJSON(w, 200, st.snapshot(true)) return } writeErr(w, 404, "配置不存在") } func (s *Server) handleCreate(w http.ResponseWriter, r *http.Request) { var cfg RepoConfig if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { writeErr(w, 400, "请求体格式错误: "+err.Error()) return } if err := s.engine.Create(&cfg); err != nil { writeErr(w, 400, err.Error()) return } writeJSON(w, 200, map[string]any{"ok": true, "name": cfg.Name}) } func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request, name string) { var cfg RepoConfig if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { writeErr(w, 400, "请求体格式错误: "+err.Error()) return } cfg.Name = name if err := s.engine.Update(&cfg); err != nil { writeErr(w, 400, err.Error()) return } writeJSON(w, 200, map[string]any{"ok": true}) } func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, name string) { if err := s.engine.Delete(name); err != nil { writeErr(w, 400, err.Error()) return } writeJSON(w, 200, map[string]any{"ok": true}) } func (s *Server) startRepo(w http.ResponseWriter, r *http.Request, name string) { if err := s.engine.Start(name); err != nil { writeErr(w, 400, err.Error()) return } writeJSON(w, 200, map[string]any{"ok": true}) } func (s *Server) stopRepo(w http.ResponseWriter, r *http.Request, name string) { if err := s.engine.Stop(name); err != nil { writeErr(w, 400, err.Error()) return } writeJSON(w, 200, map[string]any{"ok": true}) } func (s *Server) syncRepo(w http.ResponseWriter, r *http.Request, name string) { if err := s.engine.SyncNow(name); err != nil { writeErr(w, 400, err.Error()) return } writeJSON(w, 200, map[string]any{"ok": true}) } func (s *Server) actionRepo(w http.ResponseWriter, r *http.Request, name string) { var req struct { Action string `json:"action"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErr(w, 400, "请求体格式错误") return } switch req.Action { case "abort": if err := s.engine.ConflictAbort(name); err != nil { writeErr(w, 500, err.Error()) return } case "resolve": if err := s.engine.ConflictResolve(name); err != nil { writeErr(w, 500, err.Error()) return } case "fetch", "pull", "push": if err := s.engine.ManualGit(name, req.Action); err != nil { writeErr(w, 500, err.Error()) return } default: writeErr(w, 400, "未知操作: "+req.Action) return } writeJSON(w, 200, map[string]any{"ok": true}) } func (s *Server) conflictsRepo(w http.ResponseWriter, r *http.Request, name string) { writeJSON(w, 200, s.engine.ConflictDiffs(name)) } func (s *Server) logRepo(w http.ResponseWriter, r *http.Request, name string) { n := 300 if v, err := strconv.Atoi(r.URL.Query().Get("lines")); err == nil && v > 0 && v <= 5000 { n = v } writeJSON(w, 200, map[string]any{"lines": s.engine.ReadLog(name, n)}) } func (s *Server) statsRepo(w http.ResponseWriter, r *http.Request, name string) { writeJSON(w, 200, s.engine.ReadStats(name)) } func (s *Server) commitsRepo(w http.ResponseWriter, r *http.Request, name string) { limit := 50 if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && v > 0 && v <= 200 { limit = v } writeJSON(w, 200, s.engine.ReadCommits(name, limit)) } func (s *Server) commitDiffRepo(w http.ResponseWriter, r *http.Request, name string) { diff, err := s.engine.ReadCommitDiff(name, r.PathValue("hash")) if err != nil { writeErr(w, 400, err.Error()) return } writeJSON(w, 200, map[string]string{"diff": diff}) } func (s *Server) handleEvents(w http.ResponseWriter, r *http.Request) { fl, ok := w.(http.Flusher) if !ok { http.Error(w, "streaming unsupported", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("X-Accel-Buffering", "no") id, ch := s.engine.Bus().Subscribe() defer s.engine.Bus().Unsubscribe(id) if snap, err := json.Marshal(map[string]any{"type": "snapshot", "data": s.engine.Snapshots(true)}); err == nil { fmt.Fprintf(w, "data: %s\n\n", snap) fl.Flush() } tick := time.NewTicker(25 * time.Second) defer tick.Stop() ctx := r.Context() for { select { case <-ctx.Done(): return case <-tick.C: fmt.Fprint(w, ": ping\n\n") fl.Flush() case data, ok := <-ch: if !ok { return } fmt.Fprintf(w, "data: %s\n\n", data) fl.Flush() } } } func (s *Server) forcePushRepo(w http.ResponseWriter, r *http.Request, name string) { if err := s.engine.ForcePush(name); err != nil { writeErr(w, 400, err.Error()) return } writeJSON(w, 200, map[string]any{"ok": true}) } func (s *Server) abortPushRepo(w http.ResponseWriter, r *http.Request, name string) { if err := s.engine.AbortPush(name); err != nil { writeErr(w, 400, err.Error()) return } writeJSON(w, 200, map[string]any{"ok": true}) }