package wire import ( "context" "fmt" "net" "strconv" "time" "go.uber.org/zap" "google.golang.org/protobuf/proto" "nhooyr.io/websocket" iop "iop/proto/gen/iop" proto_socket "git.toki-labs.com/toki/proto-socket/go" ) // PortalServer wraps the proto-socket WebSocket server for Portal-Control Plane communication. type PortalServer struct { server *proto_socket.WsServer logger *zap.Logger host string port int } // NewPortalServer creates a new PortalServer instance listening on the specified address. func NewPortalServer(listenAddr string, logger *zap.Logger) (*PortalServer, error) { host, portStr, err := net.SplitHostPort(listenAddr) if err != nil { return nil, fmt.Errorf("invalid listen address %q: %w", listenAddr, err) } port, err := strconv.Atoi(portStr) if err != nil { return nil, fmt.Errorf("invalid port in listen address %q: %w", listenAddr, err) } parserMap := proto_socket.ParserMap{ proto_socket.TypeNameOf(&iop.PortalHelloRequest{}): func(b []byte) (proto.Message, error) { req := &iop.PortalHelloRequest{} return req, proto.Unmarshal(b, req) }, proto_socket.TypeNameOf(&iop.PortalHelloResponse{}): func(b []byte) (proto.Message, error) { res := &iop.PortalHelloResponse{} return res, proto.Unmarshal(b, res) }, } opts := proto_socket.WsServerOptions{ AcceptOptions: &websocket.AcceptOptions{ OriginPatterns: []string{ "localhost", "localhost:*", "127.0.0.1", "127.0.0.1:*", }, }, } wsServer := proto_socket.NewWsServerWithOptions(host, port, "/portal", opts, func(conn *websocket.Conn) *proto_socket.WsClient { client := proto_socket.NewWsClient(conn, proto_socket.DefaultHeartbeatIntervalSec, proto_socket.DefaultHeartbeatWaitSec, parserMap) proto_socket.AddRequestListenerTyped(&client.Communicator, func(req *iop.PortalHelloRequest) (*iop.PortalHelloResponse, error) { logger.Info("portal client hello request received", zap.String("client_id", req.ClientId), zap.String("client_version", req.ClientVersion), ) return &iop.PortalHelloResponse{ Ready: true, Protocol: Protocol, ServerTimeUnixNano: time.Now().UnixNano(), Message: "Hello from IOP Control Plane", }, nil }) return client }) wsServer.OnClientConnected = func(client *proto_socket.WsClient) { logger.Info("portal client connected via proto-socket WS") } return &PortalServer{ server: wsServer, logger: logger, host: host, port: port, }, nil } // Start starts the portal wire WebSocket server. func (s *PortalServer) Start(ctx context.Context) error { s.logger.Info("starting portal wire WS server", zap.String("host", s.host), zap.Int("port", s.port), zap.String("path", "/portal"), ) return s.server.Start(ctx) } // Stop stops the portal wire WebSocket server. func (s *PortalServer) Stop() error { s.logger.Info("stopping portal wire WS server") return s.server.Stop() } // Host returns the host the server is listening on. func (s *PortalServer) Host() string { return s.host } // Port returns the port the server is listening on. func (s *PortalServer) Port() int { return s.port }