From c7c3c9c6b0f2cc93191f01616127eb117467a019 Mon Sep 17 00:00:00 2001 From: Juliusz Chroboczek Date: Tue, 26 Oct 2021 22:22:48 +0200 Subject: [PATCH] Export group status in .status.json. --- README.PROTOCOL | 61 +++++++++++++++++++++++++++++------------- group/group.go | 44 ++++++++++++++++++++---------- rtpconn/webclient.go | 23 +++------------- static/galene.js | 19 +++++++++++-- webserver/webserver.go | 41 ++++++++++++++++++++++++++-- 5 files changed, 132 insertions(+), 56 deletions(-) diff --git a/README.PROTOCOL b/README.PROTOCOL index 3fc00ed..aadcbc4 100644 --- a/README.PROTOCOL +++ b/README.PROTOCOL @@ -1,24 +1,5 @@ # Galène's protocol -Galène uses a symmetric, asynchronous protocol. In client-server -usage, some messages are only sent in the client to server or in the -server to client direction. - -## Message syntax - -All messages are sent as JSON objects. All fields except `type` are -optional; however, there are some fields that are common across multiple -message types. - - - `type`, the type of the message; - - `kind`, the subtype of the message; - - `id`, the id of the object being manipulated; - - `source`, the client-id of the originating client; - - `username`, the username of the originating client; - - `dest`, the client-id of the destination client; - - `privileged`, set by the server to indicate that the originating client - had the `op` privilege at the time it sent the message. - ## Data structures ### Group @@ -41,6 +22,48 @@ exactly one peer connection (PC) (multiple streams in a single PC are not allowed). The offerer is also the RTP sender (i.e. all tracks sent by the offerer are of type `sendonly`). +Galène uses a symmetric, asynchronous protocol. In client-server +usage, some messages are only sent in the client to server or in the +server to client direction. + +## Before connecting + +Before it connects and joins a group, a client may perform an HTTP GET +request on the URL `/public-groups.json`. This yields a JSON array of +objects, one for each group that has been marked public in its +configuration file. Each object has the following fields: + + - `name`: the group's name + - `displayName` (optional): a longer version of the name used for display; + - `description` (optional): a user-readable description. + - `locked`: true if the group is locked; + - `clientCount`: the number of clients currently in the group. + +A client may also fetch the URL `/group/name/.status.json` to retrieve the +status of a single group. If the group has not been marked as public, +then the fields `locked` and `clientCount` are omitted. + +## Connecting + +The client connects to the websocket at `/ws`. Galene uses a symmetric, +asynchronous protocol: there are no requests and responses, and most +messages may be sent by either peer. + +## Message syntax + +All messages are sent as JSON objects. All fields except `type` are +optional; however, there are some fields that are common across multiple +message types: + + - `type`, the type of the message; + - `kind`, the subtype of the message; + - `id`, the id of the object being manipulated; + - `source`, the client-id of the originating client; + - `username`, the username of the originating client; + - `dest`, the client-id of the destination client; + - `privileged`, set by the server to indicate that the originating client + had the `op` privilege at the time when it sent the message. + ## Establishing and maintaining a connection The peer establishing the connection (the WebSocket client) sends diff --git a/group/group.go b/group/group.go index ee687ca..6d00806 100644 --- a/group/group.go +++ b/group/group.go @@ -112,6 +112,12 @@ func (g *Group) Description() *Description { return g.description } +func (g *Group) ClientCount() int { + g.mu.Lock() + defer g.mu.Unlock() + return len(g.clients) +} + func (g *Group) EmptyTime() time.Duration { g.mu.Lock() defer g.mu.Unlock() @@ -1052,27 +1058,37 @@ func (desc *Description) GetPermission(group string, creds ClientCredentials) (C return p, ErrNotAuthorised } -type Public struct { +type Status struct { Name string `json:"name"` DisplayName string `json:"displayName,omitempty"` Description string `json:"description,omitempty"` Locked bool `json:"locked,omitempty"` - ClientCount int `json:"clientCount"` + ClientCount *int `json:"clientCount,omitempty"` } -func GetPublic() []Public { - gs := make([]Public, 0) +func GetStatus(g *Group, authentified bool) Status { + desc := g.Description() + d := Status{ + Name: g.name, + DisplayName: desc.DisplayName, + Description: desc.Description, + } + + if authentified || desc.Public { + // these are considered private information + locked, _ := g.Locked() + count := g.ClientCount() + d.Locked = locked + d.ClientCount = &count + } + return d +} + +func GetPublic() []Status { + gs := make([]Status, 0) Range(func(g *Group) bool { - desc := g.Description() - if desc.Public { - locked, _ := g.Locked() - gs = append(gs, Public{ - Name: g.name, - DisplayName: desc.DisplayName, - Description: desc.Description, - Locked: locked, - ClientCount: len(g.clients), - }) + if g.Description().Public { + gs = append(gs, GetStatus(g, false)) } return true }) diff --git a/rtpconn/webclient.go b/rtpconn/webclient.go index 5d7480a..949fec7 100644 --- a/rtpconn/webclient.go +++ b/rtpconn/webclient.go @@ -111,7 +111,7 @@ type clientMessage struct { Password string `json:"password,omitempty"` Privileged bool `json:"privileged,omitempty"` Permissions *group.ClientPermissions `json:"permissions,omitempty"` - Status map[string]interface{} `json:"status,omitempty"` + Status interface{} `json:"status,omitempty"` Group string `json:"group,omitempty"` Value interface{} `json:"value,omitempty"` NoEcho bool `json:"noecho,omitempty"` @@ -813,21 +813,6 @@ func (c *webClient) PushConn(g *group.Group, id string, up conn.Up, tracks []con return nil } -func getGroupStatus(g *group.Group) map[string]interface{} { - status := make(map[string]interface{}) - if locked, message := g.Locked(); locked { - if message == "" { - status["locked"] = true - } else { - status["locked"] = message - } - } - if dn := g.Description().DisplayName; dn != "" { - status["displayName"] = dn - } - return status -} - func readMessage(conn *websocket.Conn, m *clientMessage) error { err := conn.SetReadDeadline(time.Now().Add(15 * time.Second)) if err != nil { @@ -1120,11 +1105,11 @@ func handleAction(c *webClient, a interface{}) error { Status: a.status, }) case joinedAction: - var status map[string]interface{} + var status interface{} if a.group != "" { g := group.Get(a.group) if g != nil { - status = getGroupStatus(g) + status = group.GetStatus(g, true) } } perms := c.permissions @@ -1149,7 +1134,7 @@ func handleAction(c *webClient, a interface{}) error { Group: g.Name(), Username: c.username, Permissions: &perms, - Status: getGroupStatus(g), + Status: group.GetStatus(g, true), RTCConfiguration: ice.ICEConfiguration(), }) if !c.permissions.Present { diff --git a/static/galene.js b/static/galene.js index 60e9445..de1485d 100644 --- a/static/galene.js +++ b/static/galene.js @@ -26,6 +26,9 @@ let group; /** @type {ServerConnection} */ let serverConnection; +/** @type {Object} */ +let groupStatus = {}; + /** * @typedef {Object} userpass * @property {string} username @@ -2149,6 +2152,7 @@ async function gotJoined(kind, group, perms, status, message) { return; case 'join': case 'change': + groupStatus = status; setTitle((status && status.displayName) || capitalise(group)); displayUsername(); setButtonsVisibility(); @@ -3095,11 +3099,22 @@ async function serverConnect() { } } -function start() { +async function start() { group = decodeURIComponent( location.pathname.replace(/^\/[a-z]*\//, '').replace(/\/$/, '') ); - setTitle(capitalise(group)); + /** @type {Object} */ + try { + let r = await fetch(".status.json") + if(!r.ok) + throw new Error(`${r.status} ${r.statusText}`); + groupStatus = await r.json() + } catch(e) { + console.error(e); + return; + } + + setTitle(groupStatus.displayName || capitalise(group)); addFilters(); setMediaChoices(false).then(e => reflectSettings()); diff --git a/webserver/webserver.go b/webserver/webserver.go index da3bc08..803fb2b 100644 --- a/webserver/webserver.go +++ b/webserver/webserver.go @@ -278,7 +278,11 @@ func groupHandler(w http.ResponseWriter, r *http.Request) { return } - mungeHeader(w) + if strings.HasSuffix(r.URL.Path, "/.status.json") { + groupStatusHandler(w, r) + return + } + name := parseGroupName("/group/", r.URL.Path) if name == "" { notFound(w) @@ -290,7 +294,7 @@ func groupHandler(w http.ResponseWriter, r *http.Request) { if os.IsNotExist(err) { notFound(w) } else { - log.Printf("addGroup: %v", err) + log.Printf("group.Add: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) } @@ -308,9 +312,42 @@ func groupHandler(w http.ResponseWriter, r *http.Request) { return } + mungeHeader(w) serveFile(w, r, filepath.Join(StaticRoot, "galene.html")) } +func groupStatusHandler(w http.ResponseWriter, r *http.Request) { + path := path.Dir(r.URL.Path) + name := parseGroupName("/group/", path) + if name == "" { + notFound(w) + return + } + + g, err := group.Add(name, nil) + if err != nil { + if os.IsNotExist(err) { + notFound(w) + } else { + http.Error(w, "Internal server error", + http.StatusInternalServerError) + } + return + } + + d := group.GetStatus(g, false) + w.Header().Set("content-type", "application/json") + w.Header().Set("cache-control", "no-cache") + + if r.Method == "HEAD" { + return + } + + e := json.NewEncoder(w) + e.Encode(d) + return +} + func publicHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", "application/json") w.Header().Set("cache-control", "no-cache")