1
Fork 0
galene/group/group.go

744 lines
14 KiB
Go
Raw Normal View History

2020-04-24 19:38:21 +02:00
// Copyright (c) 2020 by Juliusz Chroboczek.
// This is not open source software. Copy it, and I'll break into your
// house and tell your three year-old that Santa doesn't exist.
package group
2020-04-24 19:38:21 +02:00
import (
2020-04-25 02:25:51 +02:00
"encoding/json"
2020-12-02 00:07:31 +01:00
"errors"
2020-04-24 19:38:21 +02:00
"log"
2020-04-25 02:25:51 +02:00
"os"
2020-11-22 19:54:54 +01:00
"path"
2020-04-25 02:25:51 +02:00
"path/filepath"
2020-04-29 16:08:07 +02:00
"sort"
2020-04-25 04:08:43 +02:00
"strings"
2020-04-25 17:36:35 +02:00
"sync"
"time"
2020-04-24 19:38:21 +02:00
2020-10-06 06:08:29 +02:00
"github.com/pion/ice/v2"
2020-07-16 20:17:32 +02:00
"github.com/pion/webrtc/v3"
2020-04-24 19:38:21 +02:00
)
var Directory string
2020-10-06 06:08:29 +02:00
var UseMDNS bool
2020-12-02 00:07:31 +01:00
var ErrNotAuthorised = errors.New("not authorised")
type UserError string
func (err UserError) Error() string {
return string(err)
}
2020-11-30 16:26:11 +01:00
type KickError struct {
Id string
Username string
Message string
}
func (err KickError) Error() string {
m := "kicked out"
if err.Message != "" {
m += "(" + err.Message + ")"
}
if err.Username != "" {
m += " by " + err.Username
}
return m
}
type ProtocolError string
func (err ProtocolError) Error() string {
return string(err)
}
var IceFilename string
var iceConf webrtc.Configuration
var iceOnce sync.Once
func IceConfiguration() webrtc.Configuration {
iceOnce.Do(func() {
var iceServers []webrtc.ICEServer
file, err := os.Open(IceFilename)
if err != nil {
log.Printf("Open %v: %v", IceFilename, err)
return
}
defer file.Close()
d := json.NewDecoder(file)
err = d.Decode(&iceServers)
if err != nil {
log.Printf("Get ICE configuration: %v", err)
return
}
iceConf = webrtc.Configuration{
ICEServers: iceServers,
}
})
return iceConf
}
type ChatHistoryEntry struct {
Id string
User string
2020-10-08 14:38:33 +02:00
Time int64
Kind string
Value string
2020-04-25 21:16:49 +02:00
}
const (
MinBitrate = 200000
)
type Group struct {
name string
2020-04-24 19:38:21 +02:00
mu sync.Mutex
2020-09-24 22:03:41 +02:00
description *description
locked *string
clients map[string]Client
history []ChatHistoryEntry
timestamp time.Time
2020-04-24 19:38:21 +02:00
}
func (g *Group) Name() string {
return g.name
}
2020-09-18 11:40:00 +02:00
func (g *Group) Locked() (bool, string) {
g.mu.Lock()
defer g.mu.Unlock()
2020-09-24 22:03:41 +02:00
if g.locked != nil {
2020-09-18 11:40:00 +02:00
return true, *g.locked
} else {
return false, ""
}
}
2020-09-18 11:40:00 +02:00
func (g *Group) SetLocked(locked bool, message string) {
g.mu.Lock()
defer g.mu.Unlock()
2020-09-18 11:40:00 +02:00
if locked {
g.locked = &message
} else {
g.locked = nil
}
}
func (g *Group) Public() bool {
g.mu.Lock()
defer g.mu.Unlock()
return g.description.Public
}
func (g *Group) Redirect() string {
g.mu.Lock()
defer g.mu.Unlock()
return g.description.Redirect
}
func (g *Group) AllowRecording() bool {
g.mu.Lock()
defer g.mu.Unlock()
return g.description.AllowRecording
}
2020-04-24 19:38:21 +02:00
var groups struct {
mu sync.Mutex
groups map[string]*Group
2020-04-24 19:38:21 +02:00
api *webrtc.API
}
func (g *Group) API() *webrtc.API {
2020-09-13 13:01:06 +02:00
return groups.api
}
2020-09-24 22:03:41 +02:00
func Add(name string, desc *description) (*Group, error) {
2020-04-24 19:38:21 +02:00
groups.mu.Lock()
defer groups.mu.Unlock()
if groups.groups == nil {
groups.groups = make(map[string]*Group)
2020-04-27 03:06:45 +02:00
s := webrtc.SettingEngine{}
2020-11-03 00:58:21 +01:00
s.SetSRTPReplayProtectionWindow(512)
2020-10-06 06:08:29 +02:00
if !UseMDNS {
s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
}
2020-04-24 19:38:21 +02:00
m := webrtc.MediaEngine{}
m.RegisterCodec(webrtc.NewRTPVP8CodecExt(
webrtc.DefaultPayloadTypeVP8, 90000,
[]webrtc.RTCPFeedback{
2020-04-27 03:08:03 +02:00
{"goog-remb", ""},
{"nack", ""},
2020-04-27 03:08:03 +02:00
{"nack", "pli"},
2020-05-21 00:24:10 +02:00
{"ccm", "fir"},
},
"",
))
2020-04-24 19:38:21 +02:00
m.RegisterCodec(webrtc.NewRTPOpusCodec(
webrtc.DefaultPayloadTypeOpus, 48000,
))
2020-04-24 19:38:21 +02:00
groups.api = webrtc.NewAPI(
2020-04-27 03:06:45 +02:00
webrtc.WithSettingEngine(s),
2020-04-24 19:38:21 +02:00
webrtc.WithMediaEngine(m),
)
}
2020-04-25 04:08:43 +02:00
var err error
2020-04-24 19:38:21 +02:00
g := groups.groups[name]
if g == nil {
2020-04-25 17:36:35 +02:00
if desc == nil {
desc, err = GetDescription(name)
2020-04-25 04:08:43 +02:00
if err != nil {
return nil, err
}
2020-04-25 02:25:51 +02:00
}
g = &Group{
2020-04-25 02:25:51 +02:00
name: name,
description: desc,
clients: make(map[string]Client),
timestamp: time.Now(),
2020-04-24 19:38:21 +02:00
}
groups.groups[name] = g
return g, nil
}
g.mu.Lock()
defer g.mu.Unlock()
if desc != nil {
2020-04-25 04:08:43 +02:00
g.description = desc
return g, nil
}
if time.Since(g.description.loadTime) > 5*time.Second {
2020-11-22 19:54:54 +01:00
if descriptionChanged(name, g.description) {
desc, err := GetDescription(name)
if err != nil {
if !os.IsNotExist(err) {
log.Printf("Reading group %v: %v",
name, err)
}
deleteUnlocked(g)
return nil, err
}
g.description = desc
} else {
g.description.loadTime = time.Now()
}
2020-04-24 19:38:21 +02:00
}
return g, nil
}
func Range(f func(g *Group) bool) {
2020-04-29 16:08:07 +02:00
groups.mu.Lock()
defer groups.mu.Unlock()
2020-09-13 12:24:06 +02:00
for _, g := range groups.groups {
ok := f(g)
if !ok {
break
}
2020-04-29 16:08:07 +02:00
}
2020-09-13 12:24:06 +02:00
}
func GetNames() []string {
2020-09-13 12:24:06 +02:00
names := make([]string, 0)
Range(func(g *Group) bool {
2020-09-13 12:24:06 +02:00
names = append(names, g.name)
return true
})
2020-04-29 16:08:07 +02:00
return names
}
2020-12-02 19:47:32 +01:00
type SubGroup struct {
Name string
Clients int
}
func GetSubGroups(parent string) []SubGroup {
prefix := parent + "/"
subgroups := make([]SubGroup, 0)
Range(func(g *Group) bool {
if strings.HasPrefix(g.name, prefix) {
g.mu.Lock()
count := len(g.clients)
g.mu.Unlock()
if count > 0 {
subgroups = append(subgroups,
SubGroup{g.name, count})
}
}
return true
})
return subgroups
}
func Get(name string) *Group {
2020-04-29 16:08:07 +02:00
groups.mu.Lock()
defer groups.mu.Unlock()
return groups.groups[name]
}
func Delete(name string) bool {
groups.mu.Lock()
defer groups.mu.Unlock()
2020-04-24 19:38:21 +02:00
g := groups.groups[name]
if g == nil {
2020-12-02 00:07:31 +01:00
return false
2020-04-24 19:38:21 +02:00
}
g.mu.Lock()
defer g.mu.Unlock()
return deleteUnlocked(g)
}
// Called with both groups.mu and g.mu taken.
func deleteUnlocked(g *Group) bool {
2020-04-24 19:38:21 +02:00
if len(g.clients) != 0 {
return false
}
delete(groups.groups, g.name)
2020-04-24 19:38:21 +02:00
return true
}
func Expire() {
names := GetNames()
now := time.Now()
for _, name := range names {
g := Get(name)
if g == nil {
continue
}
old := false
g.mu.Lock()
empty := len(g.clients) == 0
if empty && !g.description.Public {
age := now.Sub(g.timestamp)
old = age > maxHistoryAge(g.description)
}
// We cannot take groups.mu at this point without a deadlock.
g.mu.Unlock()
if empty && old {
// Delete will check if the group is still empty
Delete(name)
}
}
}
2020-11-29 14:26:42 +01:00
func AddClient(group string, c Client) (*Group, error) {
g, err := Add(group, nil)
2020-04-24 19:38:21 +02:00
if err != nil {
return nil, err
2020-04-24 19:38:21 +02:00
}
g.mu.Lock()
defer g.mu.Unlock()
2020-11-30 16:26:11 +01:00
if !c.OverridePermissions(g) {
2020-11-29 14:26:42 +01:00
perms, err := g.description.GetPermission(group, c)
if err != nil {
return nil, err
}
c.SetPermissions(perms)
2020-04-25 02:25:51 +02:00
if !perms.Op && g.locked != nil {
m := *g.locked
if m == "" {
m = "group is locked"
}
return nil, UserError(m)
2020-09-18 11:40:00 +02:00
}
2020-05-18 15:24:04 +02:00
if !perms.Op && g.description.MaxClients > 0 {
if len(g.clients) >= g.description.MaxClients {
return nil, UserError("too many users")
}
2020-04-25 02:37:41 +02:00
}
}
2020-06-08 22:14:28 +02:00
if g.clients[c.Id()] != nil {
return nil, ProtocolError("duplicate client id")
2020-04-25 21:29:21 +02:00
}
2020-06-08 22:14:28 +02:00
g.clients[c.Id()] = c
g.timestamp = time.Now()
go func(clients []Client) {
2020-11-29 14:26:42 +01:00
u := c.Username()
c.PushClient(c.Id(), u, true)
for _, cc := range clients {
2020-11-29 14:26:42 +01:00
uu := cc.Username()
c.PushClient(cc.Id(), uu, true)
cc.PushClient(c.Id(), u, true)
}
}(g.getClientsUnlocked(c))
return g, nil
2020-04-24 19:38:21 +02:00
}
func DelClient(c Client) {
2020-06-08 22:14:28 +02:00
g := c.Group()
if g == nil {
return
}
2020-05-28 02:35:09 +02:00
g.mu.Lock()
defer g.mu.Unlock()
2020-04-25 21:29:21 +02:00
2020-06-08 22:14:28 +02:00
if g.clients[c.Id()] != c {
2020-04-25 21:29:21 +02:00
log.Printf("Deleting unknown client")
return
}
2020-06-08 22:14:28 +02:00
delete(g.clients, c.Id())
g.timestamp = time.Now()
go func(clients []Client) {
for _, cc := range clients {
2020-11-29 14:26:42 +01:00
cc.PushClient(c.Id(), c.Username(), false)
}
}(g.getClientsUnlocked(nil))
2020-04-24 19:38:21 +02:00
}
func (g *Group) GetClients(except Client) []Client {
2020-04-24 19:38:21 +02:00
g.mu.Lock()
defer g.mu.Unlock()
return g.getClientsUnlocked(except)
}
func (g *Group) getClientsUnlocked(except Client) []Client {
clients := make([]Client, 0, len(g.clients))
2020-04-24 19:38:21 +02:00
for _, c := range g.clients {
if c != except {
clients = append(clients, c)
}
}
return clients
}
func (g *Group) GetClient(id string) Client {
g.mu.Lock()
defer g.mu.Unlock()
return g.getClientUnlocked(id)
}
func (g *Group) getClientUnlocked(id string) Client {
2020-05-28 02:35:09 +02:00
for idd, c := range g.clients {
if idd == id {
2020-04-25 17:36:35 +02:00
return c
}
}
return nil
}
func (g *Group) Range(f func(c Client) bool) {
2020-04-24 19:38:21 +02:00
g.mu.Lock()
defer g.mu.Unlock()
for _, c := range g.clients {
ok := f(c)
2020-04-25 02:25:51 +02:00
if !ok {
break
2020-04-24 19:38:21 +02:00
}
}
}
func (g *Group) Shutdown(message string) {
g.Range(func(c Client) bool {
cc, ok := c.(Kickable)
if ok {
2020-11-30 16:26:11 +01:00
cc.Kick("", "", message)
}
return true
})
}
2020-10-08 14:38:33 +02:00
func FromJSTime(tm int64) time.Time {
if tm == 0 {
return time.Time{}
}
return time.Unix(int64(tm)/1000, (int64(tm)%1000)*1000000)
}
func ToJSTime(tm time.Time) int64 {
return int64((tm.Sub(time.Unix(0, 0)) + time.Millisecond/2) /
time.Millisecond)
}
2020-09-18 11:48:21 +02:00
const maxChatHistory = 50
2020-04-25 21:16:49 +02:00
func (g *Group) ClearChatHistory() {
2020-04-30 19:13:10 +02:00
g.mu.Lock()
defer g.mu.Unlock()
g.history = nil
}
2020-10-08 14:38:33 +02:00
func (g *Group) AddToChatHistory(id, user string, time int64, kind, value string) {
2020-04-25 21:16:49 +02:00
g.mu.Lock()
defer g.mu.Unlock()
if len(g.history) >= maxChatHistory {
copy(g.history, g.history[1:])
g.history = g.history[:len(g.history)-1]
}
g.history = append(g.history,
2020-09-30 00:33:23 +02:00
ChatHistoryEntry{Id: id, User: user, Time: time, Kind: kind, Value: value},
2020-04-25 21:16:49 +02:00
)
}
func discardObsoleteHistory(h []ChatHistoryEntry, duration time.Duration) []ChatHistoryEntry {
2020-10-08 14:38:33 +02:00
i := 0
for i < len(h) {
if time.Since(FromJSTime(h[i].Time)) <= duration {
2020-10-08 14:38:33 +02:00
break
}
i++
}
if i > 0 {
copy(h, h[i:])
h = h[:len(h)-i]
}
return h
}
func (g *Group) GetChatHistory() []ChatHistoryEntry {
2020-04-25 21:16:49 +02:00
g.mu.Lock()
2020-05-12 12:48:56 +02:00
defer g.mu.Unlock()
2020-04-25 21:16:49 +02:00
g.history = discardObsoleteHistory(
g.history, maxHistoryAge(g.description),
)
2020-10-08 14:38:33 +02:00
h := make([]ChatHistoryEntry, len(g.history))
2020-04-25 21:16:49 +02:00
copy(h, g.history)
return h
}
2020-11-29 14:26:42 +01:00
func matchClient(group string, c Challengeable, users []ClientCredentials) (bool, bool) {
2020-04-25 02:25:51 +02:00
for _, u := range users {
if u.Username == "" {
2020-11-29 14:26:42 +01:00
if c.Challenge(group, u) {
return true, true
}
2020-11-29 14:26:42 +01:00
} else if u.Username == c.Username() {
if c.Challenge(group, u) {
return true, true
} else {
return true, false
}
2020-04-25 02:25:51 +02:00
}
}
return false, false
}
2020-09-24 22:03:41 +02:00
type description struct {
2020-11-22 19:54:54 +01:00
fileName string `json:"-"`
loadTime time.Time `json:"-"`
modTime time.Time `json:"-"`
fileSize int64 `json:"-"`
2020-09-24 22:03:41 +02:00
Description string `json:"description,omitempty"`
2020-09-10 13:55:57 +02:00
Redirect string `json:"redirect,omitempty"`
Public bool `json:"public,omitempty"`
MaxClients int `json:"max-clients,omitempty"`
2020-11-22 19:54:54 +01:00
MaxHistoryAge int `json:"max-history-age,omitempty"`
AllowAnonymous bool `json:"allow-anonymous,omitempty"`
AllowRecording bool `json:"allow-recording,omitempty"`
2020-11-22 19:54:54 +01:00
AllowSubgroups bool `json:"allow-subgroups,omitempty"`
Op []ClientCredentials `json:"op,omitempty"`
Presenter []ClientCredentials `json:"presenter,omitempty"`
Other []ClientCredentials `json:"other,omitempty"`
2020-04-25 02:25:51 +02:00
}
const DefaultMaxHistoryAge = 4 * time.Hour
func maxHistoryAge(desc *description) time.Duration {
if desc.MaxHistoryAge != 0 {
return time.Duration(desc.MaxHistoryAge) * time.Second
}
return DefaultMaxHistoryAge
}
2020-11-22 19:54:54 +01:00
func openDescriptionFile(name string) (*os.File, string, bool, error) {
isParent := false
for name != "" {
fileName := filepath.Join(
Directory, path.Clean("/"+name)+".json",
)
r, err := os.Open(fileName)
if !os.IsNotExist(err) {
return r, fileName, isParent, err
}
isParent = true
name, _ = path.Split(name)
name = strings.TrimRight(name, "/")
}
2020-11-22 19:54:54 +01:00
return nil, "", false, os.ErrNotExist
}
func statDescriptionFile(name string) (os.FileInfo, string, bool, error) {
isParent := false
for name != "" {
fileName := filepath.Join(
Directory, path.Clean("/"+name)+".json",
)
fi, err := os.Stat(fileName)
if !os.IsNotExist(err) {
return fi, fileName, isParent, err
}
isParent = true
name, _ = path.Split(name)
name = strings.TrimRight(name, "/")
}
2020-11-22 19:54:54 +01:00
return nil, "", false, os.ErrNotExist
}
// descriptionChanged returns true if a group's description may have
// changed since it was last read.
func descriptionChanged(name string, desc *description) bool {
fi, fileName, _, err := statDescriptionFile(name)
if err != nil || fileName != desc.fileName {
return true
}
if fi.Size() != desc.fileSize || fi.ModTime() != desc.modTime {
return true
}
return false
}
2020-09-24 22:03:41 +02:00
func GetDescription(name string) (*description, error) {
2020-11-22 19:54:54 +01:00
r, fileName, isParent, err := openDescriptionFile(name)
2020-04-25 02:25:51 +02:00
if err != nil {
return nil, err
}
defer r.Close()
2020-09-24 22:03:41 +02:00
var desc description
fi, err := r.Stat()
if err != nil {
return nil, err
}
2020-04-25 02:25:51 +02:00
d := json.NewDecoder(r)
err = d.Decode(&desc)
if err != nil {
return nil, err
}
2020-11-22 19:54:54 +01:00
if isParent {
if !desc.AllowSubgroups {
return nil, os.ErrNotExist
}
desc.Public = false
desc.Description = ""
}
desc.fileName = fileName
desc.fileSize = fi.Size()
desc.modTime = fi.ModTime()
desc.loadTime = time.Now()
2020-11-22 19:54:54 +01:00
2020-04-25 02:25:51 +02:00
return &desc, nil
}
2020-11-29 14:26:42 +01:00
func (desc *description) GetPermission(group string, c Challengeable) (ClientPermissions, error) {
var p ClientPermissions
2020-11-29 14:26:42 +01:00
if !desc.AllowAnonymous && c.Username() == "" {
return p, UserError("anonymous users not allowed in this group, please choose a username")
2020-04-25 02:25:51 +02:00
}
2020-11-29 14:26:42 +01:00
if found, good := matchClient(group, c, desc.Op); found {
2020-04-25 02:25:51 +02:00
if good {
2020-04-25 18:09:31 +02:00
p.Op = true
2020-04-25 02:25:51 +02:00
p.Present = true
2020-05-30 00:23:54 +02:00
if desc.AllowRecording {
p.Record = true
}
2020-04-25 02:25:51 +02:00
return p, nil
}
2020-12-02 00:07:31 +01:00
return p, ErrNotAuthorised
2020-04-25 02:25:51 +02:00
}
2020-11-29 14:26:42 +01:00
if found, good := matchClient(group, c, desc.Presenter); found {
2020-04-25 02:25:51 +02:00
if good {
p.Present = true
return p, nil
}
2020-12-02 00:07:31 +01:00
return p, ErrNotAuthorised
2020-04-25 02:25:51 +02:00
}
2020-11-29 14:26:42 +01:00
if found, good := matchClient(group, c, desc.Other); found {
2020-04-25 02:25:51 +02:00
if good {
return p, nil
}
2020-12-02 00:07:31 +01:00
return p, ErrNotAuthorised
2020-04-25 02:25:51 +02:00
}
2020-12-02 00:07:31 +01:00
return p, ErrNotAuthorised
2020-04-25 17:36:35 +02:00
}
type Public struct {
2020-04-24 19:38:21 +02:00
Name string `json:"name"`
2020-09-24 22:03:41 +02:00
Description string `json:"description,omitempty"`
2020-04-24 19:38:21 +02:00
ClientCount int `json:"clientCount"`
}
func GetPublic() []Public {
gs := make([]Public, 0)
Range(func(g *Group) bool {
if g.Public() {
gs = append(gs, Public{
2020-04-24 19:38:21 +02:00
Name: g.name,
2020-09-24 22:03:41 +02:00
Description: g.description.Description,
2020-04-24 19:38:21 +02:00
ClientCount: len(g.clients),
})
}
2020-09-13 12:24:06 +02:00
return true
})
2020-04-30 22:32:44 +02:00
sort.Slice(gs, func(i, j int) bool {
return gs[i].Name < gs[j].Name
})
2020-04-24 19:38:21 +02:00
return gs
}
2020-04-25 04:08:43 +02:00
func ReadPublicGroups() {
dir, err := os.Open(Directory)
2020-04-25 04:08:43 +02:00
if err != nil {
return
}
defer dir.Close()
fis, err := dir.Readdir(-1)
if err != nil {
log.Printf("readPublicGroups: %v", err)
return
}
for _, fi := range fis {
if !strings.HasSuffix(fi.Name(), ".json") {
continue
}
2020-04-25 17:36:35 +02:00
name := fi.Name()[:len(fi.Name())-5]
desc, err := GetDescription(name)
2020-04-25 04:08:43 +02:00
if err != nil {
if !os.IsNotExist(err) {
log.Printf("Reading group %v: %v", name, err)
}
2020-04-25 04:08:43 +02:00
continue
}
if desc.Public {
Add(name, desc)
2020-04-25 04:08:43 +02:00
}
}
}