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.
|
* Implemented support for RSA tokens. Thanks to Jean Dupouy.
|
||||||
* Added support for include-inferiors in cryptographic tokens. Thanks
|
* Added support for include-inferiors in cryptographic tokens. Thanks
|
||||||
to izeau.
|
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
|
9 August 2025: Galene 1.0
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,19 @@ in this directory.
|
|||||||
* [galene.md][2]: usage and administration;
|
* [galene.md][2]: usage and administration;
|
||||||
* [galene-client.md][3]: writing clients;
|
* [galene-client.md][3]: writing clients;
|
||||||
* [galene-protocol.md][4]: the client protocol;
|
* [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
|
## Further information
|
||||||
|
|
||||||
@@ -43,3 +55,5 @@ Answers to common questions and issues are at <https://galene.org/faq.html>.
|
|||||||
[2]: <galene.md>
|
[2]: <galene.md>
|
||||||
[3]: <galene-client.md>
|
[3]: <galene-client.md>
|
||||||
[4]: <galene-protocol.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.
|
// out of memory when doing too many BCrypt hashes at the same time.
|
||||||
var hashSemaphore = make(chan struct{}, runtime.GOMAXPROCS(-1))
|
var hashSemaphore = make(chan struct{}, runtime.GOMAXPROCS(-1))
|
||||||
|
|
||||||
// constantTimeCompare compares a and b in time proportional to the length of a.
|
// ConstantTimeCompare compares a and b in time proportional to the length of a.
|
||||||
func constantTimeCompare(a, b string) bool {
|
func ConstantTimeCompare(a, b string) bool {
|
||||||
as := []byte(a)
|
as := []byte(a)
|
||||||
bs := make([]byte, len(as))
|
bs := make([]byte, len(as))
|
||||||
copy(bs, b)
|
copy(bs, b)
|
||||||
@@ -48,7 +48,7 @@ func (p Password) Match(pw string) (bool, error) {
|
|||||||
if p.Key == nil {
|
if p.Key == nil {
|
||||||
return false, errors.New("missing key")
|
return false, errors.New("missing key")
|
||||||
}
|
}
|
||||||
return constantTimeCompare(pw, *p.Key), nil
|
return ConstantTimeCompare(pw, *p.Key), nil
|
||||||
case "wildcard":
|
case "wildcard":
|
||||||
return true, nil
|
return true, nil
|
||||||
case "pbkdf2":
|
case "pbkdf2":
|
||||||
|
|||||||
@@ -761,9 +761,6 @@ func SetUserPassword(group, username string, wildcard bool, pw Password) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if desc.Users == nil {
|
|
||||||
return os.ErrNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
if wildcard {
|
if wildcard {
|
||||||
if desc.WildcardUser == nil {
|
if desc.WildcardUser == nil {
|
||||||
@@ -771,6 +768,9 @@ func SetUserPassword(group, username string, wildcard bool, pw Password) error {
|
|||||||
}
|
}
|
||||||
desc.WildcardUser.Password = pw
|
desc.WildcardUser.Password = pw
|
||||||
} else {
|
} else {
|
||||||
|
if desc.Users == nil {
|
||||||
|
return os.ErrNotExist
|
||||||
|
}
|
||||||
user, ok := desc.Users[username]
|
user, ok := desc.Users[username]
|
||||||
if !ok {
|
if !ok {
|
||||||
return os.ErrNotExist
|
return os.ErrNotExist
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ func TestConstantTimeCompare(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
e := constantTimeCompare(test.a, test.b)
|
e := ConstantTimeCompare(test.a, test.b)
|
||||||
if e != (test.a == test.b) {
|
if e != (test.a == test.b) {
|
||||||
t.Errorf("constantTimeCompare(%v, %v): got %v",
|
t.Errorf("constantTimeCompare(%v, %v): got %v",
|
||||||
test.a, test.b, e,
|
test.a, test.b, e,
|
||||||
|
|||||||
+40
-38
@@ -832,11 +832,14 @@ func readMessage(conn *websocket.Conn, m *clientMessage) error {
|
|||||||
return conn.ReadJSON(&m)
|
return conn.ReadJSON(&m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxWSMessageSize = 1024 * 1024
|
||||||
const protocolVersion = "2"
|
const protocolVersion = "2"
|
||||||
|
|
||||||
func StartClient(conn *websocket.Conn, addr net.Addr) (err error) {
|
func StartClient(conn *websocket.Conn, addr net.Addr) (err error) {
|
||||||
var m clientMessage
|
var m clientMessage
|
||||||
|
|
||||||
|
conn.SetReadLimit(maxWSMessageSize)
|
||||||
|
|
||||||
err = readMessage(conn, &m)
|
err = readMessage(conn, &m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
@@ -919,6 +922,10 @@ type pushClientAction struct {
|
|||||||
data map[string]interface{}
|
data map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type changePermissionsAction struct {
|
||||||
|
kind string
|
||||||
|
}
|
||||||
|
|
||||||
type permissionsChangedAction struct{}
|
type permissionsChangedAction struct{}
|
||||||
|
|
||||||
type joinedAction 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:
|
case permissionsChangedAction:
|
||||||
g := c.Group()
|
g := c.Group()
|
||||||
if g == nil {
|
if g == nil {
|
||||||
@@ -1335,41 +1365,6 @@ func closeDownConn(c *webClient, id string, message string) error {
|
|||||||
return nil
|
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 {
|
func (c *webClient) Kick(id string, user *string, message string) error {
|
||||||
c.action(kickAction{id, user, message})
|
c.action(kickAction{id, user, message})
|
||||||
return nil
|
return nil
|
||||||
@@ -1909,10 +1904,17 @@ func handleClientMessage(c *webClient, m clientMessage) error {
|
|||||||
if !member("op", c.permissions) {
|
if !member("op", c.permissions) {
|
||||||
return c.error(group.UserError("not authorised"))
|
return c.error(group.UserError("not authorised"))
|
||||||
}
|
}
|
||||||
err := setPermissions(g, m.Dest, m.Kind)
|
t := g.GetClient(m.Dest)
|
||||||
if err != nil {
|
if t == nil {
|
||||||
return c.error(err)
|
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":
|
case "identify":
|
||||||
if !member("op", c.permissions) {
|
if !member("op", c.permissions) {
|
||||||
return c.error(group.UserError("not authorised"))
|
return c.error(group.UserError("not authorised"))
|
||||||
|
|||||||
+3
-1
@@ -1301,6 +1301,8 @@ header .collapse:hover {
|
|||||||
#left-sidebar.active {
|
#left-sidebar.active {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
|
/* on top of video peers */
|
||||||
|
z-index: 1002;
|
||||||
}
|
}
|
||||||
|
|
||||||
#left-sidebar {
|
#left-sidebar {
|
||||||
@@ -1419,7 +1421,7 @@ header .collapse:hover {
|
|||||||
|
|
||||||
#invite-dialog input {
|
#invite-dialog input {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
width: 12em;
|
width: 16em;
|
||||||
padding: .2em
|
padding: .2em
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -196,7 +196,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<input id="preprocessingbox" type="checkbox"/ checked>
|
<input id="preprocessingbox" type="checkbox" checked/>
|
||||||
<label for="preprocessingbox">Noise suppression</label>
|
<label for="preprocessingbox">Noise suppression</label>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -305,7 +305,7 @@
|
|||||||
<input id="invite-expires" type="datetime-local"/>
|
<input id="invite-expires" type="datetime-local"/>
|
||||||
<br>
|
<br>
|
||||||
<button id="invite-cancel" value="cancel" type="button">Cancel</button>
|
<button id="invite-cancel" value="cancel" type="button">Cancel</button>
|
||||||
<button value="invite" value="invite">Invite</button>
|
<button value="invite">Invite</button>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
|||||||
+24
-7
@@ -397,7 +397,7 @@ function setChangePassword(username) {
|
|||||||
if(!(a instanceof HTMLAnchorElement))
|
if(!(a instanceof HTMLAnchorElement))
|
||||||
throw new Error('Bad type for chpwspan');
|
throw new Error('Bad type for chpwspan');
|
||||||
if(username) {
|
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';
|
a.target = '_blank';
|
||||||
s.classList.remove('invisible');
|
s.classList.remove('invisible');
|
||||||
} else {
|
} else {
|
||||||
@@ -533,7 +533,9 @@ function setViewportHeight() {
|
|||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
'--vh', `${window.innerHeight/100}px`,
|
'--vh', `${window.innerHeight/100}px`,
|
||||||
);
|
);
|
||||||
|
if (!getVisibility('left')) {
|
||||||
showVideo();
|
showVideo();
|
||||||
|
}
|
||||||
// Ajust video component size
|
// Ajust video component size
|
||||||
resizePeers();
|
resizePeers();
|
||||||
}
|
}
|
||||||
@@ -577,6 +579,16 @@ function setVisibility(id, visible) {
|
|||||||
elt.classList.add('invisible');
|
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.
|
* 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) {
|
if(frameRate && frameRate != this.frameRate) {
|
||||||
clearInterval(this.timer);
|
clearInterval(this.timer);
|
||||||
|
this.frameRate = frameRate;
|
||||||
this.timer = setInterval(() => this.draw(), 1000 / this.frameRate);
|
this.timer = setInterval(() => this.draw(), 1000 / this.frameRate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2521,7 +2534,7 @@ document.getElementById('invite-dialog').onclose = function(e) {
|
|||||||
try {
|
try {
|
||||||
expires = dateFromInput(ex.value);
|
expires = dateFromInput(ex.value);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
displayError(`Couldn't parse ${nb.value}: ${e}`);
|
displayError(`Couldn't parse ${ex.value}: ${e}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2673,7 +2686,7 @@ function addUser(id, userinfo) {
|
|||||||
user.setAttribute('tabindex', '0');
|
user.setAttribute('tabindex', '0');
|
||||||
setUserStatus(id, user, userinfo);
|
setUserStatus(id, user, userinfo);
|
||||||
user.addEventListener('click', function(e) {
|
user.addEventListener('click', function(e) {
|
||||||
let elt = e.target;
|
let elt = e.currentTarget;
|
||||||
if(!elt || !(elt instanceof HTMLElement))
|
if(!elt || !(elt instanceof HTMLElement))
|
||||||
throw new Error("Couldn't find user div");
|
throw new Error("Couldn't find user div");
|
||||||
userMenu(elt);
|
userMenu(elt);
|
||||||
@@ -3185,7 +3198,7 @@ function gotUserMessage(id, dest, username, time, privileged, kind, error, messa
|
|||||||
case 'kicked':
|
case 'kicked':
|
||||||
case 'error':
|
case 'error':
|
||||||
case 'warning':
|
case 'warning':
|
||||||
case 'info':
|
case 'info': {
|
||||||
if(!privileged) {
|
if(!privileged) {
|
||||||
console.error(`Got unprivileged message of kind ${kind}`);
|
console.error(`Got unprivileged message of kind ${kind}`);
|
||||||
return;
|
return;
|
||||||
@@ -3193,7 +3206,8 @@ function gotUserMessage(id, dest, username, time, privileged, kind, error, messa
|
|||||||
let from = id ? (username || 'Anonymous') : 'The Server';
|
let from = id ? (username || 'Anonymous') : 'The Server';
|
||||||
displayError(`${from} said: ${message}`, kind);
|
displayError(`${from} said: ${message}`, kind);
|
||||||
break;
|
break;
|
||||||
case 'mute':
|
}
|
||||||
|
case 'mute': {
|
||||||
if(!privileged) {
|
if(!privileged) {
|
||||||
console.error(`Got unprivileged message of kind ${kind}`);
|
console.error(`Got unprivileged message of kind ${kind}`);
|
||||||
return;
|
return;
|
||||||
@@ -3202,6 +3216,7 @@ function gotUserMessage(id, dest, username, time, privileged, kind, error, messa
|
|||||||
let by = username ? ' by ' + username : '';
|
let by = username ? ' by ' + username : '';
|
||||||
displayWarning(`You have been muted${by}`);
|
displayWarning(`You have been muted${by}`);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'clearchat': {
|
case 'clearchat': {
|
||||||
if(!privileged) {
|
if(!privileged) {
|
||||||
console.error(`Got unprivileged message of kind ${kind}`);
|
console.error(`Got unprivileged message of kind ${kind}`);
|
||||||
@@ -3975,7 +3990,7 @@ function parseCommand(line) {
|
|||||||
while(i < line.length && line[i] === ' ')
|
while(i < line.length && line[i] === ' ')
|
||||||
i++;
|
i++;
|
||||||
let start = ' ';
|
let start = ' ';
|
||||||
if(i < line.length && line[i] === '"' || line[i] === "'") {
|
if(i < line.length && (line[i] === '"' || line[i] === "'")) {
|
||||||
start = line[i];
|
start = line[i];
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
@@ -4334,7 +4349,9 @@ function handleInput() {
|
|||||||
} else {
|
} else {
|
||||||
let c = commands[cmd];
|
let c = commands[cmd];
|
||||||
if(!c) {
|
if(!c) {
|
||||||
displayError(`Uknown command /${cmd}, type /help for help`);
|
displayError(
|
||||||
|
`Unknown command /${cmd}, type /help for help`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(c.predicate) {
|
if(c.predicate) {
|
||||||
|
|||||||
+12
-4
@@ -84,6 +84,8 @@ func sendJSON(w http.ResponseWriter, r *http.Request, v any) {
|
|||||||
e.Encode(v)
|
e.Encode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxAPIMessageSize = 1024 * 1024
|
||||||
|
|
||||||
func getText(w http.ResponseWriter, r *http.Request) ([]byte, bool) {
|
func getText(w http.ResponseWriter, r *http.Request) ([]byte, bool) {
|
||||||
ctype, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
ctype, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
if err != nil || !strings.EqualFold(ctype, "text/plain") {
|
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
|
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 {
|
if err != nil {
|
||||||
httpError(w, err)
|
httpError(w, err)
|
||||||
return nil, true
|
return nil, true
|
||||||
@@ -111,7 +115,9 @@ func getJSON(w http.ResponseWriter, r *http.Request, v any) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
d := json.NewDecoder(r.Body)
|
d := json.NewDecoder(
|
||||||
|
http.MaxBytesReader(w, r.Body, maxAPIMessageSize),
|
||||||
|
)
|
||||||
err = d.Decode(v)
|
err = d.Decode(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpError(w, err)
|
httpError(w, err)
|
||||||
@@ -495,7 +501,7 @@ type jwkset = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func keysHandler(w http.ResponseWriter, r *http.Request, g string) {
|
func keysHandler(w http.ResponseWriter, r *http.Request, g string) {
|
||||||
if apiCORS(w, r, "HEAD, GET") {
|
if apiCORS(w, r, "PUT, DELETE") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !checkAdmin(w, r) {
|
if !checkAdmin(w, r) {
|
||||||
@@ -513,7 +519,9 @@ func keysHandler(w http.ResponseWriter, r *http.Request, g string) {
|
|||||||
http.StatusUnsupportedMediaType)
|
http.StatusUnsupportedMediaType)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d := json.NewDecoder(r.Body)
|
d := json.NewDecoder(
|
||||||
|
http.MaxBytesReader(w, r.Body, maxAPIMessageSize),
|
||||||
|
)
|
||||||
var keys jwkset
|
var keys jwkset
|
||||||
err = d.Decode(&keys)
|
err = d.Decode(&keys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+1
-1
@@ -295,7 +295,7 @@ func whipResourceHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if t := c.Token(); t != "" {
|
if t := c.Token(); t != "" {
|
||||||
token := parseBearerToken(r.Header.Get("Authorization"))
|
token := parseBearerToken(r.Header.Get("Authorization"))
|
||||||
if token != t {
|
if !group.ConstantTimeCompare(t, token) {
|
||||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user