209 lines
5.8 KiB
Go
209 lines
5.8 KiB
Go
package mesh
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/example/remote-access-platform/agents/rap-node-agent/internal/fabricproto"
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
type Client struct {
|
|
BaseURL string
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
type FabricSessionDialOptions struct {
|
|
Token string
|
|
Header http.Header
|
|
Dialer *websocket.Dialer
|
|
Timeout time.Duration
|
|
}
|
|
|
|
func NewClient(baseURL string) Client {
|
|
return Client{
|
|
BaseURL: baseURL,
|
|
HTTPClient: &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (c Client) SendHealth(ctx context.Context, message HealthMessage) (HealthAck, error) {
|
|
payload, err := json.Marshal(message)
|
|
if err != nil {
|
|
return HealthAck{}, err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/mesh/v1/health", bytes.NewReader(payload))
|
|
if err != nil {
|
|
return HealthAck{}, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
httpClient := c.HTTPClient
|
|
if httpClient == nil {
|
|
httpClient = http.DefaultClient
|
|
}
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
return HealthAck{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return HealthAck{}, fmt.Errorf("mesh health rejected with status %d", resp.StatusCode)
|
|
}
|
|
var ack HealthAck
|
|
if err := json.NewDecoder(resp.Body).Decode(&ack); err != nil {
|
|
return HealthAck{}, err
|
|
}
|
|
return ack, nil
|
|
}
|
|
|
|
func (c Client) SendSynthetic(ctx context.Context, envelope SyntheticEnvelope) (SyntheticEnvelope, error) {
|
|
payload, err := json.Marshal(envelope)
|
|
if err != nil {
|
|
return SyntheticEnvelope{}, err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/mesh/v1/synthetic/probe", bytes.NewReader(payload))
|
|
if err != nil {
|
|
return SyntheticEnvelope{}, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
httpClient := c.HTTPClient
|
|
if httpClient == nil {
|
|
httpClient = http.DefaultClient
|
|
}
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
return SyntheticEnvelope{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return SyntheticEnvelope{}, fmt.Errorf("mesh synthetic probe rejected with status %d", resp.StatusCode)
|
|
}
|
|
var ack SyntheticEnvelope
|
|
if err := json.NewDecoder(resp.Body).Decode(&ack); err != nil {
|
|
return SyntheticEnvelope{}, err
|
|
}
|
|
return ack, nil
|
|
}
|
|
|
|
func (c Client) SendProduction(ctx context.Context, envelope ProductionEnvelope) (ProductionForwardResult, error) {
|
|
payload, err := json.Marshal(envelope)
|
|
if err != nil {
|
|
return ProductionForwardResult{}, err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/mesh/v1/forward", bytes.NewReader(payload))
|
|
if err != nil {
|
|
return ProductionForwardResult{}, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
httpClient := c.HTTPClient
|
|
if httpClient == nil {
|
|
httpClient = http.DefaultClient
|
|
}
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
return ProductionForwardResult{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return ProductionForwardResult{}, fmt.Errorf("mesh production forward rejected with status %d", resp.StatusCode)
|
|
}
|
|
var result ProductionForwardResult
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return ProductionForwardResult{}, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (c Client) DialFabricSession(ctx context.Context, opts FabricSessionDialOptions) (*websocket.Conn, *http.Response, error) {
|
|
target, err := c.fabricSessionWebSocketURL()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
header := cloneHeader(opts.Header)
|
|
if strings.TrimSpace(opts.Token) != "" {
|
|
header.Set("X-RAP-Fabric-Session-Token", strings.TrimSpace(opts.Token))
|
|
}
|
|
dialer := opts.Dialer
|
|
if dialer == nil {
|
|
base := *websocket.DefaultDialer
|
|
if opts.Timeout > 0 {
|
|
base.HandshakeTimeout = opts.Timeout
|
|
}
|
|
dialer = &base
|
|
}
|
|
return dialer.DialContext(ctx, target, header)
|
|
}
|
|
|
|
func (c Client) SendFabricSessionFrame(ctx context.Context, opts FabricSessionDialOptions, frame fabricproto.Frame) (fabricproto.Frame, error) {
|
|
conn, resp, err := c.DialFabricSession(ctx, opts)
|
|
if err != nil {
|
|
if resp != nil {
|
|
return fabricproto.Frame{}, fmt.Errorf("fabric session websocket rejected with status %d: %w", resp.StatusCode, err)
|
|
}
|
|
return fabricproto.Frame{}, err
|
|
}
|
|
defer conn.Close()
|
|
payload, err := fabricproto.MarshalFrame(frame)
|
|
if err != nil {
|
|
return fabricproto.Frame{}, err
|
|
}
|
|
if err := conn.WriteMessage(websocket.BinaryMessage, payload); err != nil {
|
|
return fabricproto.Frame{}, err
|
|
}
|
|
if deadline, ok := ctx.Deadline(); ok {
|
|
_ = conn.SetReadDeadline(deadline)
|
|
} else if opts.Timeout > 0 {
|
|
_ = conn.SetReadDeadline(time.Now().Add(opts.Timeout))
|
|
}
|
|
messageType, responsePayload, err := conn.ReadMessage()
|
|
if err != nil {
|
|
return fabricproto.Frame{}, err
|
|
}
|
|
if messageType != websocket.BinaryMessage {
|
|
return fabricproto.Frame{}, fmt.Errorf("fabric session websocket returned non-binary message type %d", messageType)
|
|
}
|
|
return fabricproto.UnmarshalFrame(responsePayload, fabricproto.DefaultMaxPayload)
|
|
}
|
|
|
|
func (c Client) fabricSessionWebSocketURL() (string, error) {
|
|
base := strings.TrimSpace(c.BaseURL)
|
|
if base == "" {
|
|
return "", fmt.Errorf("mesh base url is required")
|
|
}
|
|
parsed, err := url.Parse(base)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
switch parsed.Scheme {
|
|
case "http":
|
|
parsed.Scheme = "ws"
|
|
case "https":
|
|
parsed.Scheme = "wss"
|
|
case "ws", "wss":
|
|
default:
|
|
return "", fmt.Errorf("unsupported mesh base url scheme %q", parsed.Scheme)
|
|
}
|
|
parsed.Path = strings.TrimRight(parsed.Path, "/") + "/mesh/v1/fabric/session/ws"
|
|
parsed.RawQuery = ""
|
|
parsed.Fragment = ""
|
|
return parsed.String(), nil
|
|
}
|
|
|
|
func cloneHeader(header http.Header) http.Header {
|
|
out := http.Header{}
|
|
for key, values := range header {
|
|
for _, value := range values {
|
|
out.Add(key, value)
|
|
}
|
|
}
|
|
return out
|
|
}
|