From ed6f914e15f0fed1fc2aaeffc6898d817965faca Mon Sep 17 00:00:00 2001 From: streamer45 Date: Wed, 8 Jan 2025 12:47:14 +0100 Subject: [PATCH] Implement API handler to fetch session config --- service/client.go | 76 +++++++++++++++++++++++++++++++ service/client_test.go | 100 +++++++++++++++++++++++++++++++++++++++++ service/config.go | 48 -------------------- service/rtc/server.go | 19 ++++++++ service/service.go | 1 + service/session.go | 52 +++++++++++++++++++++ 6 files changed, 248 insertions(+), 48 deletions(-) create mode 100644 service/session.go diff --git a/service/client.go b/service/client.go index d7ea6c4..316364a 100644 --- a/service/client.go +++ b/service/client.go @@ -11,9 +11,11 @@ import ( "log" "net" "net/http" + "net/url" "sync" "time" + "github.com/mattermost/rtcd/service/rtc" "github.com/mattermost/rtcd/service/ws" ) @@ -38,6 +40,52 @@ type Client struct { wg sync.WaitGroup } +type ClientConfig struct { + httpURL string + wsURL string + + ClientID string + AuthKey string + URL string + ReconnectInterval time.Duration +} + +func (c *ClientConfig) Parse() error { + if c.URL == "" { + return fmt.Errorf("invalid URL value: should not be empty") + } + + u, err := url.Parse(c.URL) + if err != nil { + return fmt.Errorf("failed to parse url: %w", err) + } + + if u.Host == "" { + return fmt.Errorf("invalid url host: should not be empty") + } + + switch u.Scheme { + case "http": + c.httpURL = c.URL + u.Scheme = "ws" + u.Path = "/ws" + c.wsURL = u.String() + case "https": + c.httpURL = c.URL + u.Scheme = "wss" + u.Path = "/ws" + c.wsURL = u.String() + default: + return fmt.Errorf("invalid url scheme: %q is not valid", u.Scheme) + } + + if c.ReconnectInterval <= 0 { + c.ReconnectInterval = defaultReconnectInterval + } + + return nil +} + func NewClient(cfg ClientConfig, opts ...ClientOption) (*Client, error) { var c Client @@ -408,3 +456,31 @@ func (c *Client) GetSystemInfo() (SystemInfo, error) { return info, nil } + +func (c *Client) GetSession(callID, sessionID string) (rtc.SessionConfig, int, error) { + reqURL := fmt.Sprintf("%s/calls/%s/sessions/%s", c.cfg.httpURL, callID, sessionID) + + req, err := http.NewRequest("GET", reqURL, nil) + if err != nil { + return rtc.SessionConfig{}, 0, fmt.Errorf("failed to build request: %w", err) + } + req.SetBasicAuth(c.cfg.ClientID, c.cfg.AuthKey) + + resp, err := c.httpClient.Do(req) + if err != nil { + return rtc.SessionConfig{}, 0, fmt.Errorf("http request failed: %w", err) + } + defer resp.Body.Close() + + var cfg rtc.SessionConfig + + if resp.StatusCode != http.StatusOK { + return rtc.SessionConfig{}, resp.StatusCode, fmt.Errorf("request failed with status %s", resp.Status) + } + + if err := json.NewDecoder(resp.Body).Decode(&cfg); err != nil { + return rtc.SessionConfig{}, resp.StatusCode, fmt.Errorf("decoding http response failed: %w", err) + } + + return cfg, resp.StatusCode, nil +} diff --git a/service/client_test.go b/service/client_test.go index 51bc9a3..0d501ed 100644 --- a/service/client_test.go +++ b/service/client_test.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net" + "net/http" "runtime" "sync" "sync/atomic" @@ -16,6 +17,7 @@ import ( "github.com/mattermost/rtcd/service/auth" "github.com/mattermost/rtcd/service/random" + "github.com/mattermost/rtcd/service/rtc" "github.com/mattermost/rtcd/service/ws" "github.com/stretchr/testify/require" @@ -780,3 +782,101 @@ func TestClientGetSystemInfo(t *testing.T) { require.NotZero(t, info.CPULoad) }) } + +func TestClientGetSession(t *testing.T) { + th := SetupTestHelper(t, nil) + defer th.Teardown() + + // register client + authKey := "Nl9OZthX5cMJz5a_HmU3kQJ4pHIIlohr" + err := th.srvc.auth.Register("clientA", authKey) + require.NoError(t, err) + + t.Run("unauthorized", func(t *testing.T) { + c, err := NewClient(ClientConfig{ + URL: th.apiURL, + }) + require.NoError(t, err) + require.NotNil(t, c) + defer c.Close() + + cfg, code, err := c.GetSession("callID", "sessionID") + require.EqualError(t, err, "request failed with status 401 Unauthorized") + require.Empty(t, cfg) + require.Equal(t, http.StatusUnauthorized, code) + }) + + t.Run("no call ongoing", func(t *testing.T) { + c, err := NewClient(ClientConfig{ + URL: th.apiURL, + ClientID: "clientA", + AuthKey: authKey, + }) + require.NoError(t, err) + require.NotNil(t, c) + defer c.Close() + + cfg, code, err := c.GetSession("callID", "sessionID") + require.EqualError(t, err, "request failed with status 404 Not Found") + require.Empty(t, cfg) + require.Equal(t, http.StatusNotFound, code) + }) + + t.Run("no session found", func(t *testing.T) { + c, err := NewClient(ClientConfig{ + URL: th.apiURL, + ClientID: "clientA", + AuthKey: authKey, + }) + require.NoError(t, err) + require.NotNil(t, c) + defer c.Close() + + cfg := rtc.SessionConfig{ + GroupID: "clientA", + CallID: "callIDA", + UserID: "userID", + SessionID: "sessionID", + } + err = th.srvc.rtcServer.InitSession(cfg, nil) + require.NoError(t, err) + defer func() { + err := th.srvc.rtcServer.CloseSession("sessionID") + require.NoError(t, err) + }() + + cfg, code, err := c.GetSession("callID", "sessionID") + require.EqualError(t, err, "request failed with status 404 Not Found") + require.Empty(t, cfg) + require.Equal(t, http.StatusNotFound, code) + }) + + t.Run("session found", func(t *testing.T) { + c, err := NewClient(ClientConfig{ + URL: th.apiURL, + ClientID: "clientA", + AuthKey: authKey, + }) + require.NoError(t, err) + require.NotNil(t, c) + defer c.Close() + + cfg := rtc.SessionConfig{ + GroupID: "clientA", + CallID: "callIDA", + UserID: "userID", + SessionID: "sessionID", + } + err = th.srvc.rtcServer.InitSession(cfg, nil) + require.NoError(t, err) + defer func() { + err := th.srvc.rtcServer.CloseSession("sessionID") + require.NoError(t, err) + }() + + sessionCfg, code, err := c.GetSession("callIDA", "sessionID") + require.NoError(t, err) + require.Equal(t, cfg, sessionCfg) + require.Equal(t, http.StatusOK, code) + }) +} diff --git a/service/config.go b/service/config.go index 691b5c9..5f5ad39 100644 --- a/service/config.go +++ b/service/config.go @@ -5,8 +5,6 @@ package service import ( "fmt" - "net/url" - "time" "github.com/mattermost/rtcd/service/auth" @@ -105,49 +103,3 @@ func (c StoreConfig) IsValid() error { } return nil } - -type ClientConfig struct { - httpURL string - wsURL string - - ClientID string - AuthKey string - URL string - ReconnectInterval time.Duration -} - -func (c *ClientConfig) Parse() error { - if c.URL == "" { - return fmt.Errorf("invalid URL value: should not be empty") - } - - u, err := url.Parse(c.URL) - if err != nil { - return fmt.Errorf("failed to parse url: %w", err) - } - - if u.Host == "" { - return fmt.Errorf("invalid url host: should not be empty") - } - - switch u.Scheme { - case "http": - c.httpURL = c.URL - u.Scheme = "ws" - u.Path = "/ws" - c.wsURL = u.String() - case "https": - c.httpURL = c.URL - u.Scheme = "wss" - u.Path = "/ws" - c.wsURL = u.String() - default: - return fmt.Errorf("invalid url scheme: %q is not valid", u.Scheme) - } - - if c.ReconnectInterval <= 0 { - c.ReconnectInterval = defaultReconnectInterval - } - - return nil -} diff --git a/service/rtc/server.go b/service/rtc/server.go index 1e218aa..ab98bd5 100644 --- a/service/rtc/server.go +++ b/service/rtc/server.go @@ -413,3 +413,22 @@ func (s *Server) handleDCMessage(data []byte, us *session, dataCh *webrtc.DataCh return nil } + +func (s *Server) GetSessionConfig(groupID, callID, sessionID string) (SessionConfig, error) { + group := s.getGroup(groupID) + if group == nil { + return SessionConfig{}, fmt.Errorf("group not found") + } + + call := group.getCall(callID) + if call == nil { + return SessionConfig{}, fmt.Errorf("call not found") + } + + session := call.getSession(sessionID) + if session == nil { + return SessionConfig{}, fmt.Errorf("session not found") + } + + return session.cfg, nil +} diff --git a/service/service.go b/service/service.go index 1bf25df..4eb399d 100644 --- a/service/service.go +++ b/service/service.go @@ -118,6 +118,7 @@ func New(cfg Config) (*Service, error) { s.apiServer.RegisterHandleFunc("/login", s.loginClient) s.apiServer.RegisterHandleFunc("/register", s.registerClient) s.apiServer.RegisterHandleFunc("/unregister", s.unregisterClient) + s.apiServer.RegisterHandleFunc("/calls/{callID}/sessions/{sessionID}", s.handleGetSession) s.apiServer.RegisterHandler("/ws", s.wsServer) if runtime.GOOS != "darwin" { diff --git a/service/session.go b/service/session.go new file mode 100644 index 0000000..99471f8 --- /dev/null +++ b/service/session.go @@ -0,0 +1,52 @@ +// Copyright (c) 2022-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package service + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/mattermost/mattermost/server/public/shared/mlog" +) + +func (s *Service) handleGetSession(w http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.NotFound(w, req) + return + } + + clientID, code, err := s.authHandler(w, req) + if err != nil { + s.log.Error("failed to authenticate", mlog.Err(err), mlog.Int("code", code)) + } + + if clientID == "" { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + callID := req.PathValue("callID") + if callID == "" { + http.Error(w, "callID is required", http.StatusBadRequest) + return + } + + sessionID := req.PathValue("sessionID") + if sessionID == "" { + http.Error(w, "sessionID is required", http.StatusBadRequest) + return + } + + cfg, err := s.rtcServer.GetSessionConfig(clientID, callID, sessionID) + if err != nil { + http.Error(w, fmt.Sprintf("failed to get session config: %s", err.Error()), http.StatusNotFound) + return + } + + w.Header().Add("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(cfg); err != nil { + s.log.Error("failed to encode data", mlog.Err(err)) + } +}