Files
Hammerhead/server.go
T
VeillaxandClaude Sonnet 5.5 227c890409
CI / test (push) Successful in 4m52s
Release / release (push) Successful in 5m24s
Add Sandbar alert channel, tags for resources and sites, use Reef's secretbox
Sandbar channels raise and resolve alerts through its API, with the API key
sealed at rest and never returned to the browser. Resources and sites can be
tagged and filtered by tag; sites can be renamed. The secretbox package now
comes from Reef v0.1.9 instead of a private copy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 03:32:43 -06:00

161 lines
8.9 KiB
Go

package hammerhead
import (
_ "embed"
"net/http"
)
//go:embed web/index.html
var indexHTML []byte
func page(html []byte) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
h.Set("Content-Type", "text/html; charset=utf-8")
h.Set("Cache-Control", "no-store")
h.Set("X-Frame-Options", "DENY")
h.Set("X-Content-Type-Options", "nosniff")
h.Set("Referrer-Policy", "no-referrer")
h.Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'")
w.Write(html)
}
}
// dashboardPaths are the addresses the dashboard shows pages at. They all serve the same app,
// which reads the path to decide what to show. Keep in step with TAB_PATH in web/index.html.
var dashboardPaths = map[string]bool{
"/": true, "/apps": true, "/resources": true, "/sites": true, "/domains": true, "/mail-egress": true,
"/alerts": true, "/access": true, "/logs": true, "/account": true,
"/server/edges": true, "/server/organizations": true, "/server/users": true, "/server/sso": true,
"/server/settings": true, "/server/alerts": true, "/server/audit": true,
}
func isDashboardPath(p string) bool { return dashboardPaths[p] }
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
index := page(indexHTML)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if !isDashboardPath(r.URL.Path) {
http.NotFound(w, r)
return
}
index(w, r)
})
s.registerAlertRoutes(mux)
mux.HandleFunc("GET /metrics", s.metrics)
// public
mux.HandleFunc("GET /api/public", s.publicInfo)
mux.HandleFunc("POST /api/setup", s.setup)
mux.HandleFunc("POST /api/login", s.login)
mux.HandleFunc("POST /api/login/totp", s.loginTOTP)
mux.HandleFunc("GET /api/invites/{token}", s.inviteInfo)
mux.HandleFunc("POST /api/invites/accept", s.acceptInvite)
mux.HandleFunc("GET /auth/oidc/{id}/start", s.oidcStart)
mux.HandleFunc("GET /auth/oidc/callback", s.oidcCallback)
// visitor login for protected ports
res := page(resourceHTML)
mux.HandleFunc("GET /auth/r/{id}", res)
mux.HandleFunc("GET /auth/r/{id}/info", s.resourceInfo)
mux.HandleFunc("POST /auth/r/{id}/password", s.resourceAuth("password", s.resourcePassword))
mux.HandleFunc("POST /auth/r/{id}/pin", s.resourceAuth("pin", s.resourcePin))
mux.HandleFunc("POST /auth/r/{id}/sso", s.resourceAuth("sso", s.resourceSSO))
mux.HandleFunc("POST /auth/r/{id}/share", s.resourceAuth("share", s.resourceShare))
mux.HandleFunc("POST /auth/r/{id}/email/start", s.resourceAuth("emailstart", s.resourceEmailStart))
mux.HandleFunc("POST /auth/r/{id}/email/verify", s.resourceAuth("emailverify", s.resourceEmailVerify))
// any signed-in user
mux.HandleFunc("POST /api/logout", s.withUser(s.logout))
mux.HandleFunc("GET /api/me", s.withUser(s.me))
mux.HandleFunc("PUT /api/me", s.withUser(s.updateMe))
mux.HandleFunc("POST /api/me/password", s.withUser(s.changePassword))
mux.HandleFunc("POST /api/me/2fa/setup", s.withUser(s.totpSetup))
mux.HandleFunc("POST /api/me/2fa/enable", s.withUser(s.totpEnable))
mux.HandleFunc("POST /api/me/2fa/disable", s.withUser(s.totpDisable))
mux.HandleFunc("GET /api/me/resources", s.withUser(s.myResources))
mux.HandleFunc("GET /api/me/sessions", s.withUser(s.mySessions))
mux.HandleFunc("DELETE /api/me/sessions/{id}", s.withUser(s.endSession))
mux.HandleFunc("POST /api/me/sessions/revoke-others", s.withUser(s.endOtherSessions))
mux.HandleFunc("GET /api/edges", s.withUser(s.listEdges))
// server administrators
mux.HandleFunc("POST /api/edges", s.withServerAdmin(s.saveEdge))
mux.HandleFunc("PUT /api/edges/{id}", s.withServerAdmin(s.saveEdge))
mux.HandleFunc("DELETE /api/edges/{id}", s.withServerAdmin(s.deleteEdge))
mux.HandleFunc("POST /api/orgs", s.withServerAdmin(s.createOrg))
mux.HandleFunc("GET /api/admin/users", s.withServerAdmin(s.adminUsers))
mux.HandleFunc("POST /api/admin/users", s.withServerAdmin(s.adminCreateUser))
mux.HandleFunc("PUT /api/admin/users/{id}", s.withServerAdmin(s.adminUpdateUser))
mux.HandleFunc("DELETE /api/admin/users/{id}", s.withServerAdmin(s.adminDeleteUser))
mux.HandleFunc("GET /api/admin/idps", s.withServerAdmin(s.listIdPs))
mux.HandleFunc("POST /api/admin/idps", s.withServerAdmin(s.saveIdP))
mux.HandleFunc("PUT /api/admin/idps/{id}", s.withServerAdmin(s.saveIdP))
mux.HandleFunc("DELETE /api/admin/idps/{id}", s.withServerAdmin(s.deleteIdP))
mux.HandleFunc("GET /api/admin/settings", s.withServerAdmin(s.getSettings))
mux.HandleFunc("PUT /api/admin/settings", s.withServerAdmin(s.putSettings))
mux.HandleFunc("POST /api/admin/settings/test-mail", s.withServerAdmin(s.testMail))
mux.HandleFunc("GET /api/admin/update", s.withServerAdmin(s.checkUpdate))
mux.HandleFunc("POST /api/admin/update/apply", s.withServerAdmin(s.applyUpdate))
mux.HandleFunc("GET /api/admin/update/status", s.withServerAdmin(s.updateStatus))
mux.HandleFunc("GET /api/versions", s.withUser(s.latestVersions))
mux.HandleFunc("GET /api/edges/{id}/update/log", s.withServerAdmin(s.edgeUpdateLog))
mux.HandleFunc("GET /api/orgs/{org}/sites/{id}/update/log", s.withOrg(true, s.siteUpdateLog))
mux.HandleFunc("POST /api/edges/{id}/update", s.withServerAdmin(s.updateEdge))
mux.HandleFunc("POST /api/orgs/{org}/sites/{id}/update", s.withOrg(true, s.updateSite))
mux.HandleFunc("GET /api/admin/audit", s.withServerAdmin(func(w http.ResponseWriter, r *http.Request, p *principal) {
writeJSON(w, 200, s.logs.queryAudit("", 300, 0))
}))
// organization scope: members read, admins write
mux.HandleFunc("GET /api/orgs/{org}/state", s.withOrg(false, s.orgState))
mux.HandleFunc("PUT /api/orgs/{org}", s.withOrg(true, s.updateOrg))
mux.HandleFunc("DELETE /api/orgs/{org}", s.withOrg(true, s.deleteOrg))
mux.HandleFunc("GET /api/orgs/{org}/roles", s.withOrg(false, s.listRoles))
mux.HandleFunc("POST /api/orgs/{org}/roles", s.withOrg(true, s.saveRole))
mux.HandleFunc("PUT /api/orgs/{org}/roles/{role}", s.withOrg(true, s.saveRole))
mux.HandleFunc("DELETE /api/orgs/{org}/roles/{role}", s.withOrg(true, s.deleteRole))
mux.HandleFunc("GET /api/orgs/{org}/members", s.withOrg(true, s.listMembers))
mux.HandleFunc("PUT /api/orgs/{org}/members/{user}", s.withOrg(true, s.setMemberRole))
mux.HandleFunc("DELETE /api/orgs/{org}/members/{user}", s.withOrg(true, s.removeMember))
mux.HandleFunc("GET /api/orgs/{org}/invites", s.withOrg(true, s.listInvites))
mux.HandleFunc("POST /api/orgs/{org}/invites", s.withOrg(true, s.createInvite))
mux.HandleFunc("DELETE /api/orgs/{org}/invites/{id}", s.withOrg(true, s.deleteInvite))
mux.HandleFunc("GET /api/orgs/{org}/apikeys", s.withOrg(true, s.listKeys))
mux.HandleFunc("POST /api/orgs/{org}/apikeys", s.withOrg(true, s.createKey))
mux.HandleFunc("DELETE /api/orgs/{org}/apikeys/{id}", s.withOrg(true, s.deleteKey))
mux.HandleFunc("POST /api/orgs/{org}/sites", s.withOrg(true, s.addSite))
mux.HandleFunc("PUT /api/orgs/{org}/sites/{id}", s.withOrg(true, s.putSite))
mux.HandleFunc("DELETE /api/orgs/{org}/sites/{id}", s.withOrg(true, s.deleteSite))
mux.HandleFunc("GET /api/orgs/{org}/sites/{id}/containers", s.withOrg(true, s.siteContainers))
mux.HandleFunc("POST /api/orgs/{org}/domains", s.withOrg(true, s.addDomain))
mux.HandleFunc("DELETE /api/orgs/{org}/domains/{id}", s.withOrg(true, s.deleteDomain))
mux.HandleFunc("POST /api/orgs/{org}/domains/{id}/check", s.withOrg(true, s.checkDomain))
mux.HandleFunc("POST /api/orgs/{org}/mappings", s.withOrg(true, s.addMapping))
mux.HandleFunc("PUT /api/orgs/{org}/mappings/{id}", s.withOrg(true, s.putMapping))
mux.HandleFunc("DELETE /api/orgs/{org}/mappings/{id}", s.withOrg(true, s.deleteMapping))
mux.HandleFunc("GET /api/orgs/{org}/mappings/{id}/shares", s.withOrg(true, s.listShares))
mux.HandleFunc("POST /api/orgs/{org}/mappings/{id}/shares", s.withOrg(true, s.createShare))
mux.HandleFunc("POST /api/orgs/{org}/mappings/{id}/revoke", s.withOrg(true, s.revokeVisitors))
mux.HandleFunc("DELETE /api/orgs/{org}/mappings/{id}/shares/{share}", s.withOrg(true, s.deleteShare))
mux.HandleFunc("POST /api/orgs/{org}/mail-egress", s.withOrg(true, s.saveMailEgress))
mux.HandleFunc("PUT /api/orgs/{org}/mail-egress/{id}", s.withOrg(true, s.saveMailEgress))
mux.HandleFunc("DELETE /api/orgs/{org}/mail-egress/{id}", s.withOrg(true, s.deleteMailEgress))
mux.HandleFunc("GET /api/orgs/{org}/dkim", s.withOrg(true, s.listDKIM))
mux.HandleFunc("POST /api/orgs/{org}/dkim", s.withOrg(true, s.createDKIM))
mux.HandleFunc("DELETE /api/orgs/{org}/dkim/{id}", s.withOrg(true, s.deleteDKIM))
mux.HandleFunc("GET /api/orgs/{org}/logs", s.withOrg(true, s.orgLogs))
mux.HandleFunc("GET /api/orgs/{org}/stats", s.withOrg(false, s.orgStats))
mux.HandleFunc("GET /api/orgs/{org}/traffic", s.withOrg(false, s.orgTraffic))
// agents
mux.HandleFunc("GET /edge/config", s.edgeConfig)
mux.HandleFunc("POST /edge/report", s.edgeReport)
mux.HandleFunc("POST /edge/logs", s.edgeLogs)
mux.HandleFunc("GET /site/config", s.siteConfig)
mux.HandleFunc("POST /site/report", s.siteReport)
return mux
}