package server import ( "bytes" "crypto/sha256" "encoding/hex" "io/fs" "net/http" "strings" "time" "housecarl/internal/api" ) func (s *Server) routes() { m := s.mux p := api.Prefix m.HandleFunc("GET /healthz", s.healthz) // Authentication, users and tokens m.HandleFunc("POST "+p+"/auth/login", s.public(s.login)) m.HandleFunc("POST "+p+"/auth/logout", s.authed(s.logout)) m.HandleFunc("GET "+p+"/auth/whoami", s.authed(s.whoami)) m.HandleFunc("POST "+p+"/me/password", s.authed(s.changeOwnPassword)) m.HandleFunc("GET "+p+"/me/tokens", s.authed(s.listOwnTokens)) m.HandleFunc("POST "+p+"/me/tokens", s.authed(s.createOwnToken)) m.HandleFunc("GET "+p+"/tokens", s.authed(s.listAllTokens)) m.HandleFunc("DELETE "+p+"/tokens/{id}", s.authed(s.revokeToken)) m.HandleFunc("POST "+p+"/tokens/{id}/rotate", s.authed(s.rotateToken)) m.HandleFunc("GET "+p+"/users", s.authed(s.listUsers)) m.HandleFunc("POST "+p+"/users", s.authed(s.createUser)) m.HandleFunc("PATCH "+p+"/users/{id}", s.authed(s.updateUser)) m.HandleFunc("DELETE "+p+"/users/{id}", s.authed(s.deleteUser)) m.HandleFunc("GET "+p+"/users/{id}/tokens", s.authed(s.listUserTokens)) m.HandleFunc("POST "+p+"/users/{id}/tokens", s.authed(s.createUserToken)) // Configuration m.HandleFunc("GET "+p+"/settings", s.authed(s.getSettings)) m.HandleFunc("PATCH "+p+"/settings", s.authed(s.updateSettings)) m.HandleFunc("GET "+p+"/sets/{ref}/elements", s.authed(s.listElements)) m.HandleFunc("POST "+p+"/sets/{ref}/elements", s.authed(s.addElement)) m.HandleFunc("DELETE "+p+"/sets/{ref}/elements/{addr}", s.authed(s.deleteElement)) m.HandleFunc("GET "+p+"/quotas", s.authed(s.listQuotas)) m.HandleFunc("GET "+p+"/connections", s.authed(s.listConnections)) m.HandleFunc("POST "+p+"/connections/close", s.authed(s.closeConnections)) m.HandleFunc("GET "+p+"/trace", s.authed(s.getTrace)) m.HandleFunc("POST "+p+"/trace", s.authed(s.startTrace)) m.HandleFunc("DELETE "+p+"/trace", s.authed(s.stopTrace)) m.HandleFunc("POST "+p+"/rules/{ref}/reset-quota", s.authed(s.resetQuota)) m.HandleFunc("GET "+p+"/changesets", s.authed(s.listChangesets)) m.HandleFunc("POST "+p+"/changesets", s.authed(s.createChangeset)) m.HandleFunc("GET "+p+"/changesets/{id}", s.authed(s.getChangeset)) m.HandleFunc("PATCH "+p+"/changesets/{id}", s.authed(s.updateChangeset)) m.HandleFunc("GET "+p+"/changesets/{id}/preview", s.authed(s.previewChangeset)) m.HandleFunc("POST "+p+"/changesets/{id}/apply", s.authed(s.applyChangeset)) m.HandleFunc("POST "+p+"/changesets/{id}/discard", s.authed(s.discardChangeset)) m.HandleFunc("DELETE "+p+"/changesets/{id}/ops/{seq}", s.authed(s.removeOp)) m.HandleFunc("GET "+p+"/{kind}", s.authed(s.listObjects)) m.HandleFunc("POST "+p+"/{kind}", s.authed(s.createObject)) m.HandleFunc("GET "+p+"/{kind}/{ref}", s.authed(s.getObject)) m.HandleFunc("PATCH "+p+"/{kind}/{ref}", s.authed(s.updateObject)) m.HandleFunc("DELETE "+p+"/{kind}/{ref}", s.authed(s.deleteObject)) m.HandleFunc("POST "+p+"/{kind}/{ref}/move", s.authed(s.moveObject)) m.HandleFunc("GET "+p+"/ruleset", s.authed(s.ruleset)) m.HandleFunc("GET "+p+"/matrix", s.authed(s.zoneMatrix)) m.HandleFunc("GET "+p+"/stats/status", s.authed(s.statsStatus)) m.HandleFunc("GET "+p+"/stats/summary", s.authed(s.statsSummary)) m.HandleFunc("GET "+p+"/stats/timeline", s.authed(s.statsTimeline)) m.HandleFunc("GET "+p+"/stats/top", s.authed(s.statsTop)) m.HandleFunc("GET "+p+"/stats/hits", s.authed(s.statsHits)) m.HandleFunc("GET "+p+"/stats/heatmap", s.authed(s.statsHeatmap)) m.HandleFunc("GET "+p+"/history", s.authed(s.listHistory)) m.HandleFunc("GET "+p+"/history/{id}", s.authed(s.getHistory)) m.HandleFunc("POST "+p+"/history/{id}/revert", s.authed(s.revertHistory)) m.HandleFunc("GET "+p+"/probation", s.authed(s.getProbation)) m.HandleFunc("POST "+p+"/confirm", s.authed(s.confirmChanges)) m.HandleFunc("POST "+p+"/rollback", s.authed(s.rollbackChanges)) m.HandleFunc("POST "+p+"/import/nft", s.authed(s.importNft)) m.HandleFunc("POST "+p+"/takeover", s.authed(s.takeover)) m.HandleFunc("GET "+p+"/catalog", s.authed(s.catalog)) m.HandleFunc("GET "+p+"/interfaces", s.authed(s.interfaces)) // Status, counters, the log and audit m.HandleFunc("GET "+p+"/status", s.authed(s.status)) m.HandleFunc("GET "+p+"/counters", s.authed(s.counters)) m.HandleFunc("GET "+p+"/log", s.authed(s.listLog)) m.HandleFunc("GET "+p+"/audit", s.authed(s.listAudit)) m.HandleFunc("GET "+p+"/certificate", s.authed(s.certificate)) m.HandleFunc("POST "+p+"/certificate/check", s.authed(s.certificateCheck)) m.HandleFunc("POST "+p+"/certificate/renew", s.authed(s.certificateRenew)) m.HandleFunc(p+"/", func(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusNotFound, "not_found", "no such API endpoint: "+r.Method+" "+r.URL.Path) }) // Web UI if s.web != nil { files := http.FileServer(http.FS(s.web)) etags := contentETags(s.web) m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodHead { w.Header().Set("Allow", "GET, HEAD") http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if r.URL.Path == "/" || r.URL.Path == "/index.html" { b, err := fs.ReadFile(s.web, "index.html") if err != nil { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("ETag", etags["index.html"]) http.ServeContent(w, r, "index.html", time.Time{}, bytes.NewReader(b)) return } if strings.HasPrefix(r.URL.Path, "/assets/") { w.Header().Set("Cache-Control", "no-cache") if tag, ok := etags[strings.TrimPrefix(r.URL.Path, "/")]; ok { w.Header().Set("ETag", tag) } files.ServeHTTP(w, r) return } http.NotFound(w, r) }) } } // contentETags gives every embedded file an ETag from its content. func contentETags(fsys fs.FS) map[string]string { tags := map[string]string{} fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { return nil } b, err := fs.ReadFile(fsys, path) if err != nil { return nil } sum := sha256.Sum256(b) tags[path] = `"` + hex.EncodeToString(sum[:12]) + `"` return nil }) return tags }