feat(openapi): integrate Tai forward handlers for proxy and VNC operations
- Replaced direct tunnel handling with a dedicated Tai integration for managing proxy and VNC requests. - Updated routing to streamline the attachment of Tai handlers, enhancing modularity and maintainability. Made-with: Cursor
This commit is contained in:
parent
223d02ebfe
commit
c4dbcdba2b
5 changed files with 254 additions and 4 deletions
|
|
@ -27,12 +27,12 @@ import (
|
|||
"github.com/yaoapp/yao/openapi/otp"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
"github.com/yaoapp/yao/openapi/sandbox"
|
||||
openapiTai "github.com/yaoapp/yao/openapi/tai"
|
||||
"github.com/yaoapp/yao/openapi/team"
|
||||
openapiTrace "github.com/yaoapp/yao/openapi/trace"
|
||||
"github.com/yaoapp/yao/openapi/user"
|
||||
openapiWorkspace "github.com/yaoapp/yao/openapi/workspace"
|
||||
taiapi "github.com/yaoapp/yao/tai/api"
|
||||
taitunnel "github.com/yaoapp/yao/tai/tunnel"
|
||||
)
|
||||
|
||||
// Server is the OpenAPI server
|
||||
|
|
@ -191,9 +191,8 @@ func (openapi *OpenAPI) Attach(router *gin.Engine) {
|
|||
// Tai nodes handlers
|
||||
nodes.Attach(group.Group("/nodes"), openapi.OAuth)
|
||||
|
||||
// Tai tunnel: gRPC Forward-based HTTP/VNC transparent proxy
|
||||
group.Any("/tai/:taiID/proxy/*path", taitunnel.HandleForwardLazy)
|
||||
group.Any("/tai/:taiID/vnc/*path", taitunnel.HandleForwardLazy)
|
||||
// Tai forward handlers (proxy + VNC, dispatches tunnel vs local)
|
||||
openapiTai.Attach(group)
|
||||
|
||||
// Tai direct registration API (uses /tai-nodes/ prefix to avoid routing conflict with /tai/:taiID/)
|
||||
group.POST("/tai-nodes/register", taiapi.HandleRegister)
|
||||
|
|
|
|||
60
openapi/tai/proxy.go
Normal file
60
openapi/tai/proxy.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package tai
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
yaoTai "github.com/yaoapp/yao/tai"
|
||||
)
|
||||
|
||||
// handleLocalProxy resolves the container's HTTP address via Docker socket
|
||||
// and reverse-proxies the request.
|
||||
func handleLocalProxy(c *gin.Context, taiID string) {
|
||||
res, ok := yaoTai.GetResources(taiID)
|
||||
if !ok || res.Proxy == nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "proxy not available for node " + taiID})
|
||||
return
|
||||
}
|
||||
|
||||
// path format: /{containerID}:{port}/{rest...}
|
||||
raw := strings.TrimPrefix(c.Param("path"), "/")
|
||||
colonIdx := strings.Index(raw, ":")
|
||||
if colonIdx < 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid proxy path, expected /{containerID}:{port}/{path}"})
|
||||
return
|
||||
}
|
||||
|
||||
containerID := raw[:colonIdx]
|
||||
rest := raw[colonIdx+1:]
|
||||
slashIdx := strings.Index(rest, "/")
|
||||
var portStr, subPath string
|
||||
if slashIdx >= 0 {
|
||||
portStr = rest[:slashIdx]
|
||||
subPath = rest[slashIdx:]
|
||||
} else {
|
||||
portStr = rest
|
||||
subPath = "/"
|
||||
}
|
||||
|
||||
var port int
|
||||
for _, ch := range portStr {
|
||||
if ch < '0' || ch > '9' {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid port in proxy path"})
|
||||
return
|
||||
}
|
||||
port = port*10 + int(ch-'0')
|
||||
}
|
||||
if port == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing port in proxy path"})
|
||||
return
|
||||
}
|
||||
|
||||
targetURL, err := res.Proxy.URL(c.Request.Context(), containerID, port, subPath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "resolve proxy target: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
reverseProxy(c, targetURL)
|
||||
}
|
||||
39
openapi/tai/tai.go
Normal file
39
openapi/tai/tai.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package tai
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
yaoTai "github.com/yaoapp/yao/tai"
|
||||
taitunnel "github.com/yaoapp/yao/tai/tunnel"
|
||||
)
|
||||
|
||||
// Attach registers Tai forward routes on the given group.
|
||||
//
|
||||
// - ANY /tai/:taiID/proxy/*path — HTTP forward (tunnel or local)
|
||||
// - GET /tai/:taiID/vnc/*path — VNC WebSocket forward (tunnel or local)
|
||||
func Attach(group *gin.RouterGroup) {
|
||||
group.Any("/tai/:taiID/proxy/*path", handleProxy)
|
||||
group.GET("/tai/:taiID/vnc/*path", handleVNC)
|
||||
}
|
||||
|
||||
func handleProxy(c *gin.Context) {
|
||||
taiID := c.Param("taiID")
|
||||
if isLocalNode(taiID) {
|
||||
handleLocalProxy(c, taiID)
|
||||
return
|
||||
}
|
||||
taitunnel.HandleForwardLazy(c)
|
||||
}
|
||||
|
||||
func handleVNC(c *gin.Context) {
|
||||
taiID := c.Param("taiID")
|
||||
if isLocalNode(taiID) {
|
||||
handleLocalVNC(c, taiID)
|
||||
return
|
||||
}
|
||||
taitunnel.HandleForwardLazy(c)
|
||||
}
|
||||
|
||||
func isLocalNode(taiID string) bool {
|
||||
meta, ok := yaoTai.GetNodeMeta(taiID)
|
||||
return ok && meta.Mode == "local"
|
||||
}
|
||||
95
openapi/tai/util.go
Normal file
95
openapi/tai/util.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package tai
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// extractContainerID parses container ID from *path param.
|
||||
// /{containerID}/ws → containerID
|
||||
func extractContainerID(path string) string {
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
path = strings.TrimSuffix(path, "/ws")
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
if path == "" || path == "__host__" {
|
||||
return "__host__"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// bridgeWebSocket copies messages bidirectionally between two WebSocket connections.
|
||||
func bridgeWebSocket(client, target *websocket.Conn) {
|
||||
done := make(chan struct{}, 2)
|
||||
|
||||
go func() {
|
||||
defer func() { done <- struct{}{} }()
|
||||
for {
|
||||
mt, data, err := client.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := target.WriteMessage(mt, data); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer func() { done <- struct{}{} }()
|
||||
for {
|
||||
mt, data, err := target.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := client.WriteMessage(mt, data); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-done
|
||||
}
|
||||
|
||||
// reverseProxy forwards an HTTP request to targetURL and streams the response back.
|
||||
func reverseProxy(c *gin.Context, targetURL string) {
|
||||
req, err := http.NewRequestWithContext(c.Request.Context(), c.Request.Method, targetURL, c.Request.Body)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "create proxy request: " + err.Error()})
|
||||
return
|
||||
}
|
||||
for k, vv := range c.Request.Header {
|
||||
for _, v := range vv {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "proxy request failed: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
for k, vv := range resp.Header {
|
||||
for _, v := range vv {
|
||||
c.Writer.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
c.Writer.WriteHeader(resp.StatusCode)
|
||||
c.Writer.Flush()
|
||||
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, readErr := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
c.Writer.Write(buf[:n])
|
||||
c.Writer.Flush()
|
||||
}
|
||||
if readErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
57
openapi/tai/vnc.go
Normal file
57
openapi/tai/vnc.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package tai
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
yaoTai "github.com/yaoapp/yao/tai"
|
||||
)
|
||||
|
||||
var wsUpgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
Subprotocols: []string{"binary"},
|
||||
}
|
||||
|
||||
// handleLocalVNC resolves the container's VNC address via Docker socket
|
||||
// and proxies the WebSocket connection.
|
||||
func handleLocalVNC(c *gin.Context, taiID string) {
|
||||
containerID := extractContainerID(c.Param("path"))
|
||||
if containerID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing container ID in path"})
|
||||
return
|
||||
}
|
||||
|
||||
res, ok := yaoTai.GetResources(taiID)
|
||||
if !ok || res.VNC == nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "VNC not available for node " + taiID})
|
||||
return
|
||||
}
|
||||
|
||||
targetURL, err := res.VNC.URL(c.Request.Context(), containerID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "resolve VNC target: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
clientConn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer clientConn.Close()
|
||||
|
||||
dialer := websocket.Dialer{
|
||||
Subprotocols: []string{"binary"},
|
||||
HandshakeTimeout: 5 * time.Second,
|
||||
}
|
||||
targetConn, _, err := dialer.Dial(targetURL, nil)
|
||||
if err != nil {
|
||||
clientConn.WriteMessage(websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "VNC connection failed"))
|
||||
return
|
||||
}
|
||||
defer targetConn.Close()
|
||||
|
||||
bridgeWebSocket(clientConn, targetConn)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue