1
Fork 0
mirror of https://github.com/jech/galene.git synced 2024-11-24 17:45:58 +01:00
galene/group/group.go

1233 lines
24 KiB
Go
Raw Normal View History

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"
"fmt"
"io/fs"
2020-04-24 19:38:21 +02:00
"log"
2021-10-29 23:37:05 +02:00
"net/url"
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"
2021-04-29 17:03:25 +02:00
"github.com/pion/sdp/v3"
2020-07-16 20:17:32 +02:00
"github.com/pion/webrtc/v3"
2021-10-29 23:37:05 +02:00
"github.com/jech/galene/token"
2020-04-24 19:38:21 +02:00
)
var Directory, DataDirectory string
2020-10-06 06:08:29 +02:00
var UseMDNS bool
2021-04-29 22:02:36 +02:00
var UDPMin, UDPMax uint16
type NotAuthorisedError struct {
err error
}
func (err *NotAuthorisedError) Error() string {
if err.err != nil {
return "not authorised: " + err.err.Error()
}
return "not authorised"
}
func (err *NotAuthorisedError) Unwrap() error {
return err.err
}
var ErrDuplicateUsername = &NotAuthorisedError{
errors.New("this username is taken"),
}
2020-12-02 00:07:31 +01:00
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
2020-11-30 16:26:11 +01:00
Message string
}
func (err KickError) Error() string {
m := "kicked out"
if err.Message != "" {
2021-08-24 00:04:30 +02:00
m += " (" + err.Message + ")"
2020-11-30 16:26:11 +01:00
}
if err.Username != nil && *err.Username != "" {
m += " by " + *err.Username
2020-11-30 16:26:11 +01:00
}
return m
}
type ProtocolError string
func (err ProtocolError) Error() string {
return string(err)
}
type ChatHistoryEntry struct {
2024-08-11 19:00:17 +02:00
Id string
Source string
User *string
Time time.Time
Kind string
Value interface{}
2020-04-25 21:16:49 +02:00
}
const (
2021-05-11 15:28:30 +02:00
LowBitrate = 100 * 1024
MinBitrate = LowBitrate * 2
2022-01-25 19:45:49 +01:00
MaxBitrate = 1024 * 1024 * 1024
)
type Group struct {
name string
2020-04-24 19:38:21 +02:00
mu sync.Mutex
description *Description
locked *string
clients map[string]Client
history []ChatHistoryEntry
timestamp time.Time
data map[string]interface{}
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()
2020-09-18 11:40:00 +02:00
if locked {
g.locked = &message
} else {
g.locked = nil
}
clients := g.getClientsUnlocked(nil)
g.mu.Unlock()
for _, c := range clients {
c.Joined(g.Name(), "change")
}
}
func (g *Group) Data() map[string]interface{} {
g.mu.Lock()
defer g.mu.Unlock()
return g.data
}
func (g *Group) UpdateData(d map[string]interface{}) {
g.mu.Lock()
if g.data == nil {
g.data = make(map[string]interface{})
}
for k, v := range d {
if v == nil {
delete(g.data, k)
} else {
g.data[k] = v
}
}
clients := g.getClientsUnlocked(nil)
g.mu.Unlock()
for _, c := range clients {
c.Joined(g.Name(), "change")
}
}
func (g *Group) Description() *Description {
g.mu.Lock()
defer g.mu.Unlock()
return g.description
}
2021-10-26 22:22:48 +02:00
func (g *Group) ClientCount() int {
g.mu.Lock()
defer g.mu.Unlock()
return len(g.clients)
}
2022-10-08 22:46:28 +02:00
func (g *Group) mayExpire() bool {
g.mu.Lock()
defer g.mu.Unlock()
2022-10-08 22:46:28 +02:00
if g.description.Public {
return false
}
if len(g.clients) > 0 {
2022-10-08 22:46:28 +02:00
return false
}
2022-10-08 22:46:28 +02:00
return time.Since(g.timestamp) > maxHistoryAge(g.description)
}
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
}
2021-04-29 20:38:06 +02:00
func (g *Group) API() (*webrtc.API, error) {
g.mu.Lock()
codecs := g.description.Codecs
g.mu.Unlock()
return APIFromNames(codecs)
}
func fmtpValue(fmtp, key string) string {
fields := strings.Split(fmtp, ";")
for _, f := range fields {
2023-12-09 18:23:06 +01:00
k, v, found := strings.Cut(f, "=")
if found && k == key {
return v
}
}
return ""
2020-09-13 13:01:06 +02:00
}
func CodecPayloadType(codec webrtc.RTPCodecCapability) (webrtc.PayloadType, error) {
switch strings.ToLower(codec.MimeType) {
case "video/vp8":
return 96, nil
case "video/vp9":
profile := fmtpValue(codec.SDPFmtpLine, "profile-id")
switch profile {
case "", "0":
return 98, nil
case "2":
return 100, nil
default:
return 0, fmt.Errorf("unknown VP9 profile %v", profile)
}
case "video/av1":
return 35, nil
case "video/h264":
profile := fmtpValue(codec.SDPFmtpLine, "profile-level-id")
if profile == "" {
return 102, nil
}
if len(profile) < 4 {
return 0, errors.New("malforned H.264 profile")
}
switch strings.ToLower(profile[:4]) {
case "4200":
return 102, nil
case "42e0":
return 108, nil
default:
return 0, fmt.Errorf(
"unknown H.264 profile %v", profile,
)
}
case "audio/opus":
return 111, nil
case "audio/g722":
return 9, nil
case "audio/pcmu":
return 0, nil
case "audio/pcma":
return 8, nil
default:
return 0, fmt.Errorf("unknown codec %v", codec.MimeType)
}
}
func codecsFromName(name string) ([]webrtc.RTPCodecParameters, error) {
fb := []webrtc.RTCPFeedback{
{"goog-remb", ""},
{"nack", ""},
{"nack", "pli"},
{"ccm", "fir"},
}
var codecs []webrtc.RTPCodecCapability
switch name {
case "vp8":
codecs = []webrtc.RTPCodecCapability{
{
"video/VP8", 90000, 0,
"",
fb,
},
}
case "vp9":
codecs = []webrtc.RTPCodecCapability{
{
"video/VP9", 90000, 0,
"profile-id=0",
fb,
},
{
"video/VP9", 90000, 0,
"profile-id=2",
fb,
},
}
case "av1":
codecs = []webrtc.RTPCodecCapability{
{
"video/AV1", 90000, 0,
"",
fb,
},
}
case "h264":
codecs = []webrtc.RTPCodecCapability{
{
"video/H264", 90000, 0,
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
fb,
},
}
case "opus":
codecs = []webrtc.RTPCodecCapability{
{
"audio/opus", 48000, 2,
"minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1",
nil,
},
}
case "g722":
codecs = []webrtc.RTPCodecCapability{
{
"audio/G722", 8000, 1,
"",
nil,
},
}
case "pcmu":
codecs = []webrtc.RTPCodecCapability{
{
"audio/PCMU", 8000, 1,
"",
nil,
},
}
case "pcma":
codecs = []webrtc.RTPCodecCapability{
{
"audio/PCMU", 8000, 1,
"",
nil,
},
}
default:
return nil, errors.New("unknown codec")
}
parms := make([]webrtc.RTPCodecParameters, 0, len(codecs))
for _, c := range codecs {
ptype, err := CodecPayloadType(c)
if err != nil {
log.Printf("Couldn't determine ptype for codec %v: %v",
c.MimeType, err)
continue
}
parms = append(parms, webrtc.RTPCodecParameters{
RTPCodecCapability: c,
PayloadType: ptype,
})
}
return parms, nil
}
func APIFromCodecs(codecs []webrtc.RTPCodecParameters) (*webrtc.API, error) {
s := webrtc.SettingEngine{}
s.SetSRTPReplayProtectionWindow(512)
s.DisableActiveTCP(true)
if !UseMDNS {
s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
}
m := webrtc.MediaEngine{}
for _, codec := range codecs {
tpe := webrtc.RTPCodecTypeVideo
if strings.HasPrefix(strings.ToLower(codec.MimeType), "audio/") {
tpe = webrtc.RTPCodecTypeAudio
}
err := m.RegisterCodec(codec, tpe)
2021-04-29 20:38:06 +02:00
if err != nil {
log.Printf("%v", err)
continue
}
2020-04-24 19:38:21 +02:00
}
2021-04-29 22:02:36 +02:00
if UDPMin > 0 && UDPMax > 0 {
s.SetEphemeralUDPPortRange(UDPMin, UDPMax)
}
2021-04-29 17:03:25 +02:00
m.RegisterHeaderExtension(
webrtc.RTPHeaderExtensionCapability{sdp.SDESMidURI},
webrtc.RTPCodecTypeVideo)
m.RegisterHeaderExtension(
webrtc.RTPHeaderExtensionCapability{sdp.SDESRTPStreamIDURI},
webrtc.RTPCodecTypeVideo)
2021-04-29 22:02:36 +02:00
return webrtc.NewAPI(
webrtc.WithSettingEngine(s),
webrtc.WithMediaEngine(&m),
2021-04-29 20:38:06 +02:00
), nil
}
2021-04-29 20:38:06 +02:00
func APIFromNames(names []string) (*webrtc.API, error) {
if len(names) == 0 {
names = []string{"vp8", "opus"}
}
var codecs []webrtc.RTPCodecParameters
for _, n := range names {
cs, err := codecsFromName(n)
if err != nil {
log.Printf("Codec %v: %v", n, err)
continue
}
codecs = append(codecs, cs...)
}
return APIFromCodecs(codecs)
}
func Add(name string, desc *Description) (*Group, error) {
g, notify, err := add(name, desc)
for _, c := range notify {
c.Joined(g.Name(), "change")
}
return g, err
}
func validGroupName(name string) bool {
if filepath.Separator != '/' &&
strings.ContainsRune(name, filepath.Separator) {
return false
}
s := path.Clean("/" + name)
if s == "/" {
return false
}
return s == "/"+name
}
func add(name string, desc *Description) (*Group, []Client, error) {
if !validGroupName(name) {
return nil, nil, UserError("illegal group name")
}
groups.mu.Lock()
defer groups.mu.Unlock()
if groups.groups == nil {
groups.groups = make(map[string]*Group)
}
2020-04-24 19:38:21 +02:00
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 = readDescription(name, true)
2020-04-25 04:08:43 +02:00
if err != nil {
return nil, nil, err
2020-04-25 04:08:43 +02:00
}
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
}
g.mu.Lock()
defer g.mu.Unlock()
notify := false
if desc != nil {
if !descriptionMatch(g.description, desc) {
g.description = desc
notify = true
}
} else if !descriptionUnchanged(name, g.description) {
desc, err = readDescription(name, true)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
log.Printf("Reading group %v: %v", name, err)
}
deleteUnlocked(g)
return nil, nil, err
}
g.description = desc
notify = true
2020-04-24 19:38:21 +02:00
}
autoLockKick(g)
2020-04-24 19:38:21 +02:00
var clients []Client
if notify {
clients = g.getClientsUnlocked(nil)
}
return g, clients, nil
2020-04-24 19:38:21 +02:00
}
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()
if groups.groups == nil {
return nil
}
2020-04-29 16:08:07 +02:00
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 member(v string, l []string) bool {
for _, w := range l {
if v == w {
return true
}
}
return false
}
func AddClient(group string, c Client, creds ClientCredentials) (*Group, error) {
2020-11-29 14:26:42 +01:00
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()
2021-01-17 21:52:26 +01:00
clients := g.getClientsUnlocked(nil)
if !member("system", c.Permissions()) {
2022-09-08 20:55:05 +02:00
username, perms, err := g.getPermission(creds)
if err != nil {
return nil, err
}
c.SetUsername(username)
c.SetPermissions(perms)
2020-04-25 02:25:51 +02:00
if !member("op", perms) {
2021-01-17 21:52:26 +01:00
if g.locked != nil {
m := *g.locked
if m == "" {
m = "this group is locked"
}
return nil, UserError(m)
}
if g.description.NotBefore != nil ||
g.description.Expires != nil {
now := time.Now()
if g.description.NotBefore != nil &&
g.description.NotBefore.After(now) {
return nil, UserError(
"this group is not open yet",
)
}
if g.description.Expires != nil &&
g.description.Expires.Before(now) {
return nil, UserError(
"this group is closed",
)
}
}
2021-01-17 21:52:26 +01:00
if g.description.Autokick {
ops := false
for _, c := range clients {
if member("op", c.Permissions()) {
2021-01-17 21:52:26 +01:00
ops = true
break
}
}
if !ops {
return nil, UserError(
"there are no operators " +
"in this group",
)
}
}
2020-09-18 11:40:00 +02:00
}
2020-05-18 15:24:04 +02:00
if !member("op", perms) && 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
}
}
id := c.Id()
if id == "" {
return nil, errors.New("client has empty id")
}
if g.clients[id] != nil {
return nil, ProtocolError("duplicate client id")
2020-04-25 21:29:21 +02:00
}
g.clients[id] = c
g.timestamp = time.Now()
c.Joined(g.Name(), "join")
u := c.Username()
p := c.Permissions()
s := c.Data()
c.PushClient(g.Name(), "add", c.Id(), u, p, s)
2021-02-15 13:43:10 +01:00
for _, cc := range clients {
pp := cc.Permissions()
uu := cc.Username()
c.PushClient(g.Name(), "add", cc.Id(), uu, pp, cc.Data())
cc.PushClient(g.Name(), "add", id, u, p, s)
}
return g, nil
2020-04-24 19:38:21 +02:00
}
2021-01-14 03:56:37 +01:00
// called locked
func autoLockKick(g *Group) {
2021-01-17 21:29:07 +01:00
if !(g.description.Autolock && g.locked == nil) &&
!g.description.Autokick {
return
}
clients := g.getClientsUnlocked(nil)
2021-01-17 21:29:07 +01:00
for _, c := range clients {
if member("op", c.Permissions()) {
2021-01-17 21:29:07 +01:00
return
2021-01-14 03:56:37 +01:00
}
}
2021-01-17 21:29:07 +01:00
if g.description.Autolock && g.locked == nil {
m := "this group is locked"
g.locked = &m
for _, c := range clients {
c.Joined(g.Name(), "change")
}
2021-01-17 21:29:07 +01:00
}
if g.description.Autokick {
2024-11-11 16:54:59 +01:00
// we cannot call kickall, since it requires the group to
// be unlocked. And calling it asynchronously might
// spuriously kick out an operator.
go func(clients []Client) {
for _, c := range clients {
c.Kick(
"", nil,
"there are no operators in this group",
)
}
}(g.getClientsUnlocked(nil))
2021-01-17 21:29:07 +01:00
}
2021-01-14 03:56:37 +01: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()
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")
g.mu.Unlock()
2020-04-25 21:29:21 +02:00
return
}
2020-06-08 22:14:28 +02:00
delete(g.clients, c.Id())
g.timestamp = time.Now()
2021-01-14 03:56:37 +01:00
clients := g.getClientsUnlocked(nil)
g.mu.Unlock()
2021-01-14 03:56:37 +01:00
c.Joined(g.Name(), "leave")
for _, cc := range clients {
cc.PushClient(
g.Name(), "delete", c.Id(), c.Username(), nil, nil,
)
}
autoLockKick(g)
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
}
}
}
2021-01-17 21:29:07 +01:00
func kickall(g *Group, message string) {
g.Range(func(c Client) bool {
c.Kick("", nil, message)
return true
})
}
2021-09-11 15:42:14 +02:00
func Shutdown(message string) {
Range(func(g *Group) bool {
g.SetLocked(true, message)
kickall(g, message)
return true
})
2021-01-17 21:29:07 +01:00
}
type warner interface {
Warn(oponly bool, message string) error
}
func (g *Group) WallOps(message string) {
clients := g.GetClients(nil)
for _, c := range clients {
w, ok := c.(warner)
if !ok {
continue
}
err := w.Warn(true, message)
if err != nil {
log.Printf("WallOps: %v", err)
}
}
}
2020-09-18 11:48:21 +02:00
const maxChatHistory = 50
2020-04-25 21:16:49 +02:00
2024-08-11 23:57:11 +02:00
// deleteFunc is just like slices.DeleteFunc.
// Remove this once we require Go 1.21.
func deleteFunc[S ~[]E, E any](s S, f func(E) bool) S {
i := 0
for i = range s {
if f(s[i]) {
break
}
}
if i >= len(s) {
return s
}
for j := i + 1; j < len(s); j++ {
if v := s[j]; !f(v) {
s[i] = v
i++
}
}
var zero E
for j := i; j < len(s); j++ {
s[j] = zero
}
return s[:i]
}
func (g *Group) ClearChatHistory(id string, userId string) {
2020-04-30 19:13:10 +02:00
g.mu.Lock()
defer g.mu.Unlock()
2024-08-11 23:57:11 +02:00
if id == "" && userId == "" {
g.history = nil
return
}
g.history = deleteFunc(g.history, func(e ChatHistoryEntry) bool {
return e.Source == userId && (id == "" || e.Id == id)
})
2020-04-30 19:13:10 +02:00
}
2024-08-11 19:00:17 +02:00
func (g *Group) AddToChatHistory(id, source string, user *string, time time.Time, kind string, value interface{}) {
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,
2024-08-11 19:00:17 +02:00
ChatHistoryEntry{Id: id, Source: source, 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(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
}
// Configuration represents the contents of the data/config.json file.
type Configuration struct {
// The modtime and size of the file. These are used to detect
// when a file has changed on disk.
modTime time.Time `json:"-"`
fileSize int64 `json:"-"`
2024-04-11 00:50:56 +02:00
PublicServer bool `json:"publicServer"`
CanonicalHost string `json:"canonicalHost"`
ProxyURL string `json:"proxyURL"`
WritableGroups bool `json:"writableGroups"`
Users map[string]UserDescription
// obsolete fields
Admin []ClientPattern `json:"admin"`
}
2021-10-31 22:39:35 +01:00
func (conf Configuration) Zero() bool {
return conf.modTime.Equal(time.Time{}) &&
conf.fileSize == 0
}
var configuration struct {
mu sync.Mutex
configuration *Configuration
}
func GetConfiguration() (*Configuration, error) {
configuration.mu.Lock()
defer configuration.mu.Unlock()
if configuration.configuration == nil {
configuration.configuration = &Configuration{}
}
filename := filepath.Join(DataDirectory, "config.json")
fi, err := os.Stat(filename)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
2021-10-31 22:39:35 +01:00
if !configuration.configuration.Zero() {
configuration.configuration = &Configuration{}
}
2021-10-31 22:39:35 +01:00
return configuration.configuration, nil
}
return nil, err
}
if configuration.configuration.modTime.Equal(fi.ModTime()) &&
configuration.configuration.fileSize == fi.Size() {
return configuration.configuration, nil
}
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
d := json.NewDecoder(f)
d.DisallowUnknownFields()
var conf Configuration
err = d.Decode(&conf)
if err != nil {
return nil, err
}
if conf.Admin != nil {
log.Printf("%v: field \"admin\" is obsolete, ignored", filename)
conf.Admin = nil
}
configuration.configuration = &conf
return configuration.configuration, nil
}
2022-09-08 20:55:05 +02:00
// called locked
func (g *Group) getPasswordPermission(creds ClientCredentials) (Permissions, error) {
2022-09-08 20:55:05 +02:00
desc := g.description
if creds.Username == nil {
return Permissions{}, errors.New("username not provided")
}
if desc.Users != nil {
if c, found := desc.Users[*creds.Username]; found {
ok, err := c.Password.Match(creds.Password)
if err != nil {
return Permissions{}, err
2020-05-30 00:23:54 +02:00
}
if ok {
return c.Permissions, nil
} else {
return Permissions{}, &NotAuthorisedError{}
2023-04-03 20:09:22 +02:00
}
2020-04-25 02:25:51 +02:00
}
}
if desc.WildcardUser != nil {
ok, _ := desc.WildcardUser.Password.Match(creds.Password)
if ok {
return desc.WildcardUser.Permissions, nil
}
}
return Permissions{}, &NotAuthorisedError{}
}
2021-10-29 23:37:05 +02:00
// Return true if there is a user entry with the given username.
// Always return false for an empty username.
func (g *Group) UserExists(username string) bool {
g.mu.Lock()
defer g.mu.Unlock()
return g.userExists(username)
}
// called locked
func (g *Group) userExists(username string) bool {
desc := g.description
if desc.Users == nil {
return false
}
_, found := desc.Users[username]
return found
}
// called locked
func (g *Group) getPermission(creds ClientCredentials) (string, []string, error) {
desc := g.description
var username string
var perms []string
if creds.Token != "" {
tok, err := token.Parse(creds.Token, desc.AuthKeys)
2021-10-29 23:37:05 +02:00
if err != nil {
return "", nil, &NotAuthorisedError{err: err}
2021-10-29 23:37:05 +02:00
}
conf, err := GetConfiguration()
if err != nil {
return "", nil, err
2021-10-29 23:37:05 +02:00
}
username, perms, err =
tok.Check(conf.CanonicalHost, g.name, creds.Username)
if err != nil {
return "", nil, &NotAuthorisedError{err: err}
}
if username == "" && creds.Username != nil {
if g.userExists(*creds.Username) {
return "", nil, ErrDuplicateUsername
}
username = *creds.Username
}
} else if creds.Username != nil {
username = *creds.Username
ps, err := g.getPasswordPermission(creds)
if err != nil {
return "", nil, err
2021-10-29 23:37:05 +02:00
}
perms = ps.Permissions(desc)
} else {
return "", nil, errors.New("neither username nor token provided")
2021-10-29 23:37:05 +02:00
}
return username, perms, nil
2020-04-25 17:36:35 +02:00
}
2022-09-08 20:55:05 +02:00
func (g *Group) GetPermission(creds ClientCredentials) (string, []string, error) {
g.mu.Lock()
defer g.mu.Unlock()
return g.getPermission(creds)
}
2021-10-26 22:22:48 +02:00
type Status struct {
2024-04-11 00:50:56 +02:00
Name string `json:"name"`
Redirect string `json:"redirect,omitempty"`
Location string `json:"location,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
DisplayName string `json:"displayName,omitempty"`
Description string `json:"description,omitempty"`
AuthServer string `json:"authServer,omitempty"`
AuthPortal string `json:"authPortal,omitempty"`
Locked bool `json:"locked,omitempty"`
ClientCount *int `json:"clientCount,omitempty"`
CanChangePassword bool `json:"canChangePassword,omitempty"`
2021-10-26 22:22:48 +02:00
}
// Status returns a group's status.
// Base is the base URL for groups; if omitted, then both the Location and
// Endpoint members are omitted from the result.
func (g *Group) Status(authentified bool, base *url.URL) Status {
2021-10-26 22:22:48 +02:00
desc := g.Description()
if desc.Redirect != "" {
return Status{
Name: g.name,
Redirect: desc.Redirect,
DisplayName: desc.DisplayName,
Description: desc.Description,
}
}
var location, endpoint string
if base != nil {
wss := "wss"
if base.Scheme == "http" {
wss = "ws"
}
l := url.URL{
Scheme: base.Scheme,
Host: base.Host,
Path: path.Join(
path.Join(base.Path, "/group/"),
g.Name()) + "/",
}
location = l.String()
e := url.URL{
Scheme: wss,
Host: base.Host,
Path: path.Join(base.Path, "/ws"),
}
endpoint = e.String()
}
2021-10-26 22:22:48 +02:00
d := Status{
Name: g.name,
Location: location,
Endpoint: endpoint,
2021-10-26 22:22:48 +02:00
DisplayName: desc.DisplayName,
2021-10-29 23:37:05 +02:00
AuthServer: desc.AuthServer,
2022-02-19 23:58:31 +01:00
AuthPortal: desc.AuthPortal,
2021-10-26 22:22:48 +02:00
Description: desc.Description,
}
if authentified || desc.Public {
// these are considered private information
locked, _ := g.Locked()
count := g.ClientCount()
d.Locked = locked
d.ClientCount = &count
}
2024-04-11 00:50:56 +02:00
if authentified {
conf, err := GetConfiguration()
if err == nil {
d.CanChangePassword = conf.WritableGroups
}
}
2021-10-26 22:22:48 +02:00
return d
2020-04-24 19:38:21 +02:00
}
func GetPublic(base *url.URL) []Status {
2021-10-26 22:22:48 +02:00
gs := make([]Status, 0)
Range(func(g *Group) bool {
2021-10-26 22:22:48 +02:00
if g.Description().Public {
gs = append(gs, g.Status(false, base))
2020-04-24 19:38:21 +02:00
}
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
// Update checks that all in-memory groups are up-to-date and updates the
// list of public groups. It also removes from memory any non-public
// groups that haven't been accessed in maxHistoryAge.
func Update() {
_, err := GetConfiguration()
if err != nil {
log.Printf("%v: %v",
filepath.Join(DataDirectory, "config.json"),
err,
)
}
names := GetNames()
for _, name := range names {
g := Get(name)
if g == nil {
continue
}
deleted := false
2022-10-08 22:46:28 +02:00
if g.mayExpire() {
// Delete checks if the group is still empty
deleted = Delete(name)
}
2022-10-08 22:46:28 +02:00
// update group description
if !deleted {
Add(name, nil)
}
}
err = filepath.WalkDir(
Directory,
func(path string, d fs.DirEntry, err error) error {
if err != nil {
log.Printf("Group file %v: %v", path, err)
2021-01-14 01:45:50 +01:00
return nil
}
if d.IsDir() {
base := filepath.Base(path)
if base[0] == '.' {
log.Printf(
"Ignoring group directory %v",
path,
)
return fs.SkipDir
}
return nil
}
filename, err := filepath.Rel(Directory, path)
if err != nil {
log.Printf("Group file %v: %v", path, err)
return nil
}
if !strings.HasSuffix(filename, ".json") {
log.Printf(
"Unexpected extension for group file %v",
path,
)
return nil
}
base := filepath.Base(filename)
if base[0] == '.' {
log.Printf("Ignoring group file %v", filename)
return nil
}
name := strings.TrimSuffix(filename, ".json")
desc, err := GetDescription(name)
if err != nil {
log.Printf("Group file %v: %v", path, err)
return nil
}
if desc.Public {
Add(name, desc)
}
return nil
},
)
2020-04-25 04:08:43 +02:00
if err != nil {
2021-01-14 01:45:50 +01:00
log.Printf("Couldn't read groups: %v", err)
2020-04-25 04:08:43 +02:00
}
}