mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 08:36:37 +08:00
fix lint
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// Package config provides shared configuration types for edge applications.
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -8,8 +9,11 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// MillisecondDuration is a time.Duration that marshals to/from JSON as an integer number of milliseconds
|
||||
// or as a Go duration string (e.g. "1s", "500ms").
|
||||
type MillisecondDuration time.Duration
|
||||
|
||||
// Duration returns the underlying time.Duration value.
|
||||
func (d MillisecondDuration) Duration() time.Duration {
|
||||
return time.Duration(d)
|
||||
}
|
||||
@@ -18,6 +22,7 @@ func (d MillisecondDuration) String() string {
|
||||
return time.Duration(d).String()
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes either a numeric millisecond value or a quoted Go duration string.
|
||||
func (d *MillisecondDuration) UnmarshalJSON(data []byte) error {
|
||||
raw := strings.TrimSpace(string(data))
|
||||
if raw == "" || raw == "null" {
|
||||
@@ -49,6 +54,7 @@ func (d *MillisecondDuration) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the duration as an integer number of milliseconds.
|
||||
func (d MillisecondDuration) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(time.Duration(d).Milliseconds())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package heartbeat handles periodic heartbeat and update checks.
|
||||
package heartbeat
|
||||
|
||||
import (
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
edgeupdater "github.com/Rain-kl/Wavelet/internal/apps/edge/updater"
|
||||
)
|
||||
|
||||
// AutoUpdateSettings defines the settings for automatic edge updates.
|
||||
type AutoUpdateSettings struct {
|
||||
AutoUpdate bool
|
||||
UpdateNow bool
|
||||
@@ -16,6 +18,7 @@ type AutoUpdateSettings struct {
|
||||
UpdateTag string
|
||||
}
|
||||
|
||||
// TryAutoUpdate attempts to check and apply auto updates for the edge service.
|
||||
func TryAutoUpdate(ctx context.Context, updater *edgeupdater.Service, settings *AutoUpdateSettings, logLabel string) {
|
||||
if settings == nil || updater == nil {
|
||||
return
|
||||
@@ -38,4 +41,4 @@ func TryAutoUpdate(ctx context.Context, updater *edgeupdater.Service, settings *
|
||||
if err != nil {
|
||||
slog.Error(logLabel+" update check failed", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package httpclient provides an authenticated HTTP client for edge services.
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client is an HTTP client wrapper for communicating with remote HTTP services.
|
||||
type Client struct {
|
||||
baseURL string
|
||||
token string
|
||||
@@ -19,6 +21,7 @@ type Client struct {
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// New creates a new Client instance.
|
||||
func New(baseURL, token string, timeout time.Duration, authHeader string) *Client {
|
||||
return &Client{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
@@ -28,11 +31,13 @@ func New(baseURL, token string, timeout time.Duration, authHeader string) *Clien
|
||||
}
|
||||
}
|
||||
|
||||
// SetToken updates the client auth token.
|
||||
func (c *Client) SetToken(token string) {
|
||||
c.token = strings.TrimSpace(token)
|
||||
slog.Debug("http client token updated")
|
||||
}
|
||||
|
||||
// GetJSON sends a GET request and decodes the response body into target.
|
||||
func (c *Client) GetJSON(ctx context.Context, path string, target any) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
|
||||
if err != nil {
|
||||
@@ -42,6 +47,7 @@ func (c *Client) GetJSON(ctx context.Context, path string, target any) error {
|
||||
return c.do(req, target)
|
||||
}
|
||||
|
||||
// PostJSON sends a POST request with JSON body and decodes the response body into target.
|
||||
func (c *Client) PostJSON(ctx context.Context, path string, body any, target any) error {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
@@ -56,6 +62,7 @@ func (c *Client) PostJSON(ctx context.Context, path string, body any, target any
|
||||
return c.do(req, target)
|
||||
}
|
||||
|
||||
// DoRaw performs an HTTP request with custom headers and returns the raw response.
|
||||
func (c *Client) DoRaw(ctx context.Context, method, path string, headers map[string]string) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, nil)
|
||||
if err != nil {
|
||||
@@ -80,12 +87,11 @@ func (c *Client) do(req *http.Request, target any) error {
|
||||
slog.Error("http request failed", "method", req.Method, "path", req.URL.Path, "error", err)
|
||||
return err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
defer func() {
|
||||
if err := res.Body.Close(); err != nil {
|
||||
slog.Error("failed to close response body", "error", err)
|
||||
}
|
||||
}(res.Body)
|
||||
}()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
@@ -106,6 +112,7 @@ func (c *Client) do(req *http.Request, target any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// APIError creates a new API error with the given message if it is not empty.
|
||||
func APIError(msg string) error {
|
||||
if strings.TrimSpace(msg) == "" {
|
||||
return nil
|
||||
@@ -113,6 +120,7 @@ func APIError(msg string) error {
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// ReadBodyError parses the error message from the response body, or returns the fallback message.
|
||||
func ReadBodyError(body []byte, fallback string) error {
|
||||
var errBody struct {
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
@@ -123,10 +131,11 @@ func ReadBodyError(body []byte, fallback string) error {
|
||||
return errors.New(fallback)
|
||||
}
|
||||
|
||||
// ReadHTTPError reads the error message from the HTTP response.
|
||||
func ReadHTTPError(res *http.Response) error {
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
return ReadBodyError(body, res.Status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package logging configures structured logging for edge applications.
|
||||
package logging
|
||||
|
||||
import (
|
||||
@@ -6,10 +7,12 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Options holds configuration options for the structured logger.
|
||||
type Options struct {
|
||||
AddSource bool
|
||||
}
|
||||
|
||||
// Setup initialises the default slog handler using the given options and the LOG_LEVEL environment variable.
|
||||
func Setup(opts Options) {
|
||||
handlerOpts := &slog.HandlerOptions{
|
||||
AddSource: opts.AddSource,
|
||||
@@ -19,6 +22,7 @@ func Setup(opts Options) {
|
||||
slog.SetDefault(slog.New(handler))
|
||||
}
|
||||
|
||||
// ParseLevel converts a log-level string (e.g. "debug", "warn") to the corresponding slog.Level.
|
||||
func ParseLevel(value string) slog.Level {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "debug":
|
||||
@@ -30,4 +34,4 @@ func ParseLevel(value string) slog.Level {
|
||||
default:
|
||||
return slog.LevelInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package nodeip detects the preferred public IP address for edge nodes.
|
||||
package nodeip
|
||||
|
||||
import (
|
||||
@@ -9,20 +10,36 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/pkg/geoip/iputil"
|
||||
)
|
||||
|
||||
const (
|
||||
outboundIPLookupTimeout = 5 * time.Second
|
||||
publicIPPriorityScore = 2 // matches iputil.Score for public IPv4 addresses
|
||||
)
|
||||
|
||||
// LookupOutboundIP and LookupLocalIP are the provider functions used to detect the node's outbound/local IP.
|
||||
// They are package-level variables so they can be overridden in tests.
|
||||
var (
|
||||
LookupOutboundIP = geoip.GetOutboundIP
|
||||
LookupLocalIP = DetectLocal
|
||||
)
|
||||
|
||||
// Detect returns the best available outbound or local IPv4 address for this node.
|
||||
func Detect() string {
|
||||
if ip := detectOutbound(); ip != "" {
|
||||
if ip := detectOutbound(context.Background()); ip != "" {
|
||||
return ip
|
||||
}
|
||||
return LookupLocalIP()
|
||||
}
|
||||
|
||||
func detectOutbound() string {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
// DetectWithContext returns the best available outbound or local IPv4 address, respecting ctx for cancellation.
|
||||
func DetectWithContext(ctx context.Context) string {
|
||||
if ip := detectOutbound(ctx); ip != "" {
|
||||
return ip
|
||||
}
|
||||
return LookupLocalIP()
|
||||
}
|
||||
|
||||
func detectOutbound(ctx context.Context) string {
|
||||
ctx, cancel := context.WithTimeout(ctx, outboundIPLookupTimeout)
|
||||
defer cancel()
|
||||
ip, err := LookupOutboundIP(ctx)
|
||||
if err != nil || ip == nil {
|
||||
@@ -31,6 +48,7 @@ func detectOutbound() string {
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
// DetectLocal returns the highest-priority non-loopback local IPv4 address found on system interfaces.
|
||||
func DetectLocal() string {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
@@ -60,10 +78,10 @@ func DetectLocal() string {
|
||||
bestIP = ipv4.String()
|
||||
bestPriority = priority
|
||||
}
|
||||
if bestPriority == 2 {
|
||||
if bestPriority == publicIPPriorityScore {
|
||||
return bestIP
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestIP
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Package observability provides helpers that read Linux /proc and /sys metrics for system monitoring.
|
||||
package observability
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -10,13 +12,20 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const (
|
||||
memInfoMinFieldCount = 2
|
||||
cpuStatMinFieldCount = 5
|
||||
netDevMinFieldCount = 16
|
||||
diskStatsMinFieldCount = 14
|
||||
)
|
||||
|
||||
// ReadLinuxOSRelease returns the OS name and version from /etc/os-release.
|
||||
func ReadLinuxOSRelease() (string, string) {
|
||||
file, err := os.Open("/etc/os-release")
|
||||
if err != nil {
|
||||
return runtime.GOOS, ""
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
values := make(map[string]string)
|
||||
scanner := bufio.NewScanner(file)
|
||||
@@ -47,7 +56,7 @@ func ReadLinuxCPUModel() string {
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
@@ -68,7 +77,7 @@ func ReadMemInfo() (int64, int64) {
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
var memTotalKB int64
|
||||
var memAvailableKB int64
|
||||
@@ -96,7 +105,7 @@ func ReadMemInfo() (int64, int64) {
|
||||
|
||||
func parseMemInfoValue(line string) int64 {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
if len(fields) < memInfoMinFieldCount {
|
||||
return 0
|
||||
}
|
||||
value, err := strconv.ParseInt(fields[1], 10, 64)
|
||||
@@ -135,7 +144,7 @@ func ReadLinuxCPUStat() (uint64, uint64) {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 5 {
|
||||
if len(fields) < cpuStatMinFieldCount {
|
||||
return 0, 0
|
||||
}
|
||||
var total uint64
|
||||
@@ -161,7 +170,7 @@ func ReadLinuxNetworkTotals() (int64, int64) {
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
var rx int64
|
||||
var tx int64
|
||||
@@ -179,7 +188,7 @@ func ReadLinuxNetworkTotals() (int64, int64) {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(data)
|
||||
if len(fields) < 16 {
|
||||
if len(fields) < netDevMinFieldCount {
|
||||
continue
|
||||
}
|
||||
rxValue, err := strconv.ParseInt(fields[0], 10, 64)
|
||||
@@ -200,14 +209,14 @@ func ReadLinuxDiskTotals() (int64, int64) {
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
var readBytes int64
|
||||
var writeBytes int64
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) < 14 {
|
||||
if len(fields) < diskStatsMinFieldCount {
|
||||
continue
|
||||
}
|
||||
device := fields[2]
|
||||
@@ -249,8 +258,8 @@ func StatFilesystem(path string) (int64, int64) {
|
||||
if err := syscall.Statfs(absPath, &stat); err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
total := int64(stat.Blocks) * int64(stat.Bsize)
|
||||
free := int64(stat.Bavail) * int64(stat.Bsize)
|
||||
total := multiplyUint64ToInt64(stat.Blocks, uint64(stat.Bsize))
|
||||
free := multiplyUint64ToInt64(stat.Bavail, uint64(stat.Bsize))
|
||||
used := total - free
|
||||
if used < 0 {
|
||||
used = 0
|
||||
@@ -258,11 +267,21 @@ func StatFilesystem(path string) (int64, int64) {
|
||||
return total, used
|
||||
}
|
||||
|
||||
func multiplyUint64ToInt64(a uint64, b uint64) int64 {
|
||||
if a == 0 || b == 0 {
|
||||
return 0
|
||||
}
|
||||
if a > math.MaxInt64/b {
|
||||
return math.MaxInt64
|
||||
}
|
||||
return int64(a * b) //nolint:gosec // product is bounded to math.MaxInt64 above
|
||||
}
|
||||
|
||||
// ReadFirstLine reads and returns the trimmed first line of a file.
|
||||
func ReadFirstLine(path string) string {
|
||||
content, err := os.ReadFile(path)
|
||||
content, err := os.ReadFile(path) //nolint:gosec // path is a fixed /proc or /sys path from internal callers, not user input
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(content))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package runner provides shared WebSocket reconnect helpers for edge daemons.
|
||||
package runner
|
||||
|
||||
import (
|
||||
@@ -6,10 +7,12 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// WSConnection defines the minimum interface required for a WebSocket connection that can be closed.
|
||||
type WSConnection interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
// WSReconnectConfig specifies configuration parameters for the WebSocket reconnect loop.
|
||||
type WSReconnectConfig struct {
|
||||
ComponentName string
|
||||
ConnectBackoff time.Duration
|
||||
@@ -17,6 +20,7 @@ type WSReconnectConfig struct {
|
||||
OnShutdown func()
|
||||
}
|
||||
|
||||
// RunWSReconnectLoop runs a loop that attempts to keep a WebSocket connection active, automatically reconnecting when closed or failed.
|
||||
func RunWSReconnectLoop(ctx context.Context, cfg WSReconnectConfig,
|
||||
connect func(context.Context) (WSConnection, error),
|
||||
handle func(context.Context, WSConnection),
|
||||
@@ -40,6 +44,7 @@ func RunWSReconnectLoop(ctx context.Context, cfg WSReconnectConfig,
|
||||
}
|
||||
return ctx.Err()
|
||||
default:
|
||||
// Continue reconnect loop
|
||||
}
|
||||
|
||||
conn, err := connect(ctx)
|
||||
@@ -56,9 +61,10 @@ func RunWSReconnectLoop(ctx context.Context, cfg WSReconnectConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// SleepContext pauses execution for the given duration or until the context is canceled.
|
||||
func SleepContext(ctx context.Context, d time.Duration) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(d):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//go:build !windows
|
||||
|
||||
// Package updater provides capabilities to check for, download, and apply updates.
|
||||
package updater
|
||||
|
||||
import (
|
||||
@@ -33,7 +34,7 @@ func replaceAndRestart(execPath string, tmpPath string) error {
|
||||
if err := removeBackupBinary(backupPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := syscall.Exec(execPath, os.Args, os.Environ()); err != nil {
|
||||
if err := syscall.Exec(execPath, os.Args, os.Environ()); err != nil { //nolint:gosec // execPath is the validated edge updater binary path
|
||||
return fmt.Errorf("exec restart: %w", err)
|
||||
}
|
||||
return fmt.Errorf("unreachable after exec")
|
||||
@@ -48,4 +49,4 @@ func removeBackupBinary(path string) error {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package updater provides capabilities to check for, download, and apply updates.
|
||||
package updater
|
||||
|
||||
import (
|
||||
@@ -17,16 +18,23 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/pkg/utils"
|
||||
)
|
||||
|
||||
const maxChecksumAssetSize = 64 * 1024
|
||||
const (
|
||||
maxChecksumAssetSize = 64 * 1024
|
||||
goosWindows = "windows"
|
||||
updateTmpFilePerm = 0o600
|
||||
updateBinaryFilePerm = 0o755
|
||||
)
|
||||
|
||||
var replaceAndRestartFunc = replaceAndRestart
|
||||
|
||||
// Config defines the configuration for the update service.
|
||||
type Config struct {
|
||||
LocalVersion string
|
||||
AssetPrefix string
|
||||
LogLabel string
|
||||
}
|
||||
|
||||
// Service handles checking and applying application binary updates.
|
||||
type Service struct {
|
||||
httpClient *http.Client
|
||||
lastCheckKey string
|
||||
@@ -35,6 +43,7 @@ type Service struct {
|
||||
logLabel string
|
||||
}
|
||||
|
||||
// New creates a new updater Service with the provided configuration.
|
||||
func New(cfg Config) *Service {
|
||||
return &Service{
|
||||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||
@@ -44,6 +53,7 @@ func New(cfg Config) *Service {
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateOptions specifies parameters for checking and applying updates.
|
||||
type UpdateOptions struct {
|
||||
Channel string
|
||||
TagName string
|
||||
@@ -62,6 +72,7 @@ type githubAsset struct {
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
}
|
||||
|
||||
// CheckAndUpdate checks for a newer release on GitHub and performs an update if available.
|
||||
func (s *Service) CheckAndUpdate(ctx context.Context, repo string, options UpdateOptions) error {
|
||||
release, err := s.getRelease(ctx, repo, options)
|
||||
if err != nil {
|
||||
@@ -152,7 +163,7 @@ func (s *Service) getLatestPreviewRelease(ctx context.Context, repo string) (*gi
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("github api returned %s", resp.Status)
|
||||
@@ -188,12 +199,11 @@ func (s *Service) fetchReleaseFromURL(ctx context.Context, url string) (*githubR
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
slog.Error("failed to close response body", "error", err)
|
||||
}
|
||||
}(resp.Body)
|
||||
}()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
@@ -222,7 +232,7 @@ func (s *Service) downloadChecksum(ctx context.Context, url string, assetName st
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("checksum download returned %s", resp.Status)
|
||||
@@ -309,37 +319,37 @@ func (s *Service) downloadAndRestart(ctx context.Context, url string, expectedCh
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("download returned %s", resp.Status)
|
||||
}
|
||||
|
||||
tmpPath := targetPath + ".update"
|
||||
if runtime.GOOS == "windows" && !strings.HasSuffix(strings.ToLower(tmpPath), ".exe") {
|
||||
if runtime.GOOS == goosWindows && !strings.HasSuffix(strings.ToLower(tmpPath), ".exe") {
|
||||
tmpPath += ".exe"
|
||||
}
|
||||
tmpFile, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
|
||||
tmpFile, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, updateTmpFilePerm) //nolint:gosec // tmpPath is derived from the configured updater binary location
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hasher := sha256.New()
|
||||
if _, err = io.Copy(io.MultiWriter(tmpFile, hasher), resp.Body); err != nil {
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpPath)
|
||||
_ = tmpFile.Close()
|
||||
_ = os.Remove(tmpPath)
|
||||
return err
|
||||
}
|
||||
if err = tmpFile.Close(); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
_ = os.Remove(tmpPath)
|
||||
return err
|
||||
}
|
||||
actualChecksum := hex.EncodeToString(hasher.Sum(nil))
|
||||
if actualChecksum != expectedChecksum {
|
||||
os.Remove(tmpPath)
|
||||
_ = os.Remove(tmpPath)
|
||||
return fmt.Errorf("sha256 checksum mismatch: expected %s, got %s", expectedChecksum, actualChecksum)
|
||||
}
|
||||
if err = os.Chmod(tmpPath, 0o755); err != nil && runtime.GOOS != "windows" {
|
||||
os.Remove(tmpPath)
|
||||
if err = os.Chmod(tmpPath, updateBinaryFilePerm); err != nil && runtime.GOOS != goosWindows { //nolint:gosec // downloaded edge binary must remain executable
|
||||
_ = os.Remove(tmpPath)
|
||||
return fmt.Errorf("set executable permission: %w", err)
|
||||
}
|
||||
|
||||
@@ -349,7 +359,7 @@ func (s *Service) downloadAndRestart(ctx context.Context, url string, expectedCh
|
||||
|
||||
func (s *Service) assetNameForGOOSGOARCH(goos string, goarch string) string {
|
||||
name := fmt.Sprintf("%s-%s-%s", s.assetPrefix, goos, goarch)
|
||||
if goos == "windows" {
|
||||
if goos == goosWindows {
|
||||
return name + ".exe"
|
||||
}
|
||||
return name
|
||||
@@ -378,4 +388,4 @@ func buildReleaseCheckKey(options UpdateOptions, remoteVersion string) string {
|
||||
|
||||
func compareVersions(local string, remote string) int {
|
||||
return utils.CompareVersions(local, remote)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package wsclient provides WebSocket client abstractions for edge node communication.
|
||||
package wsclient
|
||||
|
||||
import (
|
||||
@@ -8,14 +9,21 @@ import (
|
||||
shared "github.com/Rain-kl/Wavelet/pkg/wsclient"
|
||||
)
|
||||
|
||||
// WSMessage is an alias for the shared WebSocket message type.
|
||||
type WSMessage = shared.WSMessage
|
||||
|
||||
// MessageHandler is an alias for the shared WebSocket message handler type.
|
||||
type MessageHandler = shared.MessageHandler
|
||||
|
||||
// Preset represents a predefined connection configuration for a specific edge role.
|
||||
type Preset int
|
||||
|
||||
const (
|
||||
// PresetAgent is the configuration preset for agent connections.
|
||||
PresetAgent Preset = iota
|
||||
// PresetRelay is the configuration preset for relay connections.
|
||||
PresetRelay
|
||||
// PresetFlared is the configuration preset for flared (tunnel) connections.
|
||||
PresetFlared
|
||||
)
|
||||
|
||||
@@ -30,18 +38,22 @@ var presets = map[Preset]presetConfig{
|
||||
PresetFlared: {HeaderKey: "X-Tunnel-Token", WSPath: "/api/v1/tunnel/ws"},
|
||||
}
|
||||
|
||||
// PresetHeaderKey returns the HTTP header key used for authentication with the given preset.
|
||||
func PresetHeaderKey(preset Preset) string {
|
||||
return presets[preset].HeaderKey
|
||||
}
|
||||
|
||||
// PresetWSPath returns the WebSocket path used for the given preset.
|
||||
func PresetWSPath(preset Preset) string {
|
||||
return presets[preset].WSPath
|
||||
}
|
||||
|
||||
// Client is a WebSocket client configured for a specific edge preset.
|
||||
type Client struct {
|
||||
sharedClient *shared.Client
|
||||
}
|
||||
|
||||
// New creates a new Client for the given preset, base URL, token, and timeout.
|
||||
func New(preset Preset, baseURL, token string, timeout time.Duration) *Client {
|
||||
cfg := presets[preset]
|
||||
return &Client{
|
||||
@@ -55,18 +67,22 @@ func New(preset Preset, baseURL, token string, timeout time.Duration) *Client {
|
||||
}
|
||||
}
|
||||
|
||||
// SetToken updates the authentication token used by the client.
|
||||
func (c *Client) SetToken(token string) {
|
||||
c.sharedClient.SetToken(token)
|
||||
}
|
||||
|
||||
// URL returns the fully resolved WebSocket URL for this client.
|
||||
func (c *Client) URL() string {
|
||||
return c.sharedClient.URL()
|
||||
}
|
||||
|
||||
// Connection represents an established WebSocket connection to an edge node.
|
||||
type Connection struct {
|
||||
sharedConn *shared.Connection
|
||||
}
|
||||
|
||||
// Connect establishes a WebSocket connection using the client configuration.
|
||||
func (c *Client) Connect(ctx context.Context) (*Connection, error) {
|
||||
conn, err := c.sharedClient.Connect(ctx)
|
||||
if err != nil {
|
||||
@@ -75,10 +91,12 @@ func (c *Client) Connect(ctx context.Context) (*Connection, error) {
|
||||
return &Connection{sharedConn: conn}, nil
|
||||
}
|
||||
|
||||
// AgentConnection is a Connection specialized for agent node communication.
|
||||
type AgentConnection struct {
|
||||
Connection
|
||||
}
|
||||
|
||||
// ConnectAgent establishes a WebSocket connection and returns it as an AgentConnection.
|
||||
func (c *Client) ConnectAgent(ctx context.Context) (*AgentConnection, error) {
|
||||
conn, err := c.Connect(ctx)
|
||||
if err != nil {
|
||||
@@ -87,6 +105,7 @@ func (c *Client) ConnectAgent(ctx context.Context) (*AgentConnection, error) {
|
||||
return &AgentConnection{Connection: *conn}, nil
|
||||
}
|
||||
|
||||
// URL returns the resolved WebSocket URL of this connection.
|
||||
func (conn *Connection) URL() string {
|
||||
if conn == nil || conn.sharedConn == nil {
|
||||
return ""
|
||||
@@ -94,18 +113,22 @@ func (conn *Connection) URL() string {
|
||||
return conn.sharedConn.URL
|
||||
}
|
||||
|
||||
// SendPing sends a ping message over the connection.
|
||||
func (conn *Connection) SendPing() error {
|
||||
return conn.sharedConn.SendMessage(pkgprotocol.WSMessageTypePing, nil)
|
||||
}
|
||||
|
||||
// SendPong sends a pong message over the connection.
|
||||
func (conn *Connection) SendPong() error {
|
||||
return conn.sharedConn.SendMessage(pkgprotocol.WSMessageTypePong, nil)
|
||||
}
|
||||
|
||||
// SendMessage sends a typed message with an optional payload over the connection.
|
||||
func (conn *Connection) SendMessage(msgType string, payload any) error {
|
||||
return conn.sharedConn.SendMessage(msgType, payload)
|
||||
}
|
||||
|
||||
// Receive reads the next message from the connection.
|
||||
func (conn *Connection) Receive() (pkgprotocol.WSMessage, error) {
|
||||
var message pkgprotocol.WSMessage
|
||||
if err := conn.sharedConn.Receive(&message); err != nil {
|
||||
@@ -114,10 +137,12 @@ func (conn *Connection) Receive() (pkgprotocol.WSMessage, error) {
|
||||
return message, nil
|
||||
}
|
||||
|
||||
// RunReceiveLoop blocks and dispatches incoming messages to the handler until the context is canceled.
|
||||
func (conn *Connection) RunReceiveLoop(ctx context.Context, handler MessageHandler) error {
|
||||
return conn.sharedConn.RunReceiveLoop(ctx, handler)
|
||||
}
|
||||
|
||||
// Close gracefully closes the WebSocket connection.
|
||||
func (conn *Connection) Close() error {
|
||||
if conn == nil || conn.sharedConn == nil {
|
||||
return nil
|
||||
@@ -125,6 +150,7 @@ func (conn *Connection) Close() error {
|
||||
return conn.sharedConn.Close()
|
||||
}
|
||||
|
||||
// SendStatus sends a node status payload over the agent connection.
|
||||
func (conn *AgentConnection) SendStatus(payload pkgprotocol.NodePayload) error {
|
||||
return conn.sharedConn.SendMessage(pkgprotocol.WSMessageTypeStatus, payload)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user