Bamboo reports where its SSH server listens. When a terminal is opened, Hammerhead lists the session in the site's config and the agent connects out and joins that connection to its SSH server; Hammerhead signs in over it. No port resource or edge is involved. A site whose Bamboo does not report an SSH server still works through an SSH port. The long poll now returns at once for a session the agent has not been told about, so a request between two polls is not missed. The SSH dialog shows whether the SSH server is running, says when no port is needed, and offers the port only for an ordinary ssh client. Builds against Reef v0.1.12 and Bamboo v0.1.9. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
283 lines
7.3 KiB
Go
283 lines
7.3 KiB
Go
package hammerhead
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"git.veillax.com/Veillax/reef/model"
|
|
)
|
|
|
|
// Endpoints used by blacktip and bamboo. They authenticate with the edge/site token.
|
|
|
|
func (s *Server) edgeByToken(r *http.Request) *model.Edge {
|
|
t := bearer(r)
|
|
if t == "" {
|
|
return nil
|
|
}
|
|
for _, e := range s.st.Edges {
|
|
if subtle.ConstantTimeCompare([]byte(e.Token), []byte(t)) == 1 {
|
|
return e
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) siteByToken(r *http.Request) *model.Site {
|
|
t := bearer(r)
|
|
if t == "" {
|
|
return nil
|
|
}
|
|
for _, e := range s.st.Sites {
|
|
if !e.Local && subtle.ConstantTimeCompare([]byte(e.Token), []byte(t)) == 1 {
|
|
return e
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) edgeConfig(w http.ResponseWriter, r *http.Request) {
|
|
s.mu.Lock()
|
|
e := s.edgeByToken(r)
|
|
s.mu.Unlock()
|
|
if e == nil {
|
|
fail(w, 401, errors.New("bad edge token"))
|
|
return
|
|
}
|
|
have, _ := strconv.ParseInt(r.URL.Query().Get("v"), 10, 64)
|
|
if !s.wait(r, have) {
|
|
s.mu.Lock()
|
|
if e := s.edgeByToken(r); e != nil {
|
|
e.LastSeen = time.Now()
|
|
}
|
|
s.mu.Unlock()
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if e = s.edgeByToken(r); e == nil {
|
|
fail(w, 401, errors.New("bad edge token"))
|
|
return
|
|
}
|
|
e.LastSeen = time.Now()
|
|
cfg := model.EdgeConfig{
|
|
Version: s.st.Version, EdgeID: e.ID, AuthKey: e.AuthKey, Dashboard: s.st.Settings.DashboardURL,
|
|
Mappings: []model.Mapping{}, Peers: []model.WGPeer{}, Local: []string{}, TunnelIP: e.TunnelIP,
|
|
AcmeEmail: s.st.Settings.AcmeEmail, AcmeStages: s.st.Settings.AcmeStaging,
|
|
}
|
|
for _, m := range s.st.Mappings {
|
|
if m.EdgeID == e.ID && m.Enabled {
|
|
cfg.Mappings = append(cfg.Mappings, *m)
|
|
}
|
|
}
|
|
if m := s.selfMapping(e); m != nil {
|
|
cfg.Mappings = append(cfg.Mappings, *m)
|
|
}
|
|
cfg.UpdateID = e.UpdateID
|
|
cfg.MailEgress = []model.MailPolicy{}
|
|
for _, x := range s.st.MailEgress {
|
|
if x.EdgeID == e.ID && x.Enabled {
|
|
cfg.MailEgress = append(cfg.MailEgress, model.MailPolicy{ID: x.ID, SiteID: x.SiteID, SourceIP: x.SourceIP, HeloName: x.HeloName,
|
|
Smarthost: x.Smarthost, RequireTLS: x.RequireTLS, RatePerMinute: x.RatePerMinute, DKIM: s.signersLocked(x.OrgID)})
|
|
}
|
|
}
|
|
for _, st := range s.st.Sites {
|
|
switch {
|
|
case st.Local:
|
|
if st.EdgeID == e.ID {
|
|
cfg.Local = append(cfg.Local, st.ID)
|
|
}
|
|
case st.PublicKey != "" && st.TunnelIP != "" && s.edgeAllowedFor(e, st.OrgID):
|
|
cfg.Peers = append(cfg.Peers, model.WGPeer{SiteID: st.ID, PublicKey: st.PublicKey, IP: st.TunnelIP})
|
|
}
|
|
}
|
|
writeJSON(w, 200, cfg)
|
|
}
|
|
|
|
func (s *Server) edgeReport(w http.ResponseWriter, r *http.Request) {
|
|
var in model.EdgeReport
|
|
if !decode(w, r, &in) {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
e := s.edgeByToken(r)
|
|
if e == nil {
|
|
fail(w, 401, errors.New("bad edge token"))
|
|
return
|
|
}
|
|
e.LastSeen = time.Now()
|
|
e.IPs = in.IPs
|
|
oldVersion := e.Version
|
|
answered := noteUpdateReport(&e.Version, &e.AutoUpdate, &e.UpdateID, &e.UpdateMsg, in.Version, in.AutoUpdate, in.UpdateID, in.UpdateMsg)
|
|
s.recordUpdateProgress(e.ID, in.UpdateID, in.UpdateStage, in.UpdatePct, in.UpdateLog, oldVersion, e.Version)
|
|
for _, x := range s.st.MailEgress {
|
|
if x.EdgeID == e.ID {
|
|
if chk, found := in.MailChecks[x.ID]; found {
|
|
s.mailChecks[x.ID] = chk
|
|
}
|
|
}
|
|
}
|
|
for _, m := range s.st.Mappings {
|
|
if m.EdgeID != e.ID {
|
|
continue
|
|
}
|
|
if st, found := in.Status[m.ID]; found {
|
|
s.status[m.ID] = st
|
|
} else {
|
|
delete(s.status, m.ID)
|
|
}
|
|
if d, found := in.Stats[m.ID]; found {
|
|
t := s.st.Totals[m.ID]
|
|
if t == nil {
|
|
t = &Total{}
|
|
s.st.Totals[m.ID] = t
|
|
}
|
|
t.Conns += d.Conns
|
|
t.BytesIn += d.BytesIn
|
|
t.BytesOut += d.BytesOut
|
|
s.db.addTraffic(m.ID, d)
|
|
}
|
|
}
|
|
for _, st := range s.st.Sites {
|
|
if st.Local && st.EdgeID == e.ID {
|
|
s.containers[st.ID] = in.Containers
|
|
}
|
|
}
|
|
if e.PublicKey != in.PublicKey {
|
|
e.PublicKey = in.PublicKey
|
|
s.bump() // sites need the edge's new key
|
|
} else if answered {
|
|
s.bump() // the edge's config no longer carries the update request
|
|
} else {
|
|
s.save()
|
|
}
|
|
ok(w)
|
|
}
|
|
|
|
func (s *Server) edgeLogs(w http.ResponseWriter, r *http.Request) {
|
|
var in []model.LogEntry
|
|
if !decode(w, r, &in) {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
e := s.edgeByToken(r)
|
|
if e == nil {
|
|
s.mu.Unlock()
|
|
fail(w, 401, errors.New("bad edge token"))
|
|
return
|
|
}
|
|
out := make([]model.LogEntry, 0, len(in))
|
|
for _, l := range in {
|
|
if m := s.findMapping(l.Mapping); m != nil && m.EdgeID == e.ID {
|
|
l.OrgID = m.OrgID
|
|
} else if x := s.findMailEgress(l.Mapping); x != nil && x.EdgeID == e.ID {
|
|
l.OrgID = x.OrgID
|
|
} else {
|
|
continue
|
|
}
|
|
out = append(out, l)
|
|
}
|
|
s.mu.Unlock()
|
|
s.logs.addAccess(out)
|
|
ok(w)
|
|
}
|
|
|
|
func (s *Server) siteConfig(w http.ResponseWriter, r *http.Request) {
|
|
s.mu.Lock()
|
|
st := s.siteByToken(r)
|
|
s.mu.Unlock()
|
|
if st == nil {
|
|
fail(w, 401, errors.New("bad site token"))
|
|
return
|
|
}
|
|
have, _ := strconv.ParseInt(r.URL.Query().Get("v"), 10, 64)
|
|
if !s.waitUntil(r, have, func() bool { return s.newTerminalsLocked(st.ID) }) {
|
|
s.mu.Lock()
|
|
if st := s.siteByToken(r); st != nil {
|
|
st.LastSeen = time.Now()
|
|
}
|
|
s.mu.Unlock()
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if st = s.siteByToken(r); st == nil {
|
|
fail(w, 401, errors.New("bad site token"))
|
|
return
|
|
}
|
|
st.LastSeen = time.Now()
|
|
cfg := model.SiteConfig{Version: s.st.Version, SiteID: st.ID, TunnelIP: st.TunnelIP, Edges: []model.EdgeRef{}, MailEgress: []model.MailSpec{}, UpdateID: st.UpdateID}
|
|
seen := map[string]bool{}
|
|
addEdge := func(id string) {
|
|
if seen[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
if e := s.findEdge(id); e != nil {
|
|
cfg.Edges = append(cfg.Edges, model.EdgeRef{ID: e.ID, Name: e.Name, Endpoint: e.TunnelAddr, PublicKey: e.PublicKey, IP: e.TunnelIP})
|
|
}
|
|
}
|
|
for _, m := range s.st.Mappings {
|
|
for _, t := range m.Targets {
|
|
if t.SiteID == st.ID {
|
|
addEdge(m.EdgeID)
|
|
}
|
|
}
|
|
}
|
|
for _, x := range s.st.MailEgress {
|
|
if x.SiteID == st.ID && x.Enabled {
|
|
addEdge(x.EdgeID)
|
|
cfg.MailEgress = append(cfg.MailEgress, model.MailSpec{ID: x.ID, EdgeID: x.EdgeID, Listen: x.Listen, Username: x.Username, Password: x.Password})
|
|
}
|
|
}
|
|
cfg.Terminals = s.waitingTerminalsLocked(st.ID)
|
|
writeJSON(w, 200, cfg)
|
|
}
|
|
|
|
func (s *Server) siteReport(w http.ResponseWriter, r *http.Request) {
|
|
var in model.SiteReport
|
|
if !decode(w, r, &in) {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
st := s.siteByToken(r)
|
|
if st == nil {
|
|
fail(w, 401, errors.New("bad site token"))
|
|
return
|
|
}
|
|
st.LastSeen = time.Now()
|
|
if in.Connected == nil {
|
|
in.Connected = []string{}
|
|
}
|
|
st.Connected = in.Connected
|
|
oldVersion := st.Version
|
|
answered := noteUpdateReport(&st.Version, &st.AutoUpdate, &st.UpdateID, &st.UpdateMsg, in.Version, in.AutoUpdate, in.UpdateID, in.UpdateMsg)
|
|
s.recordUpdateProgress(st.ID, in.UpdateID, in.UpdateStage, in.UpdatePct, in.UpdateLog, oldVersion, st.Version)
|
|
s.containers[st.ID] = in.Containers
|
|
s.discovered[st.ID] = cleanDiscovered(in.Discovered)
|
|
st.SSH, st.SSHRelay = clipText(in.SSH, 100), in.SSHRelay
|
|
for _, x := range s.st.MailEgress {
|
|
if x.SiteID != st.ID {
|
|
continue
|
|
}
|
|
if msg, known := in.MailEgress[x.ID]; known {
|
|
s.mailStatus[x.ID] = msg
|
|
} else {
|
|
delete(s.mailStatus, x.ID)
|
|
}
|
|
}
|
|
if in.PublicKey != "" && st.PublicKey != in.PublicKey {
|
|
st.PublicKey = in.PublicKey
|
|
s.bump() // every edge this site talks to needs the new key
|
|
} else if answered {
|
|
s.bump() // the site's config no longer carries the update request
|
|
}
|
|
ok(w)
|
|
}
|