mirror of
https://github.com/jech/galene.git
synced 2024-12-22 07:15:47 +01:00
Implement token authentication.
This commit is contained in:
parent
b4d1ef398f
commit
03811db37d
12 changed files with 433 additions and 28 deletions
88
README
88
README
|
@ -91,9 +91,10 @@ optional, but unless you specify at least one user definition (`op`,
|
|||
following fields are allowed:
|
||||
|
||||
- `op`, `presenter`, `other`: each of these is an array of user
|
||||
definitions (see below) and specifies the users allowed to connect
|
||||
respectively with operator privileges, with presenter privileges, and
|
||||
as passive listeners;
|
||||
definitions (see *Authorisation* below) and specifies the users allowed
|
||||
to connect respectively with operator privileges, with presenter
|
||||
privileges, and as passive listeners;
|
||||
- `authServer` and `authKeys`: see *Authorisation* below;
|
||||
- `public`: if true, then the group is visible on the landing page;
|
||||
- `displayName`: a human-friendly version of the group name;
|
||||
- `description`: a human-readable description of the group; this is
|
||||
|
@ -131,31 +132,53 @@ Supported video codecs include:
|
|||
Supported audio codecs include `"opus"`, `"g722"`, `"pcmu"` and `"pcma"`.
|
||||
Only Opus can be recorded to disk. There is no good reason to use
|
||||
anything except Opus.
|
||||
|
||||
A user definition is a dictionary with the following fields:
|
||||
|
||||
- `username`: the username of the user; if omitted, any username is
|
||||
allowed;
|
||||
- `password`: if omitted, then no password is required. Otherwise, this
|
||||
can either be a string, specifying a plain text password, or
|
||||
a dictionary generated by the `galene-password-generator` utility.
|
||||
|
||||
For example,
|
||||
|
||||
## Client Authorisation
|
||||
|
||||
Galene implements two authorisation methods: a simple username/password
|
||||
authorisation scheme that is built into the Galene server, and
|
||||
a token-based mechanism that relies on an external server. The simple
|
||||
mechanism is intended to be used in standalone installations, while the
|
||||
server-based mechanism is designed to allow easy integration with an
|
||||
existing authorisation infrastructure (such as LDAP, OAuth2, or even Unix
|
||||
passwords).
|
||||
|
||||
### Password authorisation
|
||||
|
||||
When password authorisation is used, authorised usernames and password are
|
||||
defined directly in the group configuration file, in the `op`, `presenter`
|
||||
and `other` arrays. Each member of the array is a dictionary, that may
|
||||
contain the fields `username` and `password`:
|
||||
|
||||
- if `username` is present, then the entry only matches clients that
|
||||
specify this exact username; otherwise, any username matches;
|
||||
- if `password` is present, then the entry only matches clients that
|
||||
specify this exact password; otherwise, any password matches.
|
||||
|
||||
For example, the entry
|
||||
|
||||
{"username": "jch", "password": "1234"}
|
||||
|
||||
specifies user *jch* with password *1234*, while
|
||||
specifies username *jch* with password *1234*, while
|
||||
|
||||
{"password": "1234"}
|
||||
|
||||
specifies that any (non-empty) username will do, and
|
||||
allows any username with password *1234*, and
|
||||
|
||||
{}
|
||||
|
||||
allows any (non-empty) username with any password.
|
||||
allows any username with any password.
|
||||
|
||||
By default, empty usernames are forbidden; set the `allow-anonymous`
|
||||
option to allow empty usernames. By default, recording is forbidden;
|
||||
specify the `allow-recording` option to allow operators to record.
|
||||
|
||||
|
||||
### Hashed passwords
|
||||
|
||||
If you don't wish to store cleartext passwords on the server, you may
|
||||
generate hashed password with the `galene-password-generator` utility. A
|
||||
generate hashed passwords with the `galene-password-generator` utility. A
|
||||
user entry with a hashed password looks like this:
|
||||
|
||||
{
|
||||
|
@ -170,6 +193,39 @@ user entry with a hashed password looks like this:
|
|||
}
|
||||
|
||||
|
||||
### Authorisation servers
|
||||
|
||||
Galene is able to delegate authorisation decisions to an external
|
||||
authorisation server. This makes it possible to integrate Galene with an
|
||||
existing authentication and authorisation infrastructure, such as LDAP,
|
||||
OAuth2 or even Unix passwords.
|
||||
|
||||
When an authorisation server is used, the group configuration file
|
||||
specifies the URL of the authorisation server and one or more public keys
|
||||
in JWK format:
|
||||
|
||||
{
|
||||
"authServer": "https://auth.example.org",
|
||||
"authKeys": [{
|
||||
"kty": "oct",
|
||||
"alg": "HS256",
|
||||
"k": "MYz3IfCq4Yq-UmPdNqWEOdPl4C_m9imHHs9uveDUJGQ",
|
||||
"kid": "20211030"
|
||||
}, {
|
||||
"kty": "EC",
|
||||
"alg": "ES256",
|
||||
"crv": "P-256",
|
||||
"x": "dElK9qBNyCpRXdvJsn4GdjrFzScSzpkz_I0JhKbYC88",
|
||||
"y": "pBhVb37haKvwEoleoW3qxnT4y5bK35_RTP7_RmFKR6Q",
|
||||
"kid": "20211101"
|
||||
}]
|
||||
}
|
||||
|
||||
The `kid` field serves to distinguish among multiple keys, and must match
|
||||
the value provided by the authorisation server. If the server doesn't
|
||||
provide a `kid`, the first key with a matching `alg` field will be used.
|
||||
|
||||
|
||||
# Further information
|
||||
|
||||
Galène's web page is at <https://galene.org>.
|
||||
|
|
|
@ -39,6 +39,11 @@ configuration file. Each object has the following fields:
|
|||
- `locked`: true if the group is locked;
|
||||
- `clientCount`: the number of clients currently in the group.
|
||||
|
||||
If token-based authorisation is in use for the group, then the dictionary
|
||||
contains the following additional field:
|
||||
|
||||
- `authServer`: the URL of the authorisation server.
|
||||
|
||||
A client may also fetch the URL `/group/name/.status.json` to retrieve the
|
||||
status of a single group. If the group has not been marked as public,
|
||||
then the fields `locked` and `clientCount` are omitted.
|
||||
|
@ -112,6 +117,9 @@ The `join` message requests that the sender join or leave a group:
|
|||
}
|
||||
```
|
||||
|
||||
If token-based authorisation is beling used, then the `password` field is
|
||||
omitted, and a `token` field is included instead.
|
||||
|
||||
When the sender has effectively joined the group, the peer will send
|
||||
a 'joined' message of kind 'join'; it may then send a 'joined' message of
|
||||
kind 'change' at any time, in order to inform the client of a change in
|
||||
|
@ -355,3 +363,37 @@ Finally, a group action requests that the server act on the current group.
|
|||
Currently defined kinds include `clearchat` (not to be confused with the
|
||||
`clearchat` user message), `lock`, `unlock`, `record`, `unrecord`,
|
||||
`subgroups` and `setdata`.
|
||||
|
||||
# Authorisation protocol
|
||||
|
||||
If a group's status dictionary has a non-empty `authServer` field, then
|
||||
the group uses token authentication. Before joining, the client sends
|
||||
a POST request to the authorisation server URL containing in its body
|
||||
a JSON dictionary of the following form:
|
||||
```javascript
|
||||
{
|
||||
"location": "https://galene.example.org/group/groupname",
|
||||
"username": username,
|
||||
"password": password
|
||||
}
|
||||
```
|
||||
If the user is not allowed to join the group, then the authorisation
|
||||
server replies with a code of 403 ("not authorised"). If the user is
|
||||
allowed to join, then the authorisation server replies with a signed JWT
|
||||
(a "JWS") the body of which has the following form:
|
||||
```javascript
|
||||
{
|
||||
"sub": username,
|
||||
"aud": "https://galene.example.org/group/groupname",
|
||||
"permissions": ["present": true],
|
||||
"iat": now,
|
||||
"exp": now + 30s,
|
||||
"iss": authorisation server URL
|
||||
}
|
||||
```
|
||||
The `permissions` field contains the permissions granted to the client, in
|
||||
the same format as in the `joined` message. Since the client will only
|
||||
use the token once, at the very beginning of the session, the tokens
|
||||
issued may have a short lifetime (on the order of 30s).
|
||||
|
||||
|
||||
|
|
3
go.mod
3
go.mod
|
@ -1,9 +1,10 @@
|
|||
module github.com/jech/galene
|
||||
|
||||
go 1.13
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/at-wat/ebml-go v0.16.0
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/jech/cert v0.0.0-20210819231831-aca735647728
|
||||
github.com/jech/samplebuilder v0.0.0-20220125212352-4553ed6f9a6c
|
||||
|
|
2
go.sum
2
go.sum
|
@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
|
|
|
@ -92,6 +92,7 @@ type ClientCredentials struct {
|
|||
System bool
|
||||
Username string
|
||||
Password string
|
||||
Token string
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
@ -15,6 +16,8 @@ import (
|
|||
"github.com/pion/ice/v2"
|
||||
"github.com/pion/sdp/v3"
|
||||
"github.com/pion/webrtc/v3"
|
||||
|
||||
"github.com/jech/galene/token"
|
||||
)
|
||||
|
||||
var Directory, DataDirectory string
|
||||
|
@ -960,6 +963,12 @@ type Description struct {
|
|||
// A list of logins for non-presenting users.
|
||||
Other []ClientPattern `json:"other,omitempty"`
|
||||
|
||||
// The URL of the authentication server.
|
||||
AuthServer string `json:"authServer"`
|
||||
|
||||
// The (public) keys of the authentication server
|
||||
AuthKeys []map[string]interface{} `json:"authKeys"`
|
||||
|
||||
// Codec preferences. If empty, a suitable default is chosen in
|
||||
// the APIFromNames function.
|
||||
Codecs []string `json:"codecs,omitempty"`
|
||||
|
@ -1062,6 +1071,7 @@ func (desc *Description) GetPermission(group string, creds ClientCredentials) (C
|
|||
if !desc.AllowAnonymous && creds.Username == "" {
|
||||
return p, ErrAnonymousNotAuthorised
|
||||
}
|
||||
|
||||
if found, good := matchClient(group, creds, desc.Op); found {
|
||||
if good {
|
||||
p.Op = true
|
||||
|
@ -1086,6 +1096,52 @@ func (desc *Description) GetPermission(group string, creds ClientCredentials) (C
|
|||
}
|
||||
return p, ErrNotAuthorised
|
||||
}
|
||||
|
||||
if desc.AuthServer != "" && creds.Token != "" {
|
||||
aud, perms, err := token.Valid(
|
||||
creds.Username, creds.Token,
|
||||
desc.AuthKeys, desc.AuthServer,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Token authentication: %v", err)
|
||||
return p, ErrNotAuthorised
|
||||
}
|
||||
conf, err := GetConfiguration()
|
||||
if err != nil {
|
||||
log.Printf("Read config.json: %v", err)
|
||||
return p, err
|
||||
}
|
||||
ok := false
|
||||
for _, u := range aud {
|
||||
url, err := url.Parse(u)
|
||||
if err != nil {
|
||||
log.Printf("Token URL: %v", err)
|
||||
continue
|
||||
}
|
||||
// if canonicalHost is not set, we allow tokens
|
||||
// for any domain name. Hopefully different
|
||||
// servers use distinct keys.
|
||||
if conf.CanonicalHost != "" {
|
||||
if !strings.EqualFold(
|
||||
url.Host, conf.CanonicalHost,
|
||||
) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if url.Path == path.Join("/group", group)+"/" {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return p, ErrNotAuthorised
|
||||
}
|
||||
p.Op, _ = perms["op"].(bool)
|
||||
p.Present, _ = perms["present"].(bool)
|
||||
p.Record, _ = perms["record"].(bool)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
return p, ErrNotAuthorised
|
||||
}
|
||||
|
||||
|
@ -1093,6 +1149,7 @@ type Status struct {
|
|||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
AuthServer string `json:"authServer,omitempty"`
|
||||
Locked bool `json:"locked,omitempty"`
|
||||
ClientCount *int `json:"clientCount,omitempty"`
|
||||
}
|
||||
|
@ -1102,6 +1159,7 @@ func (g *Group) Status (authentified bool) Status {
|
|||
d := Status{
|
||||
Name: g.name,
|
||||
DisplayName: desc.DisplayName,
|
||||
AuthServer: desc.AuthServer,
|
||||
Description: desc.Description,
|
||||
}
|
||||
|
||||
|
|
|
@ -113,6 +113,7 @@ type clientMessage struct {
|
|||
Dest string `json:"dest,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Privileged bool `json:"privileged,omitempty"`
|
||||
Permissions *group.ClientPermissions `json:"permissions,omitempty"`
|
||||
Status *group.Status `json:"status,omitempty"`
|
||||
|
@ -1332,6 +1333,7 @@ func handleClientMessage(c *webClient, m clientMessage) error {
|
|||
group.ClientCredentials{
|
||||
Username: m.Username,
|
||||
Password: m.Password,
|
||||
Token: m.Token,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
|
|
@ -271,13 +271,25 @@ function setConnected(connected) {
|
|||
}
|
||||
|
||||
/** @this {ServerConnection} */
|
||||
function gotConnected() {
|
||||
async function gotConnected() {
|
||||
username = getInputElement('username').value.trim();
|
||||
setConnected(true);
|
||||
|
||||
let pw = getInputElement('password').value;
|
||||
getInputElement('password').value = '';
|
||||
let credentials;
|
||||
if(!groupStatus.authServer)
|
||||
credentials = pw;
|
||||
else
|
||||
credentials = {
|
||||
type: 'authServer',
|
||||
authServer: groupStatus.authServer,
|
||||
location: location.href,
|
||||
password: pw,
|
||||
};
|
||||
|
||||
try {
|
||||
let pw = getInputElement('password').value;
|
||||
getInputElement('password').value = '';
|
||||
this.join(group, username, pw);
|
||||
await this.join(group, username, credentials);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
displayError(e);
|
||||
|
|
|
@ -205,6 +205,7 @@ function ServerConnection() {
|
|||
* @property {string} [dest]
|
||||
* @property {string} [username]
|
||||
* @property {string} [password]
|
||||
* @property {string} [token]
|
||||
* @property {boolean} [privileged]
|
||||
* @property {Object<string,boolean>} [permissions]
|
||||
* @property {Object<string,any>} [status]
|
||||
|
@ -416,19 +417,52 @@ ServerConnection.prototype.connect = async function(url) {
|
|||
*
|
||||
* @param {string} group - The name of the group to join.
|
||||
* @param {string} username - the username to join as.
|
||||
* @param {string} password - the password.
|
||||
* @param {string|Object} credentials - password or authServer.
|
||||
* @param {Object<string,any>} [data] - the initial associated data.
|
||||
*/
|
||||
ServerConnection.prototype.join = function(group, username, password, data) {
|
||||
ServerConnection.prototype.join = async function(group, username, credentials, data) {
|
||||
let m = {
|
||||
type: 'join',
|
||||
kind: 'join',
|
||||
group: group,
|
||||
username: username,
|
||||
password: password,
|
||||
};
|
||||
if((typeof credentials) === 'string') {
|
||||
m.password = credentials;
|
||||
} else {
|
||||
switch(credentials.type) {
|
||||
case 'password':
|
||||
m.password = credentials.password;
|
||||
break;
|
||||
case 'token':
|
||||
m.token = credentials.token;
|
||||
break;
|
||||
case 'authServer':
|
||||
let r = await fetch(credentials.authServer, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
location: credentials.location,
|
||||
username: username,
|
||||
password: credentials.password,
|
||||
}),
|
||||
});
|
||||
if(!r.ok)
|
||||
throw new Error(
|
||||
`The authorisation server said: ${r.status} ${r.statusText}`,
|
||||
);
|
||||
m.token = await r.text();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown credentials type ${credentials.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
if(data)
|
||||
m.data = data;
|
||||
|
||||
this.send(m);
|
||||
};
|
||||
|
||||
|
|
138
token/token.go
Normal file
138
token/token.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
package token
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"math/big"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
func parseBase64(k string, d map[string]interface{}) ([]byte, error) {
|
||||
v, ok := d[k].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("key " + k + " not found")
|
||||
}
|
||||
vv, err := base64.RawURLEncoding.DecodeString(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vv, nil
|
||||
}
|
||||
|
||||
func parseKey(key map[string]interface{}) (interface{}, error) {
|
||||
kty, ok := key["kty"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("kty not found")
|
||||
}
|
||||
alg, ok := key["alg"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("alg not found")
|
||||
}
|
||||
|
||||
switch kty {
|
||||
case "oct":
|
||||
var length int
|
||||
switch(alg) {
|
||||
case "HS256":
|
||||
length = 32
|
||||
case "HS384":
|
||||
length = 48
|
||||
case "HS512":
|
||||
length = 64
|
||||
default:
|
||||
return nil, errors.New("unknown alg")
|
||||
}
|
||||
k, err := parseBase64("k", key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(k) != length {
|
||||
return nil, errors.New("bad length for key")
|
||||
}
|
||||
return k, nil
|
||||
case "EC":
|
||||
if alg != "ES256" {
|
||||
return nil, errors.New("uknown alg")
|
||||
}
|
||||
crv, ok := key["crv"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("crv not found")
|
||||
}
|
||||
if crv != "P-256" {
|
||||
return nil, errors.New("unknown crv")
|
||||
}
|
||||
curve := elliptic.P256()
|
||||
xbytes, err := parseBase64("x", key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var x big.Int
|
||||
x.SetBytes(xbytes)
|
||||
ybytes, err := parseBase64("y", key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var y big.Int
|
||||
y.SetBytes(ybytes)
|
||||
if !curve.IsOnCurve(&x, &y) {
|
||||
return nil, errors.New("key is not on curve")
|
||||
}
|
||||
return &ecdsa.PublicKey{
|
||||
Curve: curve,
|
||||
X: &x,
|
||||
Y: &y,
|
||||
}, nil
|
||||
default:
|
||||
return nil, errors.New("unknown key type")
|
||||
}
|
||||
}
|
||||
|
||||
func getKey(header map[string]interface{}, keys []map[string]interface{}) (interface{}, error) {
|
||||
alg, _ := header["alg"].(string)
|
||||
kid, _ := header["kid"].(string)
|
||||
for _, k := range keys {
|
||||
kid2, _ := k["kid"].(string)
|
||||
alg2, _ := k["alg"].(string)
|
||||
if (kid == "" || kid == kid2) && alg == alg2 {
|
||||
return parseKey(k)
|
||||
}
|
||||
}
|
||||
return nil, errors.New("key not found")
|
||||
}
|
||||
|
||||
func Valid(username, token string, keys []map[string]interface{}, issuer string) ([]string, map[string]interface{}, error) {
|
||||
tok, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
|
||||
return getKey(t.Header, keys)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
claims := tok.Claims.(jwt.MapClaims)
|
||||
|
||||
sub, ok := claims["sub"].(string)
|
||||
if !ok || sub != username {
|
||||
return nil, nil, errors.New("invalid 'sub' field")
|
||||
}
|
||||
iss, ok := claims["iss"].(string)
|
||||
if !ok || iss != issuer {
|
||||
return nil, nil, errors.New("invalid 'iss' field")
|
||||
}
|
||||
aud, ok := claims["aud"]
|
||||
var res []string
|
||||
if ok {
|
||||
switch aud := aud.(type) {
|
||||
case string:
|
||||
res = []string{aud}
|
||||
case []string:
|
||||
res = aud
|
||||
}
|
||||
}
|
||||
perms, ok := claims["permissions"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, nil, errors.New("invalid 'permissions' field")
|
||||
}
|
||||
return res, perms, nil
|
||||
}
|
54
token/token_test.go
Normal file
54
token/token_test.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package token
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHS256(t *testing.T) {
|
||||
key := `{
|
||||
"kty":"oct",
|
||||
"alg":"HS256",
|
||||
"k":"4S9YZLHK1traIaXQooCnPfBw_yR8j9VEPaAMWAog_YQ"
|
||||
}`
|
||||
var j map[string]interface{}
|
||||
err := json.Unmarshal([]byte(key), &j)
|
||||
if err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
k, err := parseKey(j)
|
||||
if err != nil {
|
||||
t.Fatalf("parseKey: %v", err)
|
||||
}
|
||||
kk, ok := k.([]byte)
|
||||
if !ok || len(kk) != 32 {
|
||||
t.Errorf("parseKey: got %v", kk)
|
||||
}
|
||||
}
|
||||
|
||||
func TestES256(t *testing.T) {
|
||||
key := `{
|
||||
"kty":"EC",
|
||||
"alg":"ES256",
|
||||
"crv":"P-256",
|
||||
"x":"dElK9qBNyCpRXdvJsn4GdjrFzScSzpkz_I0JhKbYC88",
|
||||
"y":"pBhVb37haKvwEoleoW3qxnT4y5bK35_RTP7_RmFKR6Q"
|
||||
}`
|
||||
var j map[string]interface{}
|
||||
err := json.Unmarshal([]byte(key), &j)
|
||||
if err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
k, err := parseKey(j)
|
||||
if err != nil {
|
||||
t.Fatalf("parseKey: %v", err)
|
||||
}
|
||||
kk, ok := k.(*ecdsa.PublicKey)
|
||||
if !ok || kk.Params().Name != "P-256" {
|
||||
t.Errorf("parseKey: got %v", kk)
|
||||
}
|
||||
if !kk.IsOnCurve(kk.X, kk.Y) {
|
||||
t.Errorf("point is not on curve")
|
||||
}
|
||||
}
|
|
@ -86,9 +86,13 @@ func Serve(address string, dataDir string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func cspHeader(w http.ResponseWriter) {
|
||||
func cspHeader(w http.ResponseWriter, connect string) {
|
||||
c := "connect-src ws: wss: 'self';"
|
||||
if connect != "" {
|
||||
c = "connect-src " + connect + " ws: wss: 'self';"
|
||||
}
|
||||
w.Header().Add("Content-Security-Policy",
|
||||
"connect-src ws: wss: 'self'; img-src data: 'self'; media-src blob: 'self'; default-src 'self'")
|
||||
c + " img-src data: 'self'; media-src blob: 'self'; default-src 'self'")
|
||||
}
|
||||
|
||||
func notFound(w http.ResponseWriter) {
|
||||
|
@ -172,7 +176,7 @@ func (fh *fileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
cspHeader(w)
|
||||
cspHeader(w, "")
|
||||
p := r.URL.Path
|
||||
// this ensures any leading .. are removed by path.Clean below
|
||||
if !strings.HasPrefix(p, "/") {
|
||||
|
@ -314,7 +318,8 @@ func groupHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
cspHeader(w)
|
||||
status := g.Status(false)
|
||||
cspHeader(w, status.AuthServer)
|
||||
serveFile(w, r, filepath.Join(StaticRoot, "galene.html"))
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue