The ruleset's diff names the chain each hunk is in on its @@ line, as
git names the function. A history entry shows what it did to the
ruleset (web page, hc history diff, GET /history/{id}/diff), rebuilt
from the current configuration by stepping back through the later
entries. A changed setting is listed by its own fields
(builtins.drop_invalid) instead of the whole object.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
6.7 KiB
Go
150 lines
6.7 KiB
Go
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+"/bans", s.authed(s.listBans))
|
|
m.HandleFunc("GET "+p+"/banlists/{ref}/bans", s.authed(s.listBans))
|
|
m.HandleFunc("POST "+p+"/banlists/{ref}/bans", s.authed(s.addBan))
|
|
m.HandleFunc("DELETE "+p+"/banlists/{ref}/bans", s.authed(s.deleteBans))
|
|
m.HandleFunc("DELETE "+p+"/banlists/{ref}/bans/{id}", s.authed(s.deleteBans))
|
|
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("POST "+p+"/names", s.authed(s.lookupNames))
|
|
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("GET "+p+"/history/{id}/diff", s.authed(s.historyDiff))
|
|
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
|
|
}
|