package proxmox import ( "bytes" "context" "crypto/tls" "encoding/json" "fmt" "io" "net/http" "net/url" "time" "github.com/pkg/errors" ) // HTTPClient wraps http.Client with Proxmox-specific functionality type HTTPClient struct { client *http.Client endpoint string ticket string csrfToken string token string username string password string } // NewHTTPClient creates a new HTTP client for Proxmox API // Implements FIPS 140-2 compliant TLS configuration per DoD/MilSpec requirements func NewHTTPClient(endpoint string, insecureSkipTLS bool) *HTTPClient { // FIPS-approved cipher suites for TLS 1.3 // Per NIST SP 800-53 SC-8 and NIST SP 800-171 3.13.1 fipsCipherSuites := []uint16{ tls.TLS_AES_256_GCM_SHA384, tls.TLS_CHACHA20_POLY1305_SHA256, tls.TLS_AES_128_GCM_SHA256, } tr := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: insecureSkipTLS, MinVersion: tls.VersionTLS13, // TLS 1.3 minimum MaxVersion: tls.VersionTLS13, // Only TLS 1.3 CipherSuites: fipsCipherSuites, PreferServerCipherSuites: true, }, DisableKeepAlives: false, MaxIdleConns: 10, IdleConnTimeout: 30 * time.Second, } return &HTTPClient{ client: &http.Client{ Transport: tr, Timeout: 30 * time.Second, }, endpoint: endpoint, } } // Authenticate authenticates with Proxmox API func (c *HTTPClient) Authenticate(ctx context.Context) error { if c.token != "" { // Token authentication - token format: "user@realm!token-name=token-secret" return nil // Token will be used in requests } if c.username == "" || c.password == "" { return fmt.Errorf("username/password or token required") } authURL := fmt.Sprintf("%s/api2/json/access/ticket", c.endpoint) data := url.Values{} data.Set("username", c.username) data.Set("password", c.password) req, err := http.NewRequestWithContext(ctx, "POST", authURL, bytes.NewBufferString(data.Encode())) if err != nil { return errors.Wrap(err, "failed to create auth request") } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := c.client.Do(req) if err != nil { return errors.Wrap(err, "failed to authenticate") } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("authentication failed: %s - %s", resp.Status, string(body)) } var result struct { Data struct { Ticket string `json:"ticket"` CSRFPreventionToken string `json:"CSRFPreventionToken"` } `json:"data"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return errors.Wrap(err, "failed to decode auth response") } c.ticket = result.Data.Ticket c.csrfToken = result.Data.CSRFPreventionToken return nil } // SetToken sets API token for authentication func (c *HTTPClient) SetToken(token string) { c.token = token } // SetCredentials sets username and password for authentication func (c *HTTPClient) SetCredentials(username, password string) { c.username = username c.password = password } // Do performs an HTTP request with authentication func (c *HTTPClient) Do(ctx context.Context, method, path string, body interface{}) (*http.Response, error) { url := fmt.Sprintf("%s/api2/json%s", c.endpoint, path) var reqBody io.Reader if body != nil { jsonData, err := json.Marshal(body) if err != nil { return nil, errors.Wrap(err, "failed to marshal request body") } reqBody = bytes.NewBuffer(jsonData) } req, err := http.NewRequestWithContext(ctx, method, url, reqBody) if err != nil { return nil, errors.Wrap(err, "failed to create request") } // Set headers req.Header.Set("Content-Type", "application/json") if c.token != "" { // Token authentication req.Header.Set("Authorization", fmt.Sprintf("PVEAuthCookie=%s", c.token)) } else if c.ticket != "" { // Ticket authentication req.AddCookie(&http.Cookie{ Name: "PVEAuthCookie", Value: c.ticket, }) if c.csrfToken != "" && method != "GET" { req.Header.Set("CSRFPreventionToken", c.csrfToken) } } resp, err := c.client.Do(req) if err != nil { return nil, errors.Wrap(err, "request failed") } return resp, nil } // Get performs a GET request func (c *HTTPClient) Get(ctx context.Context, path string, result interface{}) error { resp, err := c.Do(ctx, "GET", path, nil) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("GET %s failed: %s - %s", path, resp.Status, string(body)) } if result != nil { var apiResponse struct { Data interface{} `json:"data"` } if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil { return errors.Wrap(err, "failed to decode response") } // Unmarshal data into result dataBytes, _ := json.Marshal(apiResponse.Data) return json.Unmarshal(dataBytes, result) } return nil } // Post performs a POST request func (c *HTTPClient) Post(ctx context.Context, path string, body, result interface{}) error { resp, err := c.Do(ctx, "POST", path, body) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { bodyBytes, _ := io.ReadAll(resp.Body) return fmt.Errorf("POST %s failed: %s - %s", path, resp.Status, string(bodyBytes)) } if result != nil { var apiResponse struct { Data interface{} `json:"data"` } if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil { return errors.Wrap(err, "failed to decode response") } dataBytes, _ := json.Marshal(apiResponse.Data) return json.Unmarshal(dataBytes, result) } return nil } // Put performs a PUT request func (c *HTTPClient) Put(ctx context.Context, path string, body, result interface{}) error { resp, err := c.Do(ctx, "PUT", path, body) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { bodyBytes, _ := io.ReadAll(resp.Body) return fmt.Errorf("PUT %s failed: %s - %s", path, resp.Status, string(bodyBytes)) } if result != nil { var apiResponse struct { Data interface{} `json:"data"` } if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil { return errors.Wrap(err, "failed to decode response") } dataBytes, _ := json.Marshal(apiResponse.Data) return json.Unmarshal(dataBytes, result) } return nil } // Delete performs a DELETE request func (c *HTTPClient) Delete(ctx context.Context, path string) error { resp, err := c.Do(ctx, "DELETE", path, nil) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { bodyBytes, _ := io.ReadAll(resp.Body) return fmt.Errorf("DELETE %s failed: %s - %s", path, resp.Status, string(bodyBytes)) } return nil }