Merge remote-tracking branch 'upstream/master' into a11y

This commit is contained in:
Storm Dragon
2026-05-09 12:48:22 -04:00
11 changed files with 111 additions and 62 deletions
+6
View File
@@ -12,6 +12,12 @@ Galene 1.1 (unreleased):
* Implemented support for RSA tokens. Thanks to Jean Dupouy.
* Added support for include-inferiors in cryptographic tokens. Thanks
to izeau.
* Fixed an issue with the chat window and the users' list disappearing
on mobile. Thanks to Kirill smelkov.
* Fixed an issue that made it impossible to add a wildcard user to a
group with no users. Thanks to Andrea Zucchelli.
* Added some more protection against denial of service attacks. Thanks
to Vinayak Mishra.
9 August 2025: Galene 1.0
+15 -1
View File
@@ -28,7 +28,19 @@ in this directory.
* [galene.md][2]: usage and administration;
* [galene-client.md][3]: writing clients;
* [galene-protocol.md][4]: the client protocol;
* [galene-api.md][4]: Galene's administrative API.
* [galene-api.md][5]: Galene's administrative API.
## Contributing
In order to contribute to Galene, you may:
* send patches to [the Galene mailing list][6] by sending mail to
<galene@lists.galene.org>;
* submit pull requests on GitHub.
For general discussion, please use the [Galene mailing list][6] (feel free
to send mail without subscribing). Please do not use Github for general
discussion.
## Further information
@@ -43,3 +55,5 @@ Answers to common questions and issues are at <https://galene.org/faq.html>.
[2]: <galene.md>
[3]: <galene-client.md>
[4]: <galene-protocol.md>
[5]: <galene-api.md>
[6]: <https://lists.galene.org/>
+3 -3
View File
@@ -31,8 +31,8 @@ type Password RawPassword
// 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 {
// 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)
@@ -48,7 +48,7 @@ func (p Password) Match(pw string) (bool, error) {
if p.Key == nil {
return false, errors.New("missing key")
}
return constantTimeCompare(pw, *p.Key), nil
return ConstantTimeCompare(pw, *p.Key), nil
case "wildcard":
return true, nil
case "pbkdf2":
+3 -3
View File
@@ -761,9 +761,6 @@ func SetUserPassword(group, username string, wildcard bool, pw Password) error {
if err != nil {
return err
}
if desc.Users == nil {
return os.ErrNotExist
}
if wildcard {
if desc.WildcardUser == nil {
@@ -771,6 +768,9 @@ func SetUserPassword(group, username string, wildcard bool, pw Password) error {
}
desc.WildcardUser.Password = pw
} else {
if desc.Users == nil {
return os.ErrNotExist
}
user, ok := desc.Users[username]
if !ok {
return os.ErrNotExist
+1 -1
View File
@@ -25,7 +25,7 @@ func TestConstantTimeCompare(t *testing.T) {
}
for _, test := range tests {
e := constantTimeCompare(test.a, test.b)
e := ConstantTimeCompare(test.a, test.b)
if e != (test.a == test.b) {
t.Errorf("constantTimeCompare(%v, %v): got %v",
test.a, test.b, e,
+40 -38
View File
@@ -832,11 +832,14 @@ func readMessage(conn *websocket.Conn, m *clientMessage) error {
return conn.ReadJSON(&m)
}
const maxWSMessageSize = 1024 * 1024
const protocolVersion = "2"
func StartClient(conn *websocket.Conn, addr net.Addr) (err error) {
var m clientMessage
conn.SetReadLimit(maxWSMessageSize)
err = readMessage(conn, &m)
if err != nil {
conn.Close()
@@ -919,6 +922,10 @@ type pushClientAction struct {
data map[string]interface{}
}
type changePermissionsAction struct {
kind string
}
type permissionsChangedAction struct{}
type joinedAction struct {
@@ -1219,6 +1226,29 @@ func handleAction(c *webClient, a any) error {
}
}
}
case changePermissionsAction:
switch a.kind {
case "op":
c.permissions = addnew("op", c.permissions)
g := c.Group()
if g != nil && g.Description().AllowRecording {
c.permissions = addnew("record", c.permissions)
}
case "unop":
c.permissions = remove("op", c.permissions)
c.permissions = remove("record", c.permissions)
case "present":
c.permissions = addnew("present", c.permissions)
case "unpresent":
c.permissions = remove("present", c.permissions)
case "shutup":
c.permissions = remove("message", c.permissions)
case "unshutup":
c.permissions = addnew("message", c.permissions)
default:
return group.UserError("unknown permission")
}
c.action(permissionsChangedAction{})
case permissionsChangedAction:
g := c.Group()
if g == nil {
@@ -1335,41 +1365,6 @@ func closeDownConn(c *webClient, id string, message string) error {
return nil
}
func setPermissions(g *group.Group, id string, perm string) error {
client := g.GetClient(id)
if client == nil {
return group.UserError("no such user")
}
c, ok := client.(*webClient)
if !ok {
return group.UserError("this is not a real user")
}
switch perm {
case "op":
c.permissions = addnew("op", c.permissions)
if g.Description().AllowRecording {
c.permissions = addnew("record", c.permissions)
}
case "unop":
c.permissions = remove("op", c.permissions)
c.permissions = remove("record", c.permissions)
case "present":
c.permissions = addnew("present", c.permissions)
case "unpresent":
c.permissions = remove("present", c.permissions)
case "shutup":
c.permissions = remove("message", c.permissions)
case "unshutup":
c.permissions = addnew("message", c.permissions)
default:
return group.UserError("unknown permission")
}
c.action(permissionsChangedAction{})
return nil
}
func (c *webClient) Kick(id string, user *string, message string) error {
c.action(kickAction{id, user, message})
return nil
@@ -1909,10 +1904,17 @@ func handleClientMessage(c *webClient, m clientMessage) error {
if !member("op", c.permissions) {
return c.error(group.UserError("not authorised"))
}
err := setPermissions(g, m.Dest, m.Kind)
if err != nil {
return c.error(err)
t := g.GetClient(m.Dest)
if t == nil {
return c.error(group.UserError("no suck user"))
}
target, ok := t.(*webClient)
if !ok {
return c.error(group.UserError(
"this is not a real user",
))
}
target.action(changePermissionsAction{m.Kind})
case "identify":
if !member("op", c.permissions) {
return c.error(group.UserError("not authorised"))
+3 -1
View File
@@ -1301,6 +1301,8 @@ header .collapse:hover {
#left-sidebar.active {
min-width: 200px;
max-width: 200px;
/* on top of video peers */
z-index: 1002;
}
#left-sidebar {
@@ -1419,7 +1421,7 @@ header .collapse:hover {
#invite-dialog input {
margin-top: 1em;
width: 12em;
width: 16em;
padding: .2em
}
+2 -2
View File
@@ -196,7 +196,7 @@
</form>
<form>
<input id="preprocessingbox" type="checkbox"/ checked>
<input id="preprocessingbox" type="checkbox" checked/>
<label for="preprocessingbox">Noise suppression</label>
</form>
@@ -305,7 +305,7 @@
<input id="invite-expires" type="datetime-local"/>
<br>
<button id="invite-cancel" value="cancel" type="button">Cancel</button>
<button value="invite" value="invite">Invite</button>
<button value="invite">Invite</button>
</form>
</dialog>
+25 -8
View File
@@ -397,7 +397,7 @@ function setChangePassword(username) {
if(!(a instanceof HTMLAnchorElement))
throw new Error('Bad type for chpwspan');
if(username) {
a.href = `/change-password.html?group=${encodeURI(group)}&username=${encodeURI(username)}`;
a.href = `/change-password.html?group=${encodeURIComponent(group)}&username=${encodeURIComponent(username)}`;
a.target = '_blank';
s.classList.remove('invisible');
} else {
@@ -533,7 +533,9 @@ function setViewportHeight() {
document.documentElement.style.setProperty(
'--vh', `${window.innerHeight/100}px`,
);
showVideo();
if (!getVisibility('left')) {
showVideo();
}
// Ajust video component size
resizePeers();
}
@@ -577,6 +579,16 @@ function setVisibility(id, visible) {
elt.classList.add('invisible');
}
/**
* getVisibility tells whether specified element is visible.
*
* @param {string} id
*/
function getVisibility(id) {
let elt = document.getElementById(id);
return !elt.classList.contains('invisible');
}
/**
* Shows and hides various UI elements depending on the protocol state.
*/
@@ -1204,6 +1216,7 @@ Filter.prototype.draw = async function() {
});
if(frameRate && frameRate != this.frameRate) {
clearInterval(this.timer);
this.frameRate = frameRate;
this.timer = setInterval(() => this.draw(), 1000 / this.frameRate);
}
}
@@ -2521,7 +2534,7 @@ document.getElementById('invite-dialog').onclose = function(e) {
try {
expires = dateFromInput(ex.value);
} catch(e) {
displayError(`Couldn't parse ${nb.value}: ${e}`);
displayError(`Couldn't parse ${ex.value}: ${e}`);
return;
}
}
@@ -2673,7 +2686,7 @@ function addUser(id, userinfo) {
user.setAttribute('tabindex', '0');
setUserStatus(id, user, userinfo);
user.addEventListener('click', function(e) {
let elt = e.target;
let elt = e.currentTarget;
if(!elt || !(elt instanceof HTMLElement))
throw new Error("Couldn't find user div");
userMenu(elt);
@@ -3185,7 +3198,7 @@ function gotUserMessage(id, dest, username, time, privileged, kind, error, messa
case 'kicked':
case 'error':
case 'warning':
case 'info':
case 'info': {
if(!privileged) {
console.error(`Got unprivileged message of kind ${kind}`);
return;
@@ -3193,7 +3206,8 @@ function gotUserMessage(id, dest, username, time, privileged, kind, error, messa
let from = id ? (username || 'Anonymous') : 'The Server';
displayError(`${from} said: ${message}`, kind);
break;
case 'mute':
}
case 'mute': {
if(!privileged) {
console.error(`Got unprivileged message of kind ${kind}`);
return;
@@ -3202,6 +3216,7 @@ function gotUserMessage(id, dest, username, time, privileged, kind, error, messa
let by = username ? ' by ' + username : '';
displayWarning(`You have been muted${by}`);
break;
}
case 'clearchat': {
if(!privileged) {
console.error(`Got unprivileged message of kind ${kind}`);
@@ -3975,7 +3990,7 @@ function parseCommand(line) {
while(i < line.length && line[i] === ' ')
i++;
let start = ' ';
if(i < line.length && line[i] === '"' || line[i] === "'") {
if(i < line.length && (line[i] === '"' || line[i] === "'")) {
start = line[i];
i++;
}
@@ -4334,7 +4349,9 @@ function handleInput() {
} else {
let c = commands[cmd];
if(!c) {
displayError(`Uknown command /${cmd}, type /help for help`);
displayError(
`Unknown command /${cmd}, type /help for help`
);
return;
}
if(c.predicate) {
+12 -4
View File
@@ -84,6 +84,8 @@ func sendJSON(w http.ResponseWriter, r *http.Request, v any) {
e.Encode(v)
}
const maxAPIMessageSize = 1024 * 1024
func getText(w http.ResponseWriter, r *http.Request) ([]byte, bool) {
ctype, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil || !strings.EqualFold(ctype, "text/plain") {
@@ -93,7 +95,9 @@ func getText(w http.ResponseWriter, r *http.Request) ([]byte, bool) {
return nil, true
}
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 4096))
body, err := io.ReadAll(
http.MaxBytesReader(w, r.Body, maxAPIMessageSize),
)
if err != nil {
httpError(w, err)
return nil, true
@@ -111,7 +115,9 @@ func getJSON(w http.ResponseWriter, r *http.Request, v any) bool {
return true
}
d := json.NewDecoder(r.Body)
d := json.NewDecoder(
http.MaxBytesReader(w, r.Body, maxAPIMessageSize),
)
err = d.Decode(v)
if err != nil {
httpError(w, err)
@@ -495,7 +501,7 @@ type jwkset = struct {
}
func keysHandler(w http.ResponseWriter, r *http.Request, g string) {
if apiCORS(w, r, "HEAD, GET") {
if apiCORS(w, r, "PUT, DELETE") {
return
}
if !checkAdmin(w, r) {
@@ -513,7 +519,9 @@ func keysHandler(w http.ResponseWriter, r *http.Request, g string) {
http.StatusUnsupportedMediaType)
return
}
d := json.NewDecoder(r.Body)
d := json.NewDecoder(
http.MaxBytesReader(w, r.Body, maxAPIMessageSize),
)
var keys jwkset
err = d.Decode(&keys)
if err != nil {
+1 -1
View File
@@ -295,7 +295,7 @@ func whipResourceHandler(w http.ResponseWriter, r *http.Request) {
if t := c.Token(); t != "" {
token := parseBearerToken(r.Header.Get("Authorization"))
if token != t {
if !group.ConstantTimeCompare(t, token) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}