- Introduced ExpandHosts function to parse and expand comma-separated host entries, including special values like "internal" and "localhost". - Updated gRPC server to utilize the new ExpandHosts function for improved host management. - Added HostHasInternal function to check for "internal" in host strings, enhancing configuration flexibility. - Implemented new gRPC endpoints for TaiTunnel registration and forwarding, improving tunnel communication capabilities. - Refactored authentication logic to include new TaiTunnel endpoints, ensuring proper access control. Made-with: Cursor
331 lines
9.8 KiB
Go
331 lines
9.8 KiB
Go
package grpc
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
"github.com/yaoapp/kun/log"
|
|
"github.com/yaoapp/yao/config"
|
|
agenthandler "github.com/yaoapp/yao/grpc/agent"
|
|
apihandler "github.com/yaoapp/yao/grpc/api"
|
|
"github.com/yaoapp/yao/grpc/auth"
|
|
"github.com/yaoapp/yao/grpc/health"
|
|
llmhandler "github.com/yaoapp/yao/grpc/llm"
|
|
mcphandler "github.com/yaoapp/yao/grpc/mcp"
|
|
"github.com/yaoapp/yao/grpc/pb"
|
|
runhandler "github.com/yaoapp/yao/grpc/run"
|
|
sandboxhandler "github.com/yaoapp/yao/grpc/sandbox"
|
|
shellhandler "github.com/yaoapp/yao/grpc/shell"
|
|
"github.com/yaoapp/yao/tai/registry"
|
|
"github.com/yaoapp/yao/tai/tunnel"
|
|
"github.com/yaoapp/yao/tai/tunnel/taipb"
|
|
)
|
|
|
|
var (
|
|
mu sync.Mutex
|
|
server *grpc.Server
|
|
listeners []net.Listener
|
|
addrs []string
|
|
)
|
|
|
|
type yaoServer struct {
|
|
pb.UnimplementedYaoServer
|
|
health health.Handler
|
|
run runhandler.Handler
|
|
shell shellhandler.Handler
|
|
api apihandler.Handler
|
|
mcp mcphandler.Handler
|
|
llm llmhandler.Handler
|
|
agent agenthandler.Handler
|
|
sandbox *sandboxhandler.Handler
|
|
}
|
|
|
|
// ── Health ───────────────────────────────────────────────────────────────────
|
|
|
|
func (s *yaoServer) Healthz(ctx context.Context, req *pb.Empty) (*pb.HealthzResponse, error) {
|
|
return s.health.Healthz(ctx, req)
|
|
}
|
|
|
|
// ── Base ─────────────────────────────────────────────────────────────────────
|
|
|
|
func (s *yaoServer) Run(ctx context.Context, req *pb.RunRequest) (*pb.RunResponse, error) {
|
|
return s.run.Run(ctx, req)
|
|
}
|
|
|
|
func (s *yaoServer) Shell(ctx context.Context, req *pb.ShellRequest) (*pb.ShellResponse, error) {
|
|
return s.shell.Shell(ctx, req)
|
|
}
|
|
|
|
// V2 stubs — Stream and ShellStream depend on gou/stream package.
|
|
func (s *yaoServer) Stream(req *pb.RunRequest, stream grpc.ServerStreamingServer[pb.Chunk]) error {
|
|
return status.Error(codes.Unimplemented, "Stream not implemented (V2)")
|
|
}
|
|
|
|
func (s *yaoServer) ShellStream(req *pb.ShellRequest, stream grpc.ServerStreamingServer[pb.Chunk]) error {
|
|
return status.Error(codes.Unimplemented, "ShellStream not implemented (V2)")
|
|
}
|
|
|
|
// ── API ──────────────────────────────────────────────────────────────────────
|
|
|
|
func (s *yaoServer) API(ctx context.Context, req *pb.APIRequest) (*pb.APIResponse, error) {
|
|
return s.api.API(ctx, req)
|
|
}
|
|
|
|
// ── MCP ──────────────────────────────────────────────────────────────────────
|
|
|
|
func (s *yaoServer) MCPListTools(ctx context.Context, req *pb.MCPListRequest) (*pb.MCPListResponse, error) {
|
|
return s.mcp.MCPListTools(ctx, req)
|
|
}
|
|
|
|
func (s *yaoServer) MCPCallTool(ctx context.Context, req *pb.MCPCallRequest) (*pb.MCPCallResponse, error) {
|
|
return s.mcp.MCPCallTool(ctx, req)
|
|
}
|
|
|
|
func (s *yaoServer) MCPListResources(ctx context.Context, req *pb.MCPListRequest) (*pb.MCPResourcesResponse, error) {
|
|
return s.mcp.MCPListResources(ctx, req)
|
|
}
|
|
|
|
func (s *yaoServer) MCPReadResource(ctx context.Context, req *pb.MCPResourceRequest) (*pb.MCPResourceResponse, error) {
|
|
return s.mcp.MCPReadResource(ctx, req)
|
|
}
|
|
|
|
// ── LLM ──────────────────────────────────────────────────────────────────────
|
|
|
|
func (s *yaoServer) ChatCompletions(ctx context.Context, req *pb.ChatRequest) (*pb.ChatResponse, error) {
|
|
return s.llm.ChatCompletions(ctx, req)
|
|
}
|
|
|
|
func (s *yaoServer) ChatCompletionsStream(req *pb.ChatRequest, stream grpc.ServerStreamingServer[pb.ChatChunk]) error {
|
|
return s.llm.ChatCompletionsStream(req, stream)
|
|
}
|
|
|
|
// ── Agent ────────────────────────────────────────────────────────────────────
|
|
|
|
func (s *yaoServer) AgentStream(req *pb.AgentRequest, stream grpc.ServerStreamingServer[pb.AgentChunk]) error {
|
|
return s.agent.AgentStream(req, stream)
|
|
}
|
|
|
|
// ── Sandbox ──────────────────────────────────────────────────────────────────
|
|
|
|
func (s *yaoServer) Heartbeat(ctx context.Context, req *pb.HeartbeatRequest) (*pb.HeartbeatResponse, error) {
|
|
if s.sandbox == nil {
|
|
return &pb.HeartbeatResponse{Action: "ok"}, nil
|
|
}
|
|
return s.sandbox.Heartbeat(ctx, req)
|
|
}
|
|
|
|
// SandboxHandler returns the sandbox handler for external access (e.g., Manager integration).
|
|
func SandboxHandler() *sandboxhandler.Handler {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return sandboxH
|
|
}
|
|
|
|
var sandboxH *sandboxhandler.Handler
|
|
var tunnelH *tunnel.TunnelHandler
|
|
|
|
// SetSandboxOnBeat sets the heartbeat callback for the sandbox handler.
|
|
// Must be called before StartServer.
|
|
func SetSandboxOnBeat(fn func(data *sandboxhandler.HeartbeatData) string) {
|
|
sandboxH = sandboxhandler.NewHandler(fn)
|
|
}
|
|
|
|
// ── Server lifecycle ─────────────────────────────────────────────────────────
|
|
|
|
// StartServer initializes and starts the gRPC server based on config.
|
|
// It supports multiple bind addresses and returns immediately (listeners run in goroutines).
|
|
func StartServer(cfg config.Config) error {
|
|
if strings.ToLower(cfg.GRPC.Enabled) == "off" {
|
|
log.Info("gRPC server disabled (YAO_GRPC=off)")
|
|
return nil
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
server = grpc.NewServer(
|
|
grpc.ChainUnaryInterceptor(auth.UnaryInterceptor),
|
|
grpc.ChainStreamInterceptor(auth.StreamInterceptor),
|
|
)
|
|
if sandboxH == nil {
|
|
sandboxH = sandboxhandler.NewHandler(nil)
|
|
}
|
|
pb.RegisterYaoServer(server, &yaoServer{sandbox: sandboxH})
|
|
|
|
if reg := registry.Global(); reg != nil {
|
|
tunnelH = tunnel.NewTunnelHandler(reg)
|
|
taipb.RegisterTaiTunnelServer(server, tunnelH)
|
|
}
|
|
|
|
hosts := ExpandHosts(cfg.GRPC.Host)
|
|
port := strconv.Itoa(cfg.GRPC.Port)
|
|
|
|
for _, h := range hosts {
|
|
addr := net.JoinHostPort(h, port)
|
|
lis, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
stopLocked()
|
|
return err
|
|
}
|
|
listeners = append(listeners, lis)
|
|
addrs = append(addrs, lis.Addr().String())
|
|
log.Info("gRPC server listening on %s", lis.Addr().String())
|
|
|
|
go func(l net.Listener) {
|
|
if err := server.Serve(l); err != nil {
|
|
log.Error("gRPC server error on %s: %s", l.Addr().String(), err.Error())
|
|
}
|
|
}(lis)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// stopLocked performs cleanup while the caller already holds mu.
|
|
func stopLocked() {
|
|
s := server
|
|
server = nil
|
|
listeners = nil
|
|
addrs = nil
|
|
|
|
if s == nil {
|
|
return
|
|
}
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
s.GracefulStop()
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
log.Info("gRPC server stopped gracefully")
|
|
case <-time.After(5 * time.Second):
|
|
log.Warn("gRPC server graceful stop timed out, forcing stop")
|
|
s.Stop()
|
|
}
|
|
}
|
|
|
|
// Stop gracefully stops the gRPC server with a 5-second timeout.
|
|
// If GracefulStop doesn't complete in time (e.g. active streams), it forces Stop.
|
|
// Safe to call if server was never started.
|
|
func Stop() {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
stopLocked()
|
|
}
|
|
|
|
// GRPCServer returns the active gRPC server instance.
|
|
// Used by the Tai tunnel server to serve data channel connections
|
|
// on the existing gRPC server.
|
|
func GRPCServer() *grpc.Server {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return server
|
|
}
|
|
|
|
// TunnelHandler returns the gRPC tunnel handler for forward requests.
|
|
func TunnelHandler() *tunnel.TunnelHandler {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return tunnelH
|
|
}
|
|
|
|
// Addr returns all addresses the gRPC server is listening on.
|
|
func Addr() []string {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
result := make([]string, len(addrs))
|
|
copy(result, addrs)
|
|
return result
|
|
}
|
|
|
|
// expandHosts parses comma-separated host entries, expanding special values:
|
|
// - "internal" → 127.0.0.1 + all private-network IPv4 addresses (10.x, 172.16-31.x, 192.168.x)
|
|
// - "localhost" → 127.0.0.1
|
|
//
|
|
// Duplicates are removed.
|
|
func ExpandHosts(raw string) []string {
|
|
seen := map[string]bool{}
|
|
var result []string
|
|
for _, h := range strings.Split(raw, ",") {
|
|
h = strings.TrimSpace(h)
|
|
if h == "" {
|
|
continue
|
|
}
|
|
|
|
switch strings.ToLower(h) {
|
|
case "localhost":
|
|
h = "127.0.0.1"
|
|
if !seen[h] {
|
|
seen[h] = true
|
|
result = append(result, h)
|
|
}
|
|
case "internal":
|
|
if !seen["127.0.0.1"] {
|
|
seen["127.0.0.1"] = true
|
|
result = append(result, "127.0.0.1")
|
|
}
|
|
for _, ip := range InternalIPs() {
|
|
if !seen[ip] {
|
|
seen[ip] = true
|
|
result = append(result, ip)
|
|
}
|
|
}
|
|
default:
|
|
if !seen[h] {
|
|
seen[h] = true
|
|
result = append(result, h)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// InternalIPs returns all IPv4 addresses on private-network interfaces
|
|
// (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16).
|
|
func InternalIPs() []string {
|
|
var ips []string
|
|
ifaces, err := net.Interfaces()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
for _, iface := range ifaces {
|
|
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
|
continue
|
|
}
|
|
addrs, err := iface.Addrs()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, a := range addrs {
|
|
ipNet, ok := a.(*net.IPNet)
|
|
if !ok {
|
|
continue
|
|
}
|
|
ip := ipNet.IP.To4()
|
|
if ip == nil {
|
|
continue
|
|
}
|
|
if isPrivateIP(ip) {
|
|
ips = append(ips, ip.String())
|
|
}
|
|
}
|
|
}
|
|
return ips
|
|
}
|
|
|
|
func isPrivateIP(ip net.IP) bool {
|
|
return ip[0] == 10 ||
|
|
(ip[0] == 172 && ip[1] >= 16 && ip[1] <= 31) ||
|
|
(ip[0] == 192 && ip[1] == 168)
|
|
}
|