bigbluebutton-Github/bbb-graphql-middleware/internal/websrv/connhandler.go
Gustavo Trott 3c71aab4bc
refactor (gql-middlware): Refactored code to simplify channel handling and remove unnecessary routines (#20640)
* Refactor gql-middleware channels and remove dedicated channel to establish init connection

* remove unnecessary routin to wait mutations to complete before invalidate Hasura connection
2024-07-05 13:35:08 -03:00

377 lines
13 KiB
Go

package websrv
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/iMDT/bbb-graphql-middleware/internal/akka_apps"
"github.com/iMDT/bbb-graphql-middleware/internal/bbb_web"
"github.com/iMDT/bbb-graphql-middleware/internal/common"
"github.com/iMDT/bbb-graphql-middleware/internal/gql_actions"
"github.com/iMDT/bbb-graphql-middleware/internal/hasura"
"github.com/iMDT/bbb-graphql-middleware/internal/msgpatch"
"github.com/iMDT/bbb-graphql-middleware/internal/websrv/reader"
"github.com/iMDT/bbb-graphql-middleware/internal/websrv/writer"
log "github.com/sirupsen/logrus"
"net/http"
"nhooyr.io/websocket"
"os"
"strings"
"sync"
"time"
)
var lastBrowserConnectionId int
// Buffer size of the channels
var bufferSize = 100
// active browser connections
var BrowserConnections = make(map[string]*common.BrowserConnection)
var BrowserConnectionsMutex = &sync.RWMutex{}
// Handle client connection
// This is the connection that comes from browser
func ConnectionHandler(w http.ResponseWriter, r *http.Request) {
log := log.WithField("_routine", "ConnectionHandler")
common.ActivitiesOverviewStarted("__BrowserConnection")
defer common.ActivitiesOverviewCompleted("__BrowserConnection")
// Obtain id for this connection
lastBrowserConnectionId++
browserConnectionId := "BC" + fmt.Sprintf("%010d", lastBrowserConnectionId)
log = log.WithField("browserConnectionId", browserConnectionId)
// Starts a context that will be dependent on the connection, so we can cancel subroutines when the connection is dropped
browserConnectionContext, browserConnectionContextCancel := context.WithCancel(r.Context())
defer browserConnectionContextCancel()
// Add sub-protocol
var acceptOptions websocket.AcceptOptions
acceptOptions.Subprotocols = append(acceptOptions.Subprotocols, "graphql-transport-ws")
bbbOrigin := os.Getenv("BBB_GRAPHQL_MIDDLEWARE_ORIGIN")
if bbbOrigin != "" {
acceptOptions.OriginPatterns = append(acceptOptions.OriginPatterns, bbbOrigin)
}
browserWsConn, err := websocket.Accept(w, r, &acceptOptions)
browserWsConn.SetReadLimit(9999999) //10MB
if err != nil {
log.Errorf("error: %v", err)
}
defer browserWsConn.Close(websocket.StatusInternalError, "the sky is falling")
var thisConnection = common.BrowserConnection{
Id: browserConnectionId,
Websocket: browserWsConn,
BrowserRequestCookies: r.Cookies(),
ActiveSubscriptions: make(map[string]common.GraphQlSubscription, 1),
Context: browserConnectionContext,
ContextCancelFunc: browserConnectionContextCancel,
ConnAckSentToBrowser: false,
FromBrowserToHasuraChannel: common.NewSafeChannelByte(bufferSize),
FromBrowserToGqlActionsChannel: common.NewSafeChannelByte(bufferSize),
FromHasuraToBrowserChannel: common.NewSafeChannelByte(bufferSize),
}
BrowserConnectionsMutex.Lock()
BrowserConnections[browserConnectionId] = &thisConnection
BrowserConnectionsMutex.Unlock()
defer func() {
msgpatch.RemoveConnCacheDir(browserConnectionId)
BrowserConnectionsMutex.Lock()
_, bcExists := BrowserConnections[browserConnectionId]
if bcExists {
sessionTokenRemoved := BrowserConnections[browserConnectionId].SessionToken
delete(BrowserConnections, browserConnectionId)
if sessionTokenRemoved != "" {
go SendUserGraphqlConnectionClosedSysMsg(sessionTokenRemoved, browserConnectionId)
}
}
BrowserConnectionsMutex.Unlock()
log.Infof("connection removed")
}()
// Log it
log.Infof("connection accepted")
// Configure the wait group (to hold this routine execution until both are completed)
var wgAll sync.WaitGroup
wgAll.Add(2)
// Other wait group to close this connection once Browser Reader dies
var wgReader sync.WaitGroup
wgReader.Add(1)
// Reads from browser connection, writes into fromBrowserToHasuraChannel
go reader.BrowserConnectionReader(&thisConnection, []*sync.WaitGroup{&wgAll, &wgReader})
go func() {
wgReader.Wait()
log.Debug("BrowserConnectionReader finished, closing Write Channel")
thisConnection.FromHasuraToBrowserChannel.Close()
thisConnection.Disconnected = true
}()
//Check authorization and obtain user session variables from bbb-web
if errorOnInitConnection := connectionInitHandler(&thisConnection); errorOnInitConnection != nil {
//If the server wishes to reject the connection it is recommended to close the socket with `4403: Forbidden`.
//https://github.com/enisdenjo/graphql-ws/blob/63881c3372a3564bf42040e3f572dd74e41b2e49/PROTOCOL.md?plain=1#L36
wsError := &websocket.CloseError{
Code: websocket.StatusCode(4403),
Reason: errorOnInitConnection.Error(),
}
browserWsConn.Close(wsError.Code, wsError.Reason)
browserConnectionContextCancel()
}
// Ensure a hasura client is running while the browser is connected
go func() {
log.Debugf("starting hasura client")
BrowserConnectedLoop:
for {
select {
case <-browserConnectionContext.Done():
break BrowserConnectedLoop
default:
{
log.Debugf("creating hasura client")
BrowserConnectionsMutex.RLock()
thisBrowserConnection := BrowserConnections[browserConnectionId]
BrowserConnectionsMutex.RUnlock()
if thisBrowserConnection != nil {
log.Debugf("created hasura client")
hasura.HasuraClient(thisBrowserConnection)
}
time.Sleep(100 * time.Millisecond)
}
}
}
}()
// Ensure a gql-actions client is running while the browser is connected
go func() {
log.Debugf("starting gql-actions client")
BrowserConnectedLoop:
for {
select {
case <-browserConnectionContext.Done():
break BrowserConnectedLoop
default:
{
log.Debugf("creating gql-actions client")
BrowserConnectionsMutex.RLock()
thisBrowserConnection := BrowserConnections[browserConnectionId]
BrowserConnectionsMutex.RUnlock()
if thisBrowserConnection != nil {
log.Debugf("created gql-actions client")
BrowserConnectionsMutex.Lock()
thisBrowserConnection.GraphqlActionsContext, thisBrowserConnection.GraphqlActionsContextCancel = context.WithCancel(browserConnectionContext)
BrowserConnectionsMutex.Unlock()
gql_actions.GraphqlActionsClient(thisBrowserConnection)
}
time.Sleep(1000 * time.Millisecond)
}
}
}
}()
// Reads from fromHasuraToBrowserChannel, writes to browser connection
go writer.BrowserConnectionWriter(&thisConnection, &wgAll)
// Wait until all routines are finished
wgAll.Wait()
}
func InvalidateSessionTokenConnections(sessionTokenToInvalidate string) {
BrowserConnectionsMutex.RLock()
connectionsToProcess := make([]*common.BrowserConnection, 0)
for _, browserConnection := range BrowserConnections {
if browserConnection.SessionToken == sessionTokenToInvalidate {
connectionsToProcess = append(connectionsToProcess, browserConnection)
}
}
BrowserConnectionsMutex.RUnlock()
var wg sync.WaitGroup
for _, browserConnection := range connectionsToProcess {
wg.Add(1)
go func(bc *common.BrowserConnection) {
defer wg.Done()
invalidateHasuraConnectionForSessionToken(bc, sessionTokenToInvalidate)
}(browserConnection)
}
wg.Wait()
}
func invalidateHasuraConnectionForSessionToken(bc *common.BrowserConnection, sessionToken string) {
BrowserConnectionsMutex.RLock()
defer BrowserConnectionsMutex.RUnlock()
if bc.HasuraConnection == nil {
return // If there's no Hasura connection, there's nothing to invalidate.
}
log.Debugf("Processing invalidate request for sessionToken %v (hasura connection %v)", sessionToken, bc.HasuraConnection.Id)
// Stop receiving new messages from the browser.
log.Debug("freezing channel fromBrowserToHasuraChannel")
bc.FromBrowserToHasuraChannel.FreezeChannel()
//Update variables for Mutations (gql-actions requests)
go refreshUserSessionVariables(bc)
// Cancel the Hasura connection context to clean up resources.
if bc.HasuraConnection != nil && bc.HasuraConnection.ContextCancelFunc != nil {
bc.HasuraConnection.ContextCancelFunc()
}
// Send a reconnection confirmation message
go SendUserGraphqlReconnectionForcedEvtMsg(sessionToken)
}
func refreshUserSessionVariables(browserConnection *common.BrowserConnection) error {
BrowserConnectionsMutex.RLock()
browserUserId := browserConnection.UserId
browserMeetingId := browserConnection.MeetingId
browserConnectionId := browserConnection.Id
BrowserConnectionsMutex.RUnlock()
// Check authorization
sessionVariables, err := akka_apps.AkkaAppsGetSessionVariablesFrom(browserConnectionId, browserMeetingId, browserUserId)
if err != nil {
log.Error(err)
return fmt.Errorf("error on checking sessionToken authorization")
} else {
log.Trace("Session variables obtained successfully")
}
if _, exists := sessionVariables["x-hasura-role"]; !exists {
return fmt.Errorf("error on checking sessionToken authorization, X-Hasura-Role is missing")
}
if _, exists := sessionVariables["x-hasura-userid"]; !exists {
return fmt.Errorf("error on checking sessionToken authorization, X-Hasura-UserId is missing")
}
if _, exists := sessionVariables["x-hasura-meetingid"]; !exists {
return fmt.Errorf("error on checking sessionToken authorization, X-Hasura-MeetingId is missing")
}
BrowserConnectionsMutex.Lock()
browserConnection.BBBWebSessionVariables = sessionVariables
BrowserConnectionsMutex.Unlock()
return nil
}
func connectionInitHandler(browserConnection *common.BrowserConnection) error {
BrowserConnectionsMutex.RLock()
browserConnectionId := browserConnection.Id
browserConnectionCookies := browserConnection.BrowserRequestCookies
BrowserConnectionsMutex.RUnlock()
// Intercept the fromBrowserMessage channel to get the sessionToken
for {
fromBrowserMessage, ok := browserConnection.FromBrowserToHasuraChannel.Receive()
if !ok {
//Received all messages. Channel is closed
return fmt.Errorf("error on receiving init connection")
}
if bytes.Contains(fromBrowserMessage, []byte("\"connection_init\"")) {
var fromBrowserMessageAsMap map[string]interface{}
if err := json.Unmarshal(fromBrowserMessage, &fromBrowserMessageAsMap); err != nil {
log.Errorf("failed to unmarshal message: %v", err)
continue
}
var payloadAsMap = fromBrowserMessageAsMap["payload"].(map[string]interface{})
var headersAsMap = payloadAsMap["headers"].(map[string]interface{})
var sessionToken, existsSessionToken = headersAsMap["X-Session-Token"].(string)
if !existsSessionToken {
return fmt.Errorf("X-Session-Token header missing on init connection")
}
var clientSessionUUID, existsClientSessionUUID = headersAsMap["X-ClientSessionUUID"].(string)
if !existsClientSessionUUID {
return fmt.Errorf("X-ClientSessionUUID header missing on init connection")
}
var clientType, existsClientType = headersAsMap["X-ClientType"].(string)
if !existsClientType {
return fmt.Errorf("X-ClientType header missing on init connection")
}
var clientIsMobile, existsMobile = headersAsMap["X-ClientIsMobile"].(string)
if !existsMobile {
return fmt.Errorf("X-ClientIsMobile header missing on init connection")
}
var meetingId, userId string
var errCheckAuthorization error
// Check authorization
var numOfAttempts = 0
for {
meetingId, userId, errCheckAuthorization = bbb_web.BBBWebCheckAuthorization(browserConnectionId, sessionToken, browserConnectionCookies)
if errCheckAuthorization != nil {
log.Error(errCheckAuthorization)
}
if (errCheckAuthorization == nil && meetingId != "" && userId != "") || numOfAttempts > 5 {
break
}
numOfAttempts++
time.Sleep(100 * time.Millisecond)
}
if errCheckAuthorization != nil {
return fmt.Errorf("error on trying to check authorization")
}
if meetingId == "" {
return fmt.Errorf("error to obtain user meetingId from BBBWebCheckAuthorization")
}
if userId == "" {
return fmt.Errorf("error to obtain user userId from BBBWebCheckAuthorization")
}
log.Trace("Success on check authorization")
log.Debugf("[ConnectionInitHandler] intercepted Session Token %v and Client Session UUID %v", sessionToken, clientSessionUUID)
BrowserConnectionsMutex.Lock()
browserConnection.SessionToken = sessionToken
browserConnection.ClientSessionUUID = clientSessionUUID
browserConnection.MeetingId = meetingId
browserConnection.UserId = userId
browserConnection.ConnectionInitMessage = fromBrowserMessage
BrowserConnectionsMutex.Unlock()
if err := refreshUserSessionVariables(browserConnection); err != nil {
return fmt.Errorf("error on getting session variables")
}
go SendUserGraphqlConnectionEstablishedSysMsg(
sessionToken,
clientSessionUUID,
clientType,
strings.ToLower(clientIsMobile) == "true",
browserConnectionId,
)
break
}
}
return nil
}