mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 06:16:37 +08:00
4e8ec23264
完成 V2 Phase 0 安全与一致性前置:统一真实归档限额、流式拉取、候选裁剪、保留上传删除语义及 Pages 路由引用锁。
238 lines
7.4 KiB
Go
238 lines
7.4 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Package httpclient provides an authenticated HTTP client for the agent.
|
|
package httpclient
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
|
|
edgehttp "github.com/Rain-kl/Wavelet/internal/apps/edge/httpclient"
|
|
)
|
|
|
|
const pagesControlResponseMaxBytes = int64(64 * 1024)
|
|
|
|
// Client is a HTTP client used by the agent to communicate with the control plane server.
|
|
type Client struct {
|
|
base *edgehttp.Client
|
|
}
|
|
|
|
// New creates a new Client instance with the specified base URL, token, and timeout.
|
|
func New(baseURL string, token string, timeout time.Duration) *Client {
|
|
return &Client{
|
|
base: edgehttp.New(baseURL, token, timeout, "X-Agent-Token"),
|
|
}
|
|
}
|
|
|
|
// RegisterNode registers the agent node with the control plane server.
|
|
func (c *Client) RegisterNode(ctx context.Context, payload protocol.NodePayload) (*protocol.RegisterNodeResponse, error) {
|
|
resp := protocol.APIResponse[protocol.RegisterNodeResponse]{}
|
|
if err := c.base.PostJSON(ctx, "/api/v1/agent/nodes/register", payload, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := edgehttp.APIError(resp.ErrorMsg); err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp.Data, nil
|
|
}
|
|
|
|
// Heartbeat sends a heartbeat payload to the control plane and returns the response result.
|
|
func (c *Client) Heartbeat(ctx context.Context, payload protocol.NodePayload) (*protocol.HeartbeatResult, error) {
|
|
resp := protocol.APIResponse[protocol.HeartbeatData]{}
|
|
if err := c.base.PostJSON(ctx, "/api/v1/agent/nodes/heartbeat", payload, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := edgehttp.APIError(resp.ErrorMsg); err != nil {
|
|
return nil, err
|
|
}
|
|
return &protocol.HeartbeatResult{
|
|
AgentSettings: resp.Data.AgentSettings,
|
|
ActiveConfig: resp.Data.ActiveConfig,
|
|
WAFIPGroups: resp.Data.WAFIPGroups,
|
|
}, nil
|
|
}
|
|
|
|
// GetActiveConfig retrieves the current active configuration from the control plane server.
|
|
func (c *Client) GetActiveConfig(ctx context.Context) (*protocol.ActiveConfigResponse, error) {
|
|
resp := protocol.APIResponse[protocol.ActiveConfigResponse]{}
|
|
if err := c.base.GetJSON(ctx, "/api/v1/agent/config-versions/active", &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := edgehttp.APIError(resp.ErrorMsg); err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp.Data, nil
|
|
}
|
|
|
|
// ReportApplyLog reports the configuration application logs back to the control plane.
|
|
func (c *Client) ReportApplyLog(ctx context.Context, payload protocol.ApplyLogPayload) error {
|
|
resp := protocol.APIResponse[json.RawMessage]{}
|
|
if err := c.base.PostJSON(ctx, "/api/v1/agent/apply-logs", payload, &resp); err != nil {
|
|
return err
|
|
}
|
|
return edgehttp.APIError(resp.ErrorMsg)
|
|
}
|
|
|
|
// SyncWAFIPGroups synchronizes WAF IP groups with the control plane server.
|
|
func (c *Client) SyncWAFIPGroups(ctx context.Context, payload protocol.WAFIPGroupSyncRequest) (*protocol.WAFIPGroupSyncResponse, error) {
|
|
resp := protocol.APIResponse[protocol.WAFIPGroupSyncResponse]{}
|
|
if err := c.base.PostJSON(ctx, "/api/v1/agent/waf/ip-groups/sync", payload, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := edgehttp.APIError(resp.ErrorMsg); err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp.Data, nil
|
|
}
|
|
|
|
// GetPagesDeploymentHash returns the upload SHA-256 hash for the given Pages deployment ID.
|
|
func (c *Client) GetPagesDeploymentHash(ctx context.Context, deploymentID uint) (string, error) {
|
|
resp := protocol.APIResponse[protocol.PagesDeploymentHashResponse]{}
|
|
if err := c.base.GetJSON(ctx, fmt.Sprintf("/api/v1/agent/pages/deployments/%d/hash", deploymentID), &resp); err != nil {
|
|
return "", err
|
|
}
|
|
if err := edgehttp.APIError(resp.ErrorMsg); err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Data.Hash, nil
|
|
}
|
|
|
|
// DownloadPagesDeploymentPackage streams the deployment package into dst while
|
|
// enforcing maxBytes against both advertised and actual response sizes.
|
|
func (c *Client) DownloadPagesDeploymentPackage(
|
|
ctx context.Context,
|
|
deploymentID uint,
|
|
dst io.Writer,
|
|
maxBytes int64,
|
|
) (int64, error) {
|
|
return c.downloadPagesPackage(
|
|
ctx,
|
|
fmt.Sprintf("/api/v1/agent/pages/deployments/%d/package", deploymentID),
|
|
dst,
|
|
maxBytes,
|
|
)
|
|
}
|
|
|
|
// GetPagesProjectLatestHash returns the active deployment package hash for a Pages project.
|
|
func (c *Client) GetPagesProjectLatestHash(ctx context.Context, projectID uint) (*protocol.PagesProjectLatestHashResponse, error) {
|
|
res, err := c.base.DoRaw(
|
|
ctx,
|
|
http.MethodGet,
|
|
fmt.Sprintf("/api/v1/agent/pages/projects/%d/latest/hash", projectID),
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = res.Body.Close() }()
|
|
body, err := readPagesControlResponse(res, pagesControlResponseMaxBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if res.StatusCode != http.StatusOK {
|
|
return nil, edgehttp.ReadBodyError(body, res.Status)
|
|
}
|
|
resp := protocol.APIResponse[protocol.PagesProjectLatestHashResponse]{}
|
|
if err := json.Unmarshal(body, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := edgehttp.APIError(resp.ErrorMsg); err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp.Data, nil
|
|
}
|
|
|
|
// DownloadPagesProjectLatestPackage streams the active deployment package into
|
|
// dst while enforcing maxBytes against both advertised and actual sizes.
|
|
func (c *Client) DownloadPagesProjectLatestPackage(
|
|
ctx context.Context,
|
|
projectID uint,
|
|
dst io.Writer,
|
|
maxBytes int64,
|
|
) (int64, error) {
|
|
return c.downloadPagesPackage(
|
|
ctx,
|
|
fmt.Sprintf("/api/v1/agent/pages/projects/%d/latest/package", projectID),
|
|
dst,
|
|
maxBytes,
|
|
)
|
|
}
|
|
|
|
func (c *Client) downloadPagesPackage(
|
|
ctx context.Context,
|
|
path string,
|
|
dst io.Writer,
|
|
maxBytes int64,
|
|
) (int64, error) {
|
|
if dst == nil {
|
|
return 0, errors.New("pages package destination is required")
|
|
}
|
|
if maxBytes <= 0 {
|
|
return 0, errors.New("pages package byte limit must be positive")
|
|
}
|
|
res, err := c.base.DoRaw(ctx, http.MethodGet, path, nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer func() { _ = res.Body.Close() }()
|
|
if res.StatusCode != http.StatusOK {
|
|
body, readErr := readPagesControlResponse(res, pagesControlResponseMaxBytes)
|
|
if readErr != nil {
|
|
return 0, readErr
|
|
}
|
|
return 0, edgehttp.ReadBodyError(body, res.Status)
|
|
}
|
|
return copyPagesPackageResponse(dst, res, maxBytes)
|
|
}
|
|
|
|
func readPagesControlResponse(res *http.Response, maxBytes int64) ([]byte, error) {
|
|
if res.ContentLength > maxBytes {
|
|
return nil, fmt.Errorf(
|
|
"pages control response Content-Length %d exceeds limit %d",
|
|
res.ContentLength,
|
|
maxBytes,
|
|
)
|
|
}
|
|
limited := &io.LimitedReader{R: res.Body, N: maxBytes + 1}
|
|
body, err := io.ReadAll(limited)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read pages control response: %w", err)
|
|
}
|
|
if int64(len(body)) > maxBytes {
|
|
return nil, fmt.Errorf("pages control response body exceeds limit %d", maxBytes)
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
func copyPagesPackageResponse(dst io.Writer, res *http.Response, maxBytes int64) (int64, error) {
|
|
if res.ContentLength > maxBytes {
|
|
return 0, fmt.Errorf(
|
|
"pages package Content-Length %d exceeds limit %d",
|
|
res.ContentLength,
|
|
maxBytes,
|
|
)
|
|
}
|
|
|
|
limited := &io.LimitedReader{R: res.Body, N: maxBytes + 1}
|
|
written, err := io.Copy(dst, limited)
|
|
if err != nil {
|
|
return written, fmt.Errorf("stream pages package: %w", err)
|
|
}
|
|
if written > maxBytes {
|
|
return written, fmt.Errorf("pages package body exceeds limit %d", maxBytes)
|
|
}
|
|
return written, nil
|
|
}
|
|
|
|
// SetToken updates the authentication token used for API requests.
|
|
func (c *Client) SetToken(token string) {
|
|
c.base.SetToken(token)
|
|
}
|