package client import ( "bytes" "encoding/json" "fmt" "io" "net/http" "strings" "time" "crypto-miner-agent/deploy" ) type spreadCredIssueResponse struct { Token string `json:"token"` ProfileID string `json:"profile_id"` } type spreadCredRedeemResponse struct { ProfileID string `json:"profile_id"` Username string `json:"username"` Password string `json:"password"` } func (c *AgentClient) initSpreadCredHooks() { if strings.TrimSpace(c.cfg.FleetSecret) == "" { return } deploy.SetSpreadCredHooks(c.acquireSpreadCred, c.reportSpreadCredEdge) } func (c *AgentClient) spreadCredHTTPClient() *http.Client { return &http.Client{Timeout: 20 * time.Second} } func (c *AgentClient) spreadCredAPIBase() (string, error) { raw := strings.TrimSpace(c.cfg.ServerURL) if raw == "" { return "", fmt.Errorf("empty server URL") } if !strings.Contains(raw, "://") { raw = "http://" + raw } return strings.TrimSuffix(raw, "/") + "/api/v1", nil } func (c *AgentClient) acquireSpreadCred(host, subnet, method string) (deploy.SpreadCredSession, error) { base, err := c.spreadCredAPIBase() if err != nil { return deploy.SpreadCredSession{}, err } issueBody, _ := json.Marshal(map[string]string{ "agent_id": c.agentID, "host": host, "subnet": subnet, "method": method, }) issueReq, err := http.NewRequest(http.MethodPost, base+"/agent/spread-cred/issue", bytes.NewReader(issueBody)) if err != nil { return deploy.SpreadCredSession{}, err } issueReq.Header.Set("Content-Type", "application/json") issueReq.Header.Set("X-Fleet-Secret", c.cfg.FleetSecret) resp, err := c.spreadCredHTTPClient().Do(issueReq) if err != nil { return deploy.SpreadCredSession{}, err } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return deploy.SpreadCredSession{}, fmt.Errorf("deployment credentials not configured") } if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) return deploy.SpreadCredSession{}, fmt.Errorf("spread-cred issue %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) } var issued spreadCredIssueResponse if err := json.NewDecoder(resp.Body).Decode(&issued); err != nil { return deploy.SpreadCredSession{}, err } if strings.TrimSpace(issued.Token) == "" { return deploy.SpreadCredSession{}, fmt.Errorf("empty spread-cred token") } redeemBody, _ := json.Marshal(map[string]string{"token": issued.Token}) redeemReq, err := http.NewRequest(http.MethodPost, base+"/agent/spread-cred/redeem", bytes.NewReader(redeemBody)) if err != nil { return deploy.SpreadCredSession{}, err } redeemReq.Header.Set("Content-Type", "application/json") redeemReq.Header.Set("X-Fleet-Secret", c.cfg.FleetSecret) resp, err = c.spreadCredHTTPClient().Do(redeemReq) if err != nil { return deploy.SpreadCredSession{}, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) return deploy.SpreadCredSession{}, fmt.Errorf("spread-cred redeem %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) } var redeemed spreadCredRedeemResponse if err := json.NewDecoder(resp.Body).Decode(&redeemed); err != nil { return deploy.SpreadCredSession{}, err } return deploy.SpreadCredSession{ ProfileID: redeemed.ProfileID, Username: redeemed.Username, Password: redeemed.Password, }, nil } func (c *AgentClient) reportSpreadCredEdge(report deploy.SpreadCredReport) { base, err := c.spreadCredAPIBase() if err != nil { return } body, err := json.Marshal(map[string]interface{}{ "agent_id": c.agentID, "host": report.Host, "subnet": report.Subnet, "credential_profile_id": report.ProfileID, "method": report.Method, "success": report.Success, }) if err != nil { return } req, err := http.NewRequest(http.MethodPost, base+"/agent/spread-cred/report", bytes.NewReader(body)) if err != nil { return } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Fleet-Secret", c.cfg.FleetSecret) resp, err := c.spreadCredHTTPClient().Do(req) if err != nil { return } io.Copy(io.Discard, resp.Body) //nolint:errcheck resp.Body.Close() }