120 lines
3.0 KiB
Go
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
|
|
}
|