From 1550e625e1ec5a62766cdd303c58ecde9140fa21 Mon Sep 17 00:00:00 2001
From: Joachim Bauch <mail@joachim-bauch.de>
Date: Sun, 13 Aug 2017 17:51:23 +0200
Subject: [PATCH] Add option to specify a regular expression for public rooms.

Public rooms can be created / joined even if "authorizeRoomCreation" or
"authorizeRoomJoin" are set.
---
 go/channelling/config.go            |  1 +
 go/channelling/room_manager.go      | 19 +++++++++--
 go/channelling/room_manager_test.go | 50 +++++++++++++++++++++++++++++
 go/channelling/server/config.go     | 11 +++++++
 server.conf.in                      |  4 +++
 5 files changed, 82 insertions(+), 3 deletions(-)

diff --git a/go/channelling/config.go b/go/channelling/config.go
index 5fcc65ea..1f1800d2 100644
--- a/go/channelling/config.go
+++ b/go/channelling/config.go
@@ -34,6 +34,7 @@ type Config struct {
 	RoomTypes                       map[*regexp.Regexp]string `json:"-"` // Map of regular expression -> room type
 	RoomNameCaseSensitive           bool                      // Whether the room names are case sensitive.
 	LockedRoomJoinableWithPIN       bool                      // Whether locked rooms should be joinable by providing the PIN the room was locked with
+	PublicRoomNames                 *regexp.Regexp            `json:"-"` // Regular expression that specifies room paths that may be created/joined without a user account.
 }
 
 func (config *Config) WithModule(m string) bool {
diff --git a/go/channelling/room_manager.go b/go/channelling/room_manager.go
index c0c5d51e..4ed4b12d 100644
--- a/go/channelling/room_manager.go
+++ b/go/channelling/room_manager.go
@@ -209,9 +209,17 @@ func (rooms *roomManager) Get(roomID string) (room RoomWorker, ok bool) {
 	return
 }
 
+func (rooms *roomManager) isPublicRoom(roomName string) bool {
+	return rooms.PublicRoomNames != nil &&
+		rooms.PublicRoomNames.MatchString(roomName)
+}
+
 func (rooms *roomManager) GetOrCreate(roomID, roomName, roomType string, credentials *DataRoomCredentials, sessionAuthenticated bool) (RoomWorker, error) {
+	isPublic := false
 	if rooms.AuthorizeRoomJoin && rooms.UsersEnabled && !sessionAuthenticated {
-		return nil, NewDataError("room_join_requires_account", "Room join requires a user account")
+		if isPublic = rooms.isPublicRoom(roomName); !isPublic {
+			return nil, NewDataError("room_join_requires_account", "Room join requires a user account")
+		}
 	}
 
 	if room, ok := rooms.Get(roomID); ok {
@@ -231,8 +239,13 @@ func (rooms *roomManager) GetOrCreate(roomID, roomName, roomType string, credent
 	}
 
 	if rooms.UsersEnabled && rooms.AuthorizeRoomCreation && !sessionAuthenticated {
-		rooms.Unlock()
-		return nil, NewDataError("room_join_requires_account", "Room creation requires a user account")
+		// Only need to check for public room if not checked above.
+		if !isPublic {
+			if isPublic = rooms.isPublicRoom(roomName); !isPublic {
+				rooms.Unlock()
+				return nil, NewDataError("room_join_requires_account", "Room creation requires a user account")
+			}
+		}
 	}
 
 	room := NewRoomWorker(rooms, roomID, roomName, roomType, credentials)
diff --git a/go/channelling/room_manager_test.go b/go/channelling/room_manager_test.go
index 579b33fd..a31f69f6 100644
--- a/go/channelling/room_manager_test.go
+++ b/go/channelling/room_manager_test.go
@@ -22,6 +22,7 @@
 package channelling
 
 import (
+	"regexp"
 	"testing"
 
 	"github.com/strukturag/spreed-webrtc/go/channelling"
@@ -74,6 +75,55 @@ func Test_RoomManager_JoinRoom_ReturnsAnErrorForUnauthenticatedSessionsWhenJoinR
 	assertDataError(t, err, "room_join_requires_account")
 }
 
+func Test_RoomManager_JoinPublicRoom_ForUnauthenticatedSessionsWhenCreationRequiresAnAccount(t *testing.T) {
+	roomManager, config := NewTestRoomManager()
+	config.UsersEnabled = true
+	config.AuthorizeRoomCreation = true
+
+	unauthenticatedSession := &Session{}
+	_, err := roomManager.JoinRoom(channelling.RoomTypeRoom+":public", "public", channelling.RoomTypeRoom, nil, unauthenticatedSession, false, nil)
+	assertDataError(t, err, "room_join_requires_account")
+
+	config.PublicRoomNames = regexp.MustCompile("^public$")
+	_, err = roomManager.JoinRoom(channelling.RoomTypeRoom+":public", "public", channelling.RoomTypeRoom, nil, unauthenticatedSession, false, nil)
+	if err != nil {
+		t.Fatalf("Unexpected error %v joining public room", err)
+	}
+
+	_, err = roomManager.JoinRoom(channelling.RoomTypeRoom+":private", "private", channelling.RoomTypeRoom, nil, unauthenticatedSession, false, nil)
+	assertDataError(t, err, "room_join_requires_account")
+}
+
+func Test_RoomManager_JoinPublicRoom_ForUnauthenticatedSessionsWhenJoinRequiresAnAccount(t *testing.T) {
+	roomManager, config := NewTestRoomManager()
+	config.UsersEnabled = true
+	config.AuthorizeRoomJoin = true
+
+	authenticatedSession := &Session{userid: "9870457"}
+	_, err := roomManager.JoinRoom(channelling.RoomTypeRoom+":public", "public", channelling.RoomTypeRoom, nil, authenticatedSession, true, nil)
+	if err != nil {
+		t.Fatalf("Unexpected error %v joining room while authenticated", err)
+	}
+
+	unauthenticatedSession := &Session{}
+	_, err = roomManager.JoinRoom(channelling.RoomTypeRoom+":public", "public", channelling.RoomTypeRoom, nil, unauthenticatedSession, false, nil)
+	assertDataError(t, err, "room_join_requires_account")
+
+	config.PublicRoomNames = regexp.MustCompile("^public$")
+	_, err = roomManager.JoinRoom(channelling.RoomTypeRoom+":public", "public", channelling.RoomTypeRoom, nil, unauthenticatedSession, false, nil)
+	if err != nil {
+		t.Fatalf("Unexpected error %v joining public room", err)
+	}
+
+	_, err = roomManager.JoinRoom(channelling.RoomTypeRoom+":private", "private", channelling.RoomTypeRoom, nil, authenticatedSession, true, nil)
+	if err != nil {
+		t.Fatalf("Unexpected error %v joining room while authenticated", err)
+	}
+
+	_, err = roomManager.JoinRoom(channelling.RoomTypeRoom+":private", "private", channelling.RoomTypeRoom, nil, unauthenticatedSession, false, nil)
+	assertDataError(t, err, "room_join_requires_account")
+}
+
 func Test_RoomManager_UpdateRoom_ReturnsAnErrorIfNoRoomHasBeenJoined(t *testing.T) {
 	roomManager, _ := NewTestRoomManager()
 	_, err := roomManager.UpdateRoom(&Session{}, nil)
diff --git a/go/channelling/server/config.go b/go/channelling/server/config.go
index cfe57da6..f664e079 100644
--- a/go/channelling/server/config.go
+++ b/go/channelling/server/config.go
@@ -119,6 +119,16 @@ func NewConfig(container phoenix.Container, tokens bool) (*channelling.Config, e
 		}
 	}
 
+	publicRoomNamesString := container.GetStringDefault("app", "publicRooms", "")
+	var publicRoomNames *regexp.Regexp
+	if publicRoomNamesString != "" {
+		var err error
+		if publicRoomNames, err = regexp.Compile(publicRoomNamesString); err != nil {
+			return nil, fmt.Errorf("Invalid regular expression '%s': %s", publicRoomNamesString, err)
+		}
+		log.Printf("Allowed public rooms: %s\n", publicRoomNamesString)
+	}
+
 	return &channelling.Config{
 		Title:                           container.GetStringDefault("app", "title", "Spreed WebRTC"),
 		Ver:                             ver,
@@ -148,6 +158,7 @@ func NewConfig(container phoenix.Container, tokens bool) (*channelling.Config, e
 		RoomTypes:                       roomTypes,
 		RoomNameCaseSensitive:           container.GetBoolDefault("app", "caseSensitiveRooms", false),
 		LockedRoomJoinableWithPIN:       container.GetBoolDefault("app", "lockedRoomJoinableWithPIN", true),
+		PublicRoomNames:                 publicRoomNames,
 	}, nil
 }
 
diff --git a/server.conf.in b/server.conf.in
index 1641ee85..98d51ca8 100644
--- a/server.conf.in
+++ b/server.conf.in
@@ -100,6 +100,10 @@ encryptionSecret = tne-default-encryption-block-key
 ; Whether locked rooms should be joinable by providing the PIN the room was
 ; locked with. Optional, defaults to true.
 ;lockedRoomJoinableWithPIN = true
+; Regular expression specifying room names that can be created / joined without
+; a valid user account (even if "authorizeRoomJoin" or "authorizeRoomCreation"
+; is enabled).
+;publicRooms =
 ; Whether the pipelines API should be enabled. Optional, defaults to false.
 ;pipelinesEnabled = false
 ; Server token is a public random string which is used to enhance security of