Rename group package and defaults to halls
This commit is contained in:
+150
@@ -0,0 +1,150 @@
|
||||
package hall
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"hash"
|
||||
"net"
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
|
||||
"git.stormux.org/storm/skald/conn"
|
||||
)
|
||||
|
||||
type RawPassword struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Hash string `json:"hash,omitempty"`
|
||||
Key *string `json:"key,omitempty"`
|
||||
Salt string `json:"salt,omitempty"`
|
||||
Iterations int `json:"iterations,omitempty"`
|
||||
}
|
||||
|
||||
type Password RawPassword
|
||||
|
||||
// limit the number of concurrent hashing operations. This avoids running
|
||||
// out of memory when doing too many BCrypt hashes at the same time.
|
||||
var hashSemaphore = make(chan struct{}, runtime.GOMAXPROCS(-1))
|
||||
|
||||
// ConstantTimeCompare compares a and b in time proportional to the length of a.
|
||||
func ConstantTimeCompare(a, b string) bool {
|
||||
as := []byte(a)
|
||||
bs := make([]byte, len(as))
|
||||
copy(bs, b)
|
||||
equal := subtle.ConstantTimeCompare(as, bs) == 1
|
||||
return len(a) == len(b) && equal
|
||||
}
|
||||
|
||||
func (p Password) Match(pw string) (bool, error) {
|
||||
switch p.Type {
|
||||
case "":
|
||||
return false, nil
|
||||
case "plain":
|
||||
if p.Key == nil {
|
||||
return false, errors.New("missing key")
|
||||
}
|
||||
return ConstantTimeCompare(pw, *p.Key), nil
|
||||
case "wildcard":
|
||||
return true, nil
|
||||
case "pbkdf2":
|
||||
if p.Key == nil {
|
||||
return false, errors.New("missing key")
|
||||
}
|
||||
key, err := hex.DecodeString(*p.Key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
salt, err := hex.DecodeString(p.Salt)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var h func() hash.Hash
|
||||
switch p.Hash {
|
||||
case "sha-256":
|
||||
h = sha256.New
|
||||
default:
|
||||
return false, errors.New("unknown hash type")
|
||||
}
|
||||
hashSemaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-hashSemaphore
|
||||
}()
|
||||
theirKey := pbkdf2.Key(
|
||||
[]byte(pw), salt, p.Iterations, len(key), h,
|
||||
)
|
||||
return bytes.Equal(key, theirKey), nil
|
||||
case "bcrypt":
|
||||
if p.Key == nil {
|
||||
return false, errors.New("missing key")
|
||||
}
|
||||
hashSemaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-hashSemaphore
|
||||
}()
|
||||
err := bcrypt.CompareHashAndPassword([]byte(*p.Key), []byte(pw))
|
||||
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
|
||||
return false, nil
|
||||
}
|
||||
return err == nil, err
|
||||
default:
|
||||
return false, errors.New("unknown password type")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Password) UnmarshalJSON(b []byte) error {
|
||||
var k string
|
||||
err := json.Unmarshal(b, &k)
|
||||
if err == nil {
|
||||
*p = Password{
|
||||
Type: "plain",
|
||||
Key: &k,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var r RawPassword
|
||||
err = json.Unmarshal(b, &r)
|
||||
if err == nil {
|
||||
*p = Password(r)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p Password) MarshalJSON() ([]byte, error) {
|
||||
if p.Type == "plain" && p.Hash == "" && p.Salt == "" && p.Iterations == 0 {
|
||||
return json.Marshal(p.Key)
|
||||
}
|
||||
return json.Marshal(RawPassword(p))
|
||||
}
|
||||
|
||||
type ClientPattern struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
Password *Password `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
type ClientCredentials struct {
|
||||
System bool
|
||||
Username *string
|
||||
Password string
|
||||
Token string
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
Hall() *Hall
|
||||
Addr() net.Addr
|
||||
Id() string
|
||||
Username() string
|
||||
SetUsername(string)
|
||||
Permissions() []string
|
||||
SetPermissions([]string)
|
||||
Data() map[string]interface{}
|
||||
PushConn(g *Hall, id string, conn conn.Up, tracks []conn.UpTrack, replace string) error
|
||||
RequestConns(target Client, g *Hall, id string) error
|
||||
Joined(hall, kind string) error
|
||||
PushClient(hall, kind, id, username string, perms []string, data map[string]interface{}) error
|
||||
Kick(id string, user *string, message string) error
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package hall
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
var key1 = ""
|
||||
var pw1 = Password{
|
||||
Type: "plain",
|
||||
Key: &key1,
|
||||
}
|
||||
var key2 = "pass"
|
||||
var pw2 = Password{
|
||||
Type: "plain",
|
||||
Key: &key2,
|
||||
}
|
||||
var key3 = "fe499504e8f144693fae828e8e371d50e019d0e4c84994fa03f7f445bd8a570a"
|
||||
var pw3 = Password{
|
||||
Type: "pbkdf2",
|
||||
Hash: "sha-256",
|
||||
Key: &key3,
|
||||
Salt: "bcc1717851030776",
|
||||
Iterations: 4096,
|
||||
}
|
||||
var key4 = "$2a$10$afOr2f33onT/nDFFyT3mbOq5FMSw1wWXfyTXQTBMbKvZpBkoD3Qwu"
|
||||
var pw4 = Password{
|
||||
Type: "bcrypt",
|
||||
Key: &key4,
|
||||
}
|
||||
var pw5 = Password{}
|
||||
var pw6 = Password{
|
||||
Type: "bad",
|
||||
}
|
||||
|
||||
func TestGood(t *testing.T) {
|
||||
if match, err := pw1.Match(""); err != nil || !match {
|
||||
t.Errorf("pw2 doesn't match (%v)", err)
|
||||
}
|
||||
if match, err := pw2.Match("pass"); err != nil || !match {
|
||||
t.Errorf("pw2 doesn't match (%v)", err)
|
||||
}
|
||||
if match, err := pw3.Match("pass"); err != nil || !match {
|
||||
t.Errorf("pw3 doesn't match (%v)", err)
|
||||
}
|
||||
if match, err := pw4.Match("pass"); err != nil || !match {
|
||||
t.Errorf("pw4 doesn't match (%v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBad(t *testing.T) {
|
||||
if match, err := pw2.Match("bad"); err != nil || match {
|
||||
t.Errorf("pw2 matches bad")
|
||||
}
|
||||
if match, err := pw2.Match("bad1"); err != nil || match {
|
||||
t.Errorf("pw2 matches bad1")
|
||||
}
|
||||
if match, err := pw3.Match("bad"); err != nil || match {
|
||||
t.Errorf("pw3 matches")
|
||||
}
|
||||
if match, err := pw4.Match("bad"); err != nil || match {
|
||||
t.Errorf("pw4 matches")
|
||||
}
|
||||
if match, err := pw5.Match(""); err != nil || match {
|
||||
t.Errorf("pw5 matches")
|
||||
}
|
||||
if match, err := pw5.Match("bad"); err != nil || match {
|
||||
t.Errorf("pw5 matches")
|
||||
}
|
||||
if match, err := pw6.Match("bad"); err == nil || match {
|
||||
t.Errorf("pw6 matches")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyKey(t *testing.T) {
|
||||
for _, tpe := range []string{"plain", "pbkdf2", "bcrypt", "bad"} {
|
||||
pw := Password{Type: tpe}
|
||||
if match, err := pw.Match(""); err == nil || match {
|
||||
t.Errorf("empty password of type %v didn't error", tpe)
|
||||
}
|
||||
}
|
||||
|
||||
pw := Password{}
|
||||
if match, err := pw.Match(""); err != nil || match {
|
||||
t.Errorf("empty password empty type matched")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
plain, err := json.Marshal(pw2)
|
||||
if err != nil || string(plain) != `"pass"` {
|
||||
t.Errorf("Expected \"pass\", got %v", string(plain))
|
||||
}
|
||||
|
||||
for _, pw := range []Password{pw1, pw2, pw3, pw4, pw5} {
|
||||
j, err := json.Marshal(pw)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal: %v", err)
|
||||
}
|
||||
if testing.Verbose() {
|
||||
log.Printf("%v", string(j))
|
||||
}
|
||||
var pw2 Password
|
||||
err = json.Unmarshal(j, &pw2)
|
||||
if err != nil {
|
||||
t.Errorf("Unmarshal: %v", err)
|
||||
} else if !reflect.DeepEqual(pw, pw2) {
|
||||
t.Errorf("Expected %v, got %v", pw, pw2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPBKDF2(b *testing.B) {
|
||||
for iters := 1024; iters <= 1024*1024; iters *= 2 {
|
||||
b.Run(fmt.Sprintf("%d", iters), func(b *testing.B) {
|
||||
pw := []byte("Password1234")
|
||||
salt := make([]byte, 8)
|
||||
for i := range salt {
|
||||
salt[i] = byte(i)
|
||||
}
|
||||
key := pbkdf2.Key(pw, salt, iters, 32, sha256.New)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
k := pbkdf2.Key(pw, salt, iters, 32, sha256.New)
|
||||
if bytes.Compare(key, k) != 0 {
|
||||
b.Errorf("Key compare: mismatch")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBCrypt(b *testing.B) {
|
||||
for cost := 4; cost <= 15; cost++ {
|
||||
b.Run(fmt.Sprintf("%d", cost), func(b *testing.B) {
|
||||
pw := []byte("Password1234")
|
||||
key, err := bcrypt.GenerateFromPassword(pw, cost)
|
||||
if err != nil {
|
||||
b.Fatalf("GenerateFromPassword: %v", err)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := bcrypt.CompareHashAndPassword(key, pw)
|
||||
if err != nil {
|
||||
b.Errorf(
|
||||
"CompareHashAndPassword: %v", err,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,783 @@
|
||||
package hall
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/skald/token"
|
||||
)
|
||||
|
||||
var ErrTagMismatch = errors.New("tag mismatch")
|
||||
var ErrDescriptionsNotWritable = &NotAuthorisedError{}
|
||||
var ErrUnknownPermission = errors.New("unknown permission")
|
||||
|
||||
type Permissions struct {
|
||||
// non-empty for a named permissions set
|
||||
name string
|
||||
// only used when unnamed
|
||||
permissions []string
|
||||
}
|
||||
|
||||
var permissionsMap = map[string][]string{
|
||||
"op": {"op", "present", "message", "caption", "token"},
|
||||
"present": {"present", "message"},
|
||||
"message": {"message"},
|
||||
"observe": {},
|
||||
"caption": {"caption"},
|
||||
"admin": {"admin"},
|
||||
}
|
||||
|
||||
func NewPermissions(name string) (Permissions, error) {
|
||||
_, ok := permissionsMap[name]
|
||||
if !ok {
|
||||
return Permissions{}, ErrUnknownPermission
|
||||
}
|
||||
return Permissions{
|
||||
name: name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p Permissions) Permissions(desc *Description) []string {
|
||||
if p.name == "" {
|
||||
return p.permissions
|
||||
}
|
||||
|
||||
perms := permissionsMap[p.name]
|
||||
|
||||
op := false
|
||||
present := false
|
||||
token := false
|
||||
record := false
|
||||
for _, p := range perms {
|
||||
switch p {
|
||||
case "op":
|
||||
op = true
|
||||
case "present":
|
||||
present = true
|
||||
case "token":
|
||||
token = true
|
||||
case "record":
|
||||
record = true
|
||||
}
|
||||
}
|
||||
|
||||
if desc != nil && desc.AllowRecording {
|
||||
if op && !record {
|
||||
// copy the slice
|
||||
perms = append([]string{"record"}, perms...)
|
||||
}
|
||||
}
|
||||
|
||||
if desc != nil && desc.UnrestrictedTokens {
|
||||
if present && !token {
|
||||
perms = append([]string{"token"}, perms...)
|
||||
}
|
||||
}
|
||||
|
||||
return perms
|
||||
}
|
||||
|
||||
func (p Permissions) String() string {
|
||||
if p.name != "" {
|
||||
if p.permissions != nil {
|
||||
return fmt.Sprintf("(ERROR=overconstrained %v)", p.name)
|
||||
}
|
||||
return p.name
|
||||
}
|
||||
v, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("(ERROR=%v)", err)
|
||||
}
|
||||
return string(v)
|
||||
}
|
||||
|
||||
func (p *Permissions) UnmarshalJSON(b []byte) error {
|
||||
var a []string
|
||||
err := json.Unmarshal(b, &a)
|
||||
if err == nil {
|
||||
*p = Permissions{
|
||||
permissions: a,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var s string
|
||||
err = json.Unmarshal(b, &s)
|
||||
if err == nil {
|
||||
_, ok := permissionsMap[s]
|
||||
if !ok {
|
||||
return ErrUnknownPermission
|
||||
}
|
||||
*p = Permissions{
|
||||
name: s,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p Permissions) MarshalJSON() ([]byte, error) {
|
||||
if p.name != "" {
|
||||
return json.Marshal(p.name)
|
||||
}
|
||||
return json.Marshal(p.permissions)
|
||||
}
|
||||
|
||||
type UserDescription struct {
|
||||
Password Password `json:"password"`
|
||||
Permissions Permissions `json:"permissions"`
|
||||
}
|
||||
|
||||
// Custom MarshalJSON in order to omit empty fields
|
||||
func (u UserDescription) MarshalJSON() ([]byte, error) {
|
||||
uu := make(map[string]any, 2)
|
||||
if u.Password.Type != "" {
|
||||
uu["password"] = &u.Password
|
||||
}
|
||||
if u.Permissions.name != "" || u.Permissions.permissions != nil {
|
||||
uu["permissions"] = &u.Permissions
|
||||
}
|
||||
return json.Marshal(uu)
|
||||
}
|
||||
|
||||
// Description represents a hall description together with some metadata
|
||||
// about the JSON file it was deserialised from.
|
||||
type Description struct {
|
||||
// The file this was deserialised from. This is not necessarily
|
||||
// the name of the hall, for example in case of a subgroup.
|
||||
FileName string `json:"-"`
|
||||
|
||||
// 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:"-"`
|
||||
|
||||
// Whether this is an automatically generated subgroup
|
||||
isSubgroup bool `json:"-"`
|
||||
|
||||
// The user-friendly hall name
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
|
||||
// A user-readable description of the hall.
|
||||
Description string `json:"description,omitempty"`
|
||||
|
||||
// A user-readable contact, typically an e-mail address.
|
||||
Contact string `json:"contact,omitempty"`
|
||||
|
||||
// A user-readable comment. Ignored by the server.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
|
||||
// Whether to display the hall on the landing page.
|
||||
Public bool `json:"public,omitempty"`
|
||||
|
||||
// A URL to redirect the hall to. If this is not empty, most
|
||||
// other fields are ignored.
|
||||
Redirect string `json:"redirect,omitempty"`
|
||||
|
||||
// The maximum number of simultaneous clients. Unlimited if 0.
|
||||
MaxClients int `json:"max-clients,omitempty"`
|
||||
|
||||
// The time for which history entries are kept.
|
||||
MaxHistoryAge int `json:"max-history-age,omitempty"`
|
||||
|
||||
// Time after which joining is no longer allowed
|
||||
Expires *time.Time `json:"expires,omitempty"`
|
||||
|
||||
// Time before which joining is not allowed
|
||||
NotBefore *time.Time `json:"not-before,omitempty"`
|
||||
|
||||
// Whether recording is allowed.
|
||||
AllowRecording bool `json:"allow-recording,omitempty"`
|
||||
|
||||
// Whether creating tokens is allowed
|
||||
UnrestrictedTokens bool `json:"unrestricted-tokens,omitempty"`
|
||||
|
||||
// Whether subgroups are created on the fly.
|
||||
AutoSubgroups bool `json:"auto-subgroups,omitempty"`
|
||||
|
||||
// Whether to lock the hall when the last op logs out.
|
||||
Autolock bool `json:"autolock,omitempty"`
|
||||
|
||||
// Whether to kick all users when the last op logs out.
|
||||
Autokick bool `json:"autokick,omitempty"`
|
||||
|
||||
// Users allowed to login
|
||||
Users map[string]UserDescription `json:"users,omitempty"`
|
||||
|
||||
// Credentials for user with arbitrary username
|
||||
WildcardUser *UserDescription `json:"wildcard-user,omitempty"`
|
||||
|
||||
// The (public) keys used for token authentication.
|
||||
AuthKeys []map[string]interface{} `json:"authKeys,omitempty"`
|
||||
|
||||
// The URL of the authentication server, if any.
|
||||
AuthServer string `json:"authServer,omitempty"`
|
||||
|
||||
// The URL of the authentication portal, if any.
|
||||
AuthPortal string `json:"authPortal,omitempty"`
|
||||
|
||||
// Codec preferences. If empty, a suitable default is chosen in
|
||||
// the APIFromNames function.
|
||||
Codecs []string `json:"codecs,omitempty"`
|
||||
|
||||
// Obsolete fields
|
||||
Op []ClientPattern `json:"op,omitempty"`
|
||||
Presenter []ClientPattern `json:"presenter,omitempty"`
|
||||
Other []ClientPattern `json:"other,omitempty"`
|
||||
AllowSubgroups bool `json:"allow-subgroups,omitempty"`
|
||||
AllowAnonymous bool `json:"allow-anonymous,omitempty"`
|
||||
}
|
||||
|
||||
const DefaultMaxHistoryAge = 4 * time.Hour
|
||||
|
||||
func maxHistoryAge(desc *Description) time.Duration {
|
||||
if desc.MaxHistoryAge != 0 {
|
||||
return time.Duration(desc.MaxHistoryAge) * time.Second
|
||||
}
|
||||
return DefaultMaxHistoryAge
|
||||
}
|
||||
|
||||
func getDescriptionFile[T any](name string, allowSubgroups bool, get func(string) (T, error)) (T, string, bool, error) {
|
||||
isSubgroup := false
|
||||
for name != "" {
|
||||
fileName := filepath.Join(
|
||||
Directory, path.Clean("/"+name)+".json",
|
||||
)
|
||||
r, err := get(fileName)
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return r, fileName, isSubgroup, err
|
||||
}
|
||||
if !allowSubgroups {
|
||||
break
|
||||
}
|
||||
isSubgroup = true
|
||||
name, _ = path.Split(name)
|
||||
name = strings.TrimRight(name, "/")
|
||||
}
|
||||
var zero T
|
||||
return zero, "", false, os.ErrNotExist
|
||||
}
|
||||
|
||||
// descriptionMatch returns true if the description hasn't changed between
|
||||
// d1 and d2
|
||||
func descriptionMatch(d1, d2 *Description) bool {
|
||||
if d1.FileName != d2.FileName {
|
||||
return false
|
||||
}
|
||||
|
||||
if d1.fileSize != d2.fileSize || !d1.modTime.Equal(d2.modTime) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// descriptionUnchanged returns true if a hall's description hasn't
|
||||
// changed since it was last read.
|
||||
func descriptionUnchanged(name string, desc *Description) bool {
|
||||
fi, fileName, _, err := getDescriptionFile(name, true, os.Stat)
|
||||
if err != nil || fileName != desc.FileName {
|
||||
return false
|
||||
}
|
||||
|
||||
if fi.Size() != desc.fileSize || !fi.ModTime().Equal(desc.modTime) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetDescription gets a hall description, either from cache or from disk
|
||||
func GetDescription(name string) (*Description, error) {
|
||||
g := Get(name)
|
||||
if g != nil {
|
||||
if descriptionUnchanged(name, g.description) {
|
||||
return g.description, nil
|
||||
}
|
||||
}
|
||||
|
||||
return readDescription(name, true)
|
||||
}
|
||||
|
||||
// GetSanitisedDescription returns the subset of the description that is
|
||||
// published on the web interface together with a suitable ETag.
|
||||
func GetSanitisedDescription(name string) (*Description, string, error) {
|
||||
d, err := GetDescription(name)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if d.isSubgroup {
|
||||
return nil, "", os.ErrNotExist
|
||||
}
|
||||
|
||||
desc := *d
|
||||
desc.Users = nil
|
||||
desc.WildcardUser = nil
|
||||
desc.AuthKeys = nil
|
||||
return &desc, makeETag(desc.fileSize, desc.modTime), nil
|
||||
}
|
||||
|
||||
// GetDescriptionTag returns an ETag for a description.
|
||||
func GetDescriptionTag(name string) (string, error) {
|
||||
fi, _, _, err := getDescriptionFile(name, false, os.Stat)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return makeETag(fi.Size(), fi.ModTime()), nil
|
||||
}
|
||||
|
||||
func makeETag(fileSize int64, modTime time.Time) string {
|
||||
return fmt.Sprintf("\"%v-%v\"", fileSize, modTime.UnixNano())
|
||||
}
|
||||
|
||||
// DeleteDescription deletes a description (and therefore persistently
|
||||
// deletes a hall) but only if it matches a given ETag.
|
||||
func DeleteDescription(name, etag string) error {
|
||||
halls.mu.Lock()
|
||||
defer halls.mu.Unlock()
|
||||
|
||||
fi, fileName, _, err := getDescriptionFile(name, false, os.Stat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if etag != makeETag(fi.Size(), fi.ModTime()) {
|
||||
return ErrTagMismatch
|
||||
}
|
||||
return os.Remove(fileName)
|
||||
}
|
||||
|
||||
// UpdateDescription overwrites a description if it matches a given ETag.
|
||||
// In order to create a new hall, pass an empty ETag.
|
||||
func UpdateDescription(name, etag string, desc *Description) error {
|
||||
if desc.Users != nil || desc.WildcardUser != nil || desc.AuthKeys != nil {
|
||||
return errors.New("description is not sanitised")
|
||||
}
|
||||
|
||||
halls.mu.Lock()
|
||||
defer halls.mu.Unlock()
|
||||
|
||||
oldetag := ""
|
||||
var filename string
|
||||
old, err := readDescription(name, false)
|
||||
if err == nil {
|
||||
oldetag = makeETag(old.fileSize, old.modTime)
|
||||
filename = old.FileName
|
||||
} else if errors.Is(err, os.ErrNotExist) {
|
||||
old = nil
|
||||
filename = filepath.Join(
|
||||
Directory, path.Clean("/"+name)+".json",
|
||||
)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
if oldetag != etag {
|
||||
return ErrTagMismatch
|
||||
}
|
||||
|
||||
newdesc := *desc
|
||||
if old != nil {
|
||||
newdesc.Users = old.Users
|
||||
newdesc.WildcardUser = old.WildcardUser
|
||||
newdesc.AuthKeys = old.AuthKeys
|
||||
}
|
||||
|
||||
return rewriteDescriptionFile(filename, &newdesc)
|
||||
}
|
||||
|
||||
func rewriteDescriptionFile(filename string, desc *Description) error {
|
||||
conf, err := GetConfiguration()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !conf.WritableGroups {
|
||||
return ErrDescriptionsNotWritable
|
||||
}
|
||||
|
||||
dir := filepath.Dir(filename)
|
||||
|
||||
err = os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.CreateTemp(dir, "*.temp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
temp := f.Name()
|
||||
|
||||
encoder := json.NewEncoder(f)
|
||||
err = encoder.Encode(desc)
|
||||
if err == nil {
|
||||
err = f.Sync()
|
||||
}
|
||||
if err != nil {
|
||||
f.Close()
|
||||
os.Remove(temp)
|
||||
return err
|
||||
}
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
os.Remove(temp)
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.Rename(temp, filename)
|
||||
if err != nil {
|
||||
os.Remove(temp)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// readDescription reads a hall's description from disk
|
||||
func readDescription(name string, allowSubgroups bool) (*Description, error) {
|
||||
r, fileName, isSubgroup, err :=
|
||||
getDescriptionFile(name, allowSubgroups, os.Open)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
var desc Description
|
||||
|
||||
fi, err := r.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d := json.NewDecoder(r)
|
||||
d.DisallowUnknownFields()
|
||||
err = d.Decode(&desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
desc.FileName = fileName
|
||||
desc.fileSize = fi.Size()
|
||||
desc.modTime = fi.ModTime()
|
||||
|
||||
err = upgradeDescription(&desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isSubgroup {
|
||||
if !desc.AutoSubgroups {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
desc.isSubgroup = true
|
||||
desc.Public = false
|
||||
desc.Description = ""
|
||||
}
|
||||
|
||||
return &desc, nil
|
||||
}
|
||||
|
||||
func upgradeDescription(desc *Description) error {
|
||||
if desc.AllowAnonymous {
|
||||
log.Printf(
|
||||
"%v: field allow-anonymous is obsolete, ignored",
|
||||
desc.FileName,
|
||||
)
|
||||
desc.AllowAnonymous = false
|
||||
}
|
||||
|
||||
if desc.AllowSubgroups {
|
||||
desc.AutoSubgroups = true
|
||||
desc.AllowSubgroups = false
|
||||
}
|
||||
|
||||
upgradePassword := func(pw *Password) Password {
|
||||
if pw == nil {
|
||||
return Password{
|
||||
Type: "wildcard",
|
||||
}
|
||||
}
|
||||
return *pw
|
||||
}
|
||||
|
||||
upgradeUser := func(u ClientPattern, p string) UserDescription {
|
||||
return UserDescription{
|
||||
Password: upgradePassword(u.Password),
|
||||
Permissions: Permissions{
|
||||
name: p,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
upgradeUsers := func(ps []ClientPattern, p string) {
|
||||
if desc.Users == nil {
|
||||
desc.Users = make(map[string]UserDescription)
|
||||
}
|
||||
for _, u := range ps {
|
||||
if u.Username == "" {
|
||||
if desc.WildcardUser != nil {
|
||||
log.Printf("%v: duplicate wildcard user",
|
||||
desc.FileName)
|
||||
continue
|
||||
}
|
||||
u := upgradeUser(u, p)
|
||||
desc.WildcardUser = &u
|
||||
continue
|
||||
}
|
||||
_, found := desc.Users[u.Username]
|
||||
if found {
|
||||
log.Printf("%v: duplicate user %v, ignored",
|
||||
desc.FileName, u.Username)
|
||||
continue
|
||||
}
|
||||
desc.Users[u.Username] = upgradeUser(u, p)
|
||||
}
|
||||
}
|
||||
|
||||
if desc.Op != nil {
|
||||
upgradeUsers(desc.Op, "op")
|
||||
desc.Op = nil
|
||||
}
|
||||
if desc.Presenter != nil {
|
||||
upgradeUsers(desc.Presenter, "present")
|
||||
desc.Presenter = nil
|
||||
}
|
||||
if desc.Other != nil {
|
||||
upgradeUsers(desc.Other, "message")
|
||||
desc.Other = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetDescriptionNames() ([]string, error) {
|
||||
var names []string
|
||||
err := filepath.WalkDir(
|
||||
Directory,
|
||||
func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
base := filepath.Base(path)
|
||||
if d.IsDir() {
|
||||
if base[0] == '.' {
|
||||
return fs.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if base[0] == '.' {
|
||||
return nil
|
||||
}
|
||||
p, err := filepath.Rel(Directory, path)
|
||||
if err != nil || !strings.HasSuffix(p, ".json") {
|
||||
return nil
|
||||
}
|
||||
names = append(names, strings.TrimSuffix(
|
||||
p, ".json",
|
||||
))
|
||||
return nil
|
||||
},
|
||||
)
|
||||
return names, err
|
||||
}
|
||||
|
||||
func SetKeys(hall string, keys []map[string]any) error {
|
||||
if keys != nil {
|
||||
_, err := token.ParseKeys(keys, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
halls.mu.Lock()
|
||||
defer halls.mu.Unlock()
|
||||
|
||||
desc, err := readDescription(hall, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
desc.AuthKeys = keys
|
||||
return rewriteDescriptionFile(desc.FileName, desc)
|
||||
}
|
||||
|
||||
func GetUsers(hall string) ([]string, string, error) {
|
||||
desc, err := GetDescription(hall)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
users := make([]string, 0, len(desc.Users))
|
||||
for u := range desc.Users {
|
||||
users = append(users, u)
|
||||
}
|
||||
|
||||
return users, makeETag(desc.fileSize, desc.modTime), nil
|
||||
}
|
||||
|
||||
func GetSanitisedUser(hall, username string, wildcard bool) (UserDescription, string, error) {
|
||||
if wildcard && username != "" {
|
||||
return UserDescription{}, "",
|
||||
errors.New("wildcard with username")
|
||||
}
|
||||
|
||||
desc, err := GetDescription(hall)
|
||||
if err != nil {
|
||||
return UserDescription{}, "", err
|
||||
}
|
||||
|
||||
var u UserDescription
|
||||
if wildcard {
|
||||
if desc.WildcardUser == nil {
|
||||
return UserDescription{}, "", os.ErrNotExist
|
||||
}
|
||||
u = *desc.WildcardUser
|
||||
} else {
|
||||
if desc.Users == nil {
|
||||
return UserDescription{}, "", os.ErrNotExist
|
||||
}
|
||||
|
||||
ok := false
|
||||
u, ok = desc.Users[username]
|
||||
if !ok {
|
||||
return UserDescription{}, "", os.ErrNotExist
|
||||
}
|
||||
}
|
||||
|
||||
u.Password = Password{}
|
||||
return u, makeETag(desc.fileSize, desc.modTime), nil
|
||||
}
|
||||
|
||||
func GetUserTag(hall, username string, wildcard bool) (string, error) {
|
||||
_, etag, err := GetSanitisedUser(hall, username, wildcard)
|
||||
return etag, err
|
||||
}
|
||||
|
||||
func DeleteUser(hall, username string, wildcard bool, etag string) error {
|
||||
if wildcard && username != "" {
|
||||
return errors.New("wildcard with username")
|
||||
}
|
||||
|
||||
halls.mu.Lock()
|
||||
defer halls.mu.Unlock()
|
||||
|
||||
desc, err := readDescription(hall, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if wildcard {
|
||||
if desc.WildcardUser == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
} else {
|
||||
if desc.Users == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
_, ok := desc.Users[username]
|
||||
if !ok {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
}
|
||||
|
||||
oldetag := makeETag(desc.fileSize, desc.modTime)
|
||||
if oldetag != etag {
|
||||
return ErrTagMismatch
|
||||
}
|
||||
|
||||
if wildcard {
|
||||
desc.WildcardUser = nil
|
||||
} else {
|
||||
delete(desc.Users, username)
|
||||
}
|
||||
|
||||
return rewriteDescriptionFile(desc.FileName, desc)
|
||||
}
|
||||
|
||||
func UpdateUser(hall, username string, wildcard bool, etag string, user *UserDescription) error {
|
||||
if wildcard && username != "" {
|
||||
return errors.New("wildcard with username")
|
||||
}
|
||||
if user.Password.Type != "" || user.Password.Key != nil {
|
||||
return errors.New("user description is not sanitised")
|
||||
}
|
||||
|
||||
halls.mu.Lock()
|
||||
defer halls.mu.Unlock()
|
||||
|
||||
desc, err := readDescription(hall, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var old UserDescription
|
||||
var ok bool
|
||||
if wildcard {
|
||||
if desc.WildcardUser != nil {
|
||||
ok = true
|
||||
old = *desc.WildcardUser
|
||||
}
|
||||
} else {
|
||||
if desc.Users == nil {
|
||||
desc.Users = make(map[string]UserDescription)
|
||||
}
|
||||
old, ok = desc.Users[username]
|
||||
}
|
||||
|
||||
var oldetag string
|
||||
if ok {
|
||||
oldetag = makeETag(desc.fileSize, desc.modTime)
|
||||
} else {
|
||||
oldetag = ""
|
||||
}
|
||||
|
||||
if oldetag != etag {
|
||||
return ErrTagMismatch
|
||||
}
|
||||
|
||||
newuser := *user
|
||||
newuser.Password = old.Password
|
||||
|
||||
if wildcard {
|
||||
desc.WildcardUser = &newuser
|
||||
} else {
|
||||
desc.Users[username] = newuser
|
||||
}
|
||||
return rewriteDescriptionFile(desc.FileName, desc)
|
||||
}
|
||||
|
||||
func SetUserPassword(hall, username string, wildcard bool, pw Password) error {
|
||||
if wildcard && username != "" {
|
||||
return errors.New("wildcard with username")
|
||||
}
|
||||
|
||||
halls.mu.Lock()
|
||||
defer halls.mu.Unlock()
|
||||
|
||||
desc, err := readDescription(hall, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if wildcard {
|
||||
if desc.WildcardUser == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
desc.WildcardUser.Password = pw
|
||||
} else {
|
||||
if desc.Users == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
user, ok := desc.Users[username]
|
||||
if !ok {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
user.Password = pw
|
||||
desc.Users[username] = user
|
||||
}
|
||||
return rewriteDescriptionFile(desc.FileName, desc)
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package hall
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMarshalUserDescription(t *testing.T) {
|
||||
tests := []string{
|
||||
`{}`,
|
||||
`{"permissions":"present"}`,
|
||||
`{"password":"secret"}`,
|
||||
`{"password":"secret","permissions":"present"}`,
|
||||
`{"password":"secret","permissions":["present"]}`,
|
||||
`{"password":{"type":"wildcard"},"permissions":"observe"}`,
|
||||
`{"password":{"type":"wildcard"},"permissions":[]}`,
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
var u UserDescription
|
||||
err := json.Unmarshal([]byte(test), &u)
|
||||
if err != nil {
|
||||
t.Errorf("Unmarshal %v: %v", t, err)
|
||||
continue
|
||||
}
|
||||
v, err := json.Marshal(u)
|
||||
if err != nil || string(v) != test {
|
||||
t.Errorf("Marshal %v: got %v %v", test, string(v), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyJSON(t *testing.T) {
|
||||
type emptyTest struct {
|
||||
value any
|
||||
result string
|
||||
name string
|
||||
}
|
||||
|
||||
emptyTests := []emptyTest{
|
||||
{Password{}, "{}", "password"},
|
||||
{Permissions{}, "null", "permissions"},
|
||||
{UserDescription{}, "{}", "user description"},
|
||||
}
|
||||
|
||||
for _, v := range emptyTests {
|
||||
b, err := json.Marshal(v.value)
|
||||
if err != nil || string(b) != v.result {
|
||||
t.Errorf("Marshal empty %v: %#v %v, expected %#v",
|
||||
v.name, string(b), err, v.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var descJSON = `
|
||||
{
|
||||
"max-history-age": 10,
|
||||
"auto-subgroups": true,
|
||||
"users": {
|
||||
"jch": {"password": "topsecret", "permissions": "op"},
|
||||
"john": {"password": "secret", "permissions": "present"},
|
||||
"james": {"password": "secret2", "permissions": "message"},
|
||||
"peter": {"password": "secret4"}
|
||||
},
|
||||
"wildcard-user":
|
||||
{"permissions": "message", "password": {"type":"wildcard"}}
|
||||
}`
|
||||
|
||||
func TestDescriptionJSON(t *testing.T) {
|
||||
var d Description
|
||||
err := json.Unmarshal([]byte(descJSON), &d)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
dd, err := json.Marshal(d)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
|
||||
var ddd Description
|
||||
err = json.Unmarshal([]byte(dd), &ddd)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(d, ddd) {
|
||||
t.Errorf("Got %v, expected %v", ddd, d)
|
||||
}
|
||||
}
|
||||
|
||||
var obsoleteJSON = `
|
||||
{
|
||||
"op": [{"username": "jch","password": "topsecret"}],
|
||||
"max-history-age": 10,
|
||||
"allow-subgroups": true,
|
||||
"presenter": [
|
||||
{"username": "john", "password": "secret"}
|
||||
],
|
||||
"other": [
|
||||
{"username": "james", "password": "secret2"},
|
||||
{"username": "peter", "password": "secret4"},
|
||||
{}
|
||||
]
|
||||
}`
|
||||
|
||||
func TestUpgradeDescription(t *testing.T) {
|
||||
var d1 Description
|
||||
err := json.Unmarshal([]byte(descJSON), &d1)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
var d2 Description
|
||||
err = json.Unmarshal([]byte(obsoleteJSON), &d2)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
err = upgradeDescription(&d2)
|
||||
if err != nil {
|
||||
t.Fatalf("upgradeDescription: %v", err)
|
||||
}
|
||||
|
||||
if d1.AutoSubgroups != d2.AutoSubgroups ||
|
||||
d1.AllowSubgroups != d2.AllowSubgroups {
|
||||
t.Errorf("AllowSubgroups not upgraded correctly")
|
||||
}
|
||||
|
||||
if d2.Op != nil || d2.Presenter != nil || d2.Other != nil {
|
||||
t.Errorf("legacy field is not nil")
|
||||
}
|
||||
|
||||
if len(d1.Users) != len(d2.Users) {
|
||||
t.Errorf("length not equal: %v != %v",
|
||||
len(d1.Users), len(d2.Users))
|
||||
}
|
||||
|
||||
for k, v1 := range d1.Users {
|
||||
if k == "peter" {
|
||||
// not representable in the old format
|
||||
continue
|
||||
}
|
||||
v2 := d2.Users[k]
|
||||
if !reflect.DeepEqual(v1.Password, v2.Password) ||
|
||||
!permissionsEqual(
|
||||
v1.Permissions.Permissions(&d1),
|
||||
v2.Permissions.Permissions(&d2),
|
||||
) {
|
||||
t.Errorf("%v not equal: %v != %v", k, v1, v2)
|
||||
}
|
||||
}
|
||||
|
||||
if d1.WildcardUser != nil || d2.WildcardUser != nil {
|
||||
if !reflect.DeepEqual(
|
||||
d1.WildcardUser.Password, d2.WildcardUser.Password,
|
||||
) || !permissionsEqual(
|
||||
d1.WildcardUser.Permissions.Permissions(&d1),
|
||||
d2.WildcardUser.Permissions.Permissions(&d2),
|
||||
) {
|
||||
t.Errorf("WildcardUser not equal: %v != %v",
|
||||
d1.WildcardUser, d2.WildcardUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupTest(dir, datadir string, writable bool) error {
|
||||
Directory = dir
|
||||
DataDirectory = datadir
|
||||
f, err := os.Create(filepath.Join(datadir, "config.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
conf := `{}`
|
||||
if writable {
|
||||
conf = `{"writableGroups": true}`
|
||||
}
|
||||
_, err = f.WriteString(conf)
|
||||
return err
|
||||
}
|
||||
|
||||
func TestNonWritableGroups(t *testing.T) {
|
||||
err := setupTest(t.TempDir(), t.TempDir(), false)
|
||||
if err != nil {
|
||||
t.Fatalf("setupTest: %v", err)
|
||||
}
|
||||
|
||||
_, err = GetDescription("test")
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
t.Errorf("GetDescription: got %#v, expected ErrNotExist", err)
|
||||
}
|
||||
|
||||
err = UpdateDescription("test", "", &Description{})
|
||||
if !errors.Is(err, ErrDescriptionsNotWritable) {
|
||||
t.Errorf("UpdateDescription: got %#v, "+
|
||||
"expected ErrDescriptionsNotWritable", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWritableGroups(t *testing.T) {
|
||||
err := setupTest(t.TempDir(), t.TempDir(), true)
|
||||
if err != nil {
|
||||
t.Fatalf("setupTest: %v", err)
|
||||
}
|
||||
|
||||
_, err = GetDescription("test")
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
t.Errorf("GetDescription: got %v, expected ErrNotExist", err)
|
||||
}
|
||||
|
||||
err = UpdateDescription("test", "\"etag\"", &Description{})
|
||||
if !errors.Is(err, ErrTagMismatch) {
|
||||
t.Errorf("UpdateDescription: got %v, expected ErrTagMismatch",
|
||||
err)
|
||||
}
|
||||
|
||||
err = UpdateDescription("test", "", &Description{})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateDescription: got %v", err)
|
||||
}
|
||||
|
||||
_, err = GetDescription("test")
|
||||
if err != nil {
|
||||
t.Errorf("GetDescription: got %v", err)
|
||||
}
|
||||
|
||||
fi, err := os.Stat(filepath.Join(Directory, "test.json"))
|
||||
if err != nil {
|
||||
t.Errorf("Stat: %v", err)
|
||||
}
|
||||
if mode := fi.Mode(); mode != 0o600 {
|
||||
t.Errorf("Mode is 0o%03o (expected 0o600)\n", mode)
|
||||
}
|
||||
|
||||
desc, token, err := GetSanitisedDescription("test")
|
||||
if err != nil || token == "" {
|
||||
t.Errorf("GetSanitisedDescription: got %v", err)
|
||||
}
|
||||
|
||||
desc.DisplayName = "Test"
|
||||
|
||||
err = UpdateDescription("test", "\"badetag\"", desc)
|
||||
if !errors.Is(err, ErrTagMismatch) {
|
||||
t.Errorf("UpdateDescription: got %v, expected ErrTagMismatch",
|
||||
err)
|
||||
}
|
||||
|
||||
err = UpdateDescription("test", token, desc)
|
||||
if err != nil {
|
||||
t.Errorf("UpdateDescription: got %v", err)
|
||||
}
|
||||
|
||||
desc, err = GetDescription("test")
|
||||
if err != nil || desc.DisplayName != "Test" {
|
||||
t.Errorf("GetDescription: expected %v %v, got %v %v",
|
||||
nil, "Test", err, desc.AllowAnonymous,
|
||||
)
|
||||
}
|
||||
testUser(t, "jch", false)
|
||||
testUser(t, "", true)
|
||||
}
|
||||
|
||||
func testUser(t *testing.T, username string, wildcard bool) {
|
||||
_, _, err := GetSanitisedUser("test", username, wildcard)
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
t.Errorf("GetSanitisedUser: got %v, expected ErrNotExist", err)
|
||||
}
|
||||
|
||||
err = UpdateUser("test", username, wildcard, "", &UserDescription{
|
||||
Permissions: Permissions{name: "observe"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("UpdateUser: got %v", err)
|
||||
}
|
||||
|
||||
user, token, err := GetSanitisedUser("test", username, wildcard)
|
||||
if err != nil || token == "" || user.Permissions.name != "observe" {
|
||||
t.Errorf("GetDescription: got %v %v, expected %v %v",
|
||||
err, user.Permissions.name, nil, "observe",
|
||||
)
|
||||
}
|
||||
|
||||
err = UpdateUser("test", username, wildcard, "", &UserDescription{
|
||||
Permissions: Permissions{name: "present"},
|
||||
})
|
||||
if !errors.Is(err, ErrTagMismatch) {
|
||||
t.Errorf("UpdateDescription: got %v, expected ErrTagMismatch",
|
||||
err)
|
||||
}
|
||||
|
||||
err = UpdateUser("test", username, wildcard, token, &UserDescription{
|
||||
Permissions: Permissions{name: "present"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("UpdateUser: got %v", err)
|
||||
}
|
||||
|
||||
pw := "pw"
|
||||
err = SetUserPassword("test", username, wildcard, Password{
|
||||
Type: "plain",
|
||||
Key: &pw,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("SetUserPassword: got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubGroup(t *testing.T) {
|
||||
err := setupTest(t.TempDir(), t.TempDir(), true)
|
||||
if err != nil {
|
||||
t.Fatalf("setupTest: %v", err)
|
||||
}
|
||||
|
||||
err = UpdateDescription("dir/test", "", &Description{})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateDescription: got %v", err)
|
||||
}
|
||||
}
|
||||
+1278
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,406 @@
|
||||
package hall
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func TestConstantTimeCompare(t *testing.T) {
|
||||
tests := []struct {
|
||||
a, b string
|
||||
}{
|
||||
{"", ""},
|
||||
{"a", "a"},
|
||||
{"a", "b"},
|
||||
{"foo", "foo"},
|
||||
{"foo", "bar"},
|
||||
{"foo", "foo1"},
|
||||
{"foo1", "foo"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
e := ConstantTimeCompare(test.a, test.b)
|
||||
if e != (test.a == test.b) {
|
||||
t.Errorf("constantTimeCompare(%v, %v): got %v",
|
||||
test.a, test.b, e,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroup(t *testing.T) {
|
||||
halls.halls = nil
|
||||
Add("hall", &Description{})
|
||||
Add("hall/subgroup", &Description{Public: true})
|
||||
if len(halls.halls) != 2 {
|
||||
t.Errorf("Expected 2, got %v", len(halls.halls))
|
||||
}
|
||||
g := Get("hall")
|
||||
g2 := Get("hall/subgroup")
|
||||
if g == nil {
|
||||
t.Fatalf("Couldn't get hall")
|
||||
}
|
||||
if g2 == nil {
|
||||
t.Fatalf("Couldn't get hall/subgroup")
|
||||
}
|
||||
if name := g.Name(); name != "hall" {
|
||||
t.Errorf("Name: expected group1, got %v", name)
|
||||
}
|
||||
if locked, _ := g.Locked(); locked {
|
||||
t.Errorf("Locked: expected false, got %v", locked)
|
||||
}
|
||||
api, err := g.API()
|
||||
if err != nil || api == nil {
|
||||
t.Errorf("Couldn't get API: %v", err)
|
||||
}
|
||||
|
||||
if names := GetNames(); len(names) != 2 {
|
||||
t.Errorf("Expected 2, got %v", names)
|
||||
}
|
||||
|
||||
if subs := GetSubGroups("hall"); len(subs) != 0 {
|
||||
t.Errorf("Expected [], got %v", subs)
|
||||
}
|
||||
|
||||
if public := GetPublic(nil); len(public) != 1 || public[0].Name != "hall/subgroup" {
|
||||
t.Errorf("Expected hall/subgroup, got %v", public)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatHistory(t *testing.T) {
|
||||
g := Hall{
|
||||
description: &Description{},
|
||||
}
|
||||
user := "user"
|
||||
for i := 0; i < 2*maxChatHistory; i++ {
|
||||
g.AddToChatHistory(
|
||||
fmt.Sprintf("id-%v", i),
|
||||
fmt.Sprintf("source-%v", i%4),
|
||||
&user, time.Now(), "",
|
||||
fmt.Sprintf("%v", i),
|
||||
)
|
||||
}
|
||||
h := g.GetChatHistory()
|
||||
if len(h) != maxChatHistory {
|
||||
t.Errorf("Expected %v, got %v", maxChatHistory, len(g.history))
|
||||
}
|
||||
for i, s := range h {
|
||||
j := i + maxChatHistory
|
||||
if s.Id != fmt.Sprintf("id-%v", j) {
|
||||
t.Errorf("Expected %v, got %v", j, s.Id)
|
||||
}
|
||||
if s.Source != fmt.Sprintf("source-%v", j%4) {
|
||||
t.Errorf("Expected %v, got %v", j%4, s.Id)
|
||||
}
|
||||
if s.Value.(string) != fmt.Sprintf("%v", j) {
|
||||
t.Errorf("Expected %v, got %v", j, s.Value)
|
||||
}
|
||||
}
|
||||
|
||||
l := len(h)
|
||||
j := maxChatHistory + 4
|
||||
g.ClearChatHistory(
|
||||
fmt.Sprintf("id-%v", j), fmt.Sprintf("source-%v", j%4),
|
||||
)
|
||||
if lh := len(g.GetChatHistory()); lh != l-1 {
|
||||
t.Errorf("Expected %v, got %v", l-1, lh)
|
||||
}
|
||||
g.ClearChatHistory("", fmt.Sprintf("source-%v", j%4))
|
||||
if lh := len(g.GetChatHistory()); lh != l*3/4 {
|
||||
t.Errorf("Expected %v, got %v", l*3/4, lh)
|
||||
}
|
||||
g.ClearChatHistory("", "")
|
||||
if lh := len(g.GetChatHistory()); lh != 0 {
|
||||
t.Errorf("Expected 0, got %v", lh)
|
||||
}
|
||||
}
|
||||
|
||||
func permissionsEqual(a, b []string) bool {
|
||||
// nil case
|
||||
if len(a) == 0 && len(b) == 0 {
|
||||
return true
|
||||
}
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
aa := append([]string(nil), a...)
|
||||
sort.Slice(aa, func(i, j int) bool {
|
||||
return aa[i] < aa[j]
|
||||
})
|
||||
bb := append([]string(nil), b...)
|
||||
sort.Slice(bb, func(i, j int) bool {
|
||||
return bb[i] < bb[j]
|
||||
})
|
||||
for i := range aa {
|
||||
if aa[i] != bb[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var jch = "jch"
|
||||
var john = "john"
|
||||
var james = "james"
|
||||
var paul = "paul"
|
||||
var peter = "peter"
|
||||
var admin = "admin"
|
||||
var stt = "speech-to-text"
|
||||
|
||||
var badClients = []ClientCredentials{
|
||||
{Username: &jch, Password: "foo"},
|
||||
{Username: &john, Password: "foo"},
|
||||
{Username: &james, Password: "foo"},
|
||||
}
|
||||
|
||||
type credPerm struct {
|
||||
c ClientCredentials
|
||||
p []string
|
||||
}
|
||||
|
||||
var desc2JSON = `
|
||||
{
|
||||
"max-history-age": 10,
|
||||
"auto-subgroups": true,
|
||||
"users": {
|
||||
"jch": {"password": "topsecret", "permissions": "op"},
|
||||
"john": {"password": "secret", "permissions": "present"},
|
||||
"james": {"password": "secret2", "permissions": "message"},
|
||||
"peter": {"password": "secret4"},
|
||||
"admin": {"password": "admin", "permissions": "admin"},
|
||||
"speech-to-text": {"password": {"type": "wildcard"},
|
||||
"permissions": "caption"}
|
||||
},
|
||||
"wildcard-user":
|
||||
{"permissions": "message", "password": {"type":"wildcard"}}
|
||||
}`
|
||||
|
||||
var goodClients = []credPerm{
|
||||
{
|
||||
ClientCredentials{Username: &jch, Password: "topsecret"},
|
||||
[]string{"op", "present", "message", "caption", "token"},
|
||||
},
|
||||
{
|
||||
ClientCredentials{Username: &john, Password: "secret"},
|
||||
[]string{"present", "message"},
|
||||
},
|
||||
{
|
||||
ClientCredentials{Username: &james, Password: "secret2"},
|
||||
[]string{"message"},
|
||||
},
|
||||
{
|
||||
ClientCredentials{Username: &paul, Password: "secret3"},
|
||||
[]string{"message"},
|
||||
},
|
||||
{
|
||||
ClientCredentials{Username: &peter, Password: "secret4"},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
ClientCredentials{Username: &admin, Password: "admin"},
|
||||
[]string{"admin"},
|
||||
},
|
||||
{
|
||||
ClientCredentials{Username: &stt},
|
||||
[]string{"caption"},
|
||||
},
|
||||
}
|
||||
|
||||
func TestPermissions(t *testing.T) {
|
||||
var g Hall
|
||||
err := json.Unmarshal([]byte(desc2JSON), &g.description)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
for _, c := range badClients {
|
||||
t.Run("bad "+*c.Username, func(t *testing.T) {
|
||||
var autherr *NotAuthorisedError
|
||||
_, p, err := g.GetPermission(c)
|
||||
if !errors.As(err, &autherr) {
|
||||
t.Errorf("GetPermission %v: %v %v", c, err, p)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, cp := range goodClients {
|
||||
t.Run("good "+*cp.c.Username, func(t *testing.T) {
|
||||
u, p, err := g.GetPermission(cp.c)
|
||||
if err != nil {
|
||||
t.Errorf("GetPermission %v: %v", cp.c, err)
|
||||
} else if u != *cp.c.Username ||
|
||||
!permissionsEqual(p, cp.p) {
|
||||
t.Errorf("%v: got %v %v, expected %v",
|
||||
cp.c, u, p, cp.p)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestExtraPermissions(t *testing.T) {
|
||||
j := `
|
||||
{
|
||||
"users": {
|
||||
"jch": {"password": "topsecret", "permissions": "op"},
|
||||
"john": {"password": "secret", "permissions": "present"},
|
||||
"james": {"password": "secret2", "permissions": "observe"}
|
||||
}
|
||||
}`
|
||||
|
||||
var d Description
|
||||
err := json.Unmarshal([]byte(j), &d)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
doit := func(u string, p []string) {
|
||||
t.Helper()
|
||||
pu := d.Users[u].Permissions.Permissions(&d)
|
||||
if !permissionsEqual(pu, p) {
|
||||
t.Errorf("%v: expected %v, got %v", u, p, pu)
|
||||
}
|
||||
}
|
||||
|
||||
doit("jch", []string{"op", "token", "present", "message", "caption"})
|
||||
doit("john", []string{"present", "message"})
|
||||
doit("james", []string{})
|
||||
|
||||
d.AllowRecording = true
|
||||
d.UnrestrictedTokens = false
|
||||
|
||||
doit("jch", []string{
|
||||
"op", "record", "token", "present", "message", "caption",
|
||||
})
|
||||
doit("john", []string{"present", "message"})
|
||||
doit("james", []string{})
|
||||
|
||||
d.AllowRecording = false
|
||||
d.UnrestrictedTokens = true
|
||||
|
||||
doit("jch", []string{"op", "token", "present", "message", "caption"})
|
||||
doit("john", []string{"token", "present", "message"})
|
||||
doit("james", []string{})
|
||||
|
||||
d.AllowRecording = true
|
||||
d.UnrestrictedTokens = true
|
||||
|
||||
doit("jch", []string{
|
||||
"op", "record", "token", "present", "message", "caption",
|
||||
})
|
||||
doit("john", []string{"token", "present", "message"})
|
||||
doit("james", []string{})
|
||||
}
|
||||
|
||||
func TestUsernameTaken(t *testing.T) {
|
||||
var g Hall
|
||||
err := json.Unmarshal([]byte(desc2JSON), &g.description)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
if g.UserExists("") {
|
||||
t.Error("UserExists(\"\") is true, expected false")
|
||||
}
|
||||
if !g.UserExists("john") {
|
||||
t.Error("UserExists(john) is false")
|
||||
}
|
||||
if !g.UserExists("john") {
|
||||
t.Error("UserExists(james) is false")
|
||||
}
|
||||
if g.UserExists("paul") {
|
||||
t.Error("UserExists(paul) is true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFmtpValue(t *testing.T) {
|
||||
type fmtpTest struct {
|
||||
fmtp string
|
||||
key string
|
||||
value string
|
||||
}
|
||||
fmtpTests := []fmtpTest{
|
||||
{"", "foo", ""},
|
||||
{"profile-id=0", "profile-id", "0"},
|
||||
{"profile-id=0", "foo", ""},
|
||||
{"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", "profile-level-id", "42001f"},
|
||||
{"foo=1;bar=2;quux=3", "foo", "1"},
|
||||
{"foo=1;bar=2;quux=3", "bar", "2"},
|
||||
{"foo=1;bar=2;quux=3", "fu", ""},
|
||||
}
|
||||
|
||||
for _, test := range fmtpTests {
|
||||
v := fmtpValue(test.fmtp, test.key)
|
||||
if v != test.value {
|
||||
t.Errorf("fmtpValue(%v, %v) = %v, expected %v",
|
||||
test.fmtp, test.key, v, test.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidHallName(t *testing.T) {
|
||||
type nameTest struct {
|
||||
name string
|
||||
result bool
|
||||
}
|
||||
tests := []nameTest{
|
||||
{"", false},
|
||||
{"/", false},
|
||||
{"/foo", false},
|
||||
{"foo/", false},
|
||||
{"./foo", false},
|
||||
{"foo/.", false},
|
||||
{"../foo", false},
|
||||
{"foo/..", false},
|
||||
{"foo/./bar", false},
|
||||
{"foo/../bar", false},
|
||||
{"foo\\bar", false},
|
||||
{"foo", true},
|
||||
{"foo/bar", true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
r := validHallName(test.name)
|
||||
if r != test.result {
|
||||
t.Errorf("Valid %v: got %v, expected %v",
|
||||
test.name, r, test.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayloadTypeDistinct(t *testing.T) {
|
||||
names := []string{
|
||||
"vp8", "vp9", "av1", "h264",
|
||||
"opus", "g722", "pcmu", "pcma",
|
||||
}
|
||||
|
||||
m := make(map[webrtc.PayloadType]string)
|
||||
|
||||
for _, n := range names {
|
||||
codec, err := codecsFromName(n)
|
||||
if err != nil {
|
||||
t.Errorf("%v: %v", n, err)
|
||||
continue
|
||||
}
|
||||
pt, err := CodecPayloadType(codec[0].RTPCodecCapability)
|
||||
if err != nil {
|
||||
t.Errorf("%v: %v", codec, err)
|
||||
continue
|
||||
}
|
||||
if other, ok := m[pt]; ok {
|
||||
t.Errorf(
|
||||
"Duplicate ptype %v: %v and %v",
|
||||
pt, n, other,
|
||||
)
|
||||
continue
|
||||
}
|
||||
m[pt] = n
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user