- Add comprehensive database migrations (001-024) for schema evolution - Enhance API schema with expanded type definitions and resolvers - Add new middleware: audit logging, rate limiting, MFA enforcement, security, tenant auth - Implement new services: AI optimization, billing, blockchain, compliance, marketplace - Add adapter layer for cloud integrations (Cloudflare, Kubernetes, Proxmox, storage) - Update Crossplane provider with enhanced VM management capabilities - Add comprehensive test suite for API endpoints and services - Update frontend components with improved GraphQL subscriptions and real-time updates - Enhance security configurations and headers (CSP, CORS, etc.) - Update documentation and configuration files - Add new CI/CD workflows and validation scripts - Implement design system improvements and UI enhancements
262 lines
6.7 KiB
Go
262 lines
6.7 KiB
Go
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
|
|
}
|
|
|