diff --git a/config.toml.sample b/config.toml.sample index 9420f37..69ff5a8 100644 --- a/config.toml.sample +++ b/config.toml.sample @@ -38,6 +38,13 @@ websocket_timeout = "3s" # Session cookie name. session_cookie = "niltoken" +# A list of predefined rooms. +[rooms] +[rooms.local] +id="local" +name="local" +password="" + # Storage kind, one of redis|memory|fs. storage = "redis" diff --git a/internal/hub/hub.go b/internal/hub/hub.go index 72d35f1..0c2b608 100644 --- a/internal/hub/hub.go +++ b/internal/hub/hub.go @@ -30,21 +30,29 @@ type Config struct { Address string `koanf:"address"` RootURL string `koanf:"root_url"` - Name string `koanf:"name"` - RoomIDLen int `koanf:"room_id_length"` - MaxCachedMessages int `koanf:"max_cached_messages"` - MaxMessageLen int `koanf:"max_message_length"` - WSTimeout time.Duration `koanf:"websocket_timeout"` - MaxMessageQueue int `koanf:"max_message_queue"` - RateLimitInterval time.Duration `koanf:"rate_limit_interval"` - RateLimitMessages int `koanf:"rate_limit_messages"` - MaxRooms int `koanf:"max_rooms"` - MaxPeersPerRoom int `koanf:"max_peers_per_room"` - PeerHandleFormat string `koanf:"peer_handle_format"` - RoomTimeout time.Duration `koanf:"room_timeout"` - RoomAge time.Duration `koanf:"room_age"` - SessionCookie string `koanf:"session_cookie"` - Storage string `koanf:"storage"` + Name string `koanf:"name"` + RoomIDLen int `koanf:"room_id_length"` + MaxCachedMessages int `koanf:"max_cached_messages"` + MaxMessageLen int `koanf:"max_message_length"` + WSTimeout time.Duration `koanf:"websocket_timeout"` + MaxMessageQueue int `koanf:"max_message_queue"` + RateLimitInterval time.Duration `koanf:"rate_limit_interval"` + RateLimitMessages int `koanf:"rate_limit_messages"` + MaxRooms int `koanf:"max_rooms"` + MaxPeersPerRoom int `koanf:"max_peers_per_room"` + PeerHandleFormat string `koanf:"peer_handle_format"` + RoomTimeout time.Duration `koanf:"room_timeout"` + RoomAge time.Duration `koanf:"room_age"` + SessionCookie string `koanf:"session_cookie"` + Storage string `koanf:"storage"` + Rooms map[string]PredefinedRoom `koanf:"rooms"` +} + +// PredefinedRoom are static rooms declared in the configuration file. +type PredefinedRoom struct { + ID string `koanf:"id"` + Name string `koanf:"name"` + Password string `koanf:"password"` } // Hub acts as the controller and container for all chat rooms. @@ -86,7 +94,23 @@ func (h *Hub) AddRoom(name string, password []byte) (*Room, error) { } // Initialize the room. - return h.initRoom(id, name, password), nil + return h.initRoom(id, name, password, false), nil +} + +// AddPredefinedRoom creates a predefined room in the store, adds it to the hub. +// If it already exists, no error is returned. +func (h *Hub) AddPredefinedRoom(ID, name string, password []byte) (*Room, error) { + // Add the room to DB. + if err := h.Store.AddPredefinedRoom(store.Room{ID: ID, + Name: name, + CreatedAt: time.Now(), + Password: password}); err != nil { + h.log.Printf("error creating room in the store: %v", err) + return nil, errors.New("error creating room") + } + + // Initialize the room. + return h.initRoom(ID, name, password, true), nil } // ActivateRoom loads a room from the store into the hub if it's not already active. @@ -104,7 +128,7 @@ func (h *Hub) ActivateRoom(id string) (*Room, error) { } // Initialize the room. - return h.initRoom(r.ID, r.Name, r.Password), nil + return h.initRoom(r.ID, r.Name, r.Password, room.Predefined), nil } // GetRoom retrives an active room from the hub. @@ -116,8 +140,8 @@ func (h *Hub) GetRoom(id string) *Room { } // initRoom initializes a room on the Hub. -func (h *Hub) initRoom(id, name string, password []byte) *Room { - r := NewRoom(id, name, password, h) +func (h *Hub) initRoom(id, name string, password []byte, predefined bool) *Room { + r := NewRoom(id, name, password, predefined, h) h.mut.Lock() h.rooms[id] = r h.mut.Unlock() diff --git a/internal/hub/peer.go b/internal/hub/peer.go index f8ecd12..8fd8b33 100644 --- a/internal/hub/peer.go +++ b/internal/hub/peer.go @@ -139,7 +139,9 @@ func (p *Peer) processMessage(b []byte) { // Dipose of a room. case TypeRoomDispose: - p.room.Dispose() + if !p.room.Predefined { + p.room.Dispose() + } default: } } diff --git a/internal/hub/room.go b/internal/hub/room.go index f1913ba..62caf6d 100644 --- a/internal/hub/room.go +++ b/internal/hub/room.go @@ -34,11 +34,12 @@ type peerReq struct { // Room represents a chat room. type Room struct { - ID string - Name string - Password []byte - hub *Hub - mut *sync.RWMutex + ID string + Name string + Password []byte + Predefined bool + hub *Hub + mut *sync.RWMutex lastActivity time.Time @@ -62,11 +63,11 @@ type Room struct { } // NewRoom returns a new instance of Room. -func NewRoom(id, name string, password []byte, h *Hub) *Room { +func NewRoom(id, name string, password []byte, predefined bool, h *Hub) *Room { return &Room{ - ID: id, - Name: name, - Password: password, + ID: id, + Name: name, + Password: password, Predefined: predefined, hub: h, peers: make(map[*Peer]bool, 100), broadcastQ: make(chan []byte, 100), @@ -165,9 +166,11 @@ loop: } // Extend the room's expiry (once every 30 seconds). - if time.Since(r.timestamp) > time.Duration(30)*time.Second { - r.timestamp = time.Now() - r.extendTTL() + if !r.Predefined { + if time.Since(r.timestamp) > time.Duration(30)*time.Second { + r.timestamp = time.Now() + r.extendTTL() + } } // Kill the room after the inactivity period. diff --git a/main.go b/main.go index f5b5413..bf4cd60 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,7 @@ import ( "github.com/knadh/niltalk/store/redis" "github.com/knadh/stuffbin" flag "github.com/spf13/pflag" + "golang.org/x/crypto/bcrypt" ) var ( @@ -251,6 +252,28 @@ func main() { app.hub = hub.NewHub(app.cfg, store, logger) + if err := ko.Unmarshal("rooms", &app.cfg.Rooms); err != nil { + logger.Fatalf("error unmarshalling 'rooms' config: %v", err) + } + // setup predefined rooms + for _, room := range app.cfg.Rooms { + pwdHash, err := bcrypt.GenerateFromPassword([]byte(room.Password), 8) + if err != nil { + logger.Printf("error hashing password: %v", err) + return + } + r, err := app.hub.AddPredefinedRoom(room.ID, room.Name, pwdHash) + if err != nil { + logger.Printf("error creating a predefined room %q: %v", room.Name, err) + continue + } + _, err = app.hub.ActivateRoom(r.ID) + if err != nil { + logger.Printf("error activating a predefined room %q: %v", room.Name, err) + continue + } + } + // Compile static templates. tpl, err := stuffbin.ParseTemplatesGlob(nil, app.fs, "/static/templates/*.html") if err != nil { diff --git a/static/templates/room.html b/static/templates/room.html index d3a48e2..63fb1c7 100644 --- a/static/templates/room.html +++ b/static/templates/room.html @@ -14,7 +14,7 @@

Join room

+ {{if not .Data.Room.Predefined}}required minlength="6"{{end}} maxlength="100" autocomplete="off" />

Logout + {{if not .Data.Room.Predefined}} Dispose × + {{end}}
{{ template "footer" . }} -{{end}} \ No newline at end of file +{{end}} diff --git a/store/fs/fs.go b/store/fs/fs.go index 00fb073..2f2d2b0 100644 --- a/store/fs/fs.go +++ b/store/fs/fs.go @@ -64,7 +64,7 @@ func (m *File) cleanup() { now := time.Now() for id, r := range m.rooms { - if r.Expire.Before(now) { + if !r.Expire.IsZero() && r.Expire.Before(now) { delete(m.rooms, id) m.dirty = true continue @@ -134,6 +134,21 @@ func (m *File) AddRoom(r store.Room, ttl time.Duration) error { return nil } +// AddPredefinedRoom adds a room to the store. +func (m *File) AddPredefinedRoom(r store.Room) error { + m.mu.Lock() + defer m.mu.Unlock() + + key := r.ID + m.rooms[key] = &room{ + Room: r, + Sessions: map[string]string{}, + } + m.dirty = true + + return nil +} + // ExtendRoomTTL extends a room's TTL. func (m *File) ExtendRoomTTL(id string, ttl time.Duration) error { m.mu.Lock() diff --git a/store/mem/mem.go b/store/mem/mem.go index fa1418c..69802a6 100644 --- a/store/mem/mem.go +++ b/store/mem/mem.go @@ -53,7 +53,7 @@ func (m *InMemory) cleanup() { now := time.Now() for id, r := range m.rooms { - if r.Expire.Before(now) { + if !r.Expire.IsZero() && r.Expire.Before(now) { delete(m.rooms, id) continue } @@ -74,6 +74,20 @@ func (m *InMemory) AddRoom(r store.Room, ttl time.Duration) error { return nil } +// AddPredefinedRoom adds a room to the store. +func (m *InMemory) AddPredefinedRoom(r store.Room) error { + m.mu.Lock() + defer m.mu.Unlock() + + key := r.ID + m.rooms[key] = &room{ + Room: r, + Sessions: map[string]string{}, + } + + return nil +} + // ExtendRoomTTL extends a room's TTL. func (m *InMemory) ExtendRoomTTL(id string, ttl time.Duration) error { m.mu.Lock() diff --git a/store/redis/redis.go b/store/redis/redis.go index d6af6dc..e7a89d4 100644 --- a/store/redis/redis.go +++ b/store/redis/redis.go @@ -77,6 +77,19 @@ func (r *Redis) AddRoom(room store.Room, ttl time.Duration) error { return c.Flush() } +// AddPredefinedRoom adds a room to the store. +func (r *Redis) AddPredefinedRoom(room store.Room) error { + c := r.pool.Get() + defer c.Close() + + key := fmt.Sprintf(r.cfg.PrefixRoom, room.ID) + c.Send("HMSET", key, + "name", room.Name, + "created_at", room.CreatedAt.Format(time.RFC3339), + "password", room.Password) + return c.Flush() +} + // ExtendRoomTTL extends a room's TTL. func (r *Redis) ExtendRoomTTL(id string, ttl time.Duration) error { c := r.pool.Get() diff --git a/store/store.go b/store/store.go index 9658924..3c1d963 100644 --- a/store/store.go +++ b/store/store.go @@ -8,6 +8,7 @@ import ( // Store represents a backend store. type Store interface { AddRoom(r Room, ttl time.Duration) error + AddPredefinedRoom(room Room) error GetRoom(id string) (Room, error) ExtendRoomTTL(id string, ttl time.Duration) error RoomExists(id string) (bool, error)