yao/tai/proxy/connect.go
Max 6408c140fc Implement tunnel mode support in Tai service
- Introduce a new "tunnel" scheme for the Tai client, allowing connections through Yao's reverse proxy.
- Enhance the Tai registry to manage tunnel-connected nodes and their ports.
- Add WebSocket and reverse proxy routes for tunnel connections in the OpenAPI server.
- Implement tunnel-specific proxy and VNC handling to facilitate communication with containerized environments.
- Update gRPC environment variable handling to support tunnel connections.

These changes improve the Tai service's flexibility and connectivity options, enabling better integration with remote and containerized environments.
2026-03-06 17:12:36 +08:00

127 lines
2.9 KiB
Go

package proxy
import (
"bufio"
"bytes"
"context"
"fmt"
"net/http"
"strings"
"github.com/gorilla/websocket"
)
// --- Remote Connect ---
func (r *remoteProxy) Connect(ctx context.Context, containerID string, opts ConnectOptions) (*Connection, error) {
baseURL, err := r.URL(ctx, containerID, opts.Port, opts.Path)
if err != nil {
return nil, err
}
return connect(ctx, baseURL, opts.Protocol, r.client)
}
// --- Tunnel Connect ---
func (t *tunnelProxy) Connect(ctx context.Context, containerID string, opts ConnectOptions) (*Connection, error) {
baseURL, err := t.URL(ctx, containerID, opts.Port, opts.Path)
if err != nil {
return nil, err
}
return connect(ctx, baseURL, opts.Protocol, http.DefaultClient)
}
// --- Local Connect ---
func (l *localProxy) Connect(ctx context.Context, containerID string, opts ConnectOptions) (*Connection, error) {
baseURL, err := l.URL(ctx, containerID, opts.Port, opts.Path)
if err != nil {
return nil, err
}
return connect(ctx, baseURL, opts.Protocol, http.DefaultClient)
}
func connect(ctx context.Context, url string, protocol string, hc *http.Client) (*Connection, error) {
switch protocol {
case "ws":
return connectWS(ctx, url)
case "sse":
return connectSSE(ctx, url, hc)
default:
return nil, fmt.Errorf("unsupported connect protocol: %q", protocol)
}
}
func connectWS(ctx context.Context, rawURL string) (*Connection, error) {
wsURL := strings.Replace(rawURL, "http://", "ws://", 1)
wsURL = strings.Replace(wsURL, "https://", "wss://", 1)
conn, _, err := websocket.DefaultDialer.DialContext(ctx, wsURL, nil)
if err != nil {
return nil, fmt.Errorf("ws dial: %w", err)
}
ch := make(chan []byte, 64)
go func() {
defer close(ch)
for {
_, msg, err := conn.ReadMessage()
if err != nil {
return
}
ch <- msg
}
}()
return &Connection{
Messages: ch,
Send: func(data []byte) error {
return conn.WriteMessage(websocket.TextMessage, data)
},
Close: func() error {
return conn.Close()
},
}, nil
}
func connectSSE(ctx context.Context, url string, hc *http.Client) (*Connection, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "text/event-stream")
resp, err := hc.Do(req)
if err != nil {
return nil, fmt.Errorf("sse connect: %w", err)
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("sse: status %d", resp.StatusCode)
}
ch := make(chan []byte, 64)
go func() {
defer close(ch)
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "data: ") {
data := strings.TrimPrefix(line, "data: ")
ch <- bytes.Clone([]byte(data))
}
}
}()
return &Connection{
Messages: ch,
Send: func(data []byte) error {
return fmt.Errorf("sse: send not supported")
},
Close: func() error {
resp.Body.Close()
return nil
},
}, nil
}