package client import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "runtime" "strings" "time" "github.com/Fimeg/RedFlag/aggregator-agent/internal/event" "github.com/Fimeg/RedFlag/aggregator-agent/internal/models" "github.com/Fimeg/RedFlag/aggregator-agent/internal/system" "github.com/google/uuid" ) // Client handles API communication with the server type Client struct { baseURL string token string http *http.Client RapidPollingEnabled bool RapidPollingUntil time.Time machineID string // Cached machine ID for security binding eventBuffer *event.Buffer agentID uuid.UUID } // NewClient creates a new API client func NewClient(baseURL, token string) *Client { // Get machine ID for security binding (v0.1.22+) machineID, err := system.GetMachineID() if err != nil { // Log warning but don't fail - older servers may not require it fmt.Printf("Warning: Failed to get machine ID: %v\n", err) machineID = "" // Will be handled by server validation } return &Client{ baseURL: baseURL, token: token, machineID: machineID, http: &http.Client{ Timeout: 30 * time.Second, }, } } // NewClientWithEventBuffer creates a new API client with event buffering capability func NewClientWithEventBuffer(baseURL, token string, statePath string, agentID uuid.UUID) *Client { client := NewClient(baseURL, token) client.agentID = agentID // Initialize event buffer if state path is provided if statePath != "" { eventBufferPath := filepath.Join(statePath, "events_buffer.json") client.eventBuffer = event.NewBuffer(eventBufferPath) } return client } // bufferEvent buffers a system event for later reporting func (c *Client) bufferEvent(eventType, eventSubtype, severity, component, message string, metadata map[string]interface{}) { if c.eventBuffer == nil { return // Event buffering not enabled } // Use agent ID if available, otherwise create event with nil agent ID var agentIDPtr *uuid.UUID if c.agentID != uuid.Nil { agentIDPtr = &c.agentID } event := &models.SystemEvent{ ID: uuid.New(), AgentID: agentIDPtr, EventType: eventType, EventSubtype: eventSubtype, Severity: severity, Component: component, Message: message, Metadata: metadata, CreatedAt: time.Now(), } // Buffer the event (best effort - don't fail if buffering fails) if err := c.eventBuffer.BufferEvent(event); err != nil { fmt.Printf("Warning: Failed to buffer event: %v\n", err) } } // GetBufferedEvents returns all buffered events and clears the buffer func (c *Client) GetBufferedEvents() ([]*models.SystemEvent, error) { if c.eventBuffer == nil { return nil, nil // Event buffering not enabled } return c.eventBuffer.GetBufferedEvents() } // addMachineIDHeader adds X-Machine-ID header to authenticated requests (v0.1.22+) func (c *Client) addMachineIDHeader(req *http.Request) { if c.machineID != "" { req.Header.Set("X-Machine-ID", c.machineID) } } // GetToken returns the current JWT token func (c *Client) GetToken() string { return c.token } // SetToken updates the JWT token func (c *Client) SetToken(token string) { c.token = token } // RegisterRequest is the payload for agent registration type RegisterRequest struct { Hostname string `json:"hostname"` OSType string `json:"os_type"` OSVersion string `json:"os_version"` OSArchitecture string `json:"os_architecture"` AgentVersion string `json:"agent_version"` RegistrationToken string `json:"registration_token,omitempty"` // Fallback method MachineID string `json:"machine_id"` PublicKeyFingerprint string `json:"public_key_fingerprint"` Metadata map[string]string `json:"metadata"` } // RegisterResponse is returned after successful registration type RegisterResponse struct { AgentID uuid.UUID `json:"agent_id"` Token string `json:"token"` // Short-lived access token (24h) RefreshToken string `json:"refresh_token"` // Long-lived refresh token (90d) Config map[string]interface{} `json:"config"` } // Register registers the agent with the server func (c *Client) Register(req RegisterRequest) (*RegisterResponse, error) { url := fmt.Sprintf("%s/api/v1/agents/register", c.baseURL) // If we have a registration token, include it in the request // Registration tokens are longer than regular JWT tokens (usually 64 chars vs JWT ~400 chars) if c.token != "" && len(c.token) > 40 { req.RegistrationToken = c.token } body, err := json.Marshal(req) if err != nil { // Buffer registration failure event c.bufferEvent("registration_failure", "marshal_error", "error", "client", fmt.Sprintf("Failed to marshal registration request: %v", err), map[string]interface{}{ "error": err.Error(), "hostname": req.Hostname, }) return nil, err } httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) if err != nil { // Buffer registration failure event c.bufferEvent("registration_failure", "request_creation_error", "error", "client", fmt.Sprintf("Failed to create registration request: %v", err), map[string]interface{}{ "error": err.Error(), "hostname": req.Hostname, }) return nil, err } httpReq.Header.Set("Content-Type", "application/json") // Add Authorization header if we have a registration token (preferred method) // Registration tokens are longer than regular JWT tokens (usually 64 chars vs JWT ~400 chars) if c.token != "" && len(c.token) > 40 { httpReq.Header.Set("Authorization", "Bearer "+c.token) } resp, err := c.http.Do(httpReq) if err != nil { // Buffer registration failure event c.bufferEvent("registration_failure", "network_error", "error", "client", fmt.Sprintf("Registration request failed: %v", err), map[string]interface{}{ "error": err.Error(), "hostname": req.Hostname, "server_url": c.baseURL, }) return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) errorMsg := fmt.Sprintf("registration failed: %s - %s", resp.Status, string(bodyBytes)) // Buffer registration failure event c.bufferEvent("registration_failure", "api_error", "error", "client", errorMsg, map[string]interface{}{ "status_code": resp.StatusCode, "response_body": string(bodyBytes), "hostname": req.Hostname, "server_url": c.baseURL, }) return nil, fmt.Errorf(errorMsg) } var result RegisterResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { // Buffer registration failure event c.bufferEvent("registration_failure", "decode_error", "error", "client", fmt.Sprintf("Failed to decode registration response: %v", err), map[string]interface{}{ "error": err.Error(), "hostname": req.Hostname, }) return nil, err } // Update client token and agent ID c.token = result.Token c.agentID = result.AgentID return &result, nil } // TokenRenewalRequest is the payload for token renewal using refresh token type TokenRenewalRequest struct { AgentID uuid.UUID `json:"agent_id"` RefreshToken string `json:"refresh_token"` AgentVersion string `json:"agent_version,omitempty"` // Agent's current version for upgrade tracking } // TokenRenewalResponse is returned after successful token renewal type TokenRenewalResponse struct { Token string `json:"token"` // New short-lived access token (24h) } // RenewToken uses refresh token to get a new access token (proper implementation) func (c *Client) RenewToken(agentID uuid.UUID, refreshToken string, agentVersion string) error { url := fmt.Sprintf("%s/api/v1/agents/renew", c.baseURL) renewalReq := TokenRenewalRequest{ AgentID: agentID, RefreshToken: refreshToken, AgentVersion: agentVersion, } body, err := json.Marshal(renewalReq) if err != nil { // Buffer token renewal failure event c.bufferEvent("token_renewal_failure", "marshal_error", "error", "client", fmt.Sprintf("Failed to marshal token renewal request: %v", err), map[string]interface{}{ "error": err.Error(), "agent_id": agentID.String(), }) return err } httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) if err != nil { // Buffer token renewal failure event c.bufferEvent("token_renewal_failure", "request_creation_error", "error", "client", fmt.Sprintf("Failed to create token renewal request: %v", err), map[string]interface{}{ "error": err.Error(), "agent_id": agentID.String(), }) return err } httpReq.Header.Set("Content-Type", "application/json") resp, err := c.http.Do(httpReq) if err != nil { // Buffer token renewal failure event c.bufferEvent("token_renewal_failure", "network_error", "error", "client", fmt.Sprintf("Token renewal request failed: %v", err), map[string]interface{}{ "error": err.Error(), "agent_id": agentID.String(), "server_url": c.baseURL, }) return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) errorMsg := fmt.Sprintf("token renewal failed: %s - %s", resp.Status, string(bodyBytes)) // Buffer token renewal failure event c.bufferEvent("token_renewal_failure", "api_error", "error", "client", errorMsg, map[string]interface{}{ "status_code": resp.StatusCode, "response_body": string(bodyBytes), "agent_id": agentID.String(), "server_url": c.baseURL, }) return fmt.Errorf(errorMsg) } var result TokenRenewalResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { // Buffer token renewal failure event c.bufferEvent("token_renewal_failure", "decode_error", "error", "client", fmt.Sprintf("Failed to decode token renewal response: %v", err), map[string]interface{}{ "error": err.Error(), "agent_id": agentID.String(), }) return err } // Update client token c.token = result.Token return nil } // Command represents a command from the server type Command struct { ID string `json:"id"` Type string `json:"type"` Params map[string]interface{} `json:"params"` Signature string `json:"signature,omitempty"` // Ed25519 signature of the command } // CommandItem is an alias for Command for consistency with server models type CommandItem = Command // CommandsResponse contains pending commands type CommandsResponse struct { Commands []Command `json:"commands"` RapidPolling *RapidPollingConfig `json:"rapid_polling,omitempty"` AcknowledgedIDs []string `json:"acknowledged_ids,omitempty"` // IDs server has received } // RapidPollingConfig contains rapid polling configuration from server type RapidPollingConfig struct { Enabled bool `json:"enabled"` Until string `json:"until"` // ISO 8601 timestamp } // SystemMetrics represents lightweight system metrics sent with check-ins type SystemMetrics struct { CPUPercent float64 `json:"cpu_percent,omitempty"` MemoryPercent float64 `json:"memory_percent,omitempty"` MemoryUsedGB float64 `json:"memory_used_gb,omitempty"` MemoryTotalGB float64 `json:"memory_total_gb,omitempty"` DiskUsedGB float64 `json:"disk_used_gb,omitempty"` DiskTotalGB float64 `json:"disk_total_gb,omitempty"` DiskPercent float64 `json:"disk_percent,omitempty"` Uptime string `json:"uptime,omitempty"` Version string `json:"version,omitempty"` // Agent version Metadata map[string]interface{} `json:"metadata,omitempty"` // Additional metadata // Command acknowledgment tracking PendingAcknowledgments []string `json:"pending_acknowledgments,omitempty"` // Command IDs awaiting ACK } // GetCommands retrieves pending commands from the server // Optionally sends lightweight system metrics in the request // Returns the full response including commands and acknowledged IDs func (c *Client) GetCommands(agentID uuid.UUID, metrics *SystemMetrics) (*CommandsResponse, error) { url := fmt.Sprintf("%s/api/v1/agents/%s/commands", c.baseURL, agentID) var req *http.Request var err error // If metrics provided, send them in request body if metrics != nil { body, err := json.Marshal(metrics) if err != nil { return nil, err } req, err = http.NewRequest("GET", url, bytes.NewBuffer(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") } else { req, err = http.NewRequest("GET", url, nil) if err != nil { return nil, err } } req.Header.Set("Authorization", "Bearer "+c.token) c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+) resp, err := c.http.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("failed to get commands: %s - %s", resp.Status, string(bodyBytes)) } var result CommandsResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } // Handle rapid polling configuration if provided if result.RapidPolling != nil { // Parse the timestamp if until, err := time.Parse(time.RFC3339, result.RapidPolling.Until); err == nil { // Update client's rapid polling configuration c.RapidPollingEnabled = result.RapidPolling.Enabled c.RapidPollingUntil = until } } return &result, nil } // UpdateReport represents discovered updates type UpdateReport struct { CommandID string `json:"command_id"` Timestamp time.Time `json:"timestamp"` Updates []UpdateReportItem `json:"updates"` } // UpdateReportItem represents a single update type UpdateReportItem struct { PackageType string `json:"package_type"` PackageName string `json:"package_name"` PackageDescription string `json:"package_description"` CurrentVersion string `json:"current_version"` AvailableVersion string `json:"available_version"` Severity string `json:"severity"` CVEList []string `json:"cve_list"` KBID string `json:"kb_id"` RepositorySource string `json:"repository_source"` SizeBytes int64 `json:"size_bytes"` Metadata map[string]interface{} `json:"metadata"` } // ReportUpdates sends discovered updates to the server func (c *Client) ReportUpdates(agentID uuid.UUID, report UpdateReport) error { url := fmt.Sprintf("%s/api/v1/agents/%s/updates", c.baseURL, agentID) body, err := json.Marshal(report) if err != nil { return err } req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.token) c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+) resp, err := c.http.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return fmt.Errorf("failed to report updates: %s - %s", resp.Status, string(bodyBytes)) } return nil } // MetricsReport represents metrics data (storage, system, CPU, memory) type MetricsReport struct { CommandID string `json:"command_id"` Timestamp time.Time `json:"timestamp"` Metrics []MetricsReportItem `json:"metrics"` } // MetricsReportItem represents a single metric type MetricsReportItem struct { PackageType string `json:"package_type"` PackageName string `json:"package_name"` CurrentVersion string `json:"current_version"` AvailableVersion string `json:"available_version"` Severity string `json:"severity"` RepositorySource string `json:"repository_source"` Metadata map[string]interface{} `json:"metadata"` } // ReportMetrics sends metrics data to the server func (c *Client) ReportMetrics(agentID uuid.UUID, report MetricsReport) error { url := fmt.Sprintf("%s/api/v1/agents/%s/metrics", c.baseURL, agentID) body, err := json.Marshal(report) if err != nil { return err } req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.token) c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+) resp, err := c.http.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return fmt.Errorf("failed to report metrics: %s - %s", resp.Status, string(bodyBytes)) } return nil } // DockerReport represents Docker image information type DockerReport struct { CommandID string `json:"command_id"` Timestamp time.Time `json:"timestamp"` Images []DockerReportItem `json:"images"` } // DockerReportItem represents a single Docker image type DockerReportItem struct { PackageType string `json:"package_type"` PackageName string `json:"package_name"` CurrentVersion string `json:"current_version"` AvailableVersion string `json:"available_version"` Severity string `json:"severity"` RepositorySource string `json:"repository_source"` Metadata map[string]interface{} `json:"metadata"` } // ReportDockerImages sends Docker image information to the server func (c *Client) ReportDockerImages(agentID uuid.UUID, report DockerReport) error { url := fmt.Sprintf("%s/api/v1/agents/%s/docker-images", c.baseURL, agentID) body, err := json.Marshal(report) if err != nil { return err } req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.token) c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+) resp, err := c.http.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return fmt.Errorf("failed to report docker images: %s - %s", resp.Status, string(bodyBytes)) } return nil } // LogReport represents an execution log type LogReport struct { CommandID string `json:"command_id"` Action string `json:"action"` Result string `json:"result"` Stdout string `json:"stdout"` Stderr string `json:"stderr"` ExitCode int `json:"exit_code"` DurationSeconds int `json:"duration_seconds"` Metadata map[string]string `json:"metadata,omitempty"` } // ReportLog sends an execution log to the server func (c *Client) ReportLog(agentID uuid.UUID, report LogReport) error { url := fmt.Sprintf("%s/api/v1/agents/%s/logs", c.baseURL, agentID) body, err := json.Marshal(report) if err != nil { return err } req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.token) c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+) resp, err := c.http.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return fmt.Errorf("failed to report log: %s - %s", resp.Status, string(bodyBytes)) } return nil } // DependencyReport represents a dependency report after dry run type DependencyReport struct { PackageName string `json:"package_name"` PackageType string `json:"package_type"` Dependencies []string `json:"dependencies"` UpdateID string `json:"update_id"` DryRunResult *InstallResult `json:"dry_run_result,omitempty"` } // InstallResult represents the result of a package installation attempt type InstallResult struct { Success bool `json:"success"` ErrorMessage string `json:"error_message,omitempty"` Stdout string `json:"stdout,omitempty"` Stderr string `json:"stderr,omitempty"` ExitCode int `json:"exit_code"` DurationSeconds int `json:"duration_seconds"` Action string `json:"action,omitempty"` PackagesInstalled []string `json:"packages_installed,omitempty"` ContainersUpdated []string `json:"containers_updated,omitempty"` Dependencies []string `json:"dependencies,omitempty"` IsDryRun bool `json:"is_dry_run"` } // ReportDependencies sends dependency report to the server func (c *Client) ReportDependencies(agentID uuid.UUID, report DependencyReport) error { url := fmt.Sprintf("%s/api/v1/agents/%s/dependencies", c.baseURL, agentID) body, err := json.Marshal(report) if err != nil { return err } req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.token) c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+) resp, err := c.http.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return fmt.Errorf("failed to report dependencies: %s - %s", resp.Status, string(bodyBytes)) } return nil } // SystemInfoReport represents system information updates type SystemInfoReport struct { Timestamp time.Time `json:"timestamp"` CPUModel string `json:"cpu_model,omitempty"` CPUCores int `json:"cpu_cores,omitempty"` CPUThreads int `json:"cpu_threads,omitempty"` MemoryTotal uint64 `json:"memory_total,omitempty"` DiskTotal uint64 `json:"disk_total,omitempty"` DiskUsed uint64 `json:"disk_used,omitempty"` IPAddress string `json:"ip_address,omitempty"` Processes int `json:"processes,omitempty"` Uptime string `json:"uptime,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` } // ReportSystemInfo sends updated system information to the server func (c *Client) ReportSystemInfo(agentID uuid.UUID, report SystemInfoReport) error { url := fmt.Sprintf("%s/api/v1/agents/%s/system-info", c.baseURL, agentID) body, err := json.Marshal(report) if err != nil { return err } req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.token) c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+) resp, err := c.http.Do(req) if err != nil { return err } defer resp.Body.Close() // Accept 200 OK or 404 Not Found (if endpoint doesn't exist yet) if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound { bodyBytes, _ := io.ReadAll(resp.Body) return fmt.Errorf("failed to report system info: %s - %s", resp.Status, string(bodyBytes)) } return nil } // DetectSystem returns basic system information (deprecated, use system.GetSystemInfo instead) func DetectSystem() (osType, osVersion, osArch string) { osType = runtime.GOOS osArch = runtime.GOARCH // Read OS version switch osType { case "linux": data, _ := os.ReadFile("/etc/os-release") if data != nil { osVersion = parseOSRelease(data) } case "windows": osVersion = "Windows" case "darwin": osVersion = "macOS" } return } // AgentInfo represents agent information from the server type AgentInfo struct { ID string `json:"id"` Hostname string `json:"hostname"` CurrentVersion string `json:"current_version"` OSType string `json:"os_type"` OSVersion string `json:"os_version"` OSArchitecture string `json:"os_architecture"` LastCheckIn string `json:"last_check_in"` } // GetAgent retrieves agent information from the server func (c *Client) GetAgent(agentID string) (*AgentInfo, error) { url := fmt.Sprintf("%s/api/v1/agents/%s", c.baseURL, agentID) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", "Bearer "+c.token) req.Header.Set("Content-Type", "application/json") c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+) resp, err := c.http.Do(req) if err != nil { return nil, fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("server returned status %d: %s", resp.StatusCode, string(body)) } var agent AgentInfo if err := json.NewDecoder(resp.Body).Decode(&agent); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &agent, nil } // parseOSRelease parses /etc/os-release to get proper distro name func parseOSRelease(data []byte) string { lines := strings.Split(string(data), "\n") id := "" prettyName := "" version := "" for _, line := range lines { if strings.HasPrefix(line, "ID=") { id = strings.Trim(strings.TrimPrefix(line, "ID="), "\"") } if strings.HasPrefix(line, "PRETTY_NAME=") { prettyName = strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"") } if strings.HasPrefix(line, "VERSION_ID=") { version = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), "\"") } } // Prefer PRETTY_NAME if available if prettyName != "" { return prettyName } // Fall back to ID + VERSION if id != "" { if version != "" { return strings.Title(id) + " " + version } return strings.Title(id) } return "Linux" } // AgentConfigResponse contains subsystem configuration from server type AgentConfigResponse struct { Subsystems map[string]interface{} `json:"subsystems"` Version int64 `json:"version"` } // GetConfig retrieves current subsystem configuration from server func (c *Client) GetConfig(agentID uuid.UUID) (*AgentConfigResponse, error) { url := fmt.Sprintf("%s/api/v1/agents/%s/config", c.baseURL, agentID) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+c.token) c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+) resp, err := c.http.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("failed to get config: %s - %s", resp.Status, string(bodyBytes)) } var result AgentConfigResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } return &result, nil }