arche / internal/archesrv/server.go

commit 154431fd
  1package archesrv
  2
  3import (
  4	"embed"
  5	"encoding/hex"
  6	"flag"
  7	"fmt"
  8	"html/template"
  9	"log/slog"
 10	"net/http"
 11	"os"
 12	"path/filepath"
 13	"runtime/debug"
 14	"strings"
 15
 16	"arche/internal/markdown"
 17	"arche/internal/object"
 18	"arche/internal/repo"
 19)
 20
 21//go:embed templates/*.html
 22var tmplFS embed.FS
 23
 24type forgeServer struct {
 25	db  *DB
 26	cfg Config
 27	log *slog.Logger
 28}
 29
 30func (s *forgeServer) dataDir() string { return s.cfg.Storage.DataDir }
 31
 32func Run() error {
 33	configPath := flag.String("config", "server.toml", "path to server.toml")
 34	flag.Parse()
 35
 36	cfg, err := LoadConfig(*configPath)
 37	if err != nil {
 38		return err
 39	}
 40
 41	if err := os.MkdirAll(cfg.Storage.DataDir, 0o755); err != nil {
 42		return fmt.Errorf("create data dir: %w", err)
 43	}
 44
 45	dbPath := filepath.Join(cfg.Storage.DataDir, "server.db")
 46	db, err := openDB(dbPath)
 47	if err != nil {
 48		return err
 49	}
 50	defer db.Close()
 51
 52	logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
 53	s := &forgeServer{db: db, cfg: cfg, log: logger}
 54
 55	for repoName, repoCfg := range cfg.Repo {
 56		rec, err := db.GetRepo(repoName)
 57		if err != nil || rec == nil {
 58			continue
 59		}
 60		_ = db.SetRepoAllowShellHooks(rec.ID, repoCfg.AllowShellHooks, repoCfg.PostReceive)
 61	}
 62	addr := cfg.Server.ListenHTTP
 63	if addr == "" {
 64		addr = ":8080"
 65	}
 66
 67	if sshAddr := cfg.Server.ListenSSH; sshAddr != "" {
 68		go func() {
 69			if err := s.RunSSH(sshAddr); err != nil {
 70				s.log.Error("SSH listener failed", "err", err)
 71			}
 72		}()
 73	}
 74
 75	if mtlsAddr := cfg.Server.ListenMTLS; mtlsAddr != "" {
 76		go func() {
 77			if err := s.RunMTLS(mtlsAddr, cfg.Server.TLSCert, cfg.Server.TLSKey); err != nil {
 78				s.log.Error("mTLS listener failed", "err", err)
 79			}
 80		}()
 81	}
 82
 83	s.log.Info("listening", "addr", "http://"+addr)
 84	return http.ListenAndServe(addr, s.routes())
 85}
 86
 87func (s *forgeServer) routes() http.Handler {
 88	mux := http.NewServeMux()
 89
 90	mux.HandleFunc("GET /setup", s.handleSetup)
 91	mux.HandleFunc("POST /setup", s.handleSetupPost)
 92
 93	mux.HandleFunc("GET /login", s.handleLoginPage)
 94	mux.HandleFunc("POST /login", s.handleLoginPost)
 95	mux.HandleFunc("GET /logout", s.handleLogout)
 96
 97	if s.cfg.Auth.Registration == "open" || s.cfg.Auth.Registration == "invite" {
 98		mux.HandleFunc("GET /register", s.handleRegisterPage)
 99		mux.HandleFunc("POST /register", s.handleRegisterPost)
100	}
101
102	mux.HandleFunc("GET /settings/keys", s.handleSettingsKeys)
103	mux.HandleFunc("POST /settings/keys", s.handleSettingsAddKey)
104	mux.HandleFunc("DELETE /settings/keys/{id}", s.handleSettingsDeleteKey)
105
106	mux.HandleFunc("GET /settings/mtls", s.handleSettingsMTLSCerts)
107	mux.HandleFunc("POST /settings/mtls", s.handleSettingsAddMTLSCert)
108	mux.HandleFunc("DELETE /settings/mtls/{id}", s.handleSettingsDeleteMTLSCert)
109
110	mux.HandleFunc("GET /settings/token", s.handleSettingsToken)
111	mux.HandleFunc("POST /settings/tokens", s.handleSettingsCreateToken)
112	mux.HandleFunc("DELETE /settings/tokens/{id}", s.handleSettingsDeleteToken)
113
114	mux.HandleFunc("POST /admin/repos", s.handleAdminCreateRepo)
115	mux.HandleFunc("DELETE /admin/repos/{name}", s.handleAdminDeleteRepo)
116
117	mux.HandleFunc("GET /admin/users", s.handleAdminUsers)
118	mux.HandleFunc("POST /admin/users", s.handleAdminCreateUser)
119	mux.HandleFunc("DELETE /admin/users/{id}", s.handleAdminDeleteUser)
120
121	mux.HandleFunc("GET /admin/invites", s.handleAdminInvites)
122	mux.HandleFunc("POST /admin/invites", s.handleAdminCreateInvite)
123	mux.HandleFunc("DELETE /admin/invites/{id}", s.handleAdminDeleteInvite)
124
125	mux.HandleFunc("/{repo}/arche/v1/", s.handleSyncProxy)
126
127	mux.HandleFunc("GET /{repo}/issues", s.handleRepoIssues)
128	mux.HandleFunc("POST /{repo}/issues", s.handleRepoCreateIssue)
129	mux.HandleFunc("GET /{repo}/issue", s.handleRepoIssue)
130	mux.HandleFunc("POST /{repo}/issue/comment", s.handleRepoAddComment)
131	mux.HandleFunc("POST /{repo}/issue/status", s.handleRepoSetStatus)
132
133	mux.HandleFunc("GET /{repo}/wiki", s.handleRepoWikiList)
134	mux.HandleFunc("GET /{repo}/wiki/{title}", s.handleRepoWikiPage)
135	mux.HandleFunc("POST /{repo}/wiki/{title}", s.handleRepoWikiSave)
136
137	mux.HandleFunc("GET /{repo}/settings", s.handleRepoSettingsPage)
138	mux.HandleFunc("POST /{repo}/settings", s.handleRepoUpdateSettings)
139	mux.HandleFunc("POST /{repo}/settings/collaborators", s.handleRepoAddCollaborator)
140	mux.HandleFunc("DELETE /{repo}/settings/collaborators/{id}", s.handleRepoRemoveCollaborator)
141	mux.HandleFunc("POST /{repo}/settings/delete", s.handleRepoDeleteRepo)
142
143	mux.HandleFunc("GET /{repo}/settings/webhooks", s.handleRepoWebhooks)
144	mux.HandleFunc("POST /{repo}/settings/webhooks", s.handleRepoCreateWebhook)
145	mux.HandleFunc("DELETE /{repo}/settings/webhooks/{id}", s.handleRepoDeleteWebhook)
146	mux.HandleFunc("GET /{repo}/settings/webhooks/{id}/deliveries", s.handleWebhookDeliveries)
147	mux.HandleFunc("POST /{repo}/settings/webhooks/{id}/deliveries/{delivery}/replay", s.handleWebhookReplay)
148	mux.HandleFunc("GET /{repo}/log", s.handleRepoLog)
149	mux.HandleFunc("GET /{repo}/commit", s.handleRepoCommit)
150	mux.HandleFunc("GET /{repo}/tree", s.handleRepoTree)
151	mux.HandleFunc("GET /{repo}/file", s.handleRepoFile)
152	mux.HandleFunc("GET /{repo}/stacks", s.handleRepoStacks)
153	mux.HandleFunc("GET /{repo}/stacks/{stackid}", s.handleRepoStackDetail)
154	mux.HandleFunc("POST /{repo}/stacks/{stackid}/reviews/{changeid}", s.handleStackSetReview)
155	mux.HandleFunc("GET /{repo}", s.handleRepoHome)
156
157	mux.HandleFunc("GET /{$}", s.handleIndex)
158
159	return s.recoverMiddleware(s.logMiddleware(mux))
160}
161
162type statusRecorder struct {
163	http.ResponseWriter
164	code int
165}
166
167func (r *statusRecorder) WriteHeader(code int) {
168	r.code = code
169	r.ResponseWriter.WriteHeader(code)
170}
171
172func (s *forgeServer) logMiddleware(next http.Handler) http.Handler {
173	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
174		rec := &statusRecorder{ResponseWriter: w, code: http.StatusOK}
175		next.ServeHTTP(rec, r)
176		s.log.Info("request", "method", r.Method, "path", r.URL.Path, "status", rec.code)
177	})
178}
179
180func (s *forgeServer) recoverMiddleware(next http.Handler) http.Handler {
181	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
182		defer func() {
183			if rec := recover(); rec != nil {
184				s.log.Error("handler panic",
185					"panic", rec,
186					"method", r.Method,
187					"path", r.URL.Path,
188					"stack", string(debug.Stack()),
189				)
190				http.Error(w, "internal server error", http.StatusInternalServerError)
191			}
192		}()
193		next.ServeHTTP(w, r)
194	})
195}
196
197func (s *forgeServer) render(w http.ResponseWriter, page string, data any) {
198	funcs := template.FuncMap{
199		"markdown":         markdown.Render,
200		"registrationOpen": func() bool { return s.cfg.Auth.Registration == "open" },
201	}
202	t, err := template.New("").Funcs(funcs).ParseFS(tmplFS,
203		"templates/srv_base.html",
204		"templates/"+page,
205	)
206	if err != nil {
207		http.Error(w, "template: "+err.Error(), http.StatusInternalServerError)
208		return
209	}
210
211	w.Header().Set("Content-Type", "text/html; charset=utf-8")
212	if err := t.ExecuteTemplate(w, page, data); err != nil {
213		s.log.Error("render failed", "page", page, "err", err)
214	}
215}
216
217func fullHex(id [32]byte) string       { return hex.EncodeToString(id[:]) }
218func shortHex(id [32]byte) string      { return hex.EncodeToString(id[:])[:8] }
219func phaseClass(p object.Phase) string { return strings.ToLower(p.String()) }
220
221func bookmarkMap(r *repo.Repo) map[string][]string {
222	bms, _ := r.Store.ListBookmarks()
223	m := make(map[string][]string, len(bms))
224	for _, b := range bms {
225		k := fullHex(b.CommitID)
226		m[k] = append(m[k], b.Name)
227	}
228	return m
229}