Commit 2546aae7 authored by Juliusz Chroboczek's avatar Juliusz Chroboczek

Eliminate login from protocol.

The login message is replaced with handshake, which only carries
the client id.  Username and password is now in the join message.
Permissions is replaced with joined.
parent b30d4fe5
...@@ -29,7 +29,7 @@ let sc = new ServerConnection() ...@@ -29,7 +29,7 @@ let sc = new ServerConnection()
serverConnection.onconnected = ...; serverConnection.onconnected = ...;
serverConnection.onclose = ...; serverConnection.onclose = ...;
serverConnection.onusermessage = ...; serverConnection.onusermessage = ...;
serverConnection.onpermissions = ...; serverConnection.onjoined = ...;
serverConnection.onuser = ...; serverConnection.onuser = ...;
serverConnection.onchat = ...; serverConnection.onchat = ...;
serverConnection.onclearchat = ...; serverConnection.onclearchat = ...;
...@@ -55,18 +55,17 @@ You may now connect to the server. ...@@ -55,18 +55,17 @@ You may now connect to the server.
serverConnection.connect(`wss://${location.host}/ws`); serverConnection.connect(`wss://${location.host}/ws`);
``` ```
You log-in, join a group and request media in the `onconnected` callback. You typically join a group and request media in the `onconnected` callback:
```javascript ```javascript
serverConnection.onconnected = function() { serverConnection.onconnected = function() {
this.login(username, password); this.join(group, 'join', username, password);
this.join(group);
this.request('everything'); this.request('everything');
} }
``` ```
You should not attempt to push a stream to the server until it has granted You should not attempt to push a stream to the server until it has granted
you the `present` permission through the `onpermissions` callback. you the `present` permission through the `onjoined` callback.
## Managing groups and users ## Managing groups and users
......
...@@ -252,11 +252,16 @@ func delUpConn(c *webClient, id string) bool { ...@@ -252,11 +252,16 @@ func delUpConn(c *webClient, id string) bool {
delete(c.up, id) delete(c.up, id)
c.mu.Unlock() c.mu.Unlock()
go func(clients []group.Client) { g := c.group
for _, c := range clients { if g != nil {
c.PushConn(conn.id, nil, nil, "") go func(clients []group.Client) {
} for _, c := range clients {
}(c.Group().GetClients(c)) c.PushConn(conn.id, nil, nil, "")
}
}(g.GetClients(c))
} else {
log.Printf("Deleting connection for client with no group")
}
conn.pc.Close() conn.pc.Close()
return true return true
...@@ -577,7 +582,12 @@ func (c *webClient) setRequested(requested map[string]uint32) error { ...@@ -577,7 +582,12 @@ func (c *webClient) setRequested(requested map[string]uint32) error {
} }
func pushConns(c group.Client) { func pushConns(c group.Client) {
clients := c.Group().GetClients(c) group := c.Group()
if group == nil {
log.Printf("Pushing connections to unjoined client")
return
}
clients := group.GetClients(c)
for _, cc := range clients { for _, cc := range clients {
ccc, ok := cc.(*webClient) ccc, ok := cc.(*webClient)
if ok { if ok {
...@@ -637,40 +647,38 @@ func (c *webClient) PushConn(id string, up conn.Up, tracks []conn.UpTrack, label ...@@ -637,40 +647,38 @@ func (c *webClient) PushConn(id string, up conn.Up, tracks []conn.UpTrack, label
return nil return nil
} }
func StartClient(conn *websocket.Conn) (err error) { func readMessage(conn *websocket.Conn, m *clientMessage) error {
var m clientMessage err := conn.SetReadDeadline(time.Now().Add(15 * time.Second))
err = conn.SetReadDeadline(time.Now().Add(15 * time.Second))
if err != nil {
conn.Close()
return
}
err = conn.ReadJSON(&m)
if err != nil { if err != nil {
conn.Close() return err
return
} }
err = conn.SetReadDeadline(time.Time{}) defer conn.SetReadDeadline(time.Time{})
return conn.ReadJSON(&m)
}
func StartClient(conn *websocket.Conn) error {
var m clientMessage
err := readMessage(conn, &m)
if err != nil { if err != nil {
conn.Close() conn.Close()
return return err
} }
if m.Type != "login" { if m.Type != "handshake" {
conn.WriteMessage(websocket.CloseMessage, conn.WriteMessage(websocket.CloseMessage,
websocket.FormatCloseMessage( websocket.FormatCloseMessage(
websocket.CloseProtocolError, websocket.CloseProtocolError,
"you must login first", "you must handshake first",
), ),
) )
conn.Close() conn.Close()
return return group.ProtocolError("client didn't handshake")
} }
c := &webClient{ c := &webClient{
id: m.Id, id: m.Id,
username: m.Username,
password: m.Password,
actionCh: make(chan interface{}, 10), actionCh: make(chan interface{}, 10),
done: make(chan struct{}), done: make(chan struct{}),
} }
...@@ -678,50 +686,20 @@ func StartClient(conn *websocket.Conn) (err error) { ...@@ -678,50 +686,20 @@ func StartClient(conn *websocket.Conn) (err error) {
defer close(c.done) defer close(c.done)
c.writeCh = make(chan interface{}, 25) c.writeCh = make(chan interface{}, 25)
c.writerDone = make(chan struct{})
go clientWriter(conn, c.writeCh, c.writerDone)
defer func() { defer func() {
if isWSNormalError(err) { var e []byte
err = nil if !isWSNormalError(err) {
c.close(nil) var m *clientMessage
} else { m, e = errorToWSCloseMessage(c.id, err)
m, e := errorToWSCloseMessage(c.id, err)
if m != nil { if m != nil {
c.write(*m) c.write(*m)
} }
c.close(e)
} }
c.close(e)
}() }()
c.writerDone = make(chan struct{})
go clientWriter(conn, c.writeCh, c.writerDone)
err = conn.ReadJSON(&m)
if err != nil {
return err
}
if m.Type != "join" {
return group.ProtocolError("you must join a group first")
}
g, err := group.AddClient(m.Group, c)
if err != nil {
if os.IsNotExist(err) {
err = group.UserError("group does not exist")
} else if err == group.ErrNotAuthorised {
err = group.UserError("not authorised")
time.Sleep(200 * time.Millisecond)
}
return
}
if redirect := g.Redirect(); redirect != "" {
// We normally redirect at the HTTP level, but the group
// description could have been edited in the meantime.
err = group.UserError("group is now at " + redirect)
return
}
c.group = g
defer group.DelClient(c)
return clientLoop(c, conn) return clientLoop(c, conn)
} }
...@@ -753,6 +731,13 @@ type kickAction struct { ...@@ -753,6 +731,13 @@ type kickAction struct {
} }
func clientLoop(c *webClient, ws *websocket.Conn) error { func clientLoop(c *webClient, ws *websocket.Conn) error {
defer func() {
if c.group != nil {
group.DelClient(c)
c.group = nil
}
}()
read := make(chan interface{}, 1) read := make(chan interface{}, 1)
go clientReader(ws, read, c.done) go clientReader(ws, read, c.done)
...@@ -765,27 +750,6 @@ func clientLoop(c *webClient, ws *websocket.Conn) error { ...@@ -765,27 +750,6 @@ func clientLoop(c *webClient, ws *websocket.Conn) error {
} }
}() }()
perms := c.permissions
c.write(clientMessage{
Type: "permissions",
Permissions: &perms,
})
h := c.group.GetChatHistory()
for _, m := range h {
err := c.write(clientMessage{
Type: "chat",
Id: m.Id,
Username: m.User,
Time: m.Time,
Value: m.Value,
Kind: m.Kind,
})
if err != nil {
return err
}
}
readTime := time.Now() readTime := time.Now()
ticker := time.NewTicker(10 * time.Second) ticker := time.NewTicker(10 * time.Second)
...@@ -889,9 +853,15 @@ func clientLoop(c *webClient, ws *websocket.Conn) error { ...@@ -889,9 +853,15 @@ func clientLoop(c *webClient, ws *websocket.Conn) error {
} }
case permissionsChangedAction: case permissionsChangedAction:
group := c.Group()
if group == nil {
return errors.New("Permissions changed in no group")
}
perms := c.permissions perms := c.permissions
c.write(clientMessage{ c.write(clientMessage{
Type: "permissions", Type: "joined",
Kind: "change",
Group: group.Name(),
Permissions: &perms, Permissions: &perms,
}) })
if !c.permissions.Present { if !c.permissions.Present {
...@@ -1018,11 +988,66 @@ func kickClient(g *group.Group, id, user, dest string, message string) error { ...@@ -1018,11 +988,66 @@ func kickClient(g *group.Group, id, user, dest string, message string) error {
func handleClientMessage(c *webClient, m clientMessage) error { func handleClientMessage(c *webClient, m clientMessage) error {
switch m.Type { switch m.Type {
case "request": case "join":
err := c.setRequested(m.Request) if m.Kind == "leave" {
if c.group == nil || c.group.Name() != m.Group {
return group.ProtocolError("you are not joined")
}
c.group = nil
c.permissions = group.ClientPermissions{}
perms := c.permissions
return c.write(clientMessage{
Type: "joined",
Kind: "leave",
Group: m.Group,
Permissions: &perms,
})
}
if m.Kind != "join" {
return group.ProtocolError("unknown kind")
}
if c.group != nil {
return group.ProtocolError("cannot join multiple groups")
}
c.username = m.Username
c.password = m.Password
g, err := group.AddClient(m.Group, c)
if err != nil { if err != nil {
if os.IsNotExist(err) {
return c.error(
group.UserError("group does not exist"),
)
} else if err == group.ErrNotAuthorised {
time.Sleep(200 * time.Millisecond)
return c.write(clientMessage{
Type: "joined",
Kind: "fail",
Group: m.Group,
Permissions: &group.ClientPermissions{},
Value: "not authorised",
})
}
return err return err
} }
if redirect := g.Redirect(); redirect != "" {
// We normally redirect at the HTTP level, but the group
// description could have been edited in the meantime.
return c.error(
group.UserError("group is now at " + redirect),
)
}
c.group = g
perms := c.permissions
return c.write(clientMessage{
Type: "joined",
Kind: "join",
Group: m.Group,
Permissions: &perms,
})
case "request":
return c.setRequested(m.Request)
case "offer": case "offer":
if !c.permissions.Present { if !c.permissions.Present {
c.write(clientMessage{ c.write(clientMessage{
...@@ -1080,16 +1105,20 @@ func handleClientMessage(c *webClient, m clientMessage) error { ...@@ -1080,16 +1105,20 @@ func handleClientMessage(c *webClient, m clientMessage) error {
} }
case "chat", "usermessage": case "chat", "usermessage":
if m.Id != c.id { if m.Id != c.id {
return group.UserError("wrong sender id") return group.ProtocolError("wrong sender id")
} }
if m.Username != "" && m.Username != c.username { if m.Username != "" && m.Username != c.username {
return group.UserError("wrong sender username") return group.ProtocolError("wrong sender username")
}
g := c.group
if g == nil {
return c.error(group.UserError("join a group first"))
} }
tm := group.ToJSTime(time.Now()) tm := group.ToJSTime(time.Now())
if m.Type == "chat" { if m.Type == "chat" {
if m.Dest == "" { if m.Dest == "" {
c.group.AddToChatHistory( g.AddToChatHistory(
m.Id, m.Username, tm, m.Kind, m.Value, m.Id, m.Username, tm, m.Kind, m.Value,
) )
} }
...@@ -1105,7 +1134,7 @@ func handleClientMessage(c *webClient, m clientMessage) error { ...@@ -1105,7 +1134,7 @@ func handleClientMessage(c *webClient, m clientMessage) error {
Value: m.Value, Value: m.Value,
} }
if m.Dest == "" { if m.Dest == "" {
clients := c.group.GetClients(nil) clients := g.GetClients(nil)
for _, cc := range clients { for _, cc := range clients {
ccc, ok := cc.(*webClient) ccc, ok := cc.(*webClient)
if ok { if ok {
...@@ -1113,7 +1142,7 @@ func handleClientMessage(c *webClient, m clientMessage) error { ...@@ -1113,7 +1142,7 @@ func handleClientMessage(c *webClient, m clientMessage) error {
} }
} }
} else { } else {
cc := c.group.GetClient(m.Dest) cc := g.GetClient(m.Dest)
if cc == nil { if cc == nil {
return c.error(group.UserError("user unknown")) return c.error(group.UserError("user unknown"))
} }
...@@ -1125,16 +1154,20 @@ func handleClientMessage(c *webClient, m clientMessage) error { ...@@ -1125,16 +1154,20 @@ func handleClientMessage(c *webClient, m clientMessage) error {
} }
case "groupaction": case "groupaction":
if m.Id != c.id { if m.Id != c.id {
return group.UserError("wrong sender id") return group.ProtocolError("wrong sender id")
} }
if m.Username != "" && m.Username != c.username { if m.Username != "" && m.Username != c.username {
return group.UserError("wrong sender username") return group.ProtocolError("wrong sender username")
}
g := c.group
if g == nil {
return c.error(group.UserError("join a group first"))
} }
switch m.Kind { switch m.Kind {
case "clearchat": case "clearchat":
c.group.ClearChatHistory() g.ClearChatHistory()
m := clientMessage{Type: "clearchat"} m := clientMessage{Type: "clearchat"}
clients := c.group.GetClients(nil) clients := g.GetClients(nil)
for _, cc := range clients { for _, cc := range clients {
cc, ok := cc.(*webClient) cc, ok := cc.(*webClient)
if ok { if ok {
...@@ -1145,19 +1178,19 @@ func handleClientMessage(c *webClient, m clientMessage) error { ...@@ -1145,19 +1178,19 @@ func handleClientMessage(c *webClient, m clientMessage) error {
if !c.permissions.Op { if !c.permissions.Op {
return c.error(group.UserError("not authorised")) return c.error(group.UserError("not authorised"))
} }
c.group.SetLocked(m.Kind == "lock", m.Value) g.SetLocked(m.Kind == "lock", m.Value)
case "record": case "record":
if !c.permissions.Record { if !c.permissions.Record {
return c.error(group.UserError("not authorised")) return c.error(group.UserError("not authorised"))
} }
for _, cc := range c.group.GetClients(c) { for _, cc := range g.GetClients(c) {
_, ok := cc.(*diskwriter.Client) _, ok := cc.(*diskwriter.Client)
if ok { if ok {
return c.error(group.UserError("already recording")) return c.error(group.UserError("already recording"))
} }
} }
disk := diskwriter.New(c.group) disk := diskwriter.New(g)
_, err := group.AddClient(c.group.Name(), disk) _, err := group.AddClient(g.Name(), disk)
if err != nil { if err != nil {
disk.Close() disk.Close()
return c.error(err) return c.error(err)
...@@ -1167,7 +1200,7 @@ func handleClientMessage(c *webClient, m clientMessage) error { ...@@ -1167,7 +1200,7 @@ func handleClientMessage(c *webClient, m clientMessage) error {
if !c.permissions.Record { if !c.permissions.Record {
return c.error(group.UserError("not authorised")) return c.error(group.UserError("not authorised"))
} }
for _, cc := range c.group.GetClients(c) { for _, cc := range g.GetClients(c) {
disk, ok := cc.(*diskwriter.Client) disk, ok := cc.(*diskwriter.Client)
if ok { if ok {
disk.Close() disk.Close()
...@@ -1179,17 +1212,21 @@ func handleClientMessage(c *webClient, m clientMessage) error { ...@@ -1179,17 +1212,21 @@ func handleClientMessage(c *webClient, m clientMessage) error {
} }
case "useraction": case "useraction":
if m.Id != c.id { if m.Id != c.id {
return group.UserError("wrong sender id") return group.ProtocolError("wrong sender id")
} }
if m.Username != "" && m.Username != c.username { if m.Username != "" && m.Username != c.username {
return group.UserError("wrong sender username") return group.ProtocolError("wrong sender username")
}
g := c.group
if g == nil {
return c.error(group.UserError("join a group first"))
} }
switch m.Kind { switch m.Kind {
case "op", "unop", "present", "unpresent": case "op", "unop", "present", "unpresent":
if !c.permissions.Op { if !c.permissions.Op {
return c.error(group.UserError("not authorised")) return c.error(group.UserError("not authorised"))
} }
err := setPermissions(c.group, m.Dest, m.Kind) err := setPermissions(g, m.Dest, m.Kind)
if err != nil { if err != nil {
return c.error(err) return c.error(err)
} }
...@@ -1197,7 +1234,7 @@ func handleClientMessage(c *webClient, m clientMessage) error { ...@@ -1197,7 +1234,7 @@ func handleClientMessage(c *webClient, m clientMessage) error {
if !c.permissions.Op { if !c.permissions.Op {
return c.error(group.UserError("not authorised")) return c.error(group.UserError("not authorised"))
} }
err := kickClient(c.group, m.Id, m.Username, m.Dest, m.Value) err := kickClient(g, m.Id, m.Username, m.Dest, m.Value)
if err != nil { if err != nil {
return c.error(err) return c.error(err)
} }
...@@ -1207,7 +1244,7 @@ func handleClientMessage(c *webClient, m clientMessage) error { ...@@ -1207,7 +1244,7 @@ func handleClientMessage(c *webClient, m clientMessage) error {
case "pong": case "pong":
// nothing // nothing
case "ping": case "ping":
c.write(clientMessage{ return c.write(clientMessage{
Type: "pong", Type: "pong",
}) })
default: default:
......
...@@ -108,11 +108,14 @@ function ServerConnection() { ...@@ -108,11 +108,14 @@ function ServerConnection() {
*/ */
this.onuser = null; this.onuser = null;
/** /**
* onpermissions is called whenever the current user's permissions change * onjoined is called whenever we join or leave a group or whenever the
* permissions we have in a group change.
* *
* @type{(this: ServerConnection, permissions: Object<string,boolean>) => void} * kind is one of 'join', 'fail', 'change' or 'leave'.
*
* @type{(this: ServerConnection, kind: string, group: string, permissions: Object<string,boolean>, message: string) => void}
*/ */
this.onpermissions = null; this.onjoined = null;
/** /**
* ondownstream is called whenever a new down stream is added. It * ondownstream is called whenever a new down stream is added. It
* should set up the stream's callbacks; actually setting up the UI * should set up the stream's callbacks; actually setting up the UI
...@@ -237,14 +240,16 @@ ServerConnection.prototype.connect = async function(url) { ...@@ -237,14 +240,16 @@ ServerConnection.prototype.connect = async function(url) {
reject(e); reject(e);
}; };
this.socket.onopen = function(e) { this.socket.onopen = function(e) {
sc.send({
type: 'handshake',
id: sc.id,
});
if(sc.onconnected) if(sc.onconnected)
sc.onconnected.call(sc); sc.onconnected.call(sc);
resolve(sc); resolve(sc);
}; };
this.socket.onclose = function(e) { this.socket.onclose = function(e) {
sc.permissions = {}; sc.permissions = {};
if(sc.onpermissions)
sc.onpermissions.call(sc, {});
for(let id in sc.down) { for(let id in sc.down) {
let c = sc.down[id]; let c = sc.down[id];
delete(sc.down[id]); delete(sc.down[id]);
...@@ -252,6 +257,9 @@ ServerConnection.prototype.connect = async function(url) { ...@@ -252,6 +257,9 @@ ServerConnection.prototype.connect = async function(url) {
if(c.onclose) if(c.onclose)
c.onclose.call(c); c.onclose.call(c);
} }
if(sc.group && sc.onjoined)
sc.onjoined.call(sc, 'leave', sc.group, {}, '');
sc.group = null;
if(sc.onclose) if(sc.onclose)
sc.onclose.call(sc, e.code, e.reason); sc.onclose.call(sc, e.code, e.reason);
reject(new Error('websocket close ' + e.code + ' ' + e.reason)); reject(new Error('websocket close ' + e.code + ' ' + e.reason));
...@@ -280,10 +288,19 @@ ServerConnection.prototype.connect = async function(url) { ...@@ -280,10 +288,19 @@ ServerConnection.prototype.connect = async function(url) {
case 'label': case 'label':
sc.gotLabel(m.id, m.value); sc.gotLabel(m.id, m.value);
break; break;
case 'permissions': case 'joined':
if(sc.group) {
if(m.group !== sc.group) {
throw new Error('Joined multiple groups');
}
} else {
sc.group = m.group;
}
sc.permissions = m.permissions; sc.permissions = m.permissions;
if(sc.onpermissions) if(sc.onjoined)
sc.onpermissions.call(sc, m.permissions); sc.onjoined.call(sc, m.kind, m.group,
m.permissions || {},
m.value || null);
break; break;
case 'user': case 'user':
if(sc.onuser) if(sc.onuser)
...@@ -324,28 +341,33 @@ ServerConnection.prototype.connect = async function(url) { ...@@ -324,28 +341,33 @@ ServerConnection.prototype.connect = async function(url) {
} }
/** /**
* login authenticates with the server. * join requests to join a group. The onjoined callback will be called
* when we've effectively joined.
* *
* @param {string} username - the username to login as. * @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} password - the password.
*/ */
ServerConnection.prototype.login = function(username, password) { ServerConnection.prototype.join = function(group, username, password) {
this.send({ this.send({
type: 'login', type: 'join',
id: this.id, kind: 'join',
group: group,
username: username, username: username,
password: password, password: password,
}); });
} }
/** /**
* join joins a group. * leave leaves a group. The onjoined callback will be called when we've
* effectively left.
* *
* @param {string} group - The name of the group to join. * @param {string} group - The name of the group to join.
*/ */
ServerConnection.prototype.join = function(group) { ServerConnection.prototype.leave = function(group) {
this.send({ this.send({
type: 'join', type: 'join',
kind: 'leave',
group: group, group: group,
}); });
} }
......
...@@ -281,9 +281,7 @@ function setConnected(connected) { ...@@ -281,9 +281,7 @@ function setConnected(connected) {
function gotConnected() { function gotConnected() {
setConnected(true); setConnected(true);
let up = getUserPass(); let up = getUserPass();
this.login(up.username, up.password); this.join(group, up.username, up.password);
this.join(group);
this.request(getSettings().request);
} }
/** /**
...@@ -1409,14 +1407,26 @@ function displayUsername() { ...@@ -1409,14 +1407,26 @@ function displayUsername() {
let presentRequested = null; let presentRequested = null;
/** /**
* @this {ServerConnection}
* @param {string} group
* @param {Object<string,boolean>} perms * @param {Object<string,boolean>} perms
*/ */
async function gotPermissions(perms) { async function gotJoined(kind, group, perms, message) {
if(kind === 'fail') {
displayError('The server said: ' + message);
this.close();
return;
}
displayUsername(); displayUsername();
setButtonsVisibility(); setButtonsVisibility();
if(kind !== 'leave')
this.request(getSettings().request);
try { try {
if(serverConnection.permissions.present && !findUpMedia('local')) { if(kind === 'join' &&
serverConnection.permissions.present && !findUpMedia('local')) {
if(presentRequested) { if(presentRequested) {
if(presentRequested === 'mike') if(presentRequested === 'mike')
updateSettings({video: ''}); updateSettings({video: ''});
...@@ -2172,7 +2182,7 @@ async function serverConnect() { ...@@ -2172,7 +2182,7 @@ async function serverConnect() {
serverConnection.onclose = gotClose; serverConnection.onclose = gotClose;
serverConnection.ondownstream = gotDownStream; serverConnection.ondownstream = gotDownStream;
serverConnection.onuser = gotUser; serverConnection.onuser = gotUser;
serverConnection.onpermissions = gotPermissions; serverConnection.onjoined = gotJoined;
serverConnection.onchat = addToChatbox; serverConnection.onchat = addToChatbox;
serverConnection.onclearchat = clearChat; serverConnection.onclearchat = clearChat;
serverConnection.onusermessage = function(id, dest, username, time, priviledged, kind, message) { serverConnection.onusermessage = function(id, dest, username, time, priviledged, kind, message) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment