Merge remote-tracking branch 'upstream/master' into a11y
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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":
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user