first commit
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user