Files
incudal/agent/internal/panel/client.go
T
2026-06-20 14:26:29 +08:00

120 lines
3.0 KiB
Go

package panel
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"incudal-agent/internal/config"
"incudal-agent/internal/protocol"
)
const heartbeatPath = "/api/agent/heartbeat"
type Client struct {
panelURL string
agentID string
agentSecret string
httpClient *http.Client
}
type HeartbeatResult struct {
StatusCode int
Body string
OK bool
Upgrade *UpgradeInstruction
LatencyMs int64
}
type UpgradeInstruction struct {
Available bool `json:"available"`
Version string `json:"version"`
URL string `json:"url"`
SHA256 string `json:"sha256"`
Gzip bool `json:"gzip"`
Size int64 `json:"size"`
}
type heartbeatResponse struct {
Upgrade *UpgradeInstruction `json:"upgrade"`
}
func New(cfg config.Config) *Client {
return &Client{
panelURL: strings.TrimRight(cfg.PanelURL, "/"),
agentID: cfg.AgentID,
agentSecret: cfg.AgentSecret,
httpClient: &http.Client{
Timeout: cfg.RequestTimeout,
},
}
}
func (client *Client) Heartbeat(ctx context.Context, payload map[string]any) (HeartbeatResult, error) {
body, err := protocol.CanonicalJSON(payload)
if err != nil {
return HeartbeatResult{}, err
}
timestamp := protocol.NewTimestamp()
nonce, err := protocol.NewNonce()
if err != nil {
return HeartbeatResult{}, err
}
bodyHash := protocol.BodySHA256(body)
signingPayload := protocol.SigningPayload(http.MethodPost, heartbeatPath, timestamp, nonce, bodyHash)
signature := protocol.Signature(client.agentSecret, signingPayload)
request, err := http.NewRequestWithContext(ctx, http.MethodPost, client.panelURL+heartbeatPath, bytes.NewReader(body))
if err != nil {
return HeartbeatResult{}, err
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("x-incudal-agent-id", client.agentID)
request.Header.Set("x-incudal-timestamp", timestamp)
request.Header.Set("x-incudal-nonce", nonce)
request.Header.Set("x-incudal-body-sha256", bodyHash)
request.Header.Set("x-incudal-signature", signature)
startedAt := time.Now()
response, err := client.httpClient.Do(request)
if err != nil {
return HeartbeatResult{}, err
}
defer response.Body.Close()
responseBody, err := io.ReadAll(io.LimitReader(response.Body, 1024*1024))
if err != nil {
return HeartbeatResult{}, err
}
result := HeartbeatResult{
StatusCode: response.StatusCode,
Body: string(responseBody),
OK: response.StatusCode >= 200 && response.StatusCode < 300,
LatencyMs: time.Since(startedAt).Milliseconds(),
}
if !result.OK {
return result, fmt.Errorf("heartbeat failed: status=%d body=%s", response.StatusCode, result.Body)
}
var parsedResponse heartbeatResponse
if err := json.Unmarshal(responseBody, &parsedResponse); err == nil {
result.Upgrade = parsedResponse.Upgrade
}
var parsed map[string]any
if err := json.Unmarshal(responseBody, &parsed); err == nil {
parsed["latencyMs"] = result.LatencyMs
if compact, err := json.Marshal(parsed); err == nil {
result.Body = string(compact)
}
}
return result, nil
}