Add the Hammerhead control plane

Dashboard and API, accounts (organizations, roles, invitations, 2FA, OIDC
single sign-on, API keys, revocable sessions), an encrypted SQLite database,
resource logins, alerts and /metrics, mail egress management with DKIM keys,
and the .env loader. Split out of the original single repository; depends on
Reef for the shared types. The end-to-end tests that run Hammerhead, Blacktip
and Bamboo together live in e2e/.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
VeillaxandClaude Sonnet 5.5 committed 2026-10-26 02:50:03 -06:00
1 parent 3daa368233
commit f521fa8668
35 files changed
+10392 -1

No files matched your search

+140
View File
@@ -0,0 +1,140 @@
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)
}
}
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
index := page(indexHTML)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if 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/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("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
}