From e423ac6a2feadc436be208f0b9554432915e6abd Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Fri, 25 Apr 2014 13:48:33 +0200 Subject: [PATCH 01/48] Rebuilt translations and translated to German (de). --- src/i18n/messages-de.po | 12 +++++++++--- src/i18n/messages-ja.po | 10 ++++++++-- src/i18n/messages-ko.po | 10 ++++++++-- src/i18n/messages-zh-cn.po | 10 ++++++++-- src/i18n/messages-zh-tw.po | 10 ++++++++-- src/i18n/messages.pot | 10 ++++++++-- 6 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/i18n/messages-de.po b/src/i18n/messages-de.po index 7ebf9bdb..bb7e3af9 100644 --- a/src/i18n/messages-de.po +++ b/src/i18n/messages-de.po @@ -8,15 +8,15 @@ msgid "" msgstr "" "Project-Id-Version: Spreed Speak Freely\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2014-04-14 16:16+0200\n" -"PO-Revision-Date: 2014-04-14 16:17+0100\n" +"POT-Creation-Date: 2014-04-25 13:40+0200\n" +"PO-Revision-Date: 2014-04-25 13:45+0100\n" "Last-Translator: Simon Eisenmann \n" "Language-Team: struktur AG \n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 0.9.6\n" +"Generated-By: Babel 1.3\n" msgid "Share your screen" msgstr "Bildschirm freigeben" @@ -36,6 +36,12 @@ msgstr "Einstellungen" msgid "Your audio level" msgstr "Ihr Audio-Pegel" +msgid "Standard view" +msgstr "Standardansicht" + +msgid "Kiosk view" +msgstr "Kiosk-Ansicht" + msgid "Start chat" msgstr "Chat starten" diff --git a/src/i18n/messages-ja.po b/src/i18n/messages-ja.po index a59cb35c..2908f664 100644 --- a/src/i18n/messages-ja.po +++ b/src/i18n/messages-ja.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Spreed Speak Freely 1.0\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2014-04-14 16:16+0200\n" +"POT-Creation-Date: 2014-04-25 13:40+0200\n" "PO-Revision-Date: 2014-04-23 22:25+0100\n" "Last-Translator: Curt Frisemo \n" "Language-Team: Curt Frisemo \n" @@ -16,7 +16,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 0.9.6\n" +"Generated-By: Babel 1.3\n" msgid "Share your screen" msgstr "画面を共有する." @@ -36,6 +36,12 @@ msgstr "設定" msgid "Your audio level" msgstr "あなたの音量" +msgid "Standard view" +msgstr "" + +msgid "Kiosk view" +msgstr "" + msgid "Start chat" msgstr "チャットを始める" diff --git a/src/i18n/messages-ko.po b/src/i18n/messages-ko.po index 5aff519e..4e1aad6a 100644 --- a/src/i18n/messages-ko.po +++ b/src/i18n/messages-ko.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Spreed Speak Freely 1.0\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2014-04-14 16:16+0200\n" +"POT-Creation-Date: 2014-04-25 13:40+0200\n" "PO-Revision-Date: 2014-04-13 20:30+0900\n" "Last-Translator: FULL NAME \n" "Language-Team: Curt Frisemo \n" @@ -16,7 +16,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 0.9.6\n" +"Generated-By: Babel 1.3\n" msgid "Share your screen" msgstr "화면 공유하기" @@ -36,6 +36,12 @@ msgstr "설정" msgid "Your audio level" msgstr "음성크기" +msgid "Standard view" +msgstr "" + +msgid "Kiosk view" +msgstr "" + msgid "Start chat" msgstr "대화시작" diff --git a/src/i18n/messages-zh-cn.po b/src/i18n/messages-zh-cn.po index 9d2218e7..ab58d5cb 100644 --- a/src/i18n/messages-zh-cn.po +++ b/src/i18n/messages-zh-cn.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Spreed Speak Freely 1.0\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2014-04-14 16:16+0200\n" +"POT-Creation-Date: 2014-04-25 13:40+0200\n" "PO-Revision-Date: 2014-03-31 23:26+0100\n" "Last-Translator: Michael P.\n" "Language-Team: Curt Frisemo \n" @@ -16,7 +16,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 0.9.6\n" +"Generated-By: Babel 1.3\n" msgid "Share your screen" msgstr "共享您的屏幕" @@ -36,6 +36,12 @@ msgstr "系统设置" msgid "Your audio level" msgstr "您的通话音量" +msgid "Standard view" +msgstr "" + +msgid "Kiosk view" +msgstr "" + msgid "Start chat" msgstr "开始聊天" diff --git a/src/i18n/messages-zh-tw.po b/src/i18n/messages-zh-tw.po index e23b5835..2dc58f63 100644 --- a/src/i18n/messages-zh-tw.po +++ b/src/i18n/messages-zh-tw.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Spreed Speak Freely 1.0\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2014-04-14 16:16+0200\n" +"POT-Creation-Date: 2014-04-25 13:40+0200\n" "PO-Revision-Date: 2014-04-07 18:09+0800\n" "Last-Translator: Michael P.\n" "Language-Team: Curt Frisemo \n" @@ -16,7 +16,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 0.9.6\n" +"Generated-By: Babel 1.3\n" msgid "Share your screen" msgstr "共享您的屏幕" @@ -36,6 +36,12 @@ msgstr "系統設置" msgid "Your audio level" msgstr "您的通話音量" +msgid "Standard view" +msgstr "" + +msgid "Kiosk view" +msgstr "" + msgid "Start chat" msgstr "開始聊天" diff --git a/src/i18n/messages.pot b/src/i18n/messages.pot index c6aaf9e7..a2c0be04 100644 --- a/src/i18n/messages.pot +++ b/src/i18n/messages.pot @@ -9,14 +9,14 @@ msgid "" msgstr "" "Project-Id-Version: Spreed Speak Freely 1.0\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2014-04-14 16:16+0200\n" +"POT-Creation-Date: 2014-04-25 13:40+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 0.9.6\n" +"Generated-By: Babel 1.3\n" msgid "Share your screen" msgstr "" @@ -36,6 +36,12 @@ msgstr "" msgid "Your audio level" msgstr "" +msgid "Standard view" +msgstr "" + +msgid "Kiosk view" +msgstr "" + msgid "Start chat" msgstr "" From 8078d0d999ac68b6bf34637b68e6dc8bca116f58 Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Fri, 25 Apr 2014 14:42:01 +0200 Subject: [PATCH 02/48] Do not show overlayActions in smally mode. --- src/styles/components/_audiovideo.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/styles/components/_audiovideo.scss b/src/styles/components/_audiovideo.scss index 38e61900..8bee7c88 100644 --- a/src/styles/components/_audiovideo.scss +++ b/src/styles/components/_audiovideo.scss @@ -320,6 +320,9 @@ .remoteVideo .peerActions i { font-size:1em; } + .overlayActions { + display: none; + } } .renderer-onepeople { From b8a585442833b755a486a894159f8fb5212ed9c3 Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Sun, 20 Apr 2014 20:23:58 +0200 Subject: [PATCH 03/48] Changed documentation to make a distinction between user and session. --- doc/CHANNELING-API.txt | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/doc/CHANNELING-API.txt b/doc/CHANNELING-API.txt index 64f193cf..afe94da1 100644 --- a/doc/CHANNELING-API.txt +++ b/doc/CHANNELING-API.txt @@ -253,7 +253,7 @@ Peer connection documents If you do not want to give a reason just send Bye as empty JSON mapping. -Additional types for user listing and notifications +Additional types for session listing and notifications Left @@ -291,6 +291,10 @@ Additional types for user listing and notifications Rev is the status update sequence for this status update entry. It is a positive integer. Higher numbers are later status updates. + When the current session is in a room (means sent Hello), a Users request + can be sent, to receive a list of sessions in that particular room. This + always returns the sessions in the same room as the calling session. + Users (Request uses empty data) { @@ -531,7 +535,7 @@ Data channel only messages "Talking": true } - The talking state sent by a given user as boolean value in "Talking" + The talking state sent by a given session as boolean value in "Talking" key (true, false). Screenshare (data channel only) @@ -545,21 +549,21 @@ Data channel only messages } The Id field is the peer where this screen sharing token is valid. Essentially - it defines the user which started screensharing. It will be empty string when + it defines the session which started screensharing. It will be empty string when received in peer to peer data channel mode. The id is the token to be used to establish a token peer connection - to the user which sent the Screenshare document. + to the session which sent the Screenshare document. Conferences and how to use them There is a new data document "Conference" to share information about conference participants. It is to be sent to the server, containing an Id - for this conference, and the user ids for the conference participants. + for this conference, and the session ids for the conference participants. Once a client recieves such a Conference document, it has to check state - for all User ids in the Conference document Conference list like this: + for all session ids in the Conference document Conference list like this: - If not in a call already -> ignore. - If in a call, and own Id is not in the Conference list -> ignore. @@ -597,12 +601,12 @@ Conferences and how to use them "Type": "Conference", "Id": "the-conference-id", "Conference": [ - "user-a-id", - "user-b-id" + "session-a-id", + "session-b-id" ] } - Use Conference documents, to create a conference / add new users to a + Use Conference documents, to create a conference / add new session to a conference. The Id is to be generated by the client and needs to be unique. It From c7bddeda5f9ca359264acb24eca6ecdc6dacf823 Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Sun, 20 Apr 2014 21:11:29 +0200 Subject: [PATCH 04/48] Renamed everything which was named user to session in preperation for user implementation. --- .../{wsdata.go => channeling.go} | 6 +- .../spreed-speakfreely-server/connection.go | 6 +- src/app/spreed-speakfreely-server/hub.go | 68 +++++++++---------- src/app/spreed-speakfreely-server/images.go | 30 ++++---- .../spreed-speakfreely-server/roomworker.go | 28 ++++---- src/app/spreed-speakfreely-server/server.go | 20 +++--- .../{user.go => session.go} | 37 +++++----- 7 files changed, 97 insertions(+), 98 deletions(-) rename src/app/spreed-speakfreely-server/{wsdata.go => channeling.go} (97%) rename src/app/spreed-speakfreely-server/{user.go => session.go} (72%) diff --git a/src/app/spreed-speakfreely-server/wsdata.go b/src/app/spreed-speakfreely-server/channeling.go similarity index 97% rename from src/app/spreed-speakfreely-server/wsdata.go rename to src/app/spreed-speakfreely-server/channeling.go index fe2691a8..65a8059c 100644 --- a/src/app/spreed-speakfreely-server/wsdata.go +++ b/src/app/spreed-speakfreely-server/channeling.go @@ -61,7 +61,7 @@ type DataTurn struct { Urls []string `json:"urls"` } -type DataUser struct { +type DataSession struct { Type string Id string Ua string @@ -120,9 +120,9 @@ type DataOutgoing struct { To string } -type DataUsers struct { +type DataSessions struct { Type string - Users []*DataUser + Users []*DataSession Index uint64 Batch uint64 } diff --git a/src/app/spreed-speakfreely-server/connection.go b/src/app/spreed-speakfreely-server/connection.go index cd667c73..2d58ac76 100644 --- a/src/app/spreed-speakfreely-server/connection.go +++ b/src/app/spreed-speakfreely-server/connection.go @@ -66,9 +66,9 @@ type Connection struct { // Metadata. Id string - Roomid string // Keep Roomid here for quick acess without locking c.User. + Roomid string // Keep Roomid here for quick acess without locking c.Session. Idx uint64 - User *User + Session *Session IsRegistered bool Hello bool Version string @@ -93,7 +93,7 @@ func (c *Connection) close() { if !c.isClosed { c.ws.Close() c.mutex.Lock() - c.User = nil + c.Session = nil c.isClosed = true for { head := c.queue.Front() diff --git a/src/app/spreed-speakfreely-server/hub.go b/src/app/spreed-speakfreely-server/hub.go index 0c865495..dd4eb7dd 100644 --- a/src/app/spreed-speakfreely-server/hub.go +++ b/src/app/spreed-speakfreely-server/hub.go @@ -51,21 +51,21 @@ type MessageRequest struct { } type HubStat struct { - Rooms int `json:"rooms"` - Connections int `json:"connections"` - Users int `json:"users"` - Count uint64 `json:"count"` - BroadcastChatMessages uint64 `json:"broadcastchatmessages"` - UnicastChatMessages uint64 `json:"unicastchatmessages"` - IdsInRoom map[string][]string `json:"idsinroom,omitempty"` - UsersById map[string]*DataUser `json:"usersbyid,omitempty"` - ConnectionsByIdx map[string]string `json:"connectionsbyidx,omitempty"` + Rooms int `json:"rooms"` + Connections int `json:"connections"` + Sessions int `json:"sessions"` + Count uint64 `json:"count"` + BroadcastChatMessages uint64 `json:"broadcastchatmessages"` + UnicastChatMessages uint64 `json:"unicastchatmessages"` + IdsInRoom map[string][]string `json:"idsinroom,omitempty"` + SessionsById map[string]*DataSession `json:"sessionsbyid,omitempty"` + ConnectionsByIdx map[string]string `json:"connectionsbyidx,omitempty"` } type Hub struct { server *Server connectionTable map[string]*Connection - userTable map[string]*User + sessionTable map[string]*Session roomTable map[string]*RoomWorker version string config *Config @@ -84,7 +84,7 @@ func NewHub(version string, config *Config, sessionSecret, turnSecret string) *H h := &Hub{ connectionTable: make(map[string]*Connection), - userTable: make(map[string]*User), + sessionTable: make(map[string]*Session), roomTable: make(map[string]*RoomWorker), version: version, config: config, @@ -105,7 +105,7 @@ func (h *Hub) Stat(details bool) *HubStat { stat := &HubStat{ Rooms: len(h.roomTable), Connections: len(h.connectionTable), - Users: len(h.userTable), + Sessions: len(h.sessionTable), Count: h.count, BroadcastChatMessages: atomic.LoadUint64(&h.broadcastChatMessages), UnicastChatMessages: atomic.LoadUint64(&h.unicastChatMessages), @@ -113,18 +113,18 @@ func (h *Hub) Stat(details bool) *HubStat { if details { rooms := make(map[string][]string) for roomid, room := range h.roomTable { - users := make([]string, 0, len(room.connections)) + sessions := make([]string, 0, len(room.connections)) for id := range room.connections { - users = append(users, id) + sessions = append(sessions, id) } - rooms[roomid] = users + rooms[roomid] = sessions } stat.IdsInRoom = rooms - users := make(map[string]*DataUser) - for userid, user := range h.userTable { - users[userid] = user.Data() + sessions := make(map[string]*DataSession) + for sessionid, session := range h.sessionTable { + sessions[sessionid] = session.Data() } - stat.UsersById = users + stat.SessionsById = sessions connections := make(map[string]string) for id, connection := range h.connectionTable { connections[fmt.Sprintf("%d", connection.Idx)] = id @@ -248,12 +248,12 @@ func (h *Hub) registerHandler(c *Connection) { h.mutex.Lock() - // Create new user instance. + // Create new session instance. h.count++ c.Idx = h.count - u := &User{Id: c.Id} - h.userTable[c.Id] = u - c.User = u + s := &Session{Id: c.Id} + h.sessionTable[c.Id] = s + c.Session = s c.IsRegistered = true // Register connection or replace existing one. @@ -281,13 +281,13 @@ func (h *Hub) unregisterHandler(c *Connection) { h.mutex.Unlock() return } - user := c.User + session := c.Session c.close() delete(h.connectionTable, c.Id) - delete(h.userTable, c.Id) + delete(h.sessionTable, c.Id) h.mutex.Unlock() - if user != nil { - h.buddyImages.DeleteUserImage(user.Id) + if session != nil { + h.buddyImages.Delete(session.Id) } //log.Printf("Unregister (%d) from %s: %s\n", c.Idx, c.RemoteAddr, c.Id) h.server.OnUnregister(c) @@ -322,21 +322,21 @@ func (h *Hub) aliveHandler(c *Connection, alive *DataAlive) { } -func (h *Hub) userupdateHandler(u *UserUpdate) uint64 { +func (h *Hub) sessionupdateHandler(s *SessionUpdate) uint64 { //fmt.Println("Userupdate", u) h.mutex.RLock() - user, ok := h.userTable[u.Id] + session, ok := h.sessionTable[s.Id] h.mutex.RUnlock() var rev uint64 if ok { - rev = user.Update(u) - if u.Status != nil { - status, ok := u.Status.(map[string]interface{}) + rev = session.Update(s) + if s.Status != nil { + status, ok := s.Status.(map[string]interface{}) if ok && status["buddyPicture"] != nil { pic := status["buddyPicture"].(string) if strings.HasPrefix(pic, "data:") { - imageId := h.buddyImages.Update(u.Id, pic[5:]) + imageId := h.buddyImages.Update(s.Id, pic[5:]) if imageId != "" { status["buddyPicture"] = "img:" + imageId } @@ -344,7 +344,7 @@ func (h *Hub) userupdateHandler(u *UserUpdate) uint64 { } } } else { - log.Printf("Update data for unknown user %s\n", u.Id) + log.Printf("Update data for unknown user %s\n", s.Id) } return rev diff --git a/src/app/spreed-speakfreely-server/images.go b/src/app/spreed-speakfreely-server/images.go index 2aaa666a..ef1b9a2f 100644 --- a/src/app/spreed-speakfreely-server/images.go +++ b/src/app/spreed-speakfreely-server/images.go @@ -38,29 +38,29 @@ type Image struct { updateIdx int lastChange time.Time lastChangeId string - userid string + sessionid string mimetype string data []byte } type ImageCache interface { - Update(userId string, image string) string + Update(sessionId string, image string) string Get(imageId string) *Image - DeleteUserImage(userId string) + Delete(sessionId string) } type imageCache struct { - images map[string]*Image - userImages map[string]string - mutex sync.RWMutex + images map[string]*Image + sessionImages map[string]string + mutex sync.RWMutex } func NewImageCache() ImageCache { result := &imageCache{} result.images = make(map[string]*Image) - result.userImages = make(map[string]string) + result.sessionImages = make(map[string]string) if imageFilenames == nil { imageFilenames = map[string]string{ "image/png": "picture.png", @@ -71,7 +71,7 @@ func NewImageCache() ImageCache { return result } -func (self *imageCache) Update(userId string, image string) string { +func (self *imageCache) Update(sessionId string, image string) string { mimetype := "image/x-unknown" pos := strings.Index(image, ";") if pos != -1 { @@ -98,7 +98,7 @@ func (self *imageCache) Update(userId string, image string) string { } var img *Image self.mutex.RLock() - result, ok := self.userImages[userId] + result, ok := self.sessionImages[sessionId] if !ok { self.mutex.RUnlock() imageId := make([]byte, 15, 15) @@ -106,11 +106,11 @@ func (self *imageCache) Update(userId string, image string) string { return "" } result = base64.URLEncoding.EncodeToString(imageId) - img = &Image{userid: userId} + img = &Image{sessionid: sessionId} self.mutex.Lock() - resultTmp, ok := self.userImages[userId] + resultTmp, ok := self.sessionImages[sessionId] if !ok { - self.userImages[userId] = result + self.sessionImages[sessionId] = result self.images[result] = img } else { result = resultTmp @@ -145,11 +145,11 @@ func (self *imageCache) Get(imageId string) *Image { return image } -func (self *imageCache) DeleteUserImage(userId string) { +func (self *imageCache) Delete(sessionId string) { self.mutex.Lock() - imageId, ok := self.userImages[userId] + imageId, ok := self.sessionImages[sessionId] if ok { - delete(self.userImages, userId) + delete(self.sessionImages, sessionId) delete(self.images, imageId) } self.mutex.Unlock() diff --git a/src/app/spreed-speakfreely-server/roomworker.go b/src/app/spreed-speakfreely-server/roomworker.go index c95da059..bf76232f 100644 --- a/src/app/spreed-speakfreely-server/roomworker.go +++ b/src/app/spreed-speakfreely-server/roomworker.go @@ -35,7 +35,7 @@ const ( type RoomConnectionUpdate struct { Id string - Userid string + Sessionid string Status bool Connection *Connection } @@ -134,15 +134,15 @@ func (r *RoomWorker) Run(f func()) bool { func (r *RoomWorker) usersHandler(c *Connection) { worker := func() { - users := &DataUsers{Type: "Users"} - var ul []*DataUser + users := &DataSessions{Type: "Users"} + var sl []*DataSession appender := func(ec *Connection) bool { - ecuser := ec.User - if ecuser != nil { - user := ecuser.Data() - user.Type = "Online" - ul = append(ul, user) - if len(ul) > maxUsersLength { + ecsession := ec.Session + if ecsession != nil { + session := ecsession.Data() + session.Type = "Online" + sl = append(sl, session) + if len(sl) > maxUsersLength { log.Println("Limiting users response length in channel", r.Id) return false } @@ -150,7 +150,7 @@ func (r *RoomWorker) usersHandler(c *Connection) { return true } r.mutex.RLock() - ul = make([]*DataUser, 0, len(r.connections)) + sl = make([]*DataSession, 0, len(r.connections)) // Include connections in this room. for _, ec := range r.connections { if !appender(ec) { @@ -164,7 +164,7 @@ func (r *RoomWorker) usersHandler(c *Connection) { break } } - users.Users = ul + users.Users = sl usersJson := c.h.buffers.New() encoder := json.NewEncoder(usersJson) err := encoder.Encode(&DataOutgoing{From: c.Id, Data: users}) @@ -209,10 +209,10 @@ func (r *RoomWorker) connectionHandler(rcu *RoomConnectionUpdate) { r.mutex.Lock() defer r.mutex.Unlock() if rcu.Status { - r.connections[rcu.Userid] = rcu.Connection + r.connections[rcu.Sessionid] = rcu.Connection } else { - if _, ok := r.connections[rcu.Userid]; ok { - delete(r.connections, rcu.Userid) + if _, ok := r.connections[rcu.Sessionid]; ok { + delete(r.connections, rcu.Sessionid) } } } diff --git a/src/app/spreed-speakfreely-server/server.go b/src/app/spreed-speakfreely-server/server.go index 054eae87..c739fb62 100644 --- a/src/app/spreed-speakfreely-server/server.go +++ b/src/app/spreed-speakfreely-server/server.go @@ -49,7 +49,7 @@ func (s *Server) OnUnregister(c *Connection) { //log.Println("OnUnregister", c.id) if c.Hello { s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid}) - s.Broadcast(c, &DataUser{Type: "Left", Id: c.Id, Status: "hard"}) + s.Broadcast(c, &DataSession{Type: "Left", Id: c.Id, Status: "hard"}) } else { //log.Println("Ingoring OnUnregister because of no Hello", c.Idx) } @@ -72,17 +72,17 @@ func (s *Server) OnText(c *Connection, b Buffer) { case "Hello": //log.Println("Hello", msg.Hello, c.Idx) // TODO(longsleep): Filter room id and user agent. - s.UpdateUser(c, &UserUpdate{Types: []string{"Roomid", "Ua"}, Roomid: msg.Hello.Id, Ua: msg.Hello.Ua}) + s.UpdateSession(c, &SessionUpdate{Types: []string{"Roomid", "Ua"}, Roomid: msg.Hello.Id, Ua: msg.Hello.Ua}) if c.Hello && c.Roomid != msg.Hello.Id { // Room changed. s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid}) - s.Broadcast(c, &DataUser{Type: "Left", Id: c.Id, Status: "soft"}) + s.Broadcast(c, &DataSession{Type: "Left", Id: c.Id, Status: "soft"}) } c.Roomid = msg.Hello.Id if c.h.config.defaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) { c.Hello = true s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid, Status: true}) - s.Broadcast(c, &DataUser{Type: "Joined", Id: c.Id, Ua: msg.Hello.Ua}) + s.Broadcast(c, &DataSession{Type: "Joined", Id: c.Id, Ua: msg.Hello.Ua}) } else { c.Hello = false } @@ -103,9 +103,9 @@ func (s *Server) OnText(c *Connection, b Buffer) { s.Unicast(c, msg.Bye.To, msg.Bye) case "Status": //log.Println("Status", msg.Status) - rev := s.UpdateUser(c, &UserUpdate{Types: []string{"Status"}, Status: msg.Status.Status}) + rev := s.UpdateSession(c, &SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status}) if c.h.config.defaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) { - s.Broadcast(c, &DataUser{Type: "Status", Id: c.Id, Status: msg.Status.Status, Rev: rev}) + s.Broadcast(c, &DataSession{Type: "Status", Id: c.Id, Status: msg.Status.Status, Rev: rev}) } case "Chat": // TODO(longsleep): Limit sent chat messages per incoming connection. @@ -170,10 +170,10 @@ func (s *Server) Alive(c *Connection, alive *DataAlive) { } -func (s *Server) UpdateUser(c *Connection, userupdate *UserUpdate) uint64 { +func (s *Server) UpdateSession(c *Connection, su *SessionUpdate) uint64 { - userupdate.Id = c.Id - return c.h.userupdateHandler(userupdate) + su.Id = c.Id + return c.h.sessionupdateHandler(su) } @@ -211,7 +211,7 @@ func (s *Server) Users(c *Connection) { func (s *Server) UpdateRoomConnection(c *Connection, rcu *RoomConnectionUpdate) { - rcu.Userid = c.Id + rcu.Sessionid = c.Id rcu.Connection = c room := c.h.GetRoom(c.Roomid) room.connectionHandler(rcu) diff --git a/src/app/spreed-speakfreely-server/user.go b/src/app/spreed-speakfreely-server/session.go similarity index 72% rename from src/app/spreed-speakfreely-server/user.go rename to src/app/spreed-speakfreely-server/session.go index ac40823e..d19f994c 100644 --- a/src/app/spreed-speakfreely-server/user.go +++ b/src/app/spreed-speakfreely-server/session.go @@ -25,7 +25,7 @@ import ( "sync" ) -type User struct { +type Session struct { Id string Roomid string Ua string @@ -34,46 +34,45 @@ type User struct { mutex sync.RWMutex } -func (u *User) Update(update *UserUpdate) uint64 { +func (s *Session) Update(update *SessionUpdate) uint64 { - //user := reflect.ValueOf(&u).Elem() - u.mutex.Lock() - defer u.mutex.Unlock() + s.mutex.Lock() + defer s.mutex.Unlock() for _, key := range update.Types { //fmt.Println("type update", key) switch key { case "Roomid": - u.Roomid = update.Roomid + s.Roomid = update.Roomid case "Ua": - u.Ua = update.Ua + s.Ua = update.Ua case "Status": - u.Status = update.Status + s.Status = update.Status } } - u.UpdateRev++ - return u.UpdateRev + s.UpdateRev++ + return s.UpdateRev } -func (u *User) Data() *DataUser { +func (s *Session) Data() *DataSession { - u.mutex.RLock() - defer u.mutex.RUnlock() + s.mutex.RLock() + defer s.mutex.RUnlock() - return &DataUser{ - Id: u.Id, - Ua: u.Ua, - Status: u.Status, - Rev: u.UpdateRev, + return &DataSession{ + Id: s.Id, + Ua: s.Ua, + Status: s.Status, + Rev: s.UpdateRev, } } -type UserUpdate struct { +type SessionUpdate struct { Id string Types []string Roomid string From 8953a1a76e1758655b37ec1e03f7313f9746f2d8 Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Sun, 20 Apr 2014 21:19:55 +0200 Subject: [PATCH 05/48] Handle users request with sessions. --- src/app/spreed-speakfreely-server/roomworker.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/spreed-speakfreely-server/roomworker.go b/src/app/spreed-speakfreely-server/roomworker.go index bf76232f..568546ea 100644 --- a/src/app/spreed-speakfreely-server/roomworker.go +++ b/src/app/spreed-speakfreely-server/roomworker.go @@ -134,7 +134,7 @@ func (r *RoomWorker) Run(f func()) bool { func (r *RoomWorker) usersHandler(c *Connection) { worker := func() { - users := &DataSessions{Type: "Users"} + sessions := &DataSessions{Type: "Users"} var sl []*DataSession appender := func(ec *Connection) bool { ecsession := ec.Session @@ -164,17 +164,17 @@ func (r *RoomWorker) usersHandler(c *Connection) { break } } - users.Users = sl - usersJson := c.h.buffers.New() - encoder := json.NewEncoder(usersJson) - err := encoder.Encode(&DataOutgoing{From: c.Id, Data: users}) + sessions.Users = sl + sessionsJson := c.h.buffers.New() + encoder := json.NewEncoder(sessionsJson) + err := encoder.Encode(&DataOutgoing{From: c.Id, Data: sessions}) if err != nil { log.Println("Users error while encoding JSON", err) - usersJson.Decref() + sessionsJson.Decref() return } - c.send(usersJson) - usersJson.Decref() + c.send(sessionsJson) + sessionsJson.Decref() } From 1195b39b1197d1b619514591ef659b4eb5436e9a Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Sun, 20 Apr 2014 23:55:55 +0200 Subject: [PATCH 06/48] Added Userid fields to DataSession and DataSelf JSON documents. --- doc/CHANNELING-API.txt | 25 ++++++++++++------- .../spreed-speakfreely-server/channeling.go | 6 +++-- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/doc/CHANNELING-API.txt b/doc/CHANNELING-API.txt index afe94da1..dcfc704a 100644 --- a/doc/CHANNELING-API.txt +++ b/doc/CHANNELING-API.txt @@ -1,5 +1,5 @@ - Spreed Speak Freely Channeling API v1.2.0 + Spreed Speak Freely Channeling API v1.3.0 ================================================= (c)2014 struktur AG @@ -85,6 +85,7 @@ Special purpose documents for channling { "Type": "Self", "Id": "4", + "Userid": "", "Token": "some-very-long-string", "Version": "server-version-number", "Turn": { @@ -107,7 +108,8 @@ Special purpose documents for channling Keys: Type : Self (string) - Id : Channel id for this connection (string) + Id : Channel id for this connection (string). + Userid : User id if this session belongs to an authenticated user. Else empty. Token : Security token (string), to restablish connection with the same session. Pass the value as URL query parameter t, to the websocket URL. Version: Server version number. Use this to detect server upgrades. @@ -259,9 +261,7 @@ Additional types for session listing and notifications { "Type": "Left", - "Id": "5", - "Ua": "", - "Status": null + "Id": "5" } Joined @@ -269,10 +269,13 @@ Additional types for session listing and notifications { "Type": "Joined", "Id": "7", + "Userid": "u7", "Ua": "Chrome 28", "Status": null } + Note: The Userid field is only present if that session belongs to a known user. + Status { @@ -313,26 +316,30 @@ Additional types for session listing and notifications { "Type": "Online", "Id": "1", - "Ua": "webrtc-publisher", - "Status": null + "Ua": "Firefox 27", + "Status": {...} }, { "Type": "Online", "Id": "3", + "Userid": "u3", "Ua": "Chrome 28", - "Status": null + "Status": {...} }, { "Type": "Online", "Id": "4", + "Userid": "u4", "Ua": "Chrome 28", - "Status": null + "Status": {...} } ], "Index": 0, "Batch": 0 } + Note: The Userid field is only present, if that session belongs to a known user. + Alive { diff --git a/src/app/spreed-speakfreely-server/channeling.go b/src/app/spreed-speakfreely-server/channeling.go index 65a8059c..b73068ae 100644 --- a/src/app/spreed-speakfreely-server/channeling.go +++ b/src/app/spreed-speakfreely-server/channeling.go @@ -48,6 +48,7 @@ type DataAnswer struct { type DataSelf struct { Type string Id string + Userid string Token string Version string Turn *DataTurn @@ -64,9 +65,10 @@ type DataTurn struct { type DataSession struct { Type string Id string + Userid string `json:"Userid,omitempty"` Ua string - Token string - Version string + Token string `json:"Token,omitempty"` + Version string `json:"Version,omitempty"` Rev uint64 Status interface{} } From c5bd56c718cbc276d90def2f508cc9f8e28110db Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Mon, 21 Apr 2014 16:54:22 +0200 Subject: [PATCH 07/48] Cleaned up token and session id generation in preperation for userids. --- server.conf.in | 2 +- .../spreed-speakfreely-server/connection.go | 11 ++-- src/app/spreed-speakfreely-server/hub.go | 51 +++++++++++-------- src/app/spreed-speakfreely-server/server.go | 4 +- src/app/spreed-speakfreely-server/session.go | 16 ++++++ 5 files changed, 55 insertions(+), 29 deletions(-) diff --git a/server.conf.in b/server.conf.in index 8c8388f9..0f529dc1 100644 --- a/server.conf.in +++ b/server.conf.in @@ -24,7 +24,7 @@ listen = 127.0.0.1:8080 #stunURIs = stun.l.google.com:19302 #turnURIs = turn:turnserver:port?transport=udp turn:anotherturnserver:port?transport=tcp turns:turnserver:443?transport=tcp #turnSecret = the-default-turn-shared-secret-do-not-keep -sessionSecret = the-default-secret-do-not-keep +sessionSecret = the-default-secret-do-not-keep-me # Use 32 or 64 bytes random data #tokenFile = tokens.txt # If set, everyone needs to give one of the tokens to launch the web client. One token per line in the file. #globalRoom = global # Enables a global room. Users in that room are in all rooms. #defaultRoomEnabled = true # Set to false to disable default room. diff --git a/src/app/spreed-speakfreely-server/connection.go b/src/app/spreed-speakfreely-server/connection.go index 2d58ac76..db7a0550 100644 --- a/src/app/spreed-speakfreely-server/connection.go +++ b/src/app/spreed-speakfreely-server/connection.go @@ -112,24 +112,25 @@ func (c *Connection) close() { func (c *Connection) register() error { - id, err := c.h.EncodeTicket("id", "") + id, err := c.h.CreateSessionid() if err != nil { log.Println("Failed to create new Id while register", err) return err } c.Id = id - //log.Println("Created new id", id) + log.Println("Created new id", len(id), id) c.h.registerHandler(c) return nil } func (c *Connection) reregister(token string) error { - if id, err := c.h.DecodeTicket("token", token); err == nil { - c.Id = id + if st, err := c.h.DecodeSessionToken(token); err == nil { + c.Id = st.Id c.h.registerHandler(c) + c.Session.Apply(st) } else { - log.Println("Error while decoding token", err) + log.Println("Error while decoding session token", err) c.register() } return nil diff --git a/src/app/spreed-speakfreely-server/hub.go b/src/app/spreed-speakfreely-server/hub.go index dd4eb7dd..9497b60a 100644 --- a/src/app/spreed-speakfreely-server/hub.go +++ b/src/app/spreed-speakfreely-server/hub.go @@ -92,6 +92,10 @@ func NewHub(version string, config *Config, sessionSecret, turnSecret string) *H turnSecret: []byte(turnSecret), } + if len(h.sessionSecret) < 32 { + log.Printf("Weak sessionSecret (only %d bytes). It is recommended to use a key with 32 or 64 bytes.\n", len(h.sessionSecret)) + } + h.tickets = securecookie.New(h.sessionSecret, nil) h.buffers = NewBufferCache(1024, bytes.MinRead) h.buddyImages = NewImageCache() @@ -155,21 +159,27 @@ func (h *Hub) CreateTurnData(id string) *DataTurn { } -func (h *Hub) EncodeTicket(key, value string) (string, error) { +func (h *Hub) CreateSessionid() (string, error) { - if value == "" { - // Create new id. - value = fmt.Sprintf("%s", securecookie.GenerateRandomKey(16)) - } - return h.tickets.Encode(key, value) + // NOTE(longsleep): Is it required to make this a secure cookie, + // random data in itself should be sufficent if we do not validate + // session ids somewhere? + value := fmt.Sprintf("%s", securecookie.GenerateRandomKey(32)) + return h.tickets.Encode("id", value) + +} + +func (h *Hub) EncodeSessionToken(st *SessionToken) (string, error) { + + return h.tickets.Encode("token", st) } -func (h *Hub) DecodeTicket(key, value string) (string, error) { +func (h *Hub) DecodeSessionToken(token string) (*SessionToken, error) { - result := "" - err := h.tickets.Decode(key, value, &result) - return result, err + st := &SessionToken{} + err := h.tickets.Decode("token", token, st) + return st, err } @@ -180,8 +190,8 @@ func (h *Hub) GetRoom(id string) *RoomWorker { if !ok { h.mutex.RUnlock() h.mutex.Lock() - // need to re-check, another thread might have created the room - // while we waited for the lock + // Need to re-check, another thread might have created the room + // while we waited for the lock. room, ok = h.roomTable[id] if !ok { room = NewRoomWorker(h, id) @@ -252,26 +262,23 @@ func (h *Hub) registerHandler(c *Connection) { h.count++ c.Idx = h.count s := &Session{Id: c.Id} - h.sessionTable[c.Id] = s c.Session = s c.IsRegistered = true // Register connection or replace existing one. if ec, ok := h.connectionTable[c.Id]; ok { - delete(h.connectionTable, ec.Id) ec.IsRegistered = false ec.close() - h.connectionTable[c.Id] = c - h.mutex.Unlock() //log.Printf("Register (%d) from %s: %s (existing)\n", c.Idx, c.RemoteAddr, c.Id) - } else { - h.connectionTable[c.Id] = c - //fmt.Println("registered", c.Id) - h.mutex.Unlock() - //log.Printf("Register (%d) from %s: %s\n", c.Idx, c.RemoteAddr, c.Id) - h.server.OnRegister(c) } + h.connectionTable[c.Id] = c + h.sessionTable[c.Id] = s + //fmt.Println("registered", c.Id) + h.mutex.Unlock() + //log.Printf("Register (%d) from %s: %s\n", c.Idx, c.RemoteAddr, c.Id) + h.server.OnRegister(c) + } func (h *Hub) unregisterHandler(c *Connection) { diff --git a/src/app/spreed-speakfreely-server/server.go b/src/app/spreed-speakfreely-server/server.go index c739fb62..fb660a1e 100644 --- a/src/app/spreed-speakfreely-server/server.go +++ b/src/app/spreed-speakfreely-server/server.go @@ -37,7 +37,9 @@ type Server struct { func (s *Server) OnRegister(c *Connection) { //log.Println("OnRegister", c.id) - if token, err := c.h.EncodeTicket("token", c.Id); err == nil { + st := &SessionToken{Id: c.Id} + if token, err := c.h.EncodeSessionToken(st); err == nil { + log.Println("Created new session token", len(token), token) // Send stuff back. s.Unicast(c, c.Id, &DataSelf{Type: "Self", Id: c.Id, Token: token, Version: c.h.version, Turn: c.h.CreateTurnData(c.Id), Stun: c.h.config.StunURIs}) } else { diff --git a/src/app/spreed-speakfreely-server/session.go b/src/app/spreed-speakfreely-server/session.go index d19f994c..2e702acb 100644 --- a/src/app/spreed-speakfreely-server/session.go +++ b/src/app/spreed-speakfreely-server/session.go @@ -27,6 +27,7 @@ import ( type Session struct { Id string + Userid string Roomid string Ua string UpdateRev uint64 @@ -58,6 +59,15 @@ func (s *Session) Update(update *SessionUpdate) uint64 { } +func (s *Session) Apply(st *SessionToken) { + + s.mutex.Lock() + defer s.mutex.Unlock() + s.Id = st.Id + s.Userid = st.Userid + +} + func (s *Session) Data() *DataSession { s.mutex.RLock() @@ -65,6 +75,7 @@ func (s *Session) Data() *DataSession { return &DataSession{ Id: s.Id, + Userid: s.Userid, Ua: s.Ua, Status: s.Status, Rev: s.UpdateRev, @@ -79,3 +90,8 @@ type SessionUpdate struct { Ua string Status interface{} } + +type SessionToken struct { + Id string + Userid string +} From 41cb800c1583d1168c6a7eb647087da1f1d9a3a6 Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Mon, 21 Apr 2014 22:22:07 +0200 Subject: [PATCH 08/48] Make sure all session data is available early enough. --- .../spreed-speakfreely-server/connection.go | 15 +++-------- src/app/spreed-speakfreely-server/hub.go | 25 +++++++++++++------ src/app/spreed-speakfreely-server/server.go | 13 +++++++--- src/app/spreed-speakfreely-server/session.go | 4 +++ 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/app/spreed-speakfreely-server/connection.go b/src/app/spreed-speakfreely-server/connection.go index db7a0550..d8191511 100644 --- a/src/app/spreed-speakfreely-server/connection.go +++ b/src/app/spreed-speakfreely-server/connection.go @@ -112,23 +112,16 @@ func (c *Connection) close() { func (c *Connection) register() error { - id, err := c.h.CreateSessionid() - if err != nil { - log.Println("Failed to create new Id while register", err) - return err - } - c.Id = id - log.Println("Created new id", len(id), id) - c.h.registerHandler(c) + s := c.h.CreateSession(nil) + c.h.registerHandler(c, s) return nil } func (c *Connection) reregister(token string) error { if st, err := c.h.DecodeSessionToken(token); err == nil { - c.Id = st.Id - c.h.registerHandler(c) - c.Session.Apply(st) + s := c.h.CreateSession(st) + c.h.registerHandler(c, s) } else { log.Println("Error while decoding session token", err) c.register() diff --git a/src/app/spreed-speakfreely-server/hub.go b/src/app/spreed-speakfreely-server/hub.go index 9497b60a..5af3705c 100644 --- a/src/app/spreed-speakfreely-server/hub.go +++ b/src/app/spreed-speakfreely-server/hub.go @@ -159,13 +159,22 @@ func (h *Hub) CreateTurnData(id string) *DataTurn { } -func (h *Hub) CreateSessionid() (string, error) { +func (h *Hub) CreateSession(st *SessionToken) *Session { // NOTE(longsleep): Is it required to make this a secure cookie, // random data in itself should be sufficent if we do not validate // session ids somewhere? - value := fmt.Sprintf("%s", securecookie.GenerateRandomKey(32)) - return h.tickets.Encode("id", value) + + session := &Session{} + + if st == nil { + session.Id, _ = h.tickets.Encode("id", fmt.Sprintf("%s", securecookie.GenerateRandomKey(32))) + log.Println("Created new session id", len(session.Id), session.Id) + } else { + session.Apply(st) + } + + return session } @@ -254,15 +263,17 @@ func (h *Hub) isDefaultRoomid(id string) bool { return id == "" } -func (h *Hub) registerHandler(c *Connection) { +func (h *Hub) registerHandler(c *Connection, s *Session) { + + // Apply session to connection. + c.Id = s.Id + c.Session = s h.mutex.Lock() - // Create new session instance. + // Set flags. h.count++ c.Idx = h.count - s := &Session{Id: c.Id} - c.Session = s c.IsRegistered = true // Register connection or replace existing one. diff --git a/src/app/spreed-speakfreely-server/server.go b/src/app/spreed-speakfreely-server/server.go index fb660a1e..35d8e3da 100644 --- a/src/app/spreed-speakfreely-server/server.go +++ b/src/app/spreed-speakfreely-server/server.go @@ -37,11 +37,18 @@ type Server struct { func (s *Server) OnRegister(c *Connection) { //log.Println("OnRegister", c.id) - st := &SessionToken{Id: c.Id} - if token, err := c.h.EncodeSessionToken(st); err == nil { + if token, err := c.h.EncodeSessionToken(c.Session.Token()); err == nil { log.Println("Created new session token", len(token), token) // Send stuff back. - s.Unicast(c, c.Id, &DataSelf{Type: "Self", Id: c.Id, Token: token, Version: c.h.version, Turn: c.h.CreateTurnData(c.Id), Stun: c.h.config.StunURIs}) + s.Unicast(c, c.Id, &DataSelf{ + Type: "Self", + Id: c.Id, + Userid: c.Session.Userid, + Token: token, + Version: c.h.version, + Turn: c.h.CreateTurnData(c.Id), + Stun: c.h.config.StunURIs, + }) } else { log.Println("Error in OnRegister", c.Idx, err) } diff --git a/src/app/spreed-speakfreely-server/session.go b/src/app/spreed-speakfreely-server/session.go index 2e702acb..b2b597a7 100644 --- a/src/app/spreed-speakfreely-server/session.go +++ b/src/app/spreed-speakfreely-server/session.go @@ -68,6 +68,10 @@ func (s *Session) Apply(st *SessionToken) { } +func (s *Session) Token() *SessionToken { + return &SessionToken{Id: s.Id, Userid: s.Userid} +} + func (s *Session) Data() *DataSession { s.mutex.RLock() From dfba1f564067ce14abf79c07475f50c2c89e1cc4 Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Tue, 22 Apr 2014 17:23:21 +0200 Subject: [PATCH 09/48] Added non public session id to prepare for authorization. --- doc/CHANNELING-API.txt | 4 +++- src/app/spreed-speakfreely-server/channeling.go | 1 + src/app/spreed-speakfreely-server/hub.go | 10 ++++++---- src/app/spreed-speakfreely-server/random.go | 17 +---------------- src/app/spreed-speakfreely-server/rooms.go | 2 +- src/app/spreed-speakfreely-server/server.go | 1 + src/app/spreed-speakfreely-server/session.go | 14 +++++++++++++- 7 files changed, 26 insertions(+), 23 deletions(-) diff --git a/doc/CHANNELING-API.txt b/doc/CHANNELING-API.txt index dcfc704a..bbd61b7e 100644 --- a/doc/CHANNELING-API.txt +++ b/doc/CHANNELING-API.txt @@ -85,6 +85,7 @@ Special purpose documents for channling { "Type": "Self", "Id": "4", + "Sid": "5157", "Userid": "", "Token": "some-very-long-string", "Version": "server-version-number", @@ -108,7 +109,8 @@ Special purpose documents for channling Keys: Type : Self (string) - Id : Channel id for this connection (string). + Id : Public Session id for this connection (string). + Sid : Secure (non public) id for this session (string). Userid : User id if this session belongs to an authenticated user. Else empty. Token : Security token (string), to restablish connection with the same session. Pass the value as URL query parameter t, to the websocket URL. diff --git a/src/app/spreed-speakfreely-server/channeling.go b/src/app/spreed-speakfreely-server/channeling.go index b73068ae..d1e2e26b 100644 --- a/src/app/spreed-speakfreely-server/channeling.go +++ b/src/app/spreed-speakfreely-server/channeling.go @@ -48,6 +48,7 @@ type DataAnswer struct { type DataSelf struct { Type string Id string + Sid string Userid string Token string Version string diff --git a/src/app/spreed-speakfreely-server/hub.go b/src/app/spreed-speakfreely-server/hub.go index 5af3705c..8fffbe00 100644 --- a/src/app/spreed-speakfreely-server/hub.go +++ b/src/app/spreed-speakfreely-server/hub.go @@ -165,13 +165,15 @@ func (h *Hub) CreateSession(st *SessionToken) *Session { // random data in itself should be sufficent if we do not validate // session ids somewhere? - session := &Session{} + var session *Session if st == nil { - session.Id, _ = h.tickets.Encode("id", fmt.Sprintf("%s", securecookie.GenerateRandomKey(32))) - log.Println("Created new session id", len(session.Id), session.Id) + sid := NewRandomString(32) + id, _ := h.tickets.Encode("id", sid) + session = NewSession(id, sid, "") + log.Println("Created new session id", len(id), id, sid) } else { - session.Apply(st) + session = NewSession(st.Id, st.Sid, st.Userid) } return session diff --git a/src/app/spreed-speakfreely-server/random.go b/src/app/spreed-speakfreely-server/random.go index 66ee8300..2e44dd41 100644 --- a/src/app/spreed-speakfreely-server/random.go +++ b/src/app/spreed-speakfreely-server/random.go @@ -23,7 +23,6 @@ package main import ( "crypto/rand" - "encoding/base64" pseudoRand "math/rand" "time" ) @@ -32,7 +31,7 @@ const ( dict = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW0123456789" ) -func RandomString(length int) string { +func NewRandomString(length int) string { buf := make([]byte, length) _, err := rand.Read(buf) @@ -50,20 +49,6 @@ func RandomString(length int) string { } -func RandomUrlString(length int) string { - - buf := make([]byte, length) - _, err := rand.Read(buf) - if err != nil { - // fallback to pseudo-random - for i := 0; i < length; i++ { - buf[i] = byte(pseudoRand.Intn(256)) - } - } - return base64.URLEncoding.EncodeToString(buf) - -} - func init() { // Make sure to seed default random generator. pseudoRand.Seed(time.Now().UTC().UnixNano()) diff --git a/src/app/spreed-speakfreely-server/rooms.go b/src/app/spreed-speakfreely-server/rooms.go index f0522ca3..32655e90 100644 --- a/src/app/spreed-speakfreely-server/rooms.go +++ b/src/app/spreed-speakfreely-server/rooms.go @@ -37,7 +37,7 @@ type Rooms struct { func (rooms *Rooms) Post(values url.Values, headers http.Header) (int, interface{}, http.Header) { - name := RandomString(11) + name := NewRandomString(11) return 200, &Room{name, fmt.Sprintf("/%s", name)}, http.Header{"Content-Type": {"application/json"}} } diff --git a/src/app/spreed-speakfreely-server/server.go b/src/app/spreed-speakfreely-server/server.go index 35d8e3da..335f06c4 100644 --- a/src/app/spreed-speakfreely-server/server.go +++ b/src/app/spreed-speakfreely-server/server.go @@ -43,6 +43,7 @@ func (s *Server) OnRegister(c *Connection) { s.Unicast(c, c.Id, &DataSelf{ Type: "Self", Id: c.Id, + Sid: c.Session.Sid, Userid: c.Session.Userid, Token: token, Version: c.h.version, diff --git a/src/app/spreed-speakfreely-server/session.go b/src/app/spreed-speakfreely-server/session.go index b2b597a7..2e044abb 100644 --- a/src/app/spreed-speakfreely-server/session.go +++ b/src/app/spreed-speakfreely-server/session.go @@ -27,6 +27,7 @@ import ( type Session struct { Id string + Sid string Userid string Roomid string Ua string @@ -35,6 +36,16 @@ type Session struct { mutex sync.RWMutex } +func NewSession(id, sid, userid string) *Session { + + return &Session{ + Id: id, + Sid: sid, + Userid: userid, + } + +} + func (s *Session) Update(update *SessionUpdate) uint64 { s.mutex.Lock() @@ -69,7 +80,7 @@ func (s *Session) Apply(st *SessionToken) { } func (s *Session) Token() *SessionToken { - return &SessionToken{Id: s.Id, Userid: s.Userid} + return &SessionToken{Id: s.Id, Sid: s.Sid, Userid: s.Userid} } func (s *Session) Data() *DataSession { @@ -97,5 +108,6 @@ type SessionUpdate struct { type SessionToken struct { Id string + Sid string Userid string } From 00dbdcee0f715d5a3df378b6796533767df5939f Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Tue, 22 Apr 2014 19:07:13 +0200 Subject: [PATCH 10/48] Updated sleepy to https://github.com/strukturag/sleepy/tree/6c9daa1e5c0fc59a23ede2af426951ddd355556f and changed handlers to new API. --- src/app/spreed-speakfreely-server/rooms.go | 3 +- .../spreed-speakfreely-server/sleepy/core.go | 35 +++++++++++++++---- src/app/spreed-speakfreely-server/stats.go | 5 ++- src/app/spreed-speakfreely-server/tokens.go | 5 ++- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/app/spreed-speakfreely-server/rooms.go b/src/app/spreed-speakfreely-server/rooms.go index 32655e90..24693de9 100644 --- a/src/app/spreed-speakfreely-server/rooms.go +++ b/src/app/spreed-speakfreely-server/rooms.go @@ -24,7 +24,6 @@ package main import ( "fmt" "net/http" - "net/url" ) type Room struct { @@ -35,7 +34,7 @@ type Room struct { type Rooms struct { } -func (rooms *Rooms) Post(values url.Values, headers http.Header) (int, interface{}, http.Header) { +func (rooms *Rooms) Post(request *http.Request) (int, interface{}, http.Header) { name := NewRandomString(11) return 200, &Room{name, fmt.Sprintf("/%s", name)}, http.Header{"Content-Type": {"application/json"}} diff --git a/src/app/spreed-speakfreely-server/sleepy/core.go b/src/app/spreed-speakfreely-server/sleepy/core.go index b5d726c7..6bf03cf0 100644 --- a/src/app/spreed-speakfreely-server/sleepy/core.go +++ b/src/app/spreed-speakfreely-server/sleepy/core.go @@ -35,7 +35,6 @@ import ( "fmt" "github.com/gorilla/mux" "net/http" - "net/url" ) const ( @@ -43,30 +42,44 @@ const ( POST = "POST" PUT = "PUT" DELETE = "DELETE" + HEAD = "HEAD" + PATCH = "PATCH" ) // GetSupported is the interface that provides the Get // method a resource must support to receive HTTP GETs. type GetSupported interface { - Get(url.Values, http.Header) (int, interface{}, http.Header) + Get(*http.Request) (int, interface{}, http.Header) } // PostSupported is the interface that provides the Post // method a resource must support to receive HTTP POSTs. type PostSupported interface { - Post(url.Values, http.Header) (int, interface{}, http.Header) + Post(*http.Request) (int, interface{}, http.Header) } // PutSupported is the interface that provides the Put // method a resource must support to receive HTTP PUTs. type PutSupported interface { - Put(url.Values, http.Header) (int, interface{}, http.Header) + Put(*http.Request) (int, interface{}, http.Header) } // DeleteSupported is the interface that provides the Delete // method a resource must support to receive HTTP DELETEs. type DeleteSupported interface { - Delete(url.Values, http.Header) (int, interface{}, http.Header) + Delete(*http.Request) (int, interface{}, http.Header) +} + +// HeadSupported is the interface that provides the Head +// method a resource must support to receive HTTP HEADs. +type HeadSupported interface { + Head(*http.Request) (int, interface{}, http.Header) +} + +// PatchSupported is the interface that provides the Patch +// method a resource must support to receive HTTP PATCHs. +type PatchSupported interface { + Patch(*http.Request) (int, interface{}, http.Header) } // Interface for arbitrary muxer support (like http.ServeMux). @@ -99,7 +112,7 @@ func (api *API) requestHandler(resource interface{}) http.HandlerFunc { return } - var handler func(url.Values, http.Header) (int, interface{}, http.Header) + var handler func(*http.Request) (int, interface{}, http.Header) switch request.Method { case GET: @@ -118,6 +131,14 @@ func (api *API) requestHandler(resource interface{}) http.HandlerFunc { if resource, ok := resource.(DeleteSupported); ok { handler = resource.Delete } + case HEAD: + if resource, ok := resource.(HeadSupported); ok { + handler = resource.Head + } + case PATCH: + if resource, ok := resource.(PatchSupported); ok { + handler = resource.Patch + } } if handler == nil { @@ -125,7 +146,7 @@ func (api *API) requestHandler(resource interface{}) http.HandlerFunc { return } - code, data, header := handler(request.Form, request.Header) + code, data, header := handler(request) content, err := json.MarshalIndent(data, "", " ") if err != nil { diff --git a/src/app/spreed-speakfreely-server/stats.go b/src/app/spreed-speakfreely-server/stats.go index 5111afee..ff79138a 100644 --- a/src/app/spreed-speakfreely-server/stats.go +++ b/src/app/spreed-speakfreely-server/stats.go @@ -23,7 +23,6 @@ package main import ( "net/http" - "net/url" "runtime" "time" ) @@ -73,9 +72,9 @@ type Stats struct { hub *Hub } -func (stats *Stats) Get(values url.Values, headers http.Header) (int, interface{}, http.Header) { +func (stats *Stats) Get(request *http.Request) (int, interface{}, http.Header) { - details := values.Get("details") == "1" + details := request.Form.Get("details") == "1" return 200, NewStat(details, stats.hub), http.Header{"Content-Type": {"application/json; charset=utf-8"}, "Access-Control-Allow-Origin": {"*"}} } diff --git a/src/app/spreed-speakfreely-server/tokens.go b/src/app/spreed-speakfreely-server/tokens.go index 7f3f550b..78550446 100644 --- a/src/app/spreed-speakfreely-server/tokens.go +++ b/src/app/spreed-speakfreely-server/tokens.go @@ -24,7 +24,6 @@ package main import ( "log" "net/http" - "net/url" "strings" ) @@ -37,9 +36,9 @@ type Tokens struct { provider TokenProvider } -func (tokens Tokens) Post(values url.Values, headers http.Header) (int, interface{}, http.Header) { +func (tokens Tokens) Post(request *http.Request) (int, interface{}, http.Header) { - auth := values.Get("a") + auth := request.Form.Get("a") if len(auth) > 100 { return 413, NewApiError("auth_too_large", "Auth too large"), nil From 8a74cfb892ed79615ee164dc4442d80e9db33c53 Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Tue, 22 Apr 2014 22:52:31 +0200 Subject: [PATCH 11/48] Implemented user authorization and authentication api and data layers. --- doc/CHANNELING-API.txt | 26 +++++ .../spreed-speakfreely-server/channeling.go | 30 +++-- src/app/spreed-speakfreely-server/hub.go | 38 ++++++- src/app/spreed-speakfreely-server/main.go | 1 + src/app/spreed-speakfreely-server/server.go | 30 ++++- src/app/spreed-speakfreely-server/session.go | 107 +++++++++++++++++- src/app/spreed-speakfreely-server/sessions.go | 95 ++++++++++++++++ static/js/mediastream/api.js | 16 +++ static/js/services/mediastream.js | 49 ++++++++ 9 files changed, 373 insertions(+), 19 deletions(-) create mode 100644 src/app/spreed-speakfreely-server/sessions.go diff --git a/doc/CHANNELING-API.txt b/doc/CHANNELING-API.txt index bbd61b7e..b8c027fe 100644 --- a/doc/CHANNELING-API.txt +++ b/doc/CHANNELING-API.txt @@ -359,6 +359,32 @@ Additional types for session listing and notifications The Alive value is a timestamp integer in milliseconds (unix time). +User authorization and session authentication + + The channeling API supports an Authentication document to bind and existing + session to a given user. The required information to do this cannot be + received through the channeling API. It depends on the server configuration + how the Nonce and Userid are generated/validated. + + Authentication + + { + "Type": "Authentication", + "Authentication": { + "Userid": "53", + "Nonce": "nonce-for-this-session-and-userid" + } + } + + The Authentication document binds a userid to the current session. The + Nonce and Userid need to be validateable by the server. If Authentication + was successfull, a new Self document will be sent. The Nonce value can + be generated by using the REST API (sessions end point). + + There is no way to undo authentication for a session. For log out, close + the session (disconnect) and forget the token. + + Chat messages and status information The chat is used to transfer simple messages ore more complex structures diff --git a/src/app/spreed-speakfreely-server/channeling.go b/src/app/spreed-speakfreely-server/channeling.go index d1e2e26b..9f153813 100644 --- a/src/app/spreed-speakfreely-server/channeling.go +++ b/src/app/spreed-speakfreely-server/channeling.go @@ -67,10 +67,10 @@ type DataSession struct { Type string Id string Userid string `json:"Userid,omitempty"` - Ua string + Ua string `json:"Ua,omitempty"` Token string `json:"Token,omitempty"` Version string `json:"Version,omitempty"` - Rev uint64 + Rev uint64 `json:"Rev,omitempty"` Status interface{} } @@ -105,16 +105,17 @@ type DataChatMessageStatus struct { } type DataIncoming struct { - Type string - Hello *DataHello - Offer *DataOffer - Candidate *DataCandidate - Answer *DataAnswer - Bye *DataBye - Status *DataStatus - Chat *DataChat - Conference *DataConference - Alive *DataAlive + Type string + Hello *DataHello + Offer *DataOffer + Candidate *DataCandidate + Answer *DataAnswer + Bye *DataBye + Status *DataStatus + Chat *DataChat + Conference *DataConference + Alive *DataAlive + Authentication *DataAuthentication } type DataOutgoing struct { @@ -140,3 +141,8 @@ type DataAlive struct { Type string Alive uint64 } + +type DataAuthentication struct { + Type string + Authentication *SessionToken +} diff --git a/src/app/spreed-speakfreely-server/hub.go b/src/app/spreed-speakfreely-server/hub.go index 8fffbe00..09063cf2 100644 --- a/src/app/spreed-speakfreely-server/hub.go +++ b/src/app/spreed-speakfreely-server/hub.go @@ -28,6 +28,7 @@ import ( "crypto/sha256" "encoding/base64" "encoding/json" + "errors" "fmt" "github.com/gorilla/securecookie" "log" @@ -180,6 +181,22 @@ func (h *Hub) CreateSession(st *SessionToken) *Session { } +func (h *Hub) ValidateSession(id, sid string) bool { + + var decoded string + err := h.tickets.Decode("id", id, &decoded) + if err != nil { + log.Println("Session validation error", err, id, sid) + return false + } + if decoded != sid { + log.Println("Session validation failed", id, sid) + return false + } + return true + +} + func (h *Hub) EncodeSessionToken(st *SessionToken) (string, error) { return h.tickets.Encode("token", st) @@ -302,7 +319,6 @@ func (h *Hub) unregisterHandler(c *Connection) { return } session := c.Session - c.close() delete(h.connectionTable, c.Id) delete(h.sessionTable, c.Id) h.mutex.Unlock() @@ -311,6 +327,7 @@ func (h *Hub) unregisterHandler(c *Connection) { } //log.Printf("Unregister (%d) from %s: %s\n", c.Idx, c.RemoteAddr, c.Id) h.server.OnUnregister(c) + c.close() } @@ -369,3 +386,22 @@ func (h *Hub) sessionupdateHandler(s *SessionUpdate) uint64 { return rev } + +func (h *Hub) sessiontokenHandler(st *SessionToken) (string, error) { + + h.mutex.RLock() + c, ok := h.connectionTable[st.Id] + h.mutex.RUnlock() + + if !ok { + return "", errors.New("no such connection") + } + + nonce, err := c.Session.Authorize(st) + if err != nil { + return "", err + } + + return nonce, nil + +} diff --git a/src/app/spreed-speakfreely-server/main.go b/src/app/spreed-speakfreely-server/main.go index 5310362e..20ac1a39 100644 --- a/src/app/spreed-speakfreely-server/main.go +++ b/src/app/spreed-speakfreely-server/main.go @@ -343,6 +343,7 @@ func runner(runtime phoenix.Runtime) error { api.SetMux(r.PathPrefix("/api/v1/").Subrouter()) api.AddResource(&Rooms{}, "/rooms") api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens") + api.AddResource(&Sessions{hub: hub}, "/sessions/{id}/") if statsEnabled { api.AddResourceWithWrapper(&Stats{hub: hub}, httputils.MakeGzipHandler, "/stats") log.Println("Stats are enabled!") diff --git a/src/app/spreed-speakfreely-server/server.go b/src/app/spreed-speakfreely-server/server.go index 335f06c4..040a109f 100644 --- a/src/app/spreed-speakfreely-server/server.go +++ b/src/app/spreed-speakfreely-server/server.go @@ -59,7 +59,7 @@ func (s *Server) OnUnregister(c *Connection) { //log.Println("OnUnregister", c.id) if c.Hello { s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid}) - s.Broadcast(c, &DataSession{Type: "Left", Id: c.Id, Status: "hard"}) + s.Broadcast(c, c.Session.DataSessionLeft("hard")) } else { //log.Println("Ingoring OnUnregister because of no Hello", c.Idx) } @@ -86,13 +86,13 @@ func (s *Server) OnText(c *Connection, b Buffer) { if c.Hello && c.Roomid != msg.Hello.Id { // Room changed. s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid}) - s.Broadcast(c, &DataSession{Type: "Left", Id: c.Id, Status: "soft"}) + s.Broadcast(c, c.Session.DataSessionLeft("soft")) } c.Roomid = msg.Hello.Id if c.h.config.defaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) { c.Hello = true s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid, Status: true}) - s.Broadcast(c, &DataSession{Type: "Joined", Id: c.Id, Ua: msg.Hello.Ua}) + s.Broadcast(c, c.Session.DataSessionJoined()) } else { c.Hello = false } @@ -109,13 +109,20 @@ func (s *Server) OnText(c *Connection, b Buffer) { if c.h.config.defaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) { s.Users(c) } + case "Authentication": + if s.Authenticate(c, msg.Authentication.Authentication) { + s.OnRegister(c) + if c.Hello { + s.Broadcast(c, c.Session.DataSessionStatus()) + } + } case "Bye": s.Unicast(c, msg.Bye.To, msg.Bye) case "Status": //log.Println("Status", msg.Status) - rev := s.UpdateSession(c, &SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status}) + s.UpdateSession(c, &SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status}) if c.h.config.defaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) { - s.Broadcast(c, &DataSession{Type: "Status", Id: c.Id, Status: msg.Status.Status, Rev: rev}) + s.Broadcast(c, c.Session.DataSessionStatus()) } case "Chat": // TODO(longsleep): Limit sent chat messages per incoming connection. @@ -219,6 +226,19 @@ func (s *Server) Users(c *Connection) { } +func (s *Server) Authenticate(c *Connection, st *SessionToken) bool { + + err := c.Session.Authenticate(st) + if err == nil { + log.Println("Authentication success", c.Id, c.Idx, st.Userid) + return true + } else { + log.Println("Authentication failed", err, c.Id, c.Idx, st.Userid, st.Nonce) + return false + } + +} + func (s *Server) UpdateRoomConnection(c *Connection, rcu *RoomConnectionUpdate) { rcu.Sessionid = c.Id diff --git a/src/app/spreed-speakfreely-server/session.go b/src/app/spreed-speakfreely-server/session.go index 2e044abb..c19bead0 100644 --- a/src/app/spreed-speakfreely-server/session.go +++ b/src/app/spreed-speakfreely-server/session.go @@ -22,9 +22,13 @@ package main import ( + "errors" + "github.com/gorilla/securecookie" "sync" ) +var sessionNonces *securecookie.SecureCookie + type Session struct { Id string Sid string @@ -33,6 +37,7 @@ type Session struct { Ua string UpdateRev uint64 Status interface{} + Nonce string mutex sync.RWMutex } @@ -70,13 +75,64 @@ func (s *Session) Update(update *SessionUpdate) uint64 { } -func (s *Session) Apply(st *SessionToken) { +func (s *Session) Apply(st *SessionToken) uint64 { s.mutex.Lock() defer s.mutex.Unlock() s.Id = st.Id + s.Sid = st.Sid s.Userid = st.Userid + s.UpdateRev++ + return s.UpdateRev + +} + +func (s *Session) Authorize(st *SessionToken) (string, error) { + + s.mutex.Lock() + defer s.mutex.Unlock() + + if s.Id != st.Id || s.Sid != st.Sid { + return "", errors.New("session id mismatch") + } + if s.Userid != "" { + return "", errors.New("session already authenticated") + } + + // Create authentication nonce. + var err error + s.Nonce, err = sessionNonces.Encode(s.Sid, st.Userid) + + return s.Nonce, err + +} + +func (s *Session) Authenticate(st *SessionToken) error { + + s.mutex.Lock() + defer s.mutex.Unlock() + + if s.Userid != "" { + return errors.New("session already authenticated") + } + if s.Nonce == "" || s.Nonce != st.Nonce { + return errors.New("nonce validation failed") + } + var userid string + err := sessionNonces.Decode(s.Sid, st.Nonce, &userid) + if err != nil { + return err + } + if st.Userid != userid { + return errors.New("user id mismatch") + } + + s.Nonce = "" + s.Userid = st.Userid + s.UpdateRev++ + return nil + } func (s *Session) Token() *SessionToken { @@ -98,6 +154,48 @@ func (s *Session) Data() *DataSession { } +func (s *Session) DataSessionLeft(state string) *DataSession { + + s.mutex.RLock() + defer s.mutex.RUnlock() + + return &DataSession{ + Type: "Left", + Id: s.Id, + Status: state, + } + +} + +func (s *Session) DataSessionJoined() *DataSession { + + s.mutex.RLock() + defer s.mutex.RUnlock() + + return &DataSession{ + Type: "Joined", + Id: s.Id, + Userid: s.Userid, + Ua: s.Ua, + } + +} + +func (s *Session) DataSessionStatus() *DataSession { + + s.mutex.RLock() + defer s.mutex.RUnlock() + + return &DataSession{ + Type: "Status", + Id: s.Id, + Userid: s.Userid, + Status: s.Status, + Rev: s.UpdateRev, + } + +} + type SessionUpdate struct { Id string Types []string @@ -110,4 +208,11 @@ type SessionToken struct { Id string Sid string Userid string + Nonce string `json:"Nonce,omitempty"` +} + +func init() { + // Create nonce generator. + sessionNonces = securecookie.New(securecookie.GenerateRandomKey(64), nil) + sessionNonces.MaxAge(60) } diff --git a/src/app/spreed-speakfreely-server/sessions.go b/src/app/spreed-speakfreely-server/sessions.go new file mode 100644 index 00000000..b89e3a55 --- /dev/null +++ b/src/app/spreed-speakfreely-server/sessions.go @@ -0,0 +1,95 @@ +/* + * Spreed Speak Freely. + * Copyright (C) 2013-2014 struktur AG + * + * This file is part of Spreed Speak Freely. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "encoding/json" + "github.com/gorilla/mux" + "net/http" +) + +type SessionNonce struct { + Nonce string `json:"nonce"` + Success bool `json:"success"` +} + +type Sessions struct { + hub *Hub +} + +// Patch is used to add a userid to a given session (login). +func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.Header) { + + // Make sure to always run all the checks to make timing attacks harder. + error := false + + decoder := json.NewDecoder(request.Body) + var st SessionToken + err := decoder.Decode(&st) + if err != nil { + error = true + } + + vars := mux.Vars(request) + id, ok := vars["id"] + if !ok { + error = true + } + + // Make sure data matches request. + if id != st.Id { + error = true + } + + // Make sure that we have a Sid. + if st.Sid == "" { + error = true + } + + // Make sure that we have a user. + if st.Userid == "" { + error = true + } + + // TODO(longsleep): Validate userid. + + // Make sure Sid matches session. + if !sessions.hub.ValidateSession(st.Id, st.Sid) { + error = true + } + + var nonce string + if !error { + // FIXME(longsleep): Not running this might releal error state with a timing attack. + nonce, err = sessions.hub.sessiontokenHandler(&st) + if err != nil { + error = true + } + } + + if error { + return 403, NewApiError("session_patch_failed", "Failed to patch session"), nil + } + + return 200, &SessionNonce{Nonce: nonce, Success: true}, http.Header{"Content-Type": {"application/json"}} + +} diff --git a/static/js/mediastream/api.js b/static/js/mediastream/api.js index c3f26718..47b0001a 100644 --- a/static/js/mediastream/api.js +++ b/static/js/mediastream/api.js @@ -26,6 +26,7 @@ define(['jquery', 'underscore'], function($, _) { var Api = function(connector) { this.id = null; + this.sid = null; this.session = {}; this.connector = connector; @@ -112,6 +113,7 @@ define(['jquery', 'underscore'], function($, _) { this.connector.token = data.Token; } this.id = data.Id; + this.sid = data.Sid; this.e.triggerHandler("received.self", [data]); break; case "Offer": @@ -225,6 +227,20 @@ define(['jquery', 'underscore'], function($, _) { }; + Api.prototype.requestAuthentication = function(userid, nonce) { + + var data = { + Type: "Authentication", + Authentication: { + Userid: userid, + Nonce: nonce + } + } + + return this.send("Authentication", data); + + }; + Api.prototype.updateStatus = function(status) { var data = { diff --git a/static/js/services/mediastream.js b/static/js/services/mediastream.js index fddfea61..1a478de6 100644 --- a/static/js/services/mediastream.js +++ b/static/js/services/mediastream.js @@ -55,6 +55,55 @@ define([ console.info("Started disconnector."); }; + (function() { + + var lastNonce = null; + var lastUserid = null; + + $window.testAuthorize = function(userid) { + + console.log("Testing authorize with userid", userid); + var url = mediaStream.url.api("sessions") + "/" + api.id + "/"; + console.log("URL", url); + var data = { + Id: api.id, + Sid: api.sid, + Userid: userid + } + console.log("Data", data); + $.ajax({ + type: "PATCH", + url: url, + contentType: "application/json", + dataType: "json", + data: JSON.stringify(data), + success: function(data) { + if (data.success) { + lastNonce = data.nonce; + lastUserid = userid; + console.log("Retrieved nonce", lastNonce, lastUserid); + } + }, + error: function() { + console.log("error", arguments) + } + }); + + }; + + $window.testAuthenticate = function() { + + if (!lastNonce || !lastUserid) { + console.log("Run testAuthorize first."); + return + } + + api.requestAuthentication(lastUserid, lastNonce); + + }; + + }()) + var mediaStream = { version: version, ws: url, From 61532d7b1453b3cdf664cffd8eb028488a81585a Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Wed, 23 Apr 2014 11:56:39 +0200 Subject: [PATCH 12/48] Added REST API docs. --- doc/REST-API.txt | 98 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 doc/REST-API.txt diff --git a/doc/REST-API.txt b/doc/REST-API.txt new file mode 100644 index 00000000..f7135493 --- /dev/null +++ b/doc/REST-API.txt @@ -0,0 +1,98 @@ + + Spreed Speak Freely REST API v1.0.0 + =============================================== + (c)2014 struktur AG + +The server provides a REST api end point to provide functionality outside the +the channeling API or without a established web socket connection. + +Available end points with request methods and content-type: + + + /api/v1/rooms + + The rooms end point can be used to generate new random room ids. + + POST application/x-www-form-urlencoded + No parameters. + Response 200: + { + "success": true, + "name": "room-name", + "url": "https://yourserver/room-name" + } + + + /api/v1/tokens + + The tokens end point is to validate client side access tokens. + + POST application/x-www-form-urlencoded + a: Authentication token as entered by the user (string) + Response 200: + { + "success": true, + "token": "validated-auth-token" + } + Response 200: + { + "success": false, + "token": "" + } + Response 413: + { + "success": false, + "code": "error-code", + "message": "error-message" + } + + + /api/v1/sessions/{id}/ + + The sessions end point is for session interaction like authorization and + can only be used with a session id passed in as subpath. Make sure to + provide the trailing slash (/). + + PATCH application/json + { + Id: "session-id", + Sid: "secure-session-id", + Userid: "user-id-to-authorize" + } + Response 200: + { + "success": true, + "nonce": "authorization-nonce" + } + Response 403: + { + "success": false, + "code": "error-code", + "message": "error-message" + } + + + /api/v1/stats + + The stats end point provides server statistics. It is only available when + the server configuration has it enabled. + + GET application/x-www-form-urlencoded + details: If 1 when the stats document contains details per connection. + Response 200: + { + Runtime: { /* Runtime stats (memory and such ..) */ }, + Hub: { /* Server stats */ } + } + Please see the implementation on exact fields of Runtime and Hub stats. + + +End of REST API. + +For latest version of Spreed Speak Freely check +https://github.com/strukturag/spreed-speakfreely + +For questions, contact mailto:opensource@struktur.de. + + +(c)2014 struktur AG \ No newline at end of file From 5c6eb060af36089b5772b209c091aac9edc613f4 Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Wed, 23 Apr 2014 12:18:38 +0200 Subject: [PATCH 13/48] Made the REST API consistent. --- doc/REST-API.txt | 12 ++++++------ src/app/spreed-speakfreely-server/sessions.go | 2 +- src/app/spreed-speakfreely-server/tokens.go | 8 +++----- static/js/services/mediastream.js | 16 +++++++++++----- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/doc/REST-API.txt b/doc/REST-API.txt index f7135493..3d6cac85 100644 --- a/doc/REST-API.txt +++ b/doc/REST-API.txt @@ -6,6 +6,11 @@ The server provides a REST api end point to provide functionality outside the the channeling API or without a established web socket connection. +The REST API does always return valid JSON data. This includes the non 200 +status responses. If non JSON is received this is an error not generated by the +API or there was a problem while JSON encoding. + + Available end points with request methods and content-type: @@ -34,12 +39,7 @@ Available end points with request methods and content-type: "success": true, "token": "validated-auth-token" } - Response 200: - { - "success": false, - "token": "" - } - Response 413: + Response 403, 413: { "success": false, "code": "error-code", diff --git a/src/app/spreed-speakfreely-server/sessions.go b/src/app/spreed-speakfreely-server/sessions.go index b89e3a55..6baf557e 100644 --- a/src/app/spreed-speakfreely-server/sessions.go +++ b/src/app/spreed-speakfreely-server/sessions.go @@ -87,7 +87,7 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H } if error { - return 403, NewApiError("session_patch_failed", "Failed to patch session"), nil + return 403, NewApiError("session_patch_failed", "Failed to patch session"), http.Header{"Content-Type": {"application/json"}} } return 200, &SessionNonce{Nonce: nonce, Success: true}, http.Header{"Content-Type": {"application/json"}} diff --git a/src/app/spreed-speakfreely-server/tokens.go b/src/app/spreed-speakfreely-server/tokens.go index 78550446..70c0d406 100644 --- a/src/app/spreed-speakfreely-server/tokens.go +++ b/src/app/spreed-speakfreely-server/tokens.go @@ -41,19 +41,17 @@ func (tokens Tokens) Post(request *http.Request) (int, interface{}, http.Header) auth := request.Form.Get("a") if len(auth) > 100 { - return 413, NewApiError("auth_too_large", "Auth too large"), nil + return 413, NewApiError("auth_too_large", "Auth too large"), http.Header{"Content-Type": {"application/json"}} } valid := tokens.provider(strings.ToLower(auth)) - response := &Token{Token: valid} if valid != "" { log.Printf("Good incoming token request: %s\n", auth) - response.Success = true + return 200, &Token{Token: valid, Success: true}, http.Header{"Content-Type": {"application/json"}} } else { log.Printf("Wrong incoming token request: %s\n", auth) + return 403, NewApiError("invalid_token", "Invalid token"), http.Header{"Content-Type": {"application/json"}} } - return 200, response, http.Header{"Content-Type": {"application/json"}} - } diff --git a/static/js/services/mediastream.js b/static/js/services/mediastream.js index 1a478de6..d022570c 100644 --- a/static/js/services/mediastream.js +++ b/static/js/services/mediastream.js @@ -98,7 +98,7 @@ define([ return } - api.requestAuthentication(lastUserid, lastNonce); + api.requestAuthentication(lastUserid, lastNonce); }; @@ -252,10 +252,16 @@ define([ }); } }). - error(function() { - alertify.dialog.error(translation._("Error"), translation._("Failed to verify access code. Check your Internet connection and try again."), function() { - prompt(); - }); + error(function(data, status) { + if ((status == 403 || status == 413) && data.success == false) { + alertify.dialog.error(translation._("Access denied"), translation._("Please provide a valid access code."), function() { + prompt(); + }); + } else { + alertify.dialog.error(translation._("Error"), translation._("Failed to verify access code. Check your Internet connection and try again."), function() { + prompt(); + }); + } }); }; if (storedCode) { From a46f36fd486cfef51b3ed53fa2faefdb01ace81f Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Fri, 25 Apr 2014 19:47:59 +0200 Subject: [PATCH 14/48] Implemented userid validation and pluggable interface to implement different authentication backends. Rewrote comments in the example configuration. Moved all global test code into a plugin (plugin-test-authorize.js). --- doc/plugin-test-authorize.js | 109 ++++++++++++++++ server.conf.in | 116 ++++++++++++++---- src/app/spreed-speakfreely-server/main.go | 7 +- src/app/spreed-speakfreely-server/session.go | 8 +- src/app/spreed-speakfreely-server/sessions.go | 45 +++++-- src/app/spreed-speakfreely-server/users.go | 112 +++++++++++++++++ static/js/base.js | 3 +- static/js/libs/sjcl.js | 26 ++++ static/js/main.js | 4 + static/js/services/mediastream.js | 64 ---------- 10 files changed, 386 insertions(+), 108 deletions(-) create mode 100644 doc/plugin-test-authorize.js create mode 100644 src/app/spreed-speakfreely-server/users.go create mode 100644 static/js/libs/sjcl.js diff --git a/doc/plugin-test-authorize.js b/doc/plugin-test-authorize.js new file mode 100644 index 00000000..339e345a --- /dev/null +++ b/doc/plugin-test-authorize.js @@ -0,0 +1,109 @@ +/* + * Spreed Speak Freely. + * Copyright (C) 2013-2014 struktur AG + * + * This file is part of Spreed Speak Freely. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +define(['angular', 'sjcl'], function(angular, sjcl) { + + return { + + initialize: function(app) { + + var lastNonce = null; + var lastUserid = null; + var disconnectTimeout = null; + + app.run(["$window", "mediaStream", function($window, mediaStream) { + + console.log("Injecting test plugin functions to window ..."); + + $window.testDisconnect = function() { + if (disconnectTimeout) { + $window.clearInterval(disconnectTimeout); + disconnectTimeout = null; + console.info("Stopped disconnector."); + return; + } + disconnectTimeout = $window.setInterval(function() { + console.info("Test disconnect!"); + mediaStream.connector.conn.close(); + }, 10000); + console.info("Started disconnector."); + }; + + $window.testCreateSuserid = function(key, userid) { + + var k = sjcl.codec.utf8String.toBits(key); + var foo = new sjcl.misc.hmac(k, sjcl.hash.sha256) + var expiration = parseInt(((new Date).getTime()/1000)+3600, 10); + var useridCombo = ""+expiration+":"+userid; + var secret = foo.mac(useridCombo); + return [useridCombo, sjcl.codec.base64.fromBits(secret)] + + }; + + $window.testAuthorize = function(useridCombo, secret) { + + console.log("Testing authorize with userid", useridCombo, secret); + var url = mediaStream.url.api("sessions") + "/" + mediaStream.api.id + "/"; + console.log("URL", url); + var data = { + id: mediaStream.api.id, + sid: mediaStream.api.sid, + useridcombo: useridCombo, + secret: secret + } + console.log("Data", data); + $.ajax({ + type: "PATCH", + url: url, + contentType: "application/json", + dataType: "json", + data: JSON.stringify(data), + success: function(data) { + if (data.success) { + lastNonce = data.nonce; + lastUserid = data.userid; + console.log("Retrieved nonce", lastNonce, lastUserid); + } + }, + error: function() { + console.log("error", arguments) + } + }); + + }; + + $window.testAuthenticate = function() { + + if (!lastNonce || !lastUserid) { + console.log("Run testAuthorize first."); + return + } + + mediaStream.api.requestAuthentication(lastUserid, lastNonce); + + }; + + }]); + + } + + } + +}); \ No newline at end of file diff --git a/server.conf.in b/server.conf.in index 0f529dc1..b4b92ecb 100644 --- a/server.conf.in +++ b/server.conf.in @@ -1,35 +1,99 @@ -# Spreed Speak Freely server example configuration +; Spreed Speak Freely server example configuration [http] +; HTTP listener in format ip:port. listen = 127.0.0.1:8080 -#root = /usr/share/spreed-speakfreely-server/www -#readtimeout = 10 -#writetimeout = 10 -#basePath = /some/sub/path/ # Set this when running behind a web server under a sub path. -#maxfd = 32768 # Try to set max open files limit on start (works only when run as root). -#stats = true # Provide stats API at /api/v1/stats (do not enable this in production or unprotected!). -#pprofListen = 127.0.0.1:6060 # See http://golang.org/pkg/net/http/pprof/ for details +; Full path to directory where to find the server web assets. +;root = /usr/share/spreed-speakfreely-server/www +; HTTP socket read timeout in seconds. +;readtimeout = 10 +; HTTP socket write timeout in seconds. +;writetimeout = 10 +; Use basePath if the server does not run on the root path (/) of your server. +;basePath = /some/sub/path/ +; Set maximum number of open files (only works when run as root). +;maxfd = 32768 +; Enable stats API /api/v1/stats for debugging (not for production use!). +;stats = false +; Enable HTTP listener for golang pprof module. See +; http://golang.org/pkg/net/http/pprof/ for details. +;pprofListen = 127.0.0.1:6060 [https] -#listen = 127.0.0.1:8443 -#certificate = server.crt # Full path to certificate. -#key = server.key # Full path to key. -#minVersion = SSLv3 # Minimal supported encryption (SSLv3, TLSv1, TLSv1.1, TLSv1.2). -#readtimeout = 10 -#writetimeout = 10 +; Native HTTPS listener in format ip:port. +;listen = 127.0.0.1:8443 +; Full path to PEM encoded certificate chain. +;certificate = server.crt +; Full path to PEM encoded private key. +;key = server.key +; Mimimal supported encryption standard (SSLv3, TLSv1, TLSv1.1 or TLSv1.2). +;minVersion = SSLv3 +; HTTPS socket read timeout in seconds. +;readtimeout = 10 +; HTTPS socket write timeout in seconds. +;writetimeout = 10 [app] -#title = Spreed Speak Freely -#ver = 1234 # version string to use for static resource -#stunURIs = stun.l.google.com:19302 -#turnURIs = turn:turnserver:port?transport=udp turn:anotherturnserver:port?transport=tcp turns:turnserver:443?transport=tcp -#turnSecret = the-default-turn-shared-secret-do-not-keep -sessionSecret = the-default-secret-do-not-keep-me # Use 32 or 64 bytes random data -#tokenFile = tokens.txt # If set, everyone needs to give one of the tokens to launch the web client. One token per line in the file. -#globalRoom = global # Enables a global room. Users in that room are in all rooms. -#defaultRoomEnabled = true # Set to false to disable default room. -#extra = /usr/share/spreed-speakfreely-server/extra # Extra templates directory. Add .html files to define extra-* template slots here. -#plugin = plugins/example1 # Plugin support. +; HTML page title +;title = Spreed Speak Freely +; Version string to use for static resources. This defaults to the server +; version and should only be changed when you use your own way to invalidate +; long cached static resources. +;ver = 1234 +; STUN server URIs in format host:port. You can provide multiple seperated by +; space. If you do not have one use a public one like stun.l.google.com:19302. +; If you have a TURN server you do not need to set an STUN server as the TURN +; server will normally do STUN too. +;stunURIs = stun.l.google.com:19302 +; TURN server URIs in format host:port?transport=udp|tcp. You can provide +; multiple seperated by space. If you do not have at least one TURN server then +; some users will not be able to use the server as the peer to peer connection +; cannot be established without a TURN server due to firewall reasons. An open +; source TURN server which is fully supported can be found at +; https://code.google.com/p/rfc5766-turn-server/. +;turnURIs = turn:turnserver:port?transport=udp turns:turnserver:443?transport=tcp +; Shared secret authentication for TURN user generation if the TURN server is +; protected (which it should be). +; See http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00 for details. +; A supported TURN server is https://code.google.com/p/rfc5766-turn-server/. +;turnSecret = the-default-turn-shared-secret-do-not-keep +; Session secret to use for session id generator. 32 or 64 bytes of random data +; are recommented. +sessionSecret = the-default-secret-do-not-keep-me +; Full path to a text file containig client tokens which a user needs to enter +; when accessing the web client. Each line in this file represents a valid token. +;tokenFile = tokens.txt +; The name of a global room. If enabled it should be kept secret. Users in that +; room are visible in all other rooms. +;globalRoom = global +; The default room is the room at the root URL of the servers base address and +; all users will join this room if enabled. If it is disabled then a room join +; form will be shown instead. +;defaultRoomEnabled = true +; Full path to an extra templates directory. Templates in this directory ending +; with .html will be parsed on startup and can be used to fill the supported +; extra-* template slots. If the extra folder has a sub folder "static", the +; resources in this static folder will be available as /extra/static/filename +; relative to your servers base URL. +;extra = /usr/share/spreed-speakfreely-server/extra +; URL relative to the servers base path for a plugin javascript file which is +; automatically loaded on web client start for all users. You can put your +; plugin in the extra/static folder (see above) or provide another folder using +; a front end webserver. Check the doc folder for more info about plugins and +; examples. +;plugin = extra/static/myplugin.js [log] -#logfile = /var/log/spreed-speakfreely-server.log +;logfile = /var/log/spreed-speakfreely-server.log + +[users] +; Set to true to enable user functionality. +;enabled = false +; Set authorization mode for users. Currently implemented is the "sharedsecret" +; mode which does validate the userid with a HMAC authentication secret. +; The format goes like this: +; BASE64(HMAC-SHA-256(secret, expirationTimestampInSeconds:userid)) +;mode = sharedsecret +; The shared secred for HMAC validation in "sharedsecret" mode. Best use 32 or +; 64 bytes of random data. +;sharedsecret_secret = some-secret-do-not-keep diff --git a/src/app/spreed-speakfreely-server/main.go b/src/app/spreed-speakfreely-server/main.go index 20ac1a39..ce827ad7 100644 --- a/src/app/spreed-speakfreely-server/main.go +++ b/src/app/spreed-speakfreely-server/main.go @@ -270,6 +270,9 @@ func runner(runtime phoenix.Runtime) error { tokenProvider = TokenFileProvider(tokenFile) } + // Create Users handler. + users := NewUsers(runtime) + // Create configuration data structure. config = NewConfig(title, ver, runtimeVersion, basePath, stunURIs, turnURIs, tokenProvider != nil, globalRoomid, defaultRoomEnabled, plugin) @@ -343,7 +346,9 @@ func runner(runtime phoenix.Runtime) error { api.SetMux(r.PathPrefix("/api/v1/").Subrouter()) api.AddResource(&Rooms{}, "/rooms") api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens") - api.AddResource(&Sessions{hub: hub}, "/sessions/{id}/") + if users.Enabled { + api.AddResource(&Sessions{hub: hub, users: users}, "/sessions/{id}/") + } if statsEnabled { api.AddResourceWithWrapper(&Stats{hub: hub}, httputils.MakeGzipHandler, "/stats") log.Println("Stats are enabled!") diff --git a/src/app/spreed-speakfreely-server/session.go b/src/app/spreed-speakfreely-server/session.go index c19bead0..8fdd53d4 100644 --- a/src/app/spreed-speakfreely-server/session.go +++ b/src/app/spreed-speakfreely-server/session.go @@ -205,10 +205,10 @@ type SessionUpdate struct { } type SessionToken struct { - Id string - Sid string - Userid string - Nonce string `json:"Nonce,omitempty"` + Id string // Public session id. + Sid string // Secret session id. + Userid string // Public user id. + Nonce string `json:"Nonce,omitempty"` // User autentication nonce. } func init() { diff --git a/src/app/spreed-speakfreely-server/sessions.go b/src/app/spreed-speakfreely-server/sessions.go index 6baf557e..f6c19182 100644 --- a/src/app/spreed-speakfreely-server/sessions.go +++ b/src/app/spreed-speakfreely-server/sessions.go @@ -24,16 +24,26 @@ package main import ( "encoding/json" "github.com/gorilla/mux" + "log" "net/http" ) type SessionNonce struct { Nonce string `json:"nonce"` + Userid string `json:"userid"` Success bool `json:"success"` } +type SessionNonceRequest struct { + Id string `json:"id"` // Public session id. + Sid string `json:"sid"` // Private session id. + UseridCombo string `json:"useridcombo"` // Public user id as used secret (Expiration:Userid) + Secret string `json:"secret"` // base64(hmac-sha265(SecretKey, UseridCombo)) +} + type Sessions struct { - hub *Hub + hub *Hub + users *Users } // Patch is used to add a userid to a given session (login). @@ -43,8 +53,8 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H error := false decoder := json.NewDecoder(request.Body) - var st SessionToken - err := decoder.Decode(&st) + var snr SessionNonceRequest + err := decoder.Decode(&snr) if err != nil { error = true } @@ -56,32 +66,42 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H } // Make sure data matches request. - if id != st.Id { + if id != snr.Id { error = true + log.Println("Session patch failed - request id mismatch.") } // Make sure that we have a Sid. - if st.Sid == "" { + if snr.Sid == "" { error = true + log.Println("Session patch failed - sid empty.") } - // Make sure that we have a user. - if st.Userid == "" { + // Validate with users handler. + userid, err := sessions.users.Handler.Validate(&snr) + if err != nil { error = true + log.Println("Session patch failed - users validation failed.", err) } - // TODO(longsleep): Validate userid. + // Make sure that we have a user. + if userid == "" { + error = true + log.Println("Session patch failed - userid empty.") + } // Make sure Sid matches session. - if !sessions.hub.ValidateSession(st.Id, st.Sid) { + if !sessions.hub.ValidateSession(snr.Id, snr.Sid) { + log.Println("Session patch failed - validation failed.") error = true } var nonce string if !error { - // FIXME(longsleep): Not running this might releal error state with a timing attack. - nonce, err = sessions.hub.sessiontokenHandler(&st) + // FIXME(longsleep): Not running this might reveal error state with a timing attack. + nonce, err = sessions.hub.sessiontokenHandler(&SessionToken{Id: snr.Id, Sid: snr.Sid, Userid: userid}) if err != nil { + log.Println("Session patch failed - handle failed.", err) error = true } } @@ -90,6 +110,7 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H return 403, NewApiError("session_patch_failed", "Failed to patch session"), http.Header{"Content-Type": {"application/json"}} } - return 200, &SessionNonce{Nonce: nonce, Success: true}, http.Header{"Content-Type": {"application/json"}} + log.Printf("Session patch successfull %s -> %s\n", snr.Id, userid) + return 200, &SessionNonce{Nonce: nonce, Userid: userid, Success: true}, http.Header{"Content-Type": {"application/json"}} } diff --git a/src/app/spreed-speakfreely-server/users.go b/src/app/spreed-speakfreely-server/users.go new file mode 100644 index 00000000..e3589945 --- /dev/null +++ b/src/app/spreed-speakfreely-server/users.go @@ -0,0 +1,112 @@ +/* + * Spreed Speak Freely. + * Copyright (C) 2013-2014 struktur AG + * + * This file is part of Spreed Speak Freely. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "errors" + "github.com/strukturag/phoenix" + "log" + "strconv" + "strings" + "time" +) + +type UsersHandler interface { + Validate(snr *SessionNonceRequest) (string, error) +} + +type UsersSharedsecretHandler struct { + secret []byte +} + +func (uh *UsersSharedsecretHandler) Validate(snr *SessionNonceRequest) (string, error) { + + // Parse UseridCombo. + useridCombo := strings.SplitN(snr.UseridCombo, ":", 2) + expirationString, userid := useridCombo[0], useridCombo[1] + + expiration, err := strconv.ParseInt(expirationString, 10, 64) + if err != nil { + return "", err + } + + // Check expiration. + if time.Unix(expiration, 0).Before(time.Now()) { + return "", errors.New("expired secret") + } + + // Check HMAC. + foo := hmac.New(sha256.New, uh.secret) + foo.Write([]byte(snr.UseridCombo)) + fooSecret := base64.StdEncoding.EncodeToString(foo.Sum(nil)) + if snr.Secret != fooSecret { + return "", errors.New("invalid secret") + } + + return userid, nil +} + +type Users struct { + Enabled bool + Handler UsersHandler +} + +func NewUsers(runtime phoenix.Runtime) *Users { + + enabled := false + enabledString, err := runtime.GetString("users", "enabled") + if err == nil { + enabled = enabledString == "true" + } + + var handler UsersHandler + + if enabled { + + mode, _ := runtime.GetString("users", "mode") + switch mode { + case "sharedsecret": + secret, _ := runtime.GetString("users", "sharedsecret_secret") + if secret != "" { + handler = &UsersSharedsecretHandler{secret: []byte(secret)} + } + default: + mode = "" + } + + if handler == nil { + enabled = false + } else { + log.Printf("Enabled users handler '%s'.\n", mode) + } + + } + + return &Users{ + Enabled: enabled, + Handler: handler, + } + +} diff --git a/static/js/base.js b/static/js/base.js index cf778247..88f4df3d 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -31,5 +31,6 @@ define([ 'audiocontext', 'rAF', 'humanize', - 'sha' + 'sha', + 'sjcl' ], function(){}); diff --git a/static/js/libs/sjcl.js b/static/js/libs/sjcl.js new file mode 100644 index 00000000..c4c5e39d --- /dev/null +++ b/static/js/libs/sjcl.js @@ -0,0 +1,26 @@ +// http://bitwiseshiftleft.github.io/sjcl/ +// ./configure --without-all --with-sha256 --with-sha512 --with-sha1 --with-hmac --with-codecBase64 --with-codecString +// Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh, Stanford University. +// SJCL is dual-licensed under the GNU GPL version 2.0 or higher, and a 2-clause BSD license. +"use strict";var sjcl={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){this.toString=function(){return"CORRUPT: "+this.message};this.message=a},invalid:function(a){this.toString=function(){return"INVALID: "+this.message};this.message=a},bug:function(a){this.toString=function(){return"BUG: "+this.message};this.message=a},notReady:function(a){this.toString=function(){return"NOT READY: "+this.message};this.message=a}}}; +"undefined"!==typeof module&&module.exports&&(module.exports=sjcl); +sjcl.bitArray={bitSlice:function(a,b,c){a=sjcl.bitArray.l(a.slice(b/32),32-(b&31)).slice(1);return void 0===c?a:sjcl.bitArray.clamp(a,c-b)},extract:function(a,b,c){var e=Math.floor(-b-c&31);return((b+c-1^b)&-32?a[b/32|0]<<32-e^a[b/32+1|0]>>>e:a[b/32|0]>>>e)&(1<>b-1,1));return a},partial:function(a,b,c){return 32===a?b:(c?b|0:b<<32-a)+0x10000000000*a},getPartial:function(a){return Math.round(a/0x10000000000)||32},equal:function(a,b){if(sjcl.bitArray.bitLength(a)!==sjcl.bitArray.bitLength(b))return!1;var c=0,e;for(e=0;e>>b),c=a[d]<<32-b;d=a.length?a[a.length-1]:0;a=sjcl.bitArray.getPartial(d);e.push(sjcl.bitArray.partial(b+a&31,32>>24),d<<=8;return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,e=0;for(c=0;c>>d)>>>26),6>d?(k=a[c]<<6-d,d+=26,c++):(k<<=6,d-=6);for(;e.length&3&&!b;)e+="=";return e},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],e,d=0,h=sjcl.codec.base64.j,k=0,f;b&&(h=h.substr(0,62)+"-_");for(e=0;ef)throw new sjcl.exception.invalid("this isn't base64!");26>>d),k=f<<32-d):(d+=6,k^=f<<32-d)}d&56&&c.push(sjcl.bitArray.partial(d&56,k,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.d[0]||this.h();a?(this.c=a.c.slice(0),this.b=a.b.slice(0),this.a=a.a):this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()}; +sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.c=this.f.slice(0);this.b=[];this.a=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.b=sjcl.bitArray.concat(this.b,a);b=this.a;a=this.a=b+sjcl.bitArray.bitLength(a);for(b=512+b&-512;b<=a;b+=512)this.e(c.splice(0,16));return this},finalize:function(){var a,b=this.b,c=this.c,b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0);b.push(Math.floor(this.a/ +4294967296));for(b.push(this.a|0);b.length;)this.e(b.splice(0,16));this.reset();return c},f:[],d:[],h:function(){function a(a){return 0x100000000*(a-Math.floor(a))|0}var b=0,c=2,e;a:for(;64>b;c++){for(e=2;e*e<=c;e++)if(0===c%e)continue a;8>b&&(this.f[b]=a(Math.pow(c,0.5)));this.d[b]=a(Math.pow(c,1/3));b++}},e:function(a){var b,c,e=a.slice(0),d=this.c,h=this.d,k=d[0],f=d[1],g=d[2],u=d[3],m=d[4],v=d[5],w=d[6],x=d[7];for(a=0;64>a;a++)16>a?b=e[a]:(b=e[a+1&15],c=e[a+14&15],b=e[a&15]=(b>>>7^b>>>18^b>>>3^ +b<<25^b<<14)+(c>>>17^c>>>19^c>>>10^c<<15^c<<13)+e[a&15]+e[a+9&15]|0),b=b+x+(m>>>6^m>>>11^m>>>25^m<<26^m<<21^m<<7)+(w^m&(v^w))+h[a],x=w,w=v,v=m,m=u+b|0,u=g,g=f,f=k,k=b+(f&g^u&(f^g))+(f>>>2^f>>>13^f>>>22^f<<30^f<<19^f<<10)|0;d[0]=d[0]+k|0;d[1]=d[1]+f|0;d[2]=d[2]+g|0;d[3]=d[3]+u|0;d[4]=d[4]+m|0;d[5]=d[5]+v|0;d[6]=d[6]+w|0;d[7]=d[7]+x|0}};sjcl.hash.sha512=function(a){this.d[0]||this.h();a?(this.c=a.c.slice(0),this.b=a.b.slice(0),this.a=a.a):this.reset()};sjcl.hash.sha512.hash=function(a){return(new sjcl.hash.sha512).update(a).finalize()}; +sjcl.hash.sha512.prototype={blockSize:1024,reset:function(){this.c=this.f.slice(0);this.b=[];this.a=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.b=sjcl.bitArray.concat(this.b,a);b=this.a;a=this.a=b+sjcl.bitArray.bitLength(a);for(b=1024+b&-1024;b<=a;b+=1024)this.e(c.splice(0,32));return this},finalize:function(){var a,b=this.b,c=this.c,b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+4;a&31;a++)b.push(0);b.push(0);b.push(0); +b.push(Math.floor(this.a/0x100000000));for(b.push(this.a|0);b.length;)this.e(b.splice(0,32));this.reset();return c},f:[],n:[12372232,13281083,9762859,1914609,15106769,4090911,4308331,8266105],d:[],o:[2666018,15689165,5061423,9034684,4764984,380953,1658779,7176472,197186,7368638,14987916,16757986,8096111,1480369,13046325,6891156,15813330,5187043,9229749,11312229,2818677,10937475,4324308,1135541,6741931,11809296,16458047,15666916,11046850,698149,229999,945776,13774844,2541862,12856045,9810911,11494366, +7844520,15576806,8533307,15795044,4337665,16291729,5553712,15684120,6662416,7413802,12308920,13816008,4303699,9366425,10176680,13195875,4295371,6546291,11712675,15708924,1519456,15772530,6568428,6495784,8568297,13007125,7492395,2515356,12632583,14740254,7262584,1535930,13146278,16321966,1853211,294276,13051027,13221564,1051980,4080310,6651434,14088940,4675607],h:function(){function a(a){return 0x100000000*(a-Math.floor(a))|0}function b(a){return 0x10000000000*(a-Math.floor(a))&255}var c=0,e=2,d;a:for(;80> +c;e++){for(d=2;d*d<=e;d++)if(0===e%d)continue a;8>c&&(this.f[2*c]=a(Math.pow(e,0.5)),this.f[2*c+1]=b(Math.pow(e,0.5))<<24|this.n[c]);this.d[2*c]=a(Math.pow(e,1/3));this.d[2*c+1]=b(Math.pow(e,1/3))<<24|this.o[c];c++}},e:function(a){var b,c,e=a.slice(0),d=this.c,h=this.d,k=d[0],f=d[1],g=d[2],u=d[3],m=d[4],v=d[5],w=d[6],x=d[7],R=d[8],H=d[9],S=d[10],I=d[11],T=d[12],J=d[13],U=d[14],K=d[15],q=k,n=f,A=g,y=u,B=m,z=v,N=w,C=x,r=R,p=H,L=S,D=I,M=T,E=J,O=U,F=K;for(a=0;80>a;a++){if(16>a)b=e[2*a],c=e[2*a+1];else{c= +e[2*(a-15)];var l=e[2*(a-15)+1];b=(l<<31|c>>>1)^(l<<24|c>>>8)^c>>>7;var s=(c<<31|l>>>1)^(c<<24|l>>>8)^(c<<25|l>>>7);c=e[2*(a-2)];var t=e[2*(a-2)+1],l=(t<<13|c>>>19)^(c<<3|t>>>29)^c>>>6,t=(c<<13|t>>>19)^(t<<3|c>>>29)^(c<<26|t>>>6),P=e[2*(a-7)],Q=e[2*(a-16)],G=e[2*(a-16)+1];c=s+e[2*(a-7)+1];b=b+P+(c>>>0>>0?1:0);c+=t;b+=l+(c>>>0>>0?1:0);c+=G;b+=Q+(c>>>0>>0?1:0)}e[2*a]=b|=0;e[2*a+1]=c|=0;var P=r&L^~r&M,V=p&D^~p&E,t=q&A^q&B^A&B,X=n&y^n&z^y&z,Q=(n<<4|q>>>28)^(q<<30|n>>>2)^(q<<25|n>>>7),G=(q<<4| +n>>>28)^(n<<30|q>>>2)^(n<<25|q>>>7),Y=h[2*a],W=h[2*a+1],l=F+((r<<18|p>>>14)^(r<<14|p>>>18)^(p<<23|r>>>9)),s=O+((p<<18|r>>>14)^(p<<14|r>>>18)^(r<<23|p>>>9))+(l>>>0>>0?1:0),l=l+V,s=s+(P+(l>>>0>>0?1:0)),l=l+W,s=s+(Y+(l>>>0>>0?1:0)),l=l+c,s=s+(b+(l>>>0>>0?1:0));c=G+X;b=Q+t+(c>>>0>>0?1:0);O=M;F=E;M=L;E=D;L=r;D=p;p=C+l|0;r=N+s+(p>>>0>>0?1:0)|0;N=B;C=z;B=A;z=y;A=q;y=n;n=l+c|0;q=s+b+(n>>>0>>0?1:0)|0}f=d[1]=f+n|0;d[0]=k+q+(f>>>0>>0?1:0)|0;u=d[3]=u+y|0;d[2]=g+A+(u>>>0>>0?1:0)|0;v= +d[5]=v+z|0;d[4]=m+B+(v>>>0>>0?1:0)|0;x=d[7]=x+C|0;d[6]=w+N+(x>>>0>>0?1:0)|0;H=d[9]=H+p|0;d[8]=R+r+(H>>>0

>>0?1:0)|0;I=d[11]=I+D|0;d[10]=S+L+(I>>>0>>0?1:0)|0;J=d[13]=J+E|0;d[12]=T+M+(J>>>0>>0?1:0)|0;K=d[15]=K+F|0;d[14]=U+O+(K>>>0>>0?1:0)|0}};sjcl.hash.sha1=function(a){a?(this.c=a.c.slice(0),this.b=a.b.slice(0),this.a=a.a):this.reset()};sjcl.hash.sha1.hash=function(a){return(new sjcl.hash.sha1).update(a).finalize()}; +sjcl.hash.sha1.prototype={blockSize:512,reset:function(){this.c=this.f.slice(0);this.b=[];this.a=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.b=sjcl.bitArray.concat(this.b,a);b=this.a;a=this.a=b+sjcl.bitArray.bitLength(a);for(b=this.blockSize+b&-this.blockSize;b<=a;b+=this.blockSize)this.e(c.splice(0,16));return this},finalize:function(){var a,b=this.b,c=this.c,b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0); +b.push(Math.floor(this.a/0x100000000));for(b.push(this.a|0);b.length;)this.e(b.splice(0,16));this.reset();return c},f:[1732584193,4023233417,2562383102,271733878,3285377520],d:[1518500249,1859775393,2400959708,3395469782],e:function(a){var b,c,e,d,h,k,f=a.slice(0),g=this.c;c=g[0];e=g[1];d=g[2];h=g[3];k=g[4];for(a=0;79>=a;a++)16<=a&&(f[a]=(f[a-3]^f[a-8]^f[a-14]^f[a-16])<<1|(f[a-3]^f[a-8]^f[a-14]^f[a-16])>>>31),b=19>=a?e&d|~e&h:39>=a?e^d^h:59>=a?e&d|e&h|d&h:79>=a?e^d^h:void 0,b=(c<<5|c>>>27)+b+k+f[a]+ +this.d[Math.floor(a/20)]|0,k=h,h=d,d=e<<30|e>>>2,e=c,c=b;g[0]=g[0]+c|0;g[1]=g[1]+e|0;g[2]=g[2]+d|0;g[3]=g[3]+h|0;g[4]=g[4]+k|0}};sjcl.misc.hmac=function(a,b){this.k=b=b||sjcl.hash.sha256;var c=[[],[]],e,d=b.prototype.blockSize/32;this.g=[new b,new b];a.length>d&&(a=b.hash(a));for(e=0;e Date: Fri, 25 Apr 2014 21:57:30 +0200 Subject: [PATCH 15/48] Implemented user creation API. --- doc/REST-API.txt | 79 ++++++++++----- doc/plugin-test-authorize.js | 50 +++++++++- server.conf.in | 9 +- src/app/spreed-speakfreely-server/main.go | 7 +- src/app/spreed-speakfreely-server/sessions.go | 12 +-- src/app/spreed-speakfreely-server/users.go | 98 +++++++++++++++++-- 6 files changed, 211 insertions(+), 44 deletions(-) diff --git a/doc/REST-API.txt b/doc/REST-API.txt index 3d6cac85..54cb4e54 100644 --- a/doc/REST-API.txt +++ b/doc/REST-API.txt @@ -14,20 +14,6 @@ API or there was a problem while JSON encoding. Available end points with request methods and content-type: - /api/v1/rooms - - The rooms end point can be used to generate new random room ids. - - POST application/x-www-form-urlencoded - No parameters. - Response 200: - { - "success": true, - "name": "room-name", - "url": "https://yourserver/room-name" - } - - /api/v1/tokens The tokens end point is to validate client side access tokens. @@ -47,29 +33,76 @@ Available end points with request methods and content-type: } - /api/v1/sessions/{id}/ + /api/v1/rooms + + The rooms end point can be used to generate new random room ids. + + POST application/x-www-form-urlencoded + No parameters. + Response 200: + { + "success": true, + "name": "room-name", + "url": "https://yourserver/room-name" + } + + + /api/v1/sessions - The sessions end point is for session interaction like authorization and - can only be used with a session id passed in as subpath. Make sure to - provide the trailing slash (/). + The sessions end point is for session interaction like authorization. - PATCH application/json + /api/v1/sessions/{id}/ + + A session id is passed in as subpath. Make sure to add the trailing slash (/). + + PATCH application/json + { + id: "session-id", + sid: "secure-session-id", + useridcombo: "authorization-id", + secret: "secret-for-this-user-id" + } + Response 200: + { + "success": true, + "userid": "user-id-for-nonce", + "nonce": "authorization-nonce" + } + Response 403: + { + "success": false, + "code": "error-code", + "message": "error-message" + } + Response 404 text/plain: + Returned when users are disabled on the server. + + + /api/v1/users + + The users end point is for user interaction like registration. + + POST application/json { - Id: "session-id", - Sid: "secure-session-id", - Userid: "user-id-to-authorize" + id: "session-id", + sid: "secure-session-id" } Response 200: { "success": true, + "userid": "user-id", + "useridcombo": "authorization-id", + "secret": "authorization-secret-for-authorization-id", "nonce": "authorization-nonce" } - Response 403: + Response 400, 403: { "success": false, "code": "error-code", "message": "error-message" } + Response 404 text/plain: + Returned when user registration is disabled on the server. /api/v1/stats diff --git a/doc/plugin-test-authorize.js b/doc/plugin-test-authorize.js index 339e345a..3da49345 100644 --- a/doc/plugin-test-authorize.js +++ b/doc/plugin-test-authorize.js @@ -26,6 +26,8 @@ define(['angular', 'sjcl'], function(angular, sjcl) { var lastNonce = null; var lastUserid = null; + var lastUseridCombo = null; + var lastSecret = null; var disconnectTimeout = null; app.run(["$window", "mediaStream", function($window, mediaStream) { @@ -46,7 +48,7 @@ define(['angular', 'sjcl'], function(angular, sjcl) { console.info("Started disconnector."); }; - $window.testCreateSuserid = function(key, userid) { + $window.testCreateSuseridLocal = function(key, userid) { var k = sjcl.codec.utf8String.toBits(key); var foo = new sjcl.misc.hmac(k, sjcl.hash.sha256) @@ -57,6 +59,37 @@ define(['angular', 'sjcl'], function(angular, sjcl) { }; + $window.testCreateSuseridServer = function() { + + var url = mediaStream.url.api("users"); + console.log("URL", url); + var data = { + id: mediaStream.api.id, + sid: mediaStream.api.sid + } + console.log("Data", data); + $.ajax({ + type: "POST", + url: url, + contentType: "application/json", + dataType: "json", + data: JSON.stringify(data), + success: function(data) { + if (data.success) { + lastNonce = data.nonce; + lastUserid = data.userid; + lastUseridCombo = data.useridcombo; + lastSecret = data.secret; + console.log("Retrieved user", data); + } + }, + error: function() { + console.log("error", arguments) + } + }); + + }; + $window.testAuthorize = function(useridCombo, secret) { console.log("Testing authorize with userid", useridCombo, secret); @@ -79,7 +112,7 @@ define(['angular', 'sjcl'], function(angular, sjcl) { if (data.success) { lastNonce = data.nonce; lastUserid = data.userid; - console.log("Retrieved nonce", lastNonce, lastUserid); + console.log("Retrieved nonce", data); } }, error: function() { @@ -89,7 +122,7 @@ define(['angular', 'sjcl'], function(angular, sjcl) { }; - $window.testAuthenticate = function() { + $window.testLastAuthenticate = function() { if (!lastNonce || !lastUserid) { console.log("Run testAuthorize first."); @@ -100,6 +133,17 @@ define(['angular', 'sjcl'], function(angular, sjcl) { }; + $window.testLastAuthorize = function() { + + if (!lastUseridCombo || !lastSecret) { + console.log("Run testCreateSuseridServer fist."); + return + } + + $window.testAuthorize(lastUseridCombo, lastSecret); + + }; + }]); } diff --git a/server.conf.in b/server.conf.in index b4b92ecb..b7007509 100644 --- a/server.conf.in +++ b/server.conf.in @@ -51,7 +51,7 @@ listen = 127.0.0.1:8080 ; cannot be established without a TURN server due to firewall reasons. An open ; source TURN server which is fully supported can be found at ; https://code.google.com/p/rfc5766-turn-server/. -;turnURIs = turn:turnserver:port?transport=udp turns:turnserver:443?transport=tcp +;turnURIs = turn:turnserver:port?transport=udp ; Shared secret authentication for TURN user generation if the TURN server is ; protected (which it should be). ; See http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00 for details. @@ -61,7 +61,8 @@ listen = 127.0.0.1:8080 ; are recommented. sessionSecret = the-default-secret-do-not-keep-me ; Full path to a text file containig client tokens which a user needs to enter -; when accessing the web client. Each line in this file represents a valid token. +; when accessing the web client. Each line in this file represents a valid +; token. ;tokenFile = tokens.txt ; The name of a global room. If enabled it should be kept secret. Users in that ; room are visible in all other rooms. @@ -97,3 +98,7 @@ sessionSecret = the-default-secret-do-not-keep-me ; The shared secred for HMAC validation in "sharedsecret" mode. Best use 32 or ; 64 bytes of random data. ;sharedsecret_secret = some-secret-do-not-keep +; The server can create new userids if enabled. Set allowRegistration to true to +; enable userid creation/registration. Users are created to match the settings +; of the currently configured mode (see above). +;allowRegistration = false diff --git a/src/app/spreed-speakfreely-server/main.go b/src/app/spreed-speakfreely-server/main.go index ce827ad7..4d175543 100644 --- a/src/app/spreed-speakfreely-server/main.go +++ b/src/app/spreed-speakfreely-server/main.go @@ -270,9 +270,6 @@ func runner(runtime phoenix.Runtime) error { tokenProvider = TokenFileProvider(tokenFile) } - // Create Users handler. - users := NewUsers(runtime) - // Create configuration data structure. config = NewConfig(title, ver, runtimeVersion, basePath, stunURIs, turnURIs, tokenProvider != nil, globalRoomid, defaultRoomEnabled, plugin) @@ -301,6 +298,9 @@ func runner(runtime phoenix.Runtime) error { // Create our hub instance. hub := NewHub(runtimeVersion, config, sessionSecret, turnSecret) + // Create Users handler. + users := NewUsers(hub, runtime) + // Set number of go routines if it is 1 if goruntime.GOMAXPROCS(0) == 1 { nCPU := goruntime.NumCPU() @@ -348,6 +348,7 @@ func runner(runtime phoenix.Runtime) error { api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens") if users.Enabled { api.AddResource(&Sessions{hub: hub, users: users}, "/sessions/{id}/") + api.AddResource(users, "/users") } if statsEnabled { api.AddResourceWithWrapper(&Stats{hub: hub}, httputils.MakeGzipHandler, "/stats") diff --git a/src/app/spreed-speakfreely-server/sessions.go b/src/app/spreed-speakfreely-server/sessions.go index f6c19182..425fe2b5 100644 --- a/src/app/spreed-speakfreely-server/sessions.go +++ b/src/app/spreed-speakfreely-server/sessions.go @@ -77,6 +77,12 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H log.Println("Session patch failed - sid empty.") } + // Make sure Sid matches session and is valid. + if !sessions.hub.ValidateSession(snr.Id, snr.Sid) { + log.Println("Session patch failed - validation failed.") + error = true + } + // Validate with users handler. userid, err := sessions.users.Handler.Validate(&snr) if err != nil { @@ -90,12 +96,6 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H log.Println("Session patch failed - userid empty.") } - // Make sure Sid matches session. - if !sessions.hub.ValidateSession(snr.Id, snr.Sid) { - log.Println("Session patch failed - validation failed.") - error = true - } - var nonce string if !error { // FIXME(longsleep): Not running this might reveal error state with a timing attack. diff --git a/src/app/spreed-speakfreely-server/users.go b/src/app/spreed-speakfreely-server/users.go index e3589945..b6aa18a0 100644 --- a/src/app/spreed-speakfreely-server/users.go +++ b/src/app/spreed-speakfreely-server/users.go @@ -25,9 +25,13 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/base64" + "encoding/json" "errors" + "fmt" + "github.com/satori/go.uuid" "github.com/strukturag/phoenix" "log" + "net/http" "strconv" "strings" "time" @@ -35,12 +39,21 @@ import ( type UsersHandler interface { Validate(snr *SessionNonceRequest) (string, error) + Create(snr *UserNonce) (*UserNonce, error) } type UsersSharedsecretHandler struct { secret []byte } +func (uh *UsersSharedsecretHandler) createHMAC(useridCombo string) string { + + m := hmac.New(sha256.New, uh.secret) + m.Write([]byte(useridCombo)) + return base64.StdEncoding.EncodeToString(m.Sum(nil)) + +} + func (uh *UsersSharedsecretHandler) Validate(snr *SessionNonceRequest) (string, error) { // Parse UseridCombo. @@ -57,23 +70,41 @@ func (uh *UsersSharedsecretHandler) Validate(snr *SessionNonceRequest) (string, return "", errors.New("expired secret") } - // Check HMAC. - foo := hmac.New(sha256.New, uh.secret) - foo.Write([]byte(snr.UseridCombo)) - fooSecret := base64.StdEncoding.EncodeToString(foo.Sum(nil)) - if snr.Secret != fooSecret { + secret := uh.createHMAC(snr.UseridCombo) + if snr.Secret != secret { return "", errors.New("invalid secret") } return userid, nil + +} + +func (uh *UsersSharedsecretHandler) Create(un *UserNonce) (*UserNonce, error) { + + // TODO(longsleep): Make this configureable - One year for now ... + expiration := time.Now().Add(time.Duration(1) * time.Hour * 24 * 31 * 12) + un.UseridCombo = fmt.Sprintf("%d:%s", expiration.Unix(), un.Userid) + un.Secret = uh.createHMAC(un.UseridCombo) + return un, nil + +} + +type UserNonce struct { + Nonce string `json:"nonce"` + Userid string `json:"userid"` + UseridCombo string `json:"useridcombo"` + Secret string `json:"secret"` + Success bool `json:"success"` } type Users struct { + hub *Hub Enabled bool + Create bool Handler UsersHandler } -func NewUsers(runtime phoenix.Runtime) *Users { +func NewUsers(hub *Hub, runtime phoenix.Runtime) *Users { enabled := false enabledString, err := runtime.GetString("users", "enabled") @@ -81,6 +112,12 @@ func NewUsers(runtime phoenix.Runtime) *Users { enabled = enabledString == "true" } + create := false + createString, err := runtime.GetString("users", "allowRegistration") + if err == nil { + create = createString == "true" + } + var handler UsersHandler if enabled { @@ -99,14 +136,61 @@ func NewUsers(runtime phoenix.Runtime) *Users { if handler == nil { enabled = false } else { - log.Printf("Enabled users handler '%s'.\n", mode) + log.Printf("Enabled users handler '%s'\n", mode) + if create { + log.Println("Enabled users registration") + } } } return &Users{ + hub: hub, Enabled: enabled, + Create: create, Handler: handler, } } + +// Post is used to create new userids for this server. +func (users *Users) Post(request *http.Request) (int, interface{}, http.Header) { + + if !users.Create { + return 404, "404 page not found", http.Header{"Content-Type": {"text/plain"}} + } + + decoder := json.NewDecoder(request.Body) + var snr SessionNonceRequest + err := decoder.Decode(&snr) + if err != nil { + return 400, NewApiError("users_bad_request", "Failed to parse request"), http.Header{"Content-Type": {"application/json"}} + } + + // Make sure that we have a Sid. + if snr.Sid == "" || snr.Id == "" { + return 400, NewApiError("users_bad_request", "Incomplete request"), http.Header{"Content-Type": {"application/json"}} + } + + // Do this before session validation to avoid timing information. + userid := uuid.NewV4().String() + + // Make sure Sid matches session and is valid. + if !users.hub.ValidateSession(snr.Id, snr.Sid) { + return 403, NewApiError("users_invalid_session", "Invalid session"), http.Header{"Content-Type": {"application/json"}} + } + + nonce, err := users.hub.sessiontokenHandler(&SessionToken{Id: snr.Id, Sid: snr.Sid, Userid: userid}) + if err != nil { + return 400, NewApiError("users_request_failed", fmt.Sprintf("Error: %q", err)), http.Header{"Content-Type": {"application/json"}} + } + + un, err := users.Handler.Create(&UserNonce{Nonce: nonce, Userid: userid, Success: true}) + if err != nil { + return 400, NewApiError("users_create_failed", fmt.Sprintf("Error: %q", err)), http.Header{"Content-Type": {"application/json"}} + } + + log.Printf("Users create successfull %s -> %s\n", snr.Id, un.Userid) + return 200, un, http.Header{"Content-Type": {"application/json"}} + +} From f27c5d10a3234fc4c23728e4370d42d395fed8bb Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Sat, 26 Apr 2014 18:20:24 +0200 Subject: [PATCH 16/48] Implemented web client integration of user authentication, authorization and creation. --- doc/plugin-test-authorize.js | 74 +- server.conf.in | 6 +- src/app/spreed-speakfreely-server/config.go | 44 +- src/app/spreed-speakfreely-server/hub.go | 6 +- src/app/spreed-speakfreely-server/main.go | 31 +- src/app/spreed-speakfreely-server/users.go | 53 +- .../js/controllers/mediastreamcontroller.js | 73 +- static/js/libs/sjcl.js | 2439 ++++++++++++++++- static/js/services/mediastream.js | 58 + 9 files changed, 2639 insertions(+), 145 deletions(-) diff --git a/doc/plugin-test-authorize.js b/doc/plugin-test-authorize.js index 3da49345..9f3683f1 100644 --- a/doc/plugin-test-authorize.js +++ b/doc/plugin-test-authorize.js @@ -60,88 +60,42 @@ define(['angular', 'sjcl'], function(angular, sjcl) { }; $window.testCreateSuseridServer = function() { - - var url = mediaStream.url.api("users"); - console.log("URL", url); - var data = { - id: mediaStream.api.id, - sid: mediaStream.api.sid - } - console.log("Data", data); - $.ajax({ - type: "POST", - url: url, - contentType: "application/json", - dataType: "json", - data: JSON.stringify(data), - success: function(data) { - if (data.success) { - lastNonce = data.nonce; - lastUserid = data.userid; - lastUseridCombo = data.useridcombo; - lastSecret = data.secret; - console.log("Retrieved user", data); - } - }, - error: function() { - console.log("error", arguments) - } + mediaStream.users.register(function(data) { + lastNonce = data.nonce; + lastUserid = data.userid; + lastUseridCombo = data.useridcombo; + lastSecret = data.secret; + console.log("Retrieved user", data); + }, function() { + console.log("Register error", arguments); }); - }; $window.testAuthorize = function(useridCombo, secret) { - console.log("Testing authorize with userid", useridCombo, secret); - var url = mediaStream.url.api("sessions") + "/" + mediaStream.api.id + "/"; - console.log("URL", url); - var data = { - id: mediaStream.api.id, - sid: mediaStream.api.sid, - useridcombo: useridCombo, - secret: secret - } - console.log("Data", data); - $.ajax({ - type: "PATCH", - url: url, - contentType: "application/json", - dataType: "json", - data: JSON.stringify(data), - success: function(data) { - if (data.success) { - lastNonce = data.nonce; - lastUserid = data.userid; - console.log("Retrieved nonce", data); - } - }, - error: function() { - console.log("error", arguments) - } + mediaStream.users.authorize(useridCombo, secret, function(data) { + lastNonce = data.nonce; + lastUserid = data.userid; + console.log("Retrieved nonce", data); + }, function() { + console.log("Authorize error", arguments); }); - }; $window.testLastAuthenticate = function() { - if (!lastNonce || !lastUserid) { console.log("Run testAuthorize first."); return } - mediaStream.api.requestAuthentication(lastUserid, lastNonce); - }; $window.testLastAuthorize = function() { - if (!lastUseridCombo || !lastSecret) { console.log("Run testCreateSuseridServer fist."); return } - $window.testAuthorize(lastUseridCombo, lastSecret); - }; }]); diff --git a/server.conf.in b/server.conf.in index b7007509..ec1cd06d 100644 --- a/server.conf.in +++ b/server.conf.in @@ -71,6 +71,10 @@ sessionSecret = the-default-secret-do-not-keep-me ; all users will join this room if enabled. If it is disabled then a room join ; form will be shown instead. ;defaultRoomEnabled = true +; Server token is a public random string which is used to enhance security of +; server generated security tokens. When the serverToken is changed all existing +; nonces become invalid. Use 32 or 64 byte random data. +;serverToken = i-did-not-change-the-public-token-boo ; Full path to an extra templates directory. Templates in this directory ending ; with .html will be parsed on startup and can be used to fill the supported ; extra-* template slots. If the extra folder has a sub folder "static", the @@ -89,7 +93,7 @@ sessionSecret = the-default-secret-do-not-keep-me [users] ; Set to true to enable user functionality. -;enabled = false +enabled = false ; Set authorization mode for users. Currently implemented is the "sharedsecret" ; mode which does validate the userid with a HMAC authentication secret. ; The format goes like this: diff --git a/src/app/spreed-speakfreely-server/config.go b/src/app/spreed-speakfreely-server/config.go index b5516a55..fe3fec09 100644 --- a/src/app/spreed-speakfreely-server/config.go +++ b/src/app/spreed-speakfreely-server/config.go @@ -26,20 +26,38 @@ import ( ) type Config struct { - Title string // Title - ver string // Version (not exported to Javascript) - S string // Static URL prefix with version - B string // Base URL - StunURIs []string // STUN server URIs - TurnURIs []string // TURN server URIs - Tokens bool // True when we got a tokens file - Version string // Server version number - globalRoomid string // Id of the global room (not exported to Javascript) - defaultRoomEnabled bool // Flag to enable default room ("") - Plugin string // Plugin to load + Title string // Title + ver string // Version (not exported to Javascript) + S string // Static URL prefix with version + B string // Base URL + Token string // Server token + StunURIs []string // STUN server URIs + TurnURIs []string // TURN server URIs + Tokens bool // True when we got a tokens file + Version string // Server version number + UsersEnabled bool // Flag if users are enabled + UsersAllowRegistration bool // Flag if users can register + Plugin string // Plugin to load + globalRoomid string // Id of the global room (not exported to Javascript) + defaultRoomEnabled bool // Flag if default room ("") is enabled } -func NewConfig(title, ver, runtimeVersion, basePath string, stunURIs, turnURIs []string, tokens bool, globalRoomid string, defaultRoomEnabled bool, plugin string) *Config { +func NewConfig(title, ver, runtimeVersion, basePath, serverToken string, stunURIs, turnURIs []string, tokens bool, globalRoomid string, defaultRoomEnabled, usersEnabled, usersAllowRegistration bool, plugin string) *Config { sv := fmt.Sprintf("static/ver=%s", ver) - return &Config{Title: title, ver: ver, S: sv, B: basePath, StunURIs: stunURIs, TurnURIs: turnURIs, Tokens: tokens, Version: runtimeVersion, globalRoomid: globalRoomid, defaultRoomEnabled: defaultRoomEnabled, Plugin: plugin} + return &Config{ + Title: title, + ver: ver, + S: sv, + B: basePath, + Token: serverToken, + StunURIs: stunURIs, + TurnURIs: turnURIs, + Tokens: tokens, + Version: runtimeVersion, + UsersEnabled: usersEnabled, + UsersAllowRegistration: usersAllowRegistration, + Plugin: plugin, + globalRoomid: globalRoomid, + defaultRoomEnabled: defaultRoomEnabled, + } } diff --git a/src/app/spreed-speakfreely-server/hub.go b/src/app/spreed-speakfreely-server/hub.go index 09063cf2..4833f6b4 100644 --- a/src/app/spreed-speakfreely-server/hub.go +++ b/src/app/spreed-speakfreely-server/hub.go @@ -174,7 +174,11 @@ func (h *Hub) CreateSession(st *SessionToken) *Session { session = NewSession(id, sid, "") log.Println("Created new session id", len(id), id, sid) } else { - session = NewSession(st.Id, st.Sid, st.Userid) + userid := st.Userid + if !h.config.UsersEnabled { + userid = "" + } + session = NewSession(st.Id, st.Sid, userid) } return session diff --git a/src/app/spreed-speakfreely-server/main.go b/src/app/spreed-speakfreely-server/main.go index 4d175543..9e8ed7c0 100644 --- a/src/app/spreed-speakfreely-server/main.go +++ b/src/app/spreed-speakfreely-server/main.go @@ -263,6 +263,24 @@ func runner(runtime phoenix.Runtime) error { defaultRoomEnabled = defaultRoomEnabledString == "true" } + serverToken, err := runtime.GetString("app", "serverToken") + if err == nil { + //TODO(longsleep): When we have a database, generate this once from random source and store it. + serverToken = "i-did-not-change-the-public-token-boo" + } + + usersEnabled := false + usersEnabledString, err := runtime.GetString("users", "enabled") + if err == nil { + usersEnabled = usersEnabledString == "true" + } + + usersAllowRegistration := false + usersAllowRegistrationString, err := runtime.GetString("users", "allowRegistration") + if err == nil { + usersAllowRegistration = usersAllowRegistrationString == "true" + } + // Create token provider. var tokenProvider TokenProvider if tokenFile != "" { @@ -271,7 +289,7 @@ func runner(runtime phoenix.Runtime) error { } // Create configuration data structure. - config = NewConfig(title, ver, runtimeVersion, basePath, stunURIs, turnURIs, tokenProvider != nil, globalRoomid, defaultRoomEnabled, plugin) + config = NewConfig(title, ver, runtimeVersion, basePath, serverToken, stunURIs, turnURIs, tokenProvider != nil, globalRoomid, defaultRoomEnabled, usersEnabled, usersAllowRegistration, plugin) // Load templates. tt := template.New("") @@ -298,9 +316,6 @@ func runner(runtime phoenix.Runtime) error { // Create our hub instance. hub := NewHub(runtimeVersion, config, sessionSecret, turnSecret) - // Create Users handler. - users := NewUsers(hub, runtime) - // Set number of go routines if it is 1 if goruntime.GOMAXPROCS(0) == 1 { nCPU := goruntime.NumCPU() @@ -346,9 +361,13 @@ func runner(runtime phoenix.Runtime) error { api.SetMux(r.PathPrefix("/api/v1/").Subrouter()) api.AddResource(&Rooms{}, "/rooms") api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens") - if users.Enabled { + if usersEnabled { + // Create Users handler. + users := NewUsers(hub, runtime) api.AddResource(&Sessions{hub: hub, users: users}, "/sessions/{id}/") - api.AddResource(users, "/users") + if usersAllowRegistration { + api.AddResource(users, "/users") + } } if statsEnabled { api.AddResourceWithWrapper(&Stats{hub: hub}, httputils.MakeGzipHandler, "/stats") diff --git a/src/app/spreed-speakfreely-server/users.go b/src/app/spreed-speakfreely-server/users.go index b6aa18a0..e71b8cd0 100644 --- a/src/app/spreed-speakfreely-server/users.go +++ b/src/app/spreed-speakfreely-server/users.go @@ -99,55 +99,32 @@ type UserNonce struct { type Users struct { hub *Hub - Enabled bool - Create bool Handler UsersHandler } func NewUsers(hub *Hub, runtime phoenix.Runtime) *Users { - enabled := false - enabledString, err := runtime.GetString("users", "enabled") - if err == nil { - enabled = enabledString == "true" - } - - create := false - createString, err := runtime.GetString("users", "allowRegistration") - if err == nil { - create = createString == "true" - } - var handler UsersHandler - if enabled { - - mode, _ := runtime.GetString("users", "mode") - switch mode { - case "sharedsecret": - secret, _ := runtime.GetString("users", "sharedsecret_secret") - if secret != "" { - handler = &UsersSharedsecretHandler{secret: []byte(secret)} - } - default: - mode = "" - } - - if handler == nil { - enabled = false - } else { - log.Printf("Enabled users handler '%s'\n", mode) - if create { - log.Println("Enabled users registration") - } + mode, _ := runtime.GetString("users", "mode") + switch mode { + case "sharedsecret": + secret, _ := runtime.GetString("users", "sharedsecret_secret") + if secret != "" { + handler = &UsersSharedsecretHandler{secret: []byte(secret)} } + default: + mode = "" + } + if handler == nil { + handler = &UsersSharedsecretHandler{secret: []byte("")} } + log.Printf("Enabled users handler '%s'\n", mode) + return &Users{ hub: hub, - Enabled: enabled, - Create: create, Handler: handler, } @@ -156,10 +133,6 @@ func NewUsers(hub *Hub, runtime phoenix.Runtime) *Users { // Post is used to create new userids for this server. func (users *Users) Post(request *http.Request) (int, interface{}, http.Header) { - if !users.Create { - return 404, "404 page not found", http.Header{"Content-Type": {"text/plain"}} - } - decoder := json.NewDecoder(request.Body) var snr SessionNonceRequest err := decoder.Decode(&snr) diff --git a/static/js/controllers/mediastreamcontroller.js b/static/js/controllers/mediastreamcontroller.js index b97e5193..3a055c7a 100644 --- a/static/js/controllers/mediastreamcontroller.js +++ b/static/js/controllers/mediastreamcontroller.js @@ -18,7 +18,7 @@ * along with this program. If not, see . * */ -define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigScreen, moment) { +define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function(_, BigScreen, moment, sjcl) { return ["$scope", "$rootScope", "$element", "$window", "$timeout", "safeDisplayName", "safeApply", "mediaStream", "appData", "playSound", "desktopNotify", "alertify", "toastr", "translation", "fileDownload", function($scope, $rootScope, $element, $window, $timeout, safeDisplayName, safeApply, mediaStream, appData, playSound, desktopNotify, alertify, toastr, translation, fileDownload) { @@ -130,6 +130,7 @@ define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigS // Default scope data. $scope.status = "initializing"; $scope.id = null; + $scope.userid = null; $scope.peer = null; $scope.dialing = null; $scope.conference = null; @@ -153,6 +154,7 @@ define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigS language: "" } }; + $scope.withStoredLogin = false; // Data voids. var cache = {}; @@ -365,9 +367,11 @@ define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigS var reloadDialog = false; mediaStream.api.e.on("received.self", function(event, data) { + $timeout.cancel(ttlTimeout); safeApply($scope, function(scope) { scope.id = scope.myid = data.Id; + scope.userid = data.Userid; scope.turn = data.Turn; scope.stun = data.Stun; scope.refreshWebrtcSettings(); @@ -385,6 +389,66 @@ define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigS }, 300); } } + + // Support authentication. + if (!data.Userid && mediaStream.config.UsersEnabled) { + + var key = mediaStream.config.Token; + + // Check if we have something in store. + var login = localStorage.getItem("mediastream-login"); + if (login) { + safeApply($scope, function(scope) { + scope.withStoredLogin = true; + }); + try { + login = sjcl.decrypt(key, login); + login = JSON.parse(login) + } catch(err) { + console.error("Failed to parse login data", err); + login = {}; + } + console.log("Trying to authorize with stored credentials ..."); + switch (login.v) { + case 1: + var useridCombo = login.a; + var secret = login.b; + var expiry = login.t; + if (useridCombo && secret) { + mediaStream.users.authorize(useridCombo, secret, function(data) { + console.info("Retrieved nonce - authenticating as user:", data.userid); + mediaStream.api.requestAuthentication(data.userid, data.nonce); + delete data.nonce; + }, function(data, status) { + console.error("Failed to authorize session", status, data); + }); + } + break; + default: + console.warn("Unknown stored credentials", login.v); + break + } + } + if (!login && mediaStream.config.UsersAllowRegistration) { + console.log("No userid - creating one ..."); + mediaStream.users.register(function(data) { + var login = sjcl.encrypt(key, JSON.stringify({ + v: 1, + t: data.timestamp || "", + a: data.useridcombo, + b: data.secret, + })); + localStorage.setItem("mediastream-login", login); + console.info("Created new userid:", data.userid); + mediaStream.api.requestAuthentication(data.userid, data.nonce); + delete data.nonce; + }, function(data, status) { + console.error("Failed to create userid", status, data); + }); + } + + } + // Support to upgrade stuff when ttl was reached. if (data.Turn.ttl) { ttlTimeout = $timeout(function() { @@ -519,6 +583,7 @@ define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigS if (opts.soft) { return; } + $scope.userid = null; break; case "error": if (reconnecting || connected) { @@ -593,6 +658,12 @@ define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigS } }); + $scope.$watch("userid", function(userid) { + if (userid) { + console.info("Session is now authenticated:", userid); + } + }); + // Apply all layout stuff as classes to our element. $scope.$watch("layout", (function() { var makeName = function(prefix, n) { diff --git a/static/js/libs/sjcl.js b/static/js/libs/sjcl.js index c4c5e39d..1d5110d1 100644 --- a/static/js/libs/sjcl.js +++ b/static/js/libs/sjcl.js @@ -1,26 +1,2419 @@ // http://bitwiseshiftleft.github.io/sjcl/ -// ./configure --without-all --with-sha256 --with-sha512 --with-sha1 --with-hmac --with-codecBase64 --with-codecString +// ./configure --without-all --with-sha256 --with-sha512 --with-sha1 --with-hmac --with-codecBase64 --with-codecString --with-aes --with-ccm --with-convenience --compress=none // Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh, Stanford University. // SJCL is dual-licensed under the GNU GPL version 2.0 or higher, and a 2-clause BSD license. -"use strict";var sjcl={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){this.toString=function(){return"CORRUPT: "+this.message};this.message=a},invalid:function(a){this.toString=function(){return"INVALID: "+this.message};this.message=a},bug:function(a){this.toString=function(){return"BUG: "+this.message};this.message=a},notReady:function(a){this.toString=function(){return"NOT READY: "+this.message};this.message=a}}}; -"undefined"!==typeof module&&module.exports&&(module.exports=sjcl); -sjcl.bitArray={bitSlice:function(a,b,c){a=sjcl.bitArray.l(a.slice(b/32),32-(b&31)).slice(1);return void 0===c?a:sjcl.bitArray.clamp(a,c-b)},extract:function(a,b,c){var e=Math.floor(-b-c&31);return((b+c-1^b)&-32?a[b/32|0]<<32-e^a[b/32+1|0]>>>e:a[b/32|0]>>>e)&(1<>b-1,1));return a},partial:function(a,b,c){return 32===a?b:(c?b|0:b<<32-a)+0x10000000000*a},getPartial:function(a){return Math.round(a/0x10000000000)||32},equal:function(a,b){if(sjcl.bitArray.bitLength(a)!==sjcl.bitArray.bitLength(b))return!1;var c=0,e;for(e=0;e>>b),c=a[d]<<32-b;d=a.length?a[a.length-1]:0;a=sjcl.bitArray.getPartial(d);e.push(sjcl.bitArray.partial(b+a&31,32>>24),d<<=8;return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,e=0;for(c=0;c>>d)>>>26),6>d?(k=a[c]<<6-d,d+=26,c++):(k<<=6,d-=6);for(;e.length&3&&!b;)e+="=";return e},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],e,d=0,h=sjcl.codec.base64.j,k=0,f;b&&(h=h.substr(0,62)+"-_");for(e=0;ef)throw new sjcl.exception.invalid("this isn't base64!");26>>d),k=f<<32-d):(d+=6,k^=f<<32-d)}d&56&&c.push(sjcl.bitArray.partial(d&56,k,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.d[0]||this.h();a?(this.c=a.c.slice(0),this.b=a.b.slice(0),this.a=a.a):this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()}; -sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.c=this.f.slice(0);this.b=[];this.a=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.b=sjcl.bitArray.concat(this.b,a);b=this.a;a=this.a=b+sjcl.bitArray.bitLength(a);for(b=512+b&-512;b<=a;b+=512)this.e(c.splice(0,16));return this},finalize:function(){var a,b=this.b,c=this.c,b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0);b.push(Math.floor(this.a/ -4294967296));for(b.push(this.a|0);b.length;)this.e(b.splice(0,16));this.reset();return c},f:[],d:[],h:function(){function a(a){return 0x100000000*(a-Math.floor(a))|0}var b=0,c=2,e;a:for(;64>b;c++){for(e=2;e*e<=c;e++)if(0===c%e)continue a;8>b&&(this.f[b]=a(Math.pow(c,0.5)));this.d[b]=a(Math.pow(c,1/3));b++}},e:function(a){var b,c,e=a.slice(0),d=this.c,h=this.d,k=d[0],f=d[1],g=d[2],u=d[3],m=d[4],v=d[5],w=d[6],x=d[7];for(a=0;64>a;a++)16>a?b=e[a]:(b=e[a+1&15],c=e[a+14&15],b=e[a&15]=(b>>>7^b>>>18^b>>>3^ -b<<25^b<<14)+(c>>>17^c>>>19^c>>>10^c<<15^c<<13)+e[a&15]+e[a+9&15]|0),b=b+x+(m>>>6^m>>>11^m>>>25^m<<26^m<<21^m<<7)+(w^m&(v^w))+h[a],x=w,w=v,v=m,m=u+b|0,u=g,g=f,f=k,k=b+(f&g^u&(f^g))+(f>>>2^f>>>13^f>>>22^f<<30^f<<19^f<<10)|0;d[0]=d[0]+k|0;d[1]=d[1]+f|0;d[2]=d[2]+g|0;d[3]=d[3]+u|0;d[4]=d[4]+m|0;d[5]=d[5]+v|0;d[6]=d[6]+w|0;d[7]=d[7]+x|0}};sjcl.hash.sha512=function(a){this.d[0]||this.h();a?(this.c=a.c.slice(0),this.b=a.b.slice(0),this.a=a.a):this.reset()};sjcl.hash.sha512.hash=function(a){return(new sjcl.hash.sha512).update(a).finalize()}; -sjcl.hash.sha512.prototype={blockSize:1024,reset:function(){this.c=this.f.slice(0);this.b=[];this.a=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.b=sjcl.bitArray.concat(this.b,a);b=this.a;a=this.a=b+sjcl.bitArray.bitLength(a);for(b=1024+b&-1024;b<=a;b+=1024)this.e(c.splice(0,32));return this},finalize:function(){var a,b=this.b,c=this.c,b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+4;a&31;a++)b.push(0);b.push(0);b.push(0); -b.push(Math.floor(this.a/0x100000000));for(b.push(this.a|0);b.length;)this.e(b.splice(0,32));this.reset();return c},f:[],n:[12372232,13281083,9762859,1914609,15106769,4090911,4308331,8266105],d:[],o:[2666018,15689165,5061423,9034684,4764984,380953,1658779,7176472,197186,7368638,14987916,16757986,8096111,1480369,13046325,6891156,15813330,5187043,9229749,11312229,2818677,10937475,4324308,1135541,6741931,11809296,16458047,15666916,11046850,698149,229999,945776,13774844,2541862,12856045,9810911,11494366, -7844520,15576806,8533307,15795044,4337665,16291729,5553712,15684120,6662416,7413802,12308920,13816008,4303699,9366425,10176680,13195875,4295371,6546291,11712675,15708924,1519456,15772530,6568428,6495784,8568297,13007125,7492395,2515356,12632583,14740254,7262584,1535930,13146278,16321966,1853211,294276,13051027,13221564,1051980,4080310,6651434,14088940,4675607],h:function(){function a(a){return 0x100000000*(a-Math.floor(a))|0}function b(a){return 0x10000000000*(a-Math.floor(a))&255}var c=0,e=2,d;a:for(;80> -c;e++){for(d=2;d*d<=e;d++)if(0===e%d)continue a;8>c&&(this.f[2*c]=a(Math.pow(e,0.5)),this.f[2*c+1]=b(Math.pow(e,0.5))<<24|this.n[c]);this.d[2*c]=a(Math.pow(e,1/3));this.d[2*c+1]=b(Math.pow(e,1/3))<<24|this.o[c];c++}},e:function(a){var b,c,e=a.slice(0),d=this.c,h=this.d,k=d[0],f=d[1],g=d[2],u=d[3],m=d[4],v=d[5],w=d[6],x=d[7],R=d[8],H=d[9],S=d[10],I=d[11],T=d[12],J=d[13],U=d[14],K=d[15],q=k,n=f,A=g,y=u,B=m,z=v,N=w,C=x,r=R,p=H,L=S,D=I,M=T,E=J,O=U,F=K;for(a=0;80>a;a++){if(16>a)b=e[2*a],c=e[2*a+1];else{c= -e[2*(a-15)];var l=e[2*(a-15)+1];b=(l<<31|c>>>1)^(l<<24|c>>>8)^c>>>7;var s=(c<<31|l>>>1)^(c<<24|l>>>8)^(c<<25|l>>>7);c=e[2*(a-2)];var t=e[2*(a-2)+1],l=(t<<13|c>>>19)^(c<<3|t>>>29)^c>>>6,t=(c<<13|t>>>19)^(t<<3|c>>>29)^(c<<26|t>>>6),P=e[2*(a-7)],Q=e[2*(a-16)],G=e[2*(a-16)+1];c=s+e[2*(a-7)+1];b=b+P+(c>>>0>>0?1:0);c+=t;b+=l+(c>>>0>>0?1:0);c+=G;b+=Q+(c>>>0>>0?1:0)}e[2*a]=b|=0;e[2*a+1]=c|=0;var P=r&L^~r&M,V=p&D^~p&E,t=q&A^q&B^A&B,X=n&y^n&z^y&z,Q=(n<<4|q>>>28)^(q<<30|n>>>2)^(q<<25|n>>>7),G=(q<<4| -n>>>28)^(n<<30|q>>>2)^(n<<25|q>>>7),Y=h[2*a],W=h[2*a+1],l=F+((r<<18|p>>>14)^(r<<14|p>>>18)^(p<<23|r>>>9)),s=O+((p<<18|r>>>14)^(p<<14|r>>>18)^(r<<23|p>>>9))+(l>>>0>>0?1:0),l=l+V,s=s+(P+(l>>>0>>0?1:0)),l=l+W,s=s+(Y+(l>>>0>>0?1:0)),l=l+c,s=s+(b+(l>>>0>>0?1:0));c=G+X;b=Q+t+(c>>>0>>0?1:0);O=M;F=E;M=L;E=D;L=r;D=p;p=C+l|0;r=N+s+(p>>>0>>0?1:0)|0;N=B;C=z;B=A;z=y;A=q;y=n;n=l+c|0;q=s+b+(n>>>0>>0?1:0)|0}f=d[1]=f+n|0;d[0]=k+q+(f>>>0>>0?1:0)|0;u=d[3]=u+y|0;d[2]=g+A+(u>>>0>>0?1:0)|0;v= -d[5]=v+z|0;d[4]=m+B+(v>>>0>>0?1:0)|0;x=d[7]=x+C|0;d[6]=w+N+(x>>>0>>0?1:0)|0;H=d[9]=H+p|0;d[8]=R+r+(H>>>0

>>0?1:0)|0;I=d[11]=I+D|0;d[10]=S+L+(I>>>0>>0?1:0)|0;J=d[13]=J+E|0;d[12]=T+M+(J>>>0>>0?1:0)|0;K=d[15]=K+F|0;d[14]=U+O+(K>>>0>>0?1:0)|0}};sjcl.hash.sha1=function(a){a?(this.c=a.c.slice(0),this.b=a.b.slice(0),this.a=a.a):this.reset()};sjcl.hash.sha1.hash=function(a){return(new sjcl.hash.sha1).update(a).finalize()}; -sjcl.hash.sha1.prototype={blockSize:512,reset:function(){this.c=this.f.slice(0);this.b=[];this.a=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.b=sjcl.bitArray.concat(this.b,a);b=this.a;a=this.a=b+sjcl.bitArray.bitLength(a);for(b=this.blockSize+b&-this.blockSize;b<=a;b+=this.blockSize)this.e(c.splice(0,16));return this},finalize:function(){var a,b=this.b,c=this.c,b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0); -b.push(Math.floor(this.a/0x100000000));for(b.push(this.a|0);b.length;)this.e(b.splice(0,16));this.reset();return c},f:[1732584193,4023233417,2562383102,271733878,3285377520],d:[1518500249,1859775393,2400959708,3395469782],e:function(a){var b,c,e,d,h,k,f=a.slice(0),g=this.c;c=g[0];e=g[1];d=g[2];h=g[3];k=g[4];for(a=0;79>=a;a++)16<=a&&(f[a]=(f[a-3]^f[a-8]^f[a-14]^f[a-16])<<1|(f[a-3]^f[a-8]^f[a-14]^f[a-16])>>>31),b=19>=a?e&d|~e&h:39>=a?e^d^h:59>=a?e&d|e&h|d&h:79>=a?e^d^h:void 0,b=(c<<5|c>>>27)+b+k+f[a]+ -this.d[Math.floor(a/20)]|0,k=h,h=d,d=e<<30|e>>>2,e=c,c=b;g[0]=g[0]+c|0;g[1]=g[1]+e|0;g[2]=g[2]+d|0;g[3]=g[3]+h|0;g[4]=g[4]+k|0}};sjcl.misc.hmac=function(a,b){this.k=b=b||sjcl.hash.sha256;var c=[[],[]],e,d=b.prototype.blockSize/32;this.g=[new b,new b];a.length>d&&(a=b.hash(a));for(e=0;e>>24]<<24 ^ sbox[tmp>>16&255]<<16 ^ sbox[tmp>>8&255]<<8 ^ sbox[tmp&255]; + + // shift rows and add rcon + if (i%keyLen === 0) { + tmp = tmp<<8 ^ tmp>>>24 ^ rcon<<24; + rcon = rcon<<1 ^ (rcon>>7)*283; + } + } + + encKey[i] = encKey[i-keyLen] ^ tmp; + } + + // schedule decryption keys + for (j = 0; i; j++, i--) { + tmp = encKey[j&3 ? i : i - 4]; + if (i<=4 || j<4) { + decKey[j] = tmp; + } else { + decKey[j] = decTable[0][sbox[tmp>>>24 ]] ^ + decTable[1][sbox[tmp>>16 & 255]] ^ + decTable[2][sbox[tmp>>8 & 255]] ^ + decTable[3][sbox[tmp & 255]]; + } + } +}; + +sjcl.cipher.aes.prototype = { + // public + /* Something like this might appear here eventually + name: "AES", + blockSize: 4, + keySizes: [4,6,8], + */ + + /** + * Encrypt an array of 4 big-endian words. + * @param {Array} data The plaintext. + * @return {Array} The ciphertext. + */ + encrypt:function (data) { return this._crypt(data,0); }, + + /** + * Decrypt an array of 4 big-endian words. + * @param {Array} data The ciphertext. + * @return {Array} The plaintext. + */ + decrypt:function (data) { return this._crypt(data,1); }, + + /** + * The expanded S-box and inverse S-box tables. These will be computed + * on the client so that we don't have to send them down the wire. + * + * There are two tables, _tables[0] is for encryption and + * _tables[1] is for decryption. + * + * The first 4 sub-tables are the expanded S-box with MixColumns. The + * last (_tables[01][4]) is the S-box itself. + * + * @private + */ + _tables: [[[],[],[],[],[]],[[],[],[],[],[]]], + + /** + * Expand the S-box tables. + * + * @private + */ + _precompute: function () { + var encTable = this._tables[0], decTable = this._tables[1], + sbox = encTable[4], sboxInv = decTable[4], + i, x, xInv, d=[], th=[], x2, x4, x8, s, tEnc, tDec; + + // Compute double and third tables + for (i = 0; i < 256; i++) { + th[( d[i] = i<<1 ^ (i>>7)*283 )^i]=i; + } + + for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) { + // Compute sbox + s = xInv ^ xInv<<1 ^ xInv<<2 ^ xInv<<3 ^ xInv<<4; + s = s>>8 ^ s&255 ^ 99; + sbox[x] = s; + sboxInv[s] = x; + + // Compute MixColumns + x8 = d[x4 = d[x2 = d[x]]]; + tDec = x8*0x1010101 ^ x4*0x10001 ^ x2*0x101 ^ x*0x1010100; + tEnc = d[s]*0x101 ^ s*0x1010100; + + for (i = 0; i < 4; i++) { + encTable[i][x] = tEnc = tEnc<<24 ^ tEnc>>>8; + decTable[i][s] = tDec = tDec<<24 ^ tDec>>>8; + } + } + + // Compactify. Considerable speedup on Firefox. + for (i = 0; i < 5; i++) { + encTable[i] = encTable[i].slice(0); + decTable[i] = decTable[i].slice(0); + } + }, + + /** + * Encryption and decryption core. + * @param {Array} input Four words to be encrypted or decrypted. + * @param dir The direction, 0 for encrypt and 1 for decrypt. + * @return {Array} The four encrypted or decrypted words. + * @private + */ + _crypt:function (input, dir) { + if (input.length !== 4) { + throw new sjcl.exception.invalid("invalid aes block size"); + } + + var key = this._key[dir], + // state variables a,b,c,d are loaded with pre-whitened data + a = input[0] ^ key[0], + b = input[dir ? 3 : 1] ^ key[1], + c = input[2] ^ key[2], + d = input[dir ? 1 : 3] ^ key[3], + a2, b2, c2, + + nInnerRounds = key.length/4 - 2, + i, + kIndex = 4, + out = [0,0,0,0], + table = this._tables[dir], + + // load up the tables + t0 = table[0], + t1 = table[1], + t2 = table[2], + t3 = table[3], + sbox = table[4]; + + // Inner rounds. Cribbed from OpenSSL. + for (i = 0; i < nInnerRounds; i++) { + a2 = t0[a>>>24] ^ t1[b>>16 & 255] ^ t2[c>>8 & 255] ^ t3[d & 255] ^ key[kIndex]; + b2 = t0[b>>>24] ^ t1[c>>16 & 255] ^ t2[d>>8 & 255] ^ t3[a & 255] ^ key[kIndex + 1]; + c2 = t0[c>>>24] ^ t1[d>>16 & 255] ^ t2[a>>8 & 255] ^ t3[b & 255] ^ key[kIndex + 2]; + d = t0[d>>>24] ^ t1[a>>16 & 255] ^ t2[b>>8 & 255] ^ t3[c & 255] ^ key[kIndex + 3]; + kIndex += 4; + a=a2; b=b2; c=c2; + } + + // Last round. + for (i = 0; i < 4; i++) { + out[dir ? 3&-i : i] = + sbox[a>>>24 ]<<24 ^ + sbox[b>>16 & 255]<<16 ^ + sbox[c>>8 & 255]<<8 ^ + sbox[d & 255] ^ + key[kIndex++]; + a2=a; a=b; b=c; c=d; d=a2; + } + + return out; + } +}; + +/** @fileOverview Arrays of bits, encoded as arrays of Numbers. + * + * @author Emily Stark + * @author Mike Hamburg + * @author Dan Boneh + */ + +/** @namespace Arrays of bits, encoded as arrays of Numbers. + * + * @description + *

+ * These objects are the currency accepted by SJCL's crypto functions. + *

+ * + *

+ * Most of our crypto primitives operate on arrays of 4-byte words internally, + * but many of them can take arguments that are not a multiple of 4 bytes. + * This library encodes arrays of bits (whose size need not be a multiple of 8 + * bits) as arrays of 32-bit words. The bits are packed, big-endian, into an + * array of words, 32 bits at a time. Since the words are double-precision + * floating point numbers, they fit some extra data. We use this (in a private, + * possibly-changing manner) to encode the number of bits actually present + * in the last word of the array. + *

+ * + *

+ * Because bitwise ops clear this out-of-band data, these arrays can be passed + * to ciphers like AES which want arrays of words. + *

+ */ +sjcl.bitArray = { + /** + * Array slices in units of bits. + * @param {bitArray} a The array to slice. + * @param {Number} bstart The offset to the start of the slice, in bits. + * @param {Number} bend The offset to the end of the slice, in bits. If this is undefined, + * slice until the end of the array. + * @return {bitArray} The requested slice. + */ + bitSlice: function (a, bstart, bend) { + a = sjcl.bitArray._shiftRight(a.slice(bstart/32), 32 - (bstart & 31)).slice(1); + return (bend === undefined) ? a : sjcl.bitArray.clamp(a, bend-bstart); + }, + + /** + * Extract a number packed into a bit array. + * @param {bitArray} a The array to slice. + * @param {Number} bstart The offset to the start of the slice, in bits. + * @param {Number} length The length of the number to extract. + * @return {Number} The requested slice. + */ + extract: function(a, bstart, blength) { + // FIXME: this Math.floor is not necessary at all, but for some reason + // seems to suppress a bug in the Chromium JIT. + var x, sh = Math.floor((-bstart-blength) & 31); + if ((bstart + blength - 1 ^ bstart) & -32) { + // it crosses a boundary + x = (a[bstart/32|0] << (32 - sh)) ^ (a[bstart/32+1|0] >>> sh); + } else { + // within a single word + x = a[bstart/32|0] >>> sh; + } + return x & ((1< 0 && len) { + a[l-1] = sjcl.bitArray.partial(len, a[l-1] & 0x80000000 >> (len-1), 1); + } + return a; + }, + + /** + * Make a partial word for a bit array. + * @param {Number} len The number of bits in the word. + * @param {Number} x The bits. + * @param {Number} [0] _end Pass 1 if x has already been shifted to the high side. + * @return {Number} The partial word. + */ + partial: function (len, x, _end) { + if (len === 32) { return x; } + return (_end ? x|0 : x << (32-len)) + len * 0x10000000000; + }, + + /** + * Get the number of bits used by a partial word. + * @param {Number} x The partial word. + * @return {Number} The number of bits used by the partial word. + */ + getPartial: function (x) { + return Math.round(x/0x10000000000) || 32; + }, + + /** + * Compare two arrays for equality in a predictable amount of time. + * @param {bitArray} a The first array. + * @param {bitArray} b The second array. + * @return {boolean} true if a == b; false otherwise. + */ + equal: function (a, b) { + if (sjcl.bitArray.bitLength(a) !== sjcl.bitArray.bitLength(b)) { + return false; + } + var x = 0, i; + for (i=0; i= 32; shift -= 32) { + out.push(carry); + carry = 0; + } + if (shift === 0) { + return out.concat(a); + } + + for (i=0; i>>shift); + carry = a[i] << (32-shift); + } + last2 = a.length ? a[a.length-1] : 0; + shift2 = sjcl.bitArray.getPartial(last2); + out.push(sjcl.bitArray.partial(shift+shift2 & 31, (shift + shift2 > 32) ? carry : out.pop(),1)); + return out; + }, + + /** xor a block of 4 words together. + * @private + */ + _xor4: function(x,y) { + return [x[0]^y[0],x[1]^y[1],x[2]^y[2],x[3]^y[3]]; + } +}; +/** @fileOverview Bit array codec implementations. + * + * @author Emily Stark + * @author Mike Hamburg + * @author Dan Boneh + */ + +/** @namespace UTF-8 strings */ +sjcl.codec.utf8String = { + /** Convert from a bitArray to a UTF-8 string. */ + fromBits: function (arr) { + var out = "", bl = sjcl.bitArray.bitLength(arr), i, tmp; + for (i=0; i>> 24); + tmp <<= 8; + } + return decodeURIComponent(escape(out)); + }, + + /** Convert from a UTF-8 string to a bitArray. */ + toBits: function (str) { + str = unescape(encodeURIComponent(str)); + var out = [], i, tmp=0; + for (i=0; i>>bits) >>> 26); + if (bits < 6) { + ta = arr[i] << (6-bits); + bits += 26; + i++; + } else { + ta <<= 6; + bits -= 6; + } + } + while ((out.length & 3) && !_noEquals) { out += "="; } + return out; + }, + + /** Convert from a base64 string to a bitArray */ + toBits: function(str, _url) { + str = str.replace(/\s|=/g,''); + var out = [], i, bits=0, c = sjcl.codec.base64._chars, ta=0, x; + if (_url) { + c = c.substr(0,62) + '-_'; + } + for (i=0; i 26) { + bits -= 26; + out.push(ta ^ x>>>bits); + ta = x << (32-bits); + } else { + bits += 6; + ta ^= x << (32-bits); + } + } + if (bits&56) { + out.push(sjcl.bitArray.partial(bits&56, ta, 1)); + } + return out; + } +}; + +sjcl.codec.base64url = { + fromBits: function (arr) { return sjcl.codec.base64.fromBits(arr,1,1); }, + toBits: function (str) { return sjcl.codec.base64.toBits(str,1); } +}; +/** @fileOverview Javascript SHA-256 implementation. + * + * An older version of this implementation is available in the public + * domain, but this one is (c) Emily Stark, Mike Hamburg, Dan Boneh, + * Stanford University 2008-2010 and BSD-licensed for liability + * reasons. + * + * Special thanks to Aldo Cortesi for pointing out several bugs in + * this code. + * + * @author Emily Stark + * @author Mike Hamburg + * @author Dan Boneh + */ + +/** + * Context for a SHA-256 operation in progress. + * @constructor + * @class Secure Hash Algorithm, 256 bits. + */ +sjcl.hash.sha256 = function (hash) { + if (!this._key[0]) { this._precompute(); } + if (hash) { + this._h = hash._h.slice(0); + this._buffer = hash._buffer.slice(0); + this._length = hash._length; + } else { + this.reset(); + } +}; + +/** + * Hash a string or an array of words. + * @static + * @param {bitArray|String} data the data to hash. + * @return {bitArray} The hash value, an array of 16 big-endian words. + */ +sjcl.hash.sha256.hash = function (data) { + return (new sjcl.hash.sha256()).update(data).finalize(); +}; + +sjcl.hash.sha256.prototype = { + /** + * The hash's block size, in bits. + * @constant + */ + blockSize: 512, + + /** + * Reset the hash state. + * @return this + */ + reset:function () { + this._h = this._init.slice(0); + this._buffer = []; + this._length = 0; + return this; + }, + + /** + * Input several words to the hash. + * @param {bitArray|String} data the data to hash. + * @return this + */ + update: function (data) { + if (typeof data === "string") { + data = sjcl.codec.utf8String.toBits(data); + } + var i, b = this._buffer = sjcl.bitArray.concat(this._buffer, data), + ol = this._length, + nl = this._length = ol + sjcl.bitArray.bitLength(data); + for (i = 512+ol & -512; i <= nl; i+= 512) { + this._block(b.splice(0,16)); + } + return this; + }, + + /** + * Complete hashing and output the hash value. + * @return {bitArray} The hash value, an array of 8 big-endian words. + */ + finalize:function () { + var i, b = this._buffer, h = this._h; + + // Round out and push the buffer + b = sjcl.bitArray.concat(b, [sjcl.bitArray.partial(1,1)]); + + // Round out the buffer to a multiple of 16 words, less the 2 length words. + for (i = b.length + 2; i & 15; i++) { + b.push(0); + } + + // append the length + b.push(Math.floor(this._length / 0x100000000)); + b.push(this._length | 0); + + while (b.length) { + this._block(b.splice(0,16)); + } + + this.reset(); + return h; + }, + + /** + * The SHA-256 initialization vector, to be precomputed. + * @private + */ + _init:[], + /* + _init:[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19], + */ + + /** + * The SHA-256 hash key, to be precomputed. + * @private + */ + _key:[], + /* + _key: + [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2], + */ + + + /** + * Function to precompute _init and _key. + * @private + */ + _precompute: function () { + var i = 0, prime = 2, factor; + + function frac(x) { return (x-Math.floor(x)) * 0x100000000 | 0; } + + outer: for (; i<64; prime++) { + for (factor=2; factor*factor <= prime; factor++) { + if (prime % factor === 0) { + // not a prime + continue outer; + } + } + + if (i<8) { + this._init[i] = frac(Math.pow(prime, 1/2)); + } + this._key[i] = frac(Math.pow(prime, 1/3)); + i++; + } + }, + + /** + * Perform one cycle of SHA-256. + * @param {bitArray} words one block of words. + * @private + */ + _block:function (words) { + var i, tmp, a, b, + w = words.slice(0), + h = this._h, + k = this._key, + h0 = h[0], h1 = h[1], h2 = h[2], h3 = h[3], + h4 = h[4], h5 = h[5], h6 = h[6], h7 = h[7]; + + /* Rationale for placement of |0 : + * If a value can overflow is original 32 bits by a factor of more than a few + * million (2^23 ish), there is a possibility that it might overflow the + * 53-bit mantissa and lose precision. + * + * To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that + * propagates around the loop, and on the hash state h[]. I don't believe + * that the clamps on h4 and on h0 are strictly necessary, but it's close + * (for h4 anyway), and better safe than sorry. + * + * The clamps on h[] are necessary for the output to be correct even in the + * common case and for short inputs. + */ + for (i=0; i<64; i++) { + // load up the input word for this round + if (i<16) { + tmp = w[i]; + } else { + a = w[(i+1 ) & 15]; + b = w[(i+14) & 15]; + tmp = w[i&15] = ((a>>>7 ^ a>>>18 ^ a>>>3 ^ a<<25 ^ a<<14) + + (b>>>17 ^ b>>>19 ^ b>>>10 ^ b<<15 ^ b<<13) + + w[i&15] + w[(i+9) & 15]) | 0; + } + + tmp = (tmp + h7 + (h4>>>6 ^ h4>>>11 ^ h4>>>25 ^ h4<<26 ^ h4<<21 ^ h4<<7) + (h6 ^ h4&(h5^h6)) + k[i]); // | 0; + + // shift register + h7 = h6; h6 = h5; h5 = h4; + h4 = h3 + tmp | 0; + h3 = h2; h2 = h1; h1 = h0; + + h0 = (tmp + ((h1&h2) ^ (h3&(h1^h2))) + (h1>>>2 ^ h1>>>13 ^ h1>>>22 ^ h1<<30 ^ h1<<19 ^ h1<<10)) | 0; + } + + h[0] = h[0]+h0 | 0; + h[1] = h[1]+h1 | 0; + h[2] = h[2]+h2 | 0; + h[3] = h[3]+h3 | 0; + h[4] = h[4]+h4 | 0; + h[5] = h[5]+h5 | 0; + h[6] = h[6]+h6 | 0; + h[7] = h[7]+h7 | 0; + } +}; + + +/** @fileOverview Javascript SHA-512 implementation. + * + * This implementation was written for CryptoJS by Jeff Mott and adapted for + * SJCL by Stefan Thomas. + * + * CryptoJS (c) 2009–2012 by Jeff Mott. All rights reserved. + * Released with New BSD License + * + * @author Emily Stark + * @author Mike Hamburg + * @author Dan Boneh + * @author Jeff Mott + * @author Stefan Thomas + */ + +/** + * Context for a SHA-512 operation in progress. + * @constructor + * @class Secure Hash Algorithm, 512 bits. + */ +sjcl.hash.sha512 = function (hash) { + if (!this._key[0]) { this._precompute(); } + if (hash) { + this._h = hash._h.slice(0); + this._buffer = hash._buffer.slice(0); + this._length = hash._length; + } else { + this.reset(); + } +}; + +/** + * Hash a string or an array of words. + * @static + * @param {bitArray|String} data the data to hash. + * @return {bitArray} The hash value, an array of 16 big-endian words. + */ +sjcl.hash.sha512.hash = function (data) { + return (new sjcl.hash.sha512()).update(data).finalize(); +}; + +sjcl.hash.sha512.prototype = { + /** + * The hash's block size, in bits. + * @constant + */ + blockSize: 1024, + + /** + * Reset the hash state. + * @return this + */ + reset:function () { + this._h = this._init.slice(0); + this._buffer = []; + this._length = 0; + return this; + }, + + /** + * Input several words to the hash. + * @param {bitArray|String} data the data to hash. + * @return this + */ + update: function (data) { + if (typeof data === "string") { + data = sjcl.codec.utf8String.toBits(data); + } + var i, b = this._buffer = sjcl.bitArray.concat(this._buffer, data), + ol = this._length, + nl = this._length = ol + sjcl.bitArray.bitLength(data); + for (i = 1024+ol & -1024; i <= nl; i+= 1024) { + this._block(b.splice(0,32)); + } + return this; + }, + + /** + * Complete hashing and output the hash value. + * @return {bitArray} The hash value, an array of 16 big-endian words. + */ + finalize:function () { + var i, b = this._buffer, h = this._h; + + // Round out and push the buffer + b = sjcl.bitArray.concat(b, [sjcl.bitArray.partial(1,1)]); + + // Round out the buffer to a multiple of 32 words, less the 4 length words. + for (i = b.length + 4; i & 31; i++) { + b.push(0); + } + + // append the length + b.push(0); + b.push(0); + b.push(Math.floor(this._length / 0x100000000)); + b.push(this._length | 0); + + while (b.length) { + this._block(b.splice(0,32)); + } + + this.reset(); + return h; + }, + + /** + * The SHA-512 initialization vector, to be precomputed. + * @private + */ + _init:[], + + /** + * Least significant 24 bits of SHA512 initialization values. + * + * Javascript only has 53 bits of precision, so we compute the 40 most + * significant bits and add the remaining 24 bits as constants. + * + * @private + */ + _initr: [ 0xbcc908, 0xcaa73b, 0x94f82b, 0x1d36f1, 0xe682d1, 0x3e6c1f, 0x41bd6b, 0x7e2179 ], + + /* + _init: + [0x6a09e667, 0xf3bcc908, 0xbb67ae85, 0x84caa73b, 0x3c6ef372, 0xfe94f82b, 0xa54ff53a, 0x5f1d36f1, + 0x510e527f, 0xade682d1, 0x9b05688c, 0x2b3e6c1f, 0x1f83d9ab, 0xfb41bd6b, 0x5be0cd19, 0x137e2179], + */ + + /** + * The SHA-512 hash key, to be precomputed. + * @private + */ + _key:[], + + /** + * Least significant 24 bits of SHA512 key values. + * @private + */ + _keyr: + [0x28ae22, 0xef65cd, 0x4d3b2f, 0x89dbbc, 0x48b538, 0x05d019, 0x194f9b, 0x6d8118, + 0x030242, 0x706fbe, 0xe4b28c, 0xffb4e2, 0x7b896f, 0x1696b1, 0xc71235, 0x692694, + 0xf14ad2, 0x4f25e3, 0x8cd5b5, 0xac9c65, 0x2b0275, 0xa6e483, 0x41fbd4, 0x1153b5, + 0x66dfab, 0xb43210, 0xfb213f, 0xef0ee4, 0xa88fc2, 0x0aa725, 0x03826f, 0x0e6e70, + 0xd22ffc, 0x26c926, 0xc42aed, 0x95b3df, 0xaf63de, 0x77b2a8, 0xedaee6, 0x82353b, + 0xf10364, 0x423001, 0xf89791, 0x54be30, 0xef5218, 0x65a910, 0x71202a, 0xbbd1b8, + 0xd2d0c8, 0x41ab53, 0x8eeb99, 0x9b48a8, 0xc95a63, 0x418acb, 0x63e373, 0xb2b8a3, + 0xefb2fc, 0x172f60, 0xf0ab72, 0x6439ec, 0x631e28, 0x82bde9, 0xc67915, 0x72532b, + 0x26619c, 0xc0c207, 0xe0eb1e, 0x6ed178, 0x176fba, 0xc898a6, 0xf90dae, 0x1c471b, + 0x047d84, 0xc72493, 0xc9bebc, 0x100d4c, 0x3e42b6, 0x657e2a, 0xd6faec, 0x475817], + + /* + _key: + [0x428a2f98, 0xd728ae22, 0x71374491, 0x23ef65cd, 0xb5c0fbcf, 0xec4d3b2f, 0xe9b5dba5, 0x8189dbbc, + 0x3956c25b, 0xf348b538, 0x59f111f1, 0xb605d019, 0x923f82a4, 0xaf194f9b, 0xab1c5ed5, 0xda6d8118, + 0xd807aa98, 0xa3030242, 0x12835b01, 0x45706fbe, 0x243185be, 0x4ee4b28c, 0x550c7dc3, 0xd5ffb4e2, + 0x72be5d74, 0xf27b896f, 0x80deb1fe, 0x3b1696b1, 0x9bdc06a7, 0x25c71235, 0xc19bf174, 0xcf692694, + 0xe49b69c1, 0x9ef14ad2, 0xefbe4786, 0x384f25e3, 0x0fc19dc6, 0x8b8cd5b5, 0x240ca1cc, 0x77ac9c65, + 0x2de92c6f, 0x592b0275, 0x4a7484aa, 0x6ea6e483, 0x5cb0a9dc, 0xbd41fbd4, 0x76f988da, 0x831153b5, + 0x983e5152, 0xee66dfab, 0xa831c66d, 0x2db43210, 0xb00327c8, 0x98fb213f, 0xbf597fc7, 0xbeef0ee4, + 0xc6e00bf3, 0x3da88fc2, 0xd5a79147, 0x930aa725, 0x06ca6351, 0xe003826f, 0x14292967, 0x0a0e6e70, + 0x27b70a85, 0x46d22ffc, 0x2e1b2138, 0x5c26c926, 0x4d2c6dfc, 0x5ac42aed, 0x53380d13, 0x9d95b3df, + 0x650a7354, 0x8baf63de, 0x766a0abb, 0x3c77b2a8, 0x81c2c92e, 0x47edaee6, 0x92722c85, 0x1482353b, + 0xa2bfe8a1, 0x4cf10364, 0xa81a664b, 0xbc423001, 0xc24b8b70, 0xd0f89791, 0xc76c51a3, 0x0654be30, + 0xd192e819, 0xd6ef5218, 0xd6990624, 0x5565a910, 0xf40e3585, 0x5771202a, 0x106aa070, 0x32bbd1b8, + 0x19a4c116, 0xb8d2d0c8, 0x1e376c08, 0x5141ab53, 0x2748774c, 0xdf8eeb99, 0x34b0bcb5, 0xe19b48a8, + 0x391c0cb3, 0xc5c95a63, 0x4ed8aa4a, 0xe3418acb, 0x5b9cca4f, 0x7763e373, 0x682e6ff3, 0xd6b2b8a3, + 0x748f82ee, 0x5defb2fc, 0x78a5636f, 0x43172f60, 0x84c87814, 0xa1f0ab72, 0x8cc70208, 0x1a6439ec, + 0x90befffa, 0x23631e28, 0xa4506ceb, 0xde82bde9, 0xbef9a3f7, 0xb2c67915, 0xc67178f2, 0xe372532b, + 0xca273ece, 0xea26619c, 0xd186b8c7, 0x21c0c207, 0xeada7dd6, 0xcde0eb1e, 0xf57d4f7f, 0xee6ed178, + 0x06f067aa, 0x72176fba, 0x0a637dc5, 0xa2c898a6, 0x113f9804, 0xbef90dae, 0x1b710b35, 0x131c471b, + 0x28db77f5, 0x23047d84, 0x32caab7b, 0x40c72493, 0x3c9ebe0a, 0x15c9bebc, 0x431d67c4, 0x9c100d4c, + 0x4cc5d4be, 0xcb3e42b6, 0x597f299c, 0xfc657e2a, 0x5fcb6fab, 0x3ad6faec, 0x6c44198c, 0x4a475817], + */ + + /** + * Function to precompute _init and _key. + * @private + */ + _precompute: function () { + // XXX: This code is for precomputing the SHA256 constants, change for + // SHA512 and re-enable. + var i = 0, prime = 2, factor; + + function frac(x) { return (x-Math.floor(x)) * 0x100000000 | 0; } + function frac2(x) { return (x-Math.floor(x)) * 0x10000000000 & 0xff; } + + outer: for (; i<80; prime++) { + for (factor=2; factor*factor <= prime; factor++) { + if (prime % factor === 0) { + // not a prime + continue outer; + } + } + + if (i<8) { + this._init[i*2] = frac(Math.pow(prime, 1/2)); + this._init[i*2+1] = (frac2(Math.pow(prime, 1/2)) << 24) | this._initr[i]; + } + this._key[i*2] = frac(Math.pow(prime, 1/3)); + this._key[i*2+1] = (frac2(Math.pow(prime, 1/3)) << 24) | this._keyr[i]; + i++; + } + }, + + /** + * Perform one cycle of SHA-512. + * @param {bitArray} words one block of words. + * @private + */ + _block:function (words) { + var i, wrh, wrl, + w = words.slice(0), + h = this._h, + k = this._key, + h0h = h[ 0], h0l = h[ 1], h1h = h[ 2], h1l = h[ 3], + h2h = h[ 4], h2l = h[ 5], h3h = h[ 6], h3l = h[ 7], + h4h = h[ 8], h4l = h[ 9], h5h = h[10], h5l = h[11], + h6h = h[12], h6l = h[13], h7h = h[14], h7l = h[15]; + + // Working variables + var ah = h0h, al = h0l, bh = h1h, bl = h1l, + ch = h2h, cl = h2l, dh = h3h, dl = h3l, + eh = h4h, el = h4l, fh = h5h, fl = h5l, + gh = h6h, gl = h6l, hh = h7h, hl = h7l; + + for (i=0; i<80; i++) { + // load up the input word for this round + if (i<16) { + wrh = w[i * 2]; + wrl = w[i * 2 + 1]; + } else { + // Gamma0 + var gamma0xh = w[(i-15) * 2]; + var gamma0xl = w[(i-15) * 2 + 1]; + var gamma0h = + ((gamma0xl << 31) | (gamma0xh >>> 1)) ^ + ((gamma0xl << 24) | (gamma0xh >>> 8)) ^ + (gamma0xh >>> 7); + var gamma0l = + ((gamma0xh << 31) | (gamma0xl >>> 1)) ^ + ((gamma0xh << 24) | (gamma0xl >>> 8)) ^ + ((gamma0xh << 25) | (gamma0xl >>> 7)); + + // Gamma1 + var gamma1xh = w[(i-2) * 2]; + var gamma1xl = w[(i-2) * 2 + 1]; + var gamma1h = + ((gamma1xl << 13) | (gamma1xh >>> 19)) ^ + ((gamma1xh << 3) | (gamma1xl >>> 29)) ^ + (gamma1xh >>> 6); + var gamma1l = + ((gamma1xh << 13) | (gamma1xl >>> 19)) ^ + ((gamma1xl << 3) | (gamma1xh >>> 29)) ^ + ((gamma1xh << 26) | (gamma1xl >>> 6)); + + // Shortcuts + var wr7h = w[(i-7) * 2]; + var wr7l = w[(i-7) * 2 + 1]; + + var wr16h = w[(i-16) * 2]; + var wr16l = w[(i-16) * 2 + 1]; + + // W(round) = gamma0 + W(round - 7) + gamma1 + W(round - 16) + wrl = gamma0l + wr7l; + wrh = gamma0h + wr7h + ((wrl >>> 0) < (gamma0l >>> 0) ? 1 : 0); + wrl += gamma1l; + wrh += gamma1h + ((wrl >>> 0) < (gamma1l >>> 0) ? 1 : 0); + wrl += wr16l; + wrh += wr16h + ((wrl >>> 0) < (wr16l >>> 0) ? 1 : 0); + } + + w[i*2] = wrh |= 0; + w[i*2 + 1] = wrl |= 0; + + // Ch + var chh = (eh & fh) ^ (~eh & gh); + var chl = (el & fl) ^ (~el & gl); + + // Maj + var majh = (ah & bh) ^ (ah & ch) ^ (bh & ch); + var majl = (al & bl) ^ (al & cl) ^ (bl & cl); + + // Sigma0 + var sigma0h = ((al << 4) | (ah >>> 28)) ^ ((ah << 30) | (al >>> 2)) ^ ((ah << 25) | (al >>> 7)); + var sigma0l = ((ah << 4) | (al >>> 28)) ^ ((al << 30) | (ah >>> 2)) ^ ((al << 25) | (ah >>> 7)); + + // Sigma1 + var sigma1h = ((el << 18) | (eh >>> 14)) ^ ((el << 14) | (eh >>> 18)) ^ ((eh << 23) | (el >>> 9)); + var sigma1l = ((eh << 18) | (el >>> 14)) ^ ((eh << 14) | (el >>> 18)) ^ ((el << 23) | (eh >>> 9)); + + // K(round) + var krh = k[i*2]; + var krl = k[i*2+1]; + + // t1 = h + sigma1 + ch + K(round) + W(round) + var t1l = hl + sigma1l; + var t1h = hh + sigma1h + ((t1l >>> 0) < (hl >>> 0) ? 1 : 0); + t1l += chl; + t1h += chh + ((t1l >>> 0) < (chl >>> 0) ? 1 : 0); + t1l += krl; + t1h += krh + ((t1l >>> 0) < (krl >>> 0) ? 1 : 0); + t1l += wrl; + t1h += wrh + ((t1l >>> 0) < (wrl >>> 0) ? 1 : 0); + + // t2 = sigma0 + maj + var t2l = sigma0l + majl; + var t2h = sigma0h + majh + ((t2l >>> 0) < (sigma0l >>> 0) ? 1 : 0); + + // Update working variables + hh = gh; + hl = gl; + gh = fh; + gl = fl; + fh = eh; + fl = el; + el = (dl + t1l) | 0; + eh = (dh + t1h + ((el >>> 0) < (dl >>> 0) ? 1 : 0)) | 0; + dh = ch; + dl = cl; + ch = bh; + cl = bl; + bh = ah; + bl = al; + al = (t1l + t2l) | 0; + ah = (t1h + t2h + ((al >>> 0) < (t1l >>> 0) ? 1 : 0)) | 0; + } + + // Intermediate hash + h0l = h[1] = (h0l + al) | 0; + h[0] = (h0h + ah + ((h0l >>> 0) < (al >>> 0) ? 1 : 0)) | 0; + h1l = h[3] = (h1l + bl) | 0; + h[2] = (h1h + bh + ((h1l >>> 0) < (bl >>> 0) ? 1 : 0)) | 0; + h2l = h[5] = (h2l + cl) | 0; + h[4] = (h2h + ch + ((h2l >>> 0) < (cl >>> 0) ? 1 : 0)) | 0; + h3l = h[7] = (h3l + dl) | 0; + h[6] = (h3h + dh + ((h3l >>> 0) < (dl >>> 0) ? 1 : 0)) | 0; + h4l = h[9] = (h4l + el) | 0; + h[8] = (h4h + eh + ((h4l >>> 0) < (el >>> 0) ? 1 : 0)) | 0; + h5l = h[11] = (h5l + fl) | 0; + h[10] = (h5h + fh + ((h5l >>> 0) < (fl >>> 0) ? 1 : 0)) | 0; + h6l = h[13] = (h6l + gl) | 0; + h[12] = (h6h + gh + ((h6l >>> 0) < (gl >>> 0) ? 1 : 0)) | 0; + h7l = h[15] = (h7l + hl) | 0; + h[14] = (h7h + hh + ((h7l >>> 0) < (hl >>> 0) ? 1 : 0)) | 0; + } +}; + + +/** @fileOverview Javascript SHA-1 implementation. + * + * Based on the implementation in RFC 3174, method 1, and on the SJCL + * SHA-256 implementation. + * + * @author Quinn Slack + */ + +/** + * Context for a SHA-1 operation in progress. + * @constructor + * @class Secure Hash Algorithm, 160 bits. + */ +sjcl.hash.sha1 = function (hash) { + if (hash) { + this._h = hash._h.slice(0); + this._buffer = hash._buffer.slice(0); + this._length = hash._length; + } else { + this.reset(); + } +}; + +/** + * Hash a string or an array of words. + * @static + * @param {bitArray|String} data the data to hash. + * @return {bitArray} The hash value, an array of 5 big-endian words. + */ +sjcl.hash.sha1.hash = function (data) { + return (new sjcl.hash.sha1()).update(data).finalize(); +}; + +sjcl.hash.sha1.prototype = { + /** + * The hash's block size, in bits. + * @constant + */ + blockSize: 512, + + /** + * Reset the hash state. + * @return this + */ + reset:function () { + this._h = this._init.slice(0); + this._buffer = []; + this._length = 0; + return this; + }, + + /** + * Input several words to the hash. + * @param {bitArray|String} data the data to hash. + * @return this + */ + update: function (data) { + if (typeof data === "string") { + data = sjcl.codec.utf8String.toBits(data); + } + var i, b = this._buffer = sjcl.bitArray.concat(this._buffer, data), + ol = this._length, + nl = this._length = ol + sjcl.bitArray.bitLength(data); + for (i = this.blockSize+ol & -this.blockSize; i <= nl; + i+= this.blockSize) { + this._block(b.splice(0,16)); + } + return this; + }, + + /** + * Complete hashing and output the hash value. + * @return {bitArray} The hash value, an array of 5 big-endian words. TODO + */ + finalize:function () { + var i, b = this._buffer, h = this._h; + + // Round out and push the buffer + b = sjcl.bitArray.concat(b, [sjcl.bitArray.partial(1,1)]); + // Round out the buffer to a multiple of 16 words, less the 2 length words. + for (i = b.length + 2; i & 15; i++) { + b.push(0); + } + + // append the length + b.push(Math.floor(this._length / 0x100000000)); + b.push(this._length | 0); + + while (b.length) { + this._block(b.splice(0,16)); + } + + this.reset(); + return h; + }, + + /** + * The SHA-1 initialization vector. + * @private + */ + _init:[0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0], + + /** + * The SHA-1 hash key. + * @private + */ + _key:[0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xCA62C1D6], + + /** + * The SHA-1 logical functions f(0), f(1), ..., f(79). + * @private + */ + _f:function(t, b, c, d) { + if (t <= 19) { + return (b & c) | (~b & d); + } else if (t <= 39) { + return b ^ c ^ d; + } else if (t <= 59) { + return (b & c) | (b & d) | (c & d); + } else if (t <= 79) { + return b ^ c ^ d; + } + }, + + /** + * Circular left-shift operator. + * @private + */ + _S:function(n, x) { + return (x << n) | (x >>> 32-n); + }, + + /** + * Perform one cycle of SHA-1. + * @param {bitArray} words one block of words. + * @private + */ + _block:function (words) { + var t, tmp, a, b, c, d, e, + w = words.slice(0), + h = this._h, + k = this._key; + + a = h[0]; b = h[1]; c = h[2]; d = h[3]; e = h[4]; + + for (t=0; t<=79; t++) { + if (t >= 16) { + w[t] = this._S(1, w[t-3] ^ w[t-8] ^ w[t-14] ^ w[t-16]); + } + tmp = (this._S(5, a) + this._f(t, b, c, d) + e + w[t] + + this._key[Math.floor(t/20)]) | 0; + e = d; + d = c; + c = this._S(30, b); + b = a; + a = tmp; + } + + h[0] = (h[0]+a) |0; + h[1] = (h[1]+b) |0; + h[2] = (h[2]+c) |0; + h[3] = (h[3]+d) |0; + h[4] = (h[4]+e) |0; + } +}; +/** @fileOverview CCM mode implementation. + * + * Special thanks to Roy Nicholson for pointing out a bug in our + * implementation. + * + * @author Emily Stark + * @author Mike Hamburg + * @author Dan Boneh + */ + +/** @namespace CTR mode with CBC MAC. */ +sjcl.mode.ccm = { + /** The name of the mode. + * @constant + */ + name: "ccm", + + /** Encrypt in CCM mode. + * @static + * @param {Object} prf The pseudorandom function. It must have a block size of 16 bytes. + * @param {bitArray} plaintext The plaintext data. + * @param {bitArray} iv The initialization value. + * @param {bitArray} [adata=[]] The authenticated data. + * @param {Number} [tlen=64] the desired tag length, in bits. + * @return {bitArray} The encrypted data, an array of bytes. + */ + encrypt: function(prf, plaintext, iv, adata, tlen) { + var L, i, out = plaintext.slice(0), tag, w=sjcl.bitArray, ivl = w.bitLength(iv) / 8, ol = w.bitLength(out) / 8; + tlen = tlen || 64; + adata = adata || []; + + if (ivl < 7) { + throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes"); + } + + // compute the length of the length + for (L=2; L<4 && ol >>> 8*L; L++) {} + if (L < 15 - ivl) { L = 15-ivl; } + iv = w.clamp(iv,8*(15-L)); + + // compute the tag + tag = sjcl.mode.ccm._computeTag(prf, plaintext, iv, adata, tlen, L); + + // encrypt + out = sjcl.mode.ccm._ctrMode(prf, out, iv, tag, tlen, L); + + return w.concat(out.data, out.tag); + }, + + /** Decrypt in CCM mode. + * @static + * @param {Object} prf The pseudorandom function. It must have a block size of 16 bytes. + * @param {bitArray} ciphertext The ciphertext data. + * @param {bitArray} iv The initialization value. + * @param {bitArray} [[]] adata The authenticated data. + * @param {Number} [64] tlen the desired tag length, in bits. + * @return {bitArray} The decrypted data. + */ + decrypt: function(prf, ciphertext, iv, adata, tlen) { + tlen = tlen || 64; + adata = adata || []; + var L, i, + w=sjcl.bitArray, + ivl = w.bitLength(iv) / 8, + ol = w.bitLength(ciphertext), + out = w.clamp(ciphertext, ol - tlen), + tag = w.bitSlice(ciphertext, ol - tlen), tag2; + + + ol = (ol - tlen) / 8; + + if (ivl < 7) { + throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes"); + } + + // compute the length of the length + for (L=2; L<4 && ol >>> 8*L; L++) {} + if (L < 15 - ivl) { L = 15-ivl; } + iv = w.clamp(iv,8*(15-L)); + + // decrypt + out = sjcl.mode.ccm._ctrMode(prf, out, iv, tag, tlen, L); + + // check the tag + tag2 = sjcl.mode.ccm._computeTag(prf, out.data, iv, adata, tlen, L); + if (!w.equal(out.tag, tag2)) { + throw new sjcl.exception.corrupt("ccm: tag doesn't match"); + } + + return out.data; + }, + + /* Compute the (unencrypted) authentication tag, according to the CCM specification + * @param {Object} prf The pseudorandom function. + * @param {bitArray} plaintext The plaintext data. + * @param {bitArray} iv The initialization value. + * @param {bitArray} adata The authenticated data. + * @param {Number} tlen the desired tag length, in bits. + * @return {bitArray} The tag, but not yet encrypted. + * @private + */ + _computeTag: function(prf, plaintext, iv, adata, tlen, L) { + // compute B[0] + var q, mac, field = 0, offset = 24, tmp, i, macData = [], w=sjcl.bitArray, xor = w._xor4; + + tlen /= 8; + + // check tag length and message length + if (tlen % 2 || tlen < 4 || tlen > 16) { + throw new sjcl.exception.invalid("ccm: invalid tag length"); + } + + if (adata.length > 0xFFFFFFFF || plaintext.length > 0xFFFFFFFF) { + // I don't want to deal with extracting high words from doubles. + throw new sjcl.exception.bug("ccm: can't deal with 4GiB or more data"); + } + + // mac the flags + mac = [w.partial(8, (adata.length ? 1<<6 : 0) | (tlen-2) << 2 | L-1)]; + + // mac the iv and length + mac = w.concat(mac, iv); + mac[3] |= w.bitLength(plaintext)/8; + mac = prf.encrypt(mac); + + + if (adata.length) { + // mac the associated data. start with its length... + tmp = w.bitLength(adata)/8; + if (tmp <= 0xFEFF) { + macData = [w.partial(16, tmp)]; + } else if (tmp <= 0xFFFFFFFF) { + macData = w.concat([w.partial(16,0xFFFE)], [tmp]); + } // else ... + + // mac the data itself + macData = w.concat(macData, adata); + for (i=0; i bs) { + key = Hash.hash(key); + } + + for (i=0; iUse sjcl.random as a singleton for this class! + *

+ * This random number generator is a derivative of Ferguson and Schneier's + * generator Fortuna. It collects entropy from various events into several + * pools, implemented by streaming SHA-256 instances. It differs from + * ordinary Fortuna in a few ways, though. + *

+ * + *

+ * Most importantly, it has an entropy estimator. This is present because + * there is a strong conflict here between making the generator available + * as soon as possible, and making sure that it doesn't "run on empty". + * In Fortuna, there is a saved state file, and the system is likely to have + * time to warm up. + *

+ * + *

+ * Second, because users are unlikely to stay on the page for very long, + * and to speed startup time, the number of pools increases logarithmically: + * a new pool is created when the previous one is actually used for a reseed. + * This gives the same asymptotic guarantees as Fortuna, but gives more + * entropy to early reseeds. + *

+ * + *

+ * The entire mechanism here feels pretty klunky. Furthermore, there are + * several improvements that should be made, including support for + * dedicated cryptographic functions that may be present in some browsers; + * state files in local storage; cookies containing randomness; etc. So + * look for improvements in future versions. + *

+ */ +sjcl.prng = function(defaultParanoia) { + + /* private */ + this._pools = [new sjcl.hash.sha256()]; + this._poolEntropy = [0]; + this._reseedCount = 0; + this._robins = {}; + this._eventId = 0; + + this._collectorIds = {}; + this._collectorIdNext = 0; + + this._strength = 0; + this._poolStrength = 0; + this._nextReseed = 0; + this._key = [0,0,0,0,0,0,0,0]; + this._counter = [0,0,0,0]; + this._cipher = undefined; + this._defaultParanoia = defaultParanoia; + + /* event listener stuff */ + this._collectorsStarted = false; + this._callbacks = {progress: {}, seeded: {}}; + this._callbackI = 0; + + /* constants */ + this._NOT_READY = 0; + this._READY = 1; + this._REQUIRES_RESEED = 2; + + this._MAX_WORDS_PER_BURST = 65536; + this._PARANOIA_LEVELS = [0,48,64,96,128,192,256,384,512,768,1024]; + this._MILLISECONDS_PER_RESEED = 30000; + this._BITS_PER_RESEED = 80; +}; + +sjcl.prng.prototype = { + /** Generate several random words, and return them in an array. + * A word consists of 32 bits (4 bytes) + * @param {Number} nwords The number of words to generate. + */ + randomWords: function (nwords, paranoia) { + var out = [], i, readiness = this.isReady(paranoia), g; + + if (readiness === this._NOT_READY) { + throw new sjcl.exception.notReady("generator isn't seeded"); + } else if (readiness & this._REQUIRES_RESEED) { + this._reseedFromPools(!(readiness & this._READY)); + } + + for (i=0; i0) { + estimatedEntropy++; + tmp = tmp >>> 1; + } + } + } + this._pools[robin].update([id,this._eventId++,2,estimatedEntropy,t,data.length].concat(data)); + } + break; + + case "string": + if (estimatedEntropy === undefined) { + /* English text has just over 1 bit per character of entropy. + * But this might be HTML or something, and have far less + * entropy than English... Oh well, let's just say one bit. + */ + estimatedEntropy = data.length; + } + this._pools[robin].update([id,this._eventId++,3,estimatedEntropy,t,data.length]); + this._pools[robin].update(data); + break; + + default: + err=1; + } + if (err) { + throw new sjcl.exception.bug("random: addEntropy only supports number, array of numbers or string"); + } + + /* record the new strength */ + this._poolEntropy[robin] += estimatedEntropy; + this._poolStrength += estimatedEntropy; + + /* fire off events */ + if (oldReady === this._NOT_READY) { + if (this.isReady() !== this._NOT_READY) { + this._fireEvent("seeded", Math.max(this._strength, this._poolStrength)); + } + this._fireEvent("progress", this.getProgress()); + } + }, + + /** Is the generator ready? */ + isReady: function (paranoia) { + var entropyRequired = this._PARANOIA_LEVELS[ (paranoia !== undefined) ? paranoia : this._defaultParanoia ]; + + if (this._strength && this._strength >= entropyRequired) { + return (this._poolEntropy[0] > this._BITS_PER_RESEED && (new Date()).valueOf() > this._nextReseed) ? + this._REQUIRES_RESEED | this._READY : + this._READY; + } else { + return (this._poolStrength >= entropyRequired) ? + this._REQUIRES_RESEED | this._NOT_READY : + this._NOT_READY; + } + }, + + /** Get the generator's progress toward readiness, as a fraction */ + getProgress: function (paranoia) { + var entropyRequired = this._PARANOIA_LEVELS[ paranoia ? paranoia : this._defaultParanoia ]; + + if (this._strength >= entropyRequired) { + return 1.0; + } else { + return (this._poolStrength > entropyRequired) ? + 1.0 : + this._poolStrength / entropyRequired; + } + }, + + /** start the built-in entropy collectors */ + startCollectors: function () { + if (this._collectorsStarted) { return; } + + this._eventListener = { + loadTimeCollector: this._bind(this._loadTimeCollector), + mouseCollector: this._bind(this._mouseCollector), + keyboardCollector: this._bind(this._keyboardCollector), + accelerometerCollector: this._bind(this._accelerometerCollector) + } + + if (window.addEventListener) { + window.addEventListener("load", this._eventListener.loadTimeCollector, false); + window.addEventListener("mousemove", this._eventListener.mouseCollector, false); + window.addEventListener("keypress", this._eventListener.keyboardCollector, false); + window.addEventListener("devicemotion", this._eventListener.accelerometerCollector, false); + } else if (document.attachEvent) { + document.attachEvent("onload", this._eventListener.loadTimeCollector); + document.attachEvent("onmousemove", this._eventListener.mouseCollector); + document.attachEvent("keypress", this._eventListener.keyboardCollector); + } else { + throw new sjcl.exception.bug("can't attach event"); + } + + this._collectorsStarted = true; + }, + + /** stop the built-in entropy collectors */ + stopCollectors: function () { + if (!this._collectorsStarted) { return; } + + if (window.removeEventListener) { + window.removeEventListener("load", this._eventListener.loadTimeCollector, false); + window.removeEventListener("mousemove", this._eventListener.mouseCollector, false); + window.removeEventListener("keypress", this._eventListener.keyboardCollector, false); + window.removeEventListener("devicemotion", this._eventListener.accelerometerCollector, false); + } else if (document.detachEvent) { + document.detachEvent("onload", this._eventListener.loadTimeCollector); + document.detachEvent("onmousemove", this._eventListener.mouseCollector); + document.detachEvent("keypress", this._eventListener.keyboardCollector); + } + + this._collectorsStarted = false; + }, + + /* use a cookie to store entropy. + useCookie: function (all_cookies) { + throw new sjcl.exception.bug("random: useCookie is unimplemented"); + },*/ + + /** add an event listener for progress or seeded-ness. */ + addEventListener: function (name, callback) { + this._callbacks[name][this._callbackI++] = callback; + }, + + /** remove an event listener for progress or seeded-ness */ + removeEventListener: function (name, cb) { + var i, j, cbs=this._callbacks[name], jsTemp=[]; + + /* I'm not sure if this is necessary; in C++, iterating over a + * collection and modifying it at the same time is a no-no. + */ + + for (j in cbs) { + if (cbs.hasOwnProperty(j) && cbs[j] === cb) { + jsTemp.push(j); + } + } + + for (i=0; i= 1 << this._pools.length) { + this._pools.push(new sjcl.hash.sha256()); + this._poolEntropy.push(0); + } + + /* how strong was this reseed? */ + this._poolStrength -= strength; + if (strength > this._strength) { + this._strength = strength; + } + + this._reseedCount ++; + this._reseed(reseedData); + }, + + _keyboardCollector: function () { + this._addCurrentTimeToEntropy(1); + }, + + _mouseCollector: function (ev) { + var x = ev.x || ev.clientX || ev.offsetX || 0, y = ev.y || ev.clientY || ev.offsetY || 0; + sjcl.random.addEntropy([x,y], 2, "mouse"); + this._addCurrentTimeToEntropy(0); + }, + + _loadTimeCollector: function () { + this._addCurrentTimeToEntropy(2); + }, + + _addCurrentTimeToEntropy: function (estimatedEntropy) { + if (window && window.performance && typeof window.performance.now === "function") { + //how much entropy do we want to add here? + sjcl.random.addEntropy(window.performance.now(), estimatedEntropy, "loadtime"); + } else { + sjcl.random.addEntropy((new Date()).valueOf(), estimatedEntropy, "loadtime"); + } + }, + _accelerometerCollector: function (ev) { + var ac = ev.accelerationIncludingGravity.x||ev.accelerationIncludingGravity.y||ev.accelerationIncludingGravity.z; + if(window.orientation){ + var or = window.orientation; + if (typeof or === "number") { + sjcl.random.addEntropy(or, 1, "accelerometer"); + } + } + if (ac) { + sjcl.random.addEntropy(ac, 2, "accelerometer"); + } + this._addCurrentTimeToEntropy(0); + }, + + _fireEvent: function (name, arg) { + var j, cbs=sjcl.random._callbacks[name], cbsTemp=[]; + /* TODO: there is a race condition between removing collectors and firing them */ + + /* I'm not sure if this is necessary; in C++, iterating over a + * collection and modifying it at the same time is a no-no. + */ + + for (j in cbs) { + if (cbs.hasOwnProperty(j)) { + cbsTemp.push(cbs[j]); + } + } + + for (j=0; j 4)) { + throw new sjcl.exception.invalid("json encrypt: invalid parameters"); + } + + if (typeof password === "string") { + tmp = sjcl.misc.cachedPbkdf2(password, p); + password = tmp.key.slice(0,p.ks/32); + p.salt = tmp.salt; + } else if (sjcl.ecc && password instanceof sjcl.ecc.elGamal.publicKey) { + tmp = password.kem(); + p.kemtag = tmp.tag; + password = tmp.key.slice(0,p.ks/32); + } + if (typeof plaintext === "string") { + plaintext = sjcl.codec.utf8String.toBits(plaintext); + } + if (typeof adata === "string") { + adata = sjcl.codec.utf8String.toBits(adata); + } + prp = new sjcl.cipher[p.cipher](password); + + /* return the json data */ + j._add(rp, p); + rp.key = password; + + /* do the encryption */ + p.ct = sjcl.mode[p.mode].encrypt(prp, plaintext, p.iv, adata, p.ts); + + //return j.encode(j._subtract(p, j.defaults)); + return p; + }, + + /** Simple encryption function. + * @param {String|bitArray} password The password or key. + * @param {String} plaintext The data to encrypt. + * @param {Object} [params] The parameters including tag, iv and salt. + * @param {Object} [rp] A returned version with filled-in parameters. + * @return {String} The ciphertext serialized data. + * @throws {sjcl.exception.invalid} if a parameter is invalid. + */ + encrypt: function (password, plaintext, params, rp) { + var j = sjcl.json, p = j._encrypt.apply(j, arguments); + return j.encode(p); + }, + + /** Simple decryption function. + * @param {String|bitArray} password The password or key. + * @param {Object} ciphertext The cipher raw data to decrypt. + * @param {Object} [params] Additional non-default parameters. + * @param {Object} [rp] A returned object with filled parameters. + * @return {String} The plaintext. + * @throws {sjcl.exception.invalid} if a parameter is invalid. + * @throws {sjcl.exception.corrupt} if the ciphertext is corrupt. + */ + _decrypt: function (password, ciphertext, params, rp) { + params = params || {}; + rp = rp || {}; + + var j = sjcl.json, p = j._add(j._add(j._add({},j.defaults),ciphertext), params, true), ct, tmp, prp, adata=p.adata; + if (typeof p.salt === "string") { + p.salt = sjcl.codec.base64.toBits(p.salt); + } + if (typeof p.iv === "string") { + p.iv = sjcl.codec.base64.toBits(p.iv); + } + + if (!sjcl.mode[p.mode] || + !sjcl.cipher[p.cipher] || + (typeof password === "string" && p.iter <= 100) || + (p.ts !== 64 && p.ts !== 96 && p.ts !== 128) || + (p.ks !== 128 && p.ks !== 192 && p.ks !== 256) || + (!p.iv) || + (p.iv.length < 2 || p.iv.length > 4)) { + throw new sjcl.exception.invalid("json decrypt: invalid parameters"); + } + + if (typeof password === "string") { + tmp = sjcl.misc.cachedPbkdf2(password, p); + password = tmp.key.slice(0,p.ks/32); + p.salt = tmp.salt; + } else if (sjcl.ecc && password instanceof sjcl.ecc.elGamal.secretKey) { + password = password.unkem(sjcl.codec.base64.toBits(p.kemtag)).slice(0,p.ks/32); + } + if (typeof adata === "string") { + adata = sjcl.codec.utf8String.toBits(adata); + } + prp = new sjcl.cipher[p.cipher](password); + + /* do the decryption */ + ct = sjcl.mode[p.mode].decrypt(prp, p.ct, p.iv, adata, p.ts); + + /* return the json data */ + j._add(rp, p); + rp.key = password; + + return sjcl.codec.utf8String.fromBits(ct); + }, + + /** Simple decryption function. + * @param {String|bitArray} password The password or key. + * @param {String} ciphertext The ciphertext to decrypt. + * @param {Object} [params] Additional non-default parameters. + * @param {Object} [rp] A returned object with filled parameters. + * @return {String} The plaintext. + * @throws {sjcl.exception.invalid} if a parameter is invalid. + * @throws {sjcl.exception.corrupt} if the ciphertext is corrupt. + */ + decrypt: function (password, ciphertext, params, rp) { + var j = sjcl.json; + return j._decrypt(password, j.decode(ciphertext), params, rp); + }, + + /** Encode a flat structure into a JSON string. + * @param {Object} obj The structure to encode. + * @return {String} A JSON string. + * @throws {sjcl.exception.invalid} if obj has a non-alphanumeric property. + * @throws {sjcl.exception.bug} if a parameter has an unsupported type. + */ + encode: function (obj) { + var i, out='{', comma=''; + for (i in obj) { + if (obj.hasOwnProperty(i)) { + if (!i.match(/^[a-z0-9]+$/i)) { + throw new sjcl.exception.invalid("json encode: invalid property name"); + } + out += comma + '"' + i + '":'; + comma = ','; + + switch (typeof obj[i]) { + case 'number': + case 'boolean': + out += obj[i]; + break; + + case 'string': + out += '"' + escape(obj[i]) + '"'; + break; + + case 'object': + out += '"' + sjcl.codec.base64.fromBits(obj[i],0) + '"'; + break; + + default: + throw new sjcl.exception.bug("json encode: unsupported type"); + } + } + } + return out+'}'; + }, + + /** Decode a simple (flat) JSON string into a structure. The ciphertext, + * adata, salt and iv will be base64-decoded. + * @param {String} str The string. + * @return {Object} The decoded structure. + * @throws {sjcl.exception.invalid} if str isn't (simple) JSON. + */ + decode: function (str) { + str = str.replace(/\s/g,''); + if (!str.match(/^\{.*\}$/)) { + throw new sjcl.exception.invalid("json decode: this isn't json!"); + } + var a = str.replace(/^\{|\}$/g, '').split(/,/), out={}, i, m; + for (i=0; i Date: Sat, 26 Apr 2014 23:23:50 +0200 Subject: [PATCH 17/48] Added realm to userids, tokens and nonces. --- doc/REST-API.txt | 1 + server.conf.in | 4 +++ src/app/spreed-speakfreely-server/hub.go | 12 +++++--- src/app/spreed-speakfreely-server/main.go | 24 ++++++++++----- src/app/spreed-speakfreely-server/server.go | 2 +- src/app/spreed-speakfreely-server/session.go | 9 +++--- src/app/spreed-speakfreely-server/sessions.go | 2 +- src/app/spreed-speakfreely-server/users.go | 17 +++++++---- .../js/controllers/mediastreamcontroller.js | 30 ++++++++++++------- 9 files changed, 67 insertions(+), 34 deletions(-) diff --git a/doc/REST-API.txt b/doc/REST-API.txt index 54cb4e54..62d541f5 100644 --- a/doc/REST-API.txt +++ b/doc/REST-API.txt @@ -92,6 +92,7 @@ Available end points with request methods and content-type: "success": true, "userid": "user-id", "useridcombo": "authorization-id", + "timestamp": 1430688014, "secret": "authorization-secret-for-authorization-id", "nonce": "authorization-nonce" } diff --git a/server.conf.in b/server.conf.in index ec1cd06d..17770e13 100644 --- a/server.conf.in +++ b/server.conf.in @@ -75,6 +75,10 @@ sessionSecret = the-default-secret-do-not-keep-me ; server generated security tokens. When the serverToken is changed all existing ; nonces become invalid. Use 32 or 64 byte random data. ;serverToken = i-did-not-change-the-public-token-boo +; The server realm is part of the validation chain of tokens and nonces and is +; added as suffix to server created user ids if user creation is enabled. When +; the realm is changed, all existing tokens and nonces become invalid. +;serverRealm = local ; Full path to an extra templates directory. Templates in this directory ending ; with .html will be parsed on startup and can be used to fill the supported ; extra-* template slots. If the extra folder has a sub folder "static", the diff --git a/src/app/spreed-speakfreely-server/hub.go b/src/app/spreed-speakfreely-server/hub.go index 4833f6b4..96ba40c1 100644 --- a/src/app/spreed-speakfreely-server/hub.go +++ b/src/app/spreed-speakfreely-server/hub.go @@ -79,9 +79,11 @@ type Hub struct { broadcastChatMessages uint64 unicastChatMessages uint64 buddyImages ImageCache + realm string + tokenName string } -func NewHub(version string, config *Config, sessionSecret, turnSecret string) *Hub { +func NewHub(version string, config *Config, sessionSecret, turnSecret, realm string) *Hub { h := &Hub{ connectionTable: make(map[string]*Connection), @@ -91,6 +93,7 @@ func NewHub(version string, config *Config, sessionSecret, turnSecret string) *H config: config, sessionSecret: []byte(sessionSecret), turnSecret: []byte(turnSecret), + realm: realm, } if len(h.sessionSecret) < 32 { @@ -100,6 +103,7 @@ func NewHub(version string, config *Config, sessionSecret, turnSecret string) *H h.tickets = securecookie.New(h.sessionSecret, nil) h.buffers = NewBufferCache(1024, bytes.MinRead) h.buddyImages = NewImageCache() + h.tokenName = fmt.Sprintf("token@%s", h.realm) return h } @@ -203,14 +207,14 @@ func (h *Hub) ValidateSession(id, sid string) bool { func (h *Hub) EncodeSessionToken(st *SessionToken) (string, error) { - return h.tickets.Encode("token", st) + return h.tickets.Encode(h.tokenName, st) } func (h *Hub) DecodeSessionToken(token string) (*SessionToken, error) { st := &SessionToken{} - err := h.tickets.Decode("token", token, st) + err := h.tickets.Decode(h.tokenName, token, st) return st, err } @@ -401,7 +405,7 @@ func (h *Hub) sessiontokenHandler(st *SessionToken) (string, error) { return "", errors.New("no such connection") } - nonce, err := c.Session.Authorize(st) + nonce, err := c.Session.Authorize(h.realm, st) if err != nil { return "", err } diff --git a/src/app/spreed-speakfreely-server/main.go b/src/app/spreed-speakfreely-server/main.go index 9e8ed7c0..1e7c1630 100644 --- a/src/app/spreed-speakfreely-server/main.go +++ b/src/app/spreed-speakfreely-server/main.go @@ -263,12 +263,6 @@ func runner(runtime phoenix.Runtime) error { defaultRoomEnabled = defaultRoomEnabledString == "true" } - serverToken, err := runtime.GetString("app", "serverToken") - if err == nil { - //TODO(longsleep): When we have a database, generate this once from random source and store it. - serverToken = "i-did-not-change-the-public-token-boo" - } - usersEnabled := false usersEnabledString, err := runtime.GetString("users", "enabled") if err == nil { @@ -281,6 +275,17 @@ func runner(runtime phoenix.Runtime) error { usersAllowRegistration = usersAllowRegistrationString == "true" } + serverToken, err := runtime.GetString("app", "serverToken") + if err != nil { + //TODO(longsleep): When we have a database, generate this once from random source and store it. + serverToken = "i-did-not-change-the-public-token-boo" + } + + serverRealm, err := runtime.GetString("app", "serverRealm") + if err != nil { + serverRealm = "local" + } + // Create token provider. var tokenProvider TokenProvider if tokenFile != "" { @@ -313,8 +318,11 @@ func runner(runtime phoenix.Runtime) error { log.Printf("Loaded extra templates from: %s", extraFolder) } + // Create realm string from config. + computedRealm := fmt.Sprintf("%s.%s", serverRealm, serverToken) + // Create our hub instance. - hub := NewHub(runtimeVersion, config, sessionSecret, turnSecret) + hub := NewHub(runtimeVersion, config, sessionSecret, turnSecret, computedRealm) // Set number of go routines if it is 1 if goruntime.GOMAXPROCS(0) == 1 { @@ -363,7 +371,7 @@ func runner(runtime phoenix.Runtime) error { api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens") if usersEnabled { // Create Users handler. - users := NewUsers(hub, runtime) + users := NewUsers(hub, serverRealm, runtime) api.AddResource(&Sessions{hub: hub, users: users}, "/sessions/{id}/") if usersAllowRegistration { api.AddResource(users, "/users") diff --git a/src/app/spreed-speakfreely-server/server.go b/src/app/spreed-speakfreely-server/server.go index 040a109f..cb949985 100644 --- a/src/app/spreed-speakfreely-server/server.go +++ b/src/app/spreed-speakfreely-server/server.go @@ -228,7 +228,7 @@ func (s *Server) Users(c *Connection) { func (s *Server) Authenticate(c *Connection, st *SessionToken) bool { - err := c.Session.Authenticate(st) + err := c.Session.Authenticate(c.h.realm, st) if err == nil { log.Println("Authentication success", c.Id, c.Idx, st.Userid) return true diff --git a/src/app/spreed-speakfreely-server/session.go b/src/app/spreed-speakfreely-server/session.go index 8fdd53d4..889ae2f6 100644 --- a/src/app/spreed-speakfreely-server/session.go +++ b/src/app/spreed-speakfreely-server/session.go @@ -23,6 +23,7 @@ package main import ( "errors" + "fmt" "github.com/gorilla/securecookie" "sync" ) @@ -88,7 +89,7 @@ func (s *Session) Apply(st *SessionToken) uint64 { } -func (s *Session) Authorize(st *SessionToken) (string, error) { +func (s *Session) Authorize(realm string, st *SessionToken) (string, error) { s.mutex.Lock() defer s.mutex.Unlock() @@ -102,13 +103,13 @@ func (s *Session) Authorize(st *SessionToken) (string, error) { // Create authentication nonce. var err error - s.Nonce, err = sessionNonces.Encode(s.Sid, st.Userid) + s.Nonce, err = sessionNonces.Encode(fmt.Sprintf("%s@%s", s.Sid, realm), st.Userid) return s.Nonce, err } -func (s *Session) Authenticate(st *SessionToken) error { +func (s *Session) Authenticate(realm string, st *SessionToken) error { s.mutex.Lock() defer s.mutex.Unlock() @@ -120,7 +121,7 @@ func (s *Session) Authenticate(st *SessionToken) error { return errors.New("nonce validation failed") } var userid string - err := sessionNonces.Decode(s.Sid, st.Nonce, &userid) + err := sessionNonces.Decode(fmt.Sprintf("%s@%s", s.Sid, realm), st.Nonce, &userid) if err != nil { return err } diff --git a/src/app/spreed-speakfreely-server/sessions.go b/src/app/spreed-speakfreely-server/sessions.go index 425fe2b5..0018c535 100644 --- a/src/app/spreed-speakfreely-server/sessions.go +++ b/src/app/spreed-speakfreely-server/sessions.go @@ -84,7 +84,7 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H } // Validate with users handler. - userid, err := sessions.users.Handler.Validate(&snr) + userid, err := sessions.users.handler.Validate(&snr) if err != nil { error = true log.Println("Session patch failed - users validation failed.", err) diff --git a/src/app/spreed-speakfreely-server/users.go b/src/app/spreed-speakfreely-server/users.go index e71b8cd0..e42616ec 100644 --- a/src/app/spreed-speakfreely-server/users.go +++ b/src/app/spreed-speakfreely-server/users.go @@ -83,8 +83,10 @@ func (uh *UsersSharedsecretHandler) Create(un *UserNonce) (*UserNonce, error) { // TODO(longsleep): Make this configureable - One year for now ... expiration := time.Now().Add(time.Duration(1) * time.Hour * 24 * 31 * 12) - un.UseridCombo = fmt.Sprintf("%d:%s", expiration.Unix(), un.Userid) + un.Timestamp = expiration.Unix() + un.UseridCombo = fmt.Sprintf("%d:%s", un.Timestamp, un.Userid) un.Secret = uh.createHMAC(un.UseridCombo) + return un, nil } @@ -94,15 +96,17 @@ type UserNonce struct { Userid string `json:"userid"` UseridCombo string `json:"useridcombo"` Secret string `json:"secret"` + Timestamp int64 `json:"timestamp"` Success bool `json:"success"` } type Users struct { hub *Hub - Handler UsersHandler + realm string + handler UsersHandler } -func NewUsers(hub *Hub, runtime phoenix.Runtime) *Users { +func NewUsers(hub *Hub, realm string, runtime phoenix.Runtime) *Users { var handler UsersHandler @@ -125,7 +129,8 @@ func NewUsers(hub *Hub, runtime phoenix.Runtime) *Users { return &Users{ hub: hub, - Handler: handler, + realm: realm, + handler: handler, } } @@ -146,7 +151,7 @@ func (users *Users) Post(request *http.Request) (int, interface{}, http.Header) } // Do this before session validation to avoid timing information. - userid := uuid.NewV4().String() + userid := fmt.Sprintf("%s@%s", uuid.NewV4().String(), users.realm) // Make sure Sid matches session and is valid. if !users.hub.ValidateSession(snr.Id, snr.Sid) { @@ -158,7 +163,7 @@ func (users *Users) Post(request *http.Request) (int, interface{}, http.Header) return 400, NewApiError("users_request_failed", fmt.Sprintf("Error: %q", err)), http.Header{"Content-Type": {"application/json"}} } - un, err := users.Handler.Create(&UserNonce{Nonce: nonce, Userid: userid, Success: true}) + un, err := users.handler.Create(&UserNonce{Nonce: nonce, Userid: userid, Success: true}) if err != nil { return 400, NewApiError("users_create_failed", fmt.Sprintf("Error: %q", err)), http.Header{"Content-Type": {"application/json"}} } diff --git a/static/js/controllers/mediastreamcontroller.js b/static/js/controllers/mediastreamcontroller.js index 3a055c7a..c60c2363 100644 --- a/static/js/controllers/mediastreamcontroller.js +++ b/static/js/controllers/mediastreamcontroller.js @@ -432,16 +432,26 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function if (!login && mediaStream.config.UsersAllowRegistration) { console.log("No userid - creating one ..."); mediaStream.users.register(function(data) { - var login = sjcl.encrypt(key, JSON.stringify({ - v: 1, - t: data.timestamp || "", - a: data.useridcombo, - b: data.secret, - })); - localStorage.setItem("mediastream-login", login); - console.info("Created new userid:", data.userid); - mediaStream.api.requestAuthentication(data.userid, data.nonce); - delete data.nonce; + console.info("Created new userid:", data.userid); + if (data.nonce) { + // If the server provided us a nonce, we can do everthing on our own. + // So we store the stuff in localStorage for later use and directly + // authenticate ourselves with the provided nonce. + var login = sjcl.encrypt(key, JSON.stringify({ + v: 1, + t: data.timestamp || "", + a: data.useridcombo, + b: data.secret, + })); + localStorage.setItem("mediastream-login", login); + mediaStream.api.requestAuthentication(data.userid, data.nonce); + delete data.nonce; + } else { + // No nonce received. So this means something we cannot do on our own. + // Make are GET request and retrieve nonce that way and let the + // browser/server do the rest. + // TODO(longsleep): Implement me. + } }, function(data, status) { console.error("Failed to create userid", status, data); }); From 8757689516c6df13ccc9f1957f61b723af90005c Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Sat, 26 Apr 2014 23:52:22 +0200 Subject: [PATCH 18/48] Fixed session vs. user confusion in buddylist component. --- static/js/services/buddylist.js | 37 +++++++++++-------- static/partials/buddy.html | 6 +-- static/partials/buddyactions.html | 2 +- .../partials/buddyactionsforaudiomixer.html | 2 +- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/static/js/services/buddylist.js b/static/js/services/buddylist.js index 7280ccfc..1bb414f5 100644 --- a/static/js/services/buddylist.js +++ b/static/js/services/buddylist.js @@ -31,7 +31,7 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! BuddyTree.prototype.create = function(id, scope) { - var sort = scope.displayName ? scope.displayName : "user "+scope.buddyIndexSortable+" "+id; + var sort = scope.displayName ? scope.displayName : "session "+scope.buddyIndexSortable+" "+id; var data = { id: id, sort: sort + "z" + id @@ -138,13 +138,17 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! $element.on("mouseenter mouseleave", ".buddy", _.bind(function(event) { // Hover handler for on Buddy actions. var buddyElement = $(event.currentTarget); - this.hover(buddyElement, event.type === "mouseenter" ? true : false, buddyElement.scope().user.Id); + this.hover(buddyElement, event.type === "mouseenter" ? true : false, buddyElement.scope().session.Id); + }, this)); + $element.on("click", ".buddy", _.bind(function(event) { + var buddyElement = $(event.currentTarget); + buddyElement.scope().doDefault(); }, this)); $element.attr("data-xthreshold", "10"); $element.on("swipeleft", ".buddy", _.bind(function(event) { event.preventDefault(); var buddyElement = $(event.currentTarget); - this.hover(buddyElement, !buddyElement.hasClass("hovered"), buddyElement.scope().user.Id); + this.hover(buddyElement, !buddyElement.hasClass("hovered"), buddyElement.scope().session.Id); }, this)); $window.setInterval(_.bind(this.soundLoop, this), 500); @@ -184,7 +188,8 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! Buddylist.prototype.onBuddyScope = function(scope) { scope.element = null; - scope.doDefault = function(id) { + scope.doDefault = function() { + var id = scope.session.Id; if (scope.status.isMixer) { return scope.doAudioConference(id); } @@ -355,18 +360,18 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! }; - Buddylist.prototype.onJoined = function(user) { + Buddylist.prototype.onJoined = function(session) { - //console.log("Joined", user); - var id = user.Id; + //console.log("Joined", session); + var id = session.Id; var scope = buddyData.get(id, this.$scope, _.bind(this.onBuddyScope, this)); - scope.user = user; + scope.session = session; buddyCount++; - if (user.Status) { - if (scope.status && scope.status.Rev >= user.Status.Rev) { - console.warn("Received old status update in join", user.Status.Rev, scope.status.Rev); + if (session.Status) { + if (scope.status && scope.status.Rev >= session.Status.Rev) { + console.warn("Received old status update in join", session.Status.Rev, scope.status.Rev); } else { - scope.status = user.Status; + scope.status = session.Status; scope.displayName = scope.status.displayName; this.updateBuddyPicture(scope.status); } @@ -380,14 +385,14 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! }; - Buddylist.prototype.onLeft = function(user) { + Buddylist.prototype.onLeft = function(session) { - //console.log("Left", user); - var id = user.Id; + //console.log("Left", session); + var id = session.Id; this.tree.remove(id); var scope = buddyData.get(id); if (!scope) { - //console.warn("Trying to remove buddy with no registered scope", user); + //console.warn("Trying to remove buddy with no registered scope", session); return; } if (buddyCount>0) { diff --git a/static/partials/buddy.html b/static/partials/buddy.html index c7ccff8a..bcad374c 100644 --- a/static/partials/buddy.html +++ b/static/partials/buddy.html @@ -1,5 +1,5 @@ -
+
-
{{user.Id|displayName}}
-
{{user.Ua}}
+
{{session.Id|displayName}}
+
{{session.Ua}}
diff --git a/static/partials/buddyactions.html b/static/partials/buddyactions.html index e4574374..6fdb45e8 100644 --- a/static/partials/buddyactions.html +++ b/static/partials/buddyactions.html @@ -1,4 +1,4 @@
- +
diff --git a/static/partials/buddyactionsforaudiomixer.html b/static/partials/buddyactionsforaudiomixer.html index 23cd32ad..5f59ae93 100644 --- a/static/partials/buddyactionsforaudiomixer.html +++ b/static/partials/buddyactionsforaudiomixer.html @@ -1,3 +1,3 @@
- +
From 242c4a0b268f9a8fca8af11a1d5ac5479f32d3ab Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Sun, 27 Apr 2014 00:10:48 +0200 Subject: [PATCH 19/48] Mark registered users in buddy list. --- static/partials/buddy.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/partials/buddy.html b/static/partials/buddy.html index bcad374c..0a022448 100644 --- a/static/partials/buddy.html +++ b/static/partials/buddy.html @@ -1,5 +1,5 @@
{{session.Id|displayName}}
-
{{session.Ua}}
+
{{session.Ua}}
From 2bdc6c8b1acd279acb04d846c16836416e0f8969 Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Sun, 27 Apr 2014 23:15:44 +0200 Subject: [PATCH 20/48] Implemented user id integration in setting (register, logout). --- .../js/controllers/mediastreamcontroller.js | 74 +++---------------- static/js/directives/settings.js | 37 +++++++++- static/js/mediastream/connector.js | 9 +++ static/js/services/mediastream.js | 46 +++++++++++- static/partials/settings.html | 15 +++- 5 files changed, 111 insertions(+), 70 deletions(-) diff --git a/static/js/controllers/mediastreamcontroller.js b/static/js/controllers/mediastreamcontroller.js index c60c2363..f90dd9f4 100644 --- a/static/js/controllers/mediastreamcontroller.js +++ b/static/js/controllers/mediastreamcontroller.js @@ -154,7 +154,6 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function language: "" } }; - $scope.withStoredLogin = false; // Data voids. var cache = {}; @@ -390,73 +389,20 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function } } - // Support authentication. + // Support authentication from localStorage. if (!data.Userid && mediaStream.config.UsersEnabled) { - - var key = mediaStream.config.Token; - - // Check if we have something in store. - var login = localStorage.getItem("mediastream-login"); - if (login) { - safeApply($scope, function(scope) { - scope.withStoredLogin = true; - }); - try { - login = sjcl.decrypt(key, login); - login = JSON.parse(login) - } catch(err) { - console.error("Failed to parse login data", err); - login = {}; - } + // Check if we can load a user. + var login = mediaStream.users.load(); + if (login !== null) { console.log("Trying to authorize with stored credentials ..."); - switch (login.v) { - case 1: - var useridCombo = login.a; - var secret = login.b; - var expiry = login.t; - if (useridCombo && secret) { - mediaStream.users.authorize(useridCombo, secret, function(data) { - console.info("Retrieved nonce - authenticating as user:", data.userid); - mediaStream.api.requestAuthentication(data.userid, data.nonce); - delete data.nonce; - }, function(data, status) { - console.error("Failed to authorize session", status, data); - }); - } - break; - default: - console.warn("Unknown stored credentials", login.v); - break - } - } - if (!login && mediaStream.config.UsersAllowRegistration) { - console.log("No userid - creating one ..."); - mediaStream.users.register(function(data) { - console.info("Created new userid:", data.userid); - if (data.nonce) { - // If the server provided us a nonce, we can do everthing on our own. - // So we store the stuff in localStorage for later use and directly - // authenticate ourselves with the provided nonce. - var login = sjcl.encrypt(key, JSON.stringify({ - v: 1, - t: data.timestamp || "", - a: data.useridcombo, - b: data.secret, - })); - localStorage.setItem("mediastream-login", login); - mediaStream.api.requestAuthentication(data.userid, data.nonce); - delete data.nonce; - } else { - // No nonce received. So this means something we cannot do on our own. - // Make are GET request and retrieve nonce that way and let the - // browser/server do the rest. - // TODO(longsleep): Implement me. - } + mediaStream.users.authorize(login, function(data) { + console.info("Retrieved nonce - authenticating as user:", data.userid); + mediaStream.api.requestAuthentication(data.userid, data.nonce); + delete data.nonce; }, function(data, status) { - console.error("Failed to create userid", status, data); + console.error("Failed to authorize session", status, data); }); } - } // Support to upgrade stuff when ttl was reached. @@ -466,6 +412,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function mediaStream.api.sendSelf(); }, data.Turn.ttl / 100 * 90 * 1000); } + // Support resurrection shrine. if (resurrect) { var resurrection = resurrect; @@ -478,6 +425,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function } }, 0); } + }); mediaStream.webrtc.e.on("peercall", function(event, peercall) { diff --git a/static/js/directives/settings.js b/static/js/directives/settings.js index 8ba1a538..504708cc 100644 --- a/static/js/directives/settings.js +++ b/static/js/directives/settings.js @@ -20,7 +20,7 @@ */ define(['underscore', 'text!partials/settings.html'], function(_, template) { - return ["$compile", function($compile) { + return ["$compile", "mediaStream", function($compile, mediaStream) { var controller = ['$scope', 'desktopNotify', 'mediaSources', 'safeApply', 'availableLanguages', 'translation', function($scope, desktopNotify, mediaSources, safeApply, availableLanguages, translation) { @@ -37,6 +37,9 @@ define(['underscore', 'text!partials/settings.html'], function(_, template) { name: translation._("Use browser language") } ]; + $scope.withUsers = mediaStream.config.UsersEnabled; + $scope.withUsersRegistration = mediaStream.config.UsersAllowRegistration; + _.each(availableLanguages, function(name, code) { $scope.availableLanguages.push({ code: code, @@ -120,9 +123,39 @@ define(['underscore', 'text!partials/settings.html'], function(_, template) { $scope.showTakePicture = false; safeApply($scope); } - } }; + + $scope.registerUserid = function() { + + console.log("No userid - creating one ..."); + mediaStream.users.register(function(data) { + console.info("Created new userid:", data.userid); + if (data.nonce) { + // If the server provided us a nonce, we can do everthing on our own. + mediaStream.users.store(data); + // Directly authenticate ourselves with the provided nonce. + mediaStream.api.requestAuthentication(data.userid, data.nonce); + delete data.nonce; + } else { + // No nonce received. So this means something we cannot do on our own. + // Make are GET request and retrieve nonce that way and let the + // browser/server do the rest. + // TODO(longsleep): Implement me. + } + }, function(data, status) { + console.error("Failed to create userid", status, data); + }); + + }; + + $scope.forgetUserid = function() { + + mediaStream.users.forget(); + mediaStream.connector.forgetAndReconnect(); + + }; + $scope.checkDefaultMediaSources = function() { if ($scope.master.settings.microphoneId && !$scope.mediaSources.hasAudioId($scope.master.settings.microphoneId)) { $scope.master.settings.microphoneId=null; diff --git a/static/js/mediastream/connector.js b/static/js/mediastream/connector.js index bb11d266..41b79260 100644 --- a/static/js/mediastream/connector.js +++ b/static/js/mediastream/connector.js @@ -111,6 +111,15 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) { }; + Connector.prototype.forgetAndReconnect = function() { + + this.token = null; + if (this.conn && this.connected) { + this.conn.close(); + } + + }; + Connector.prototype.room = function(roomid, cb) { var was_connected = this.connected; diff --git a/static/js/services/mediastream.js b/static/js/services/mediastream.js index 627cba3d..cd541e89 100644 --- a/static/js/services/mediastream.js +++ b/static/js/services/mediastream.js @@ -40,6 +40,9 @@ define([ var api = new Api(connector); var webrtc = new WebRTC(api); + // TODO(longsleep): Add client side part into this key. + var secureKey = context.Cfg.Token; + var mediaStream = { version: version, ws: url, @@ -88,18 +91,21 @@ define([ } }); }, - authorize: function(useridCombo, secret, success_cb, error_cb) { + authorize: function(data, success_cb, error_cb) { var url = mediaStream.url.api("sessions") + "/" + mediaStream.api.id + "/"; - var data = { + /*var data = { id: mediaStream.api.id, sid: mediaStream.api.sid, useridcombo: useridCombo, secret: secret - } + }*/ + var login = _.clone(data); + login.id = mediaStream.api.id; + login.sid = mediaStream.api.sid; $http({ method: "PATCH", url: url, - data: JSON.stringify(data), + data: JSON.stringify(login), headers: {'Content-Type': 'application/json'} }). success(function(data, status) { @@ -116,6 +122,38 @@ define([ error_cb(data, status) } }); + }, + store: function(data) { + // So we store the stuff in localStorage for later use. + var store = _.clone(data); + store.v = 42; // No idea what number - so use 42. + var login = sjcl.encrypt(secureKey, JSON.stringify(store)); + localStorage.setItem("mediastream-login", login); + return login; + }, + load: function() { + // Check if we have something in store. + var login = localStorage.getItem("mediastream-login"); + if (login) { + try { + login = sjcl.decrypt(secureKey, login); + login = JSON.parse(login) + } catch(err) { + console.error("Failed to parse stored login data", err); + login = {}; + } + switch (login.v) { + case 42: + return login; + default: + console.warn("Unknown stored credentials", login.v); + break + } + } + return null; + }, + forget: function() { + localStorage.removeItem("mediastream-login"); } }, initialize: function($rootScope, translation) { diff --git a/static/partials/settings.html b/static/partials/settings.html index 4c3d0b0c..c4f71397 100644 --- a/static/partials/settings.html +++ b/static/partials/settings.html @@ -7,7 +7,7 @@
-
+
+ +
+ + {{_('Only register an ID if this is your private browser.')}} +
{{userid}}
+ +
+

From 6b1ea5901fdc1e781d6d49ad09d342ccb00bdd0a Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Sun, 27 Apr 2014 23:57:49 +0200 Subject: [PATCH 21/48] Store settings implicitly on close to improve usabilityt. --- static/js/directives/settings.js | 9 +++++---- static/partials/settings.html | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/static/js/directives/settings.js b/static/js/directives/settings.js index 504708cc..92b2c504 100644 --- a/static/js/directives/settings.js +++ b/static/js/directives/settings.js @@ -49,7 +49,8 @@ define(['underscore', 'text!partials/settings.html'], function(_, template) { var localStream = null; - $scope.saveSettings = function(user) { + $scope.saveSettings = function() { + var user = $scope.user; $scope.update(user); $scope.layout.settings = false; if ($scope.rememberSettings) { @@ -150,10 +151,8 @@ define(['underscore', 'text!partials/settings.html'], function(_, template) { }; $scope.forgetUserid = function() { - mediaStream.users.forget(); mediaStream.connector.forgetAndReconnect(); - }; $scope.checkDefaultMediaSources = function() { @@ -177,7 +176,7 @@ define(['underscore', 'text!partials/settings.html'], function(_, template) { $scope.mediaSources.refresh(function() { safeApply($scope, $scope.checkDefaultMediaSources); }); - $scope.$watch("layout.settings", function(showSettings) { + $scope.$watch("layout.settings", function(showSettings, oldValue) { if (showSettings) { $scope.desktopNotify.refresh(); $scope.mediaSources.refresh(function(audio, video) { @@ -196,6 +195,8 @@ define(['underscore', 'text!partials/settings.html'], function(_, template) { } }); }); + } else if (!showSettings && oldValue) { + $scope.saveSettings(); } }); }]; diff --git a/static/partials/settings.html b/static/partials/settings.html index c4f71397..a5691973 100644 --- a/static/partials/settings.html +++ b/static/partials/settings.html @@ -144,9 +144,10 @@
+ +

{{_('Your ID will still be kept - press the log out button above to delete the ID.')}}

- {{_('Apply')}} {{_('Cancel')}} + {{_('Close')}}
From 8e920180b1142103020cdff16960e7a13a18f3d9 Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Mon, 28 Apr 2014 00:02:37 +0200 Subject: [PATCH 22/48] Rebuilt translation and completed German (de) translation. --- src/i18n/messages-de.po | 35 ++++++++++++++++++++------ src/i18n/messages-ja.po | 29 ++++++++++++++++----- src/i18n/messages-ko.po | 29 ++++++++++++++++----- src/i18n/messages-zh-cn.po | 29 ++++++++++++++++----- src/i18n/messages-zh-tw.po | 29 ++++++++++++++++----- src/i18n/messages.pot | 24 ++++++++++++++---- static/translation/messages-de.json | 2 +- static/translation/messages-ja.json | 2 +- static/translation/messages-ko.json | 2 +- static/translation/messages-zh-cn.json | 2 +- static/translation/messages-zh-tw.json | 2 +- 11 files changed, 144 insertions(+), 41 deletions(-) diff --git a/src/i18n/messages-de.po b/src/i18n/messages-de.po index bb7e3af9..0e09b709 100644 --- a/src/i18n/messages-de.po +++ b/src/i18n/messages-de.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: Spreed Speak Freely\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2014-04-25 13:40+0200\n" -"PO-Revision-Date: 2014-04-25 13:45+0100\n" +"POT-Creation-Date: 2014-04-27 23:58+0200\n" +"PO-Revision-Date: 2014-04-27 23:59+0100\n" "Last-Translator: Simon Eisenmann \n" "Language-Team: struktur AG \n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" @@ -141,6 +141,20 @@ msgstr "Name" msgid "Your picture and name are visible to others." msgstr "Ihr Bild und Name werden anderen Benutzern angezeigt." +msgid "Your ID" +msgstr "Ihre ID" + +msgid "Register" +msgstr "Registrieren" + +msgid "Only register an ID if this is your private browser." +msgstr "" +"Sie sollten sich nur registrieren wenn dies ein privater Browser ist den " +"nur Sie benutzen." + +msgid "Log out" +msgstr "Ausloggen" + msgid "Microphone" msgstr "Mikrofon" @@ -207,8 +221,15 @@ msgstr "Erweiterte Einstellungen ausblenden" msgid "Remember settings" msgstr "Einstellungen merken" -msgid "Apply" -msgstr "Übernehmen" +msgid "" +"Your ID will still be kept - press the log out button above to delete the" +" ID." +msgstr "" +"Ihre ID bleibt dennoch gespeichert. Klicken Sie Ausloggen weiter oben um " +"die ID zu löschen." + +msgid "Close" +msgstr "Schließen" msgid "Share by Email" msgstr "Per E-Mail teilen" @@ -435,9 +456,6 @@ msgstr "Weitere Informationen nötig" msgid "Ok" msgstr "Ok" -msgid "Close" -msgstr "Schließen" - msgid "Access code required" msgstr "Bitte Zugriffscode eingeben" @@ -471,3 +489,6 @@ msgstr "Unbekannt" msgid "Me" msgstr "Ich" +#~ msgid "Apply" +#~ msgstr "Übernehmen" + diff --git a/src/i18n/messages-ja.po b/src/i18n/messages-ja.po index 2908f664..c2206584 100644 --- a/src/i18n/messages-ja.po +++ b/src/i18n/messages-ja.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Spreed Speak Freely 1.0\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2014-04-25 13:40+0200\n" +"POT-Creation-Date: 2014-04-27 23:58+0200\n" "PO-Revision-Date: 2014-04-23 22:25+0100\n" "Last-Translator: Curt Frisemo \n" "Language-Team: Curt Frisemo \n" @@ -141,6 +141,18 @@ msgstr "名前" msgid "Your picture and name are visible to others." msgstr "あなたの写真と名前は公開されています." +msgid "Your ID" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "Only register an ID if this is your private browser." +msgstr "" + +msgid "Log out" +msgstr "" + msgid "Microphone" msgstr "マイク" @@ -207,8 +219,13 @@ msgstr "詳細設定を隠す" msgid "Remember settings" msgstr "設定を保存" -msgid "Apply" -msgstr "適用" +msgid "" +"Your ID will still be kept - press the log out button above to delete the" +" ID." +msgstr "" + +msgid "Close" +msgstr "閉じる" msgid "Share by Email" msgstr "Eメールでシェア" @@ -426,9 +443,6 @@ msgstr "さらなる情報が必要です" msgid "Ok" msgstr "OK" -msgid "Close" -msgstr "閉じる" - msgid "Access code required" msgstr "アクセスコードが必要です" @@ -460,3 +474,6 @@ msgstr "誰か" msgid "Me" msgstr "私" +#~ msgid "Apply" +#~ msgstr "適用" + diff --git a/src/i18n/messages-ko.po b/src/i18n/messages-ko.po index 4e1aad6a..c946a916 100644 --- a/src/i18n/messages-ko.po +++ b/src/i18n/messages-ko.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Spreed Speak Freely 1.0\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2014-04-25 13:40+0200\n" +"POT-Creation-Date: 2014-04-27 23:58+0200\n" "PO-Revision-Date: 2014-04-13 20:30+0900\n" "Last-Translator: FULL NAME \n" "Language-Team: Curt Frisemo \n" @@ -141,6 +141,18 @@ msgstr "이름" msgid "Your picture and name are visible to others." msgstr "사용자의 사진과 이름이 다른사람에게 보일수 있습니다." +msgid "Your ID" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "Only register an ID if this is your private browser." +msgstr "" + +msgid "Log out" +msgstr "" + msgid "Microphone" msgstr "마이크" @@ -207,8 +219,13 @@ msgstr "고급 설정 감추기" msgid "Remember settings" msgstr "설정 기억" -msgid "Apply" -msgstr "적용" +msgid "" +"Your ID will still be kept - press the log out button above to delete the" +" ID." +msgstr "" + +msgid "Close" +msgstr "닫음" msgid "Share by Email" msgstr "이메일로 공유" @@ -426,9 +443,6 @@ msgstr "더 많은 정보가 필요함" msgid "Ok" msgstr "오케이" -msgid "Close" -msgstr "닫음" - msgid "Access code required" msgstr "접속코드 필요함" @@ -460,3 +474,6 @@ msgstr "어떤 사람" msgid "Me" msgstr "나" +#~ msgid "Apply" +#~ msgstr "적용" + diff --git a/src/i18n/messages-zh-cn.po b/src/i18n/messages-zh-cn.po index ab58d5cb..a833e4bc 100644 --- a/src/i18n/messages-zh-cn.po +++ b/src/i18n/messages-zh-cn.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Spreed Speak Freely 1.0\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2014-04-25 13:40+0200\n" +"POT-Creation-Date: 2014-04-27 23:58+0200\n" "PO-Revision-Date: 2014-03-31 23:26+0100\n" "Last-Translator: Michael P.\n" "Language-Team: Curt Frisemo \n" @@ -142,6 +142,18 @@ msgstr "名字" msgid "Your picture and name are visible to others." msgstr "别人能看到您的图片及名字" +msgid "Your ID" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "Only register an ID if this is your private browser." +msgstr "" + +msgid "Log out" +msgstr "" + msgid "Microphone" msgstr "麦克风" @@ -208,8 +220,13 @@ msgstr "隐藏高级设置" msgid "Remember settings" msgstr "记住设置" -msgid "Apply" -msgstr "适用" +msgid "" +"Your ID will still be kept - press the log out button above to delete the" +" ID." +msgstr "" + +msgid "Close" +msgstr "关闭" msgid "Share by Email" msgstr "电子邮件共享" @@ -424,9 +441,6 @@ msgstr "需要更多信息" msgid "Ok" msgstr "Ok" -msgid "Close" -msgstr "关闭" - msgid "Access code required" msgstr "需要接入码" @@ -459,3 +473,6 @@ msgstr "" msgid "Me" msgstr "名字" +#~ msgid "Apply" +#~ msgstr "适用" + diff --git a/src/i18n/messages-zh-tw.po b/src/i18n/messages-zh-tw.po index 2dc58f63..b3441b65 100644 --- a/src/i18n/messages-zh-tw.po +++ b/src/i18n/messages-zh-tw.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Spreed Speak Freely 1.0\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2014-04-25 13:40+0200\n" +"POT-Creation-Date: 2014-04-27 23:58+0200\n" "PO-Revision-Date: 2014-04-07 18:09+0800\n" "Last-Translator: Michael P.\n" "Language-Team: Curt Frisemo \n" @@ -142,6 +142,18 @@ msgstr "名字" msgid "Your picture and name are visible to others." msgstr "別人能看到您的圖片及名字" +msgid "Your ID" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "Only register an ID if this is your private browser." +msgstr "" + +msgid "Log out" +msgstr "" + msgid "Microphone" msgstr "麥克風" @@ -208,8 +220,13 @@ msgstr "隐藏高级设置" msgid "Remember settings" msgstr "記住設置" -msgid "Apply" -msgstr "適用" +msgid "" +"Your ID will still be kept - press the log out button above to delete the" +" ID." +msgstr "" + +msgid "Close" +msgstr "關閉" msgid "Share by Email" msgstr "電子郵件共享" @@ -424,9 +441,6 @@ msgstr "需要更多信息" msgid "Ok" msgstr "Ok" -msgid "Close" -msgstr "關閉" - msgid "Access code required" msgstr "需要接入碼" @@ -459,3 +473,6 @@ msgstr "" msgid "Me" msgstr "名字" +#~ msgid "Apply" +#~ msgstr "適用" + diff --git a/src/i18n/messages.pot b/src/i18n/messages.pot index a2c0be04..30a96971 100644 --- a/src/i18n/messages.pot +++ b/src/i18n/messages.pot @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Spreed Speak Freely 1.0\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2014-04-25 13:40+0200\n" +"POT-Creation-Date: 2014-04-27 23:58+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -141,6 +141,18 @@ msgstr "" msgid "Your picture and name are visible to others." msgstr "" +msgid "Your ID" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "Only register an ID if this is your private browser." +msgstr "" + +msgid "Log out" +msgstr "" + msgid "Microphone" msgstr "" @@ -207,7 +219,12 @@ msgstr "" msgid "Remember settings" msgstr "" -msgid "Apply" +msgid "" +"Your ID will still be kept - press the log out button above to delete the" +" ID." +msgstr "" + +msgid "Close" msgstr "" msgid "Share by Email" @@ -423,9 +440,6 @@ msgstr "" msgid "Ok" msgstr "" -msgid "Close" -msgstr "" - msgid "Access code required" msgstr "" diff --git a/static/translation/messages-de.json b/static/translation/messages-de.json index 9155d882..cbf16ca9 100644 --- a/static/translation/messages-de.json +++ b/static/translation/messages-de.json @@ -1 +1 @@ -{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=2; plural=(n != 1)"},"Share your screen":[null,"Bildschirm freigeben"],"Chat":[null,"Chat"],"Mute microphone":[null,"Mikrofon abschalten"],"Turn camera off":[null,"Kamera abschalten"],"Settings":[null,"Einstellungen"],"Your audio level":[null,"Ihr Audio-Pegel"],"Start chat":[null,"Chat starten"],"Start video call":[null,"Video-Anruf starten"],"Start audio conference":[null,"Audio-Konferenz starten"],"No other users online":[null,"Niemand sonst online"],"Chat sessions":[null,"Chat-Sitzungen"],"Room chat":[null,"Raum-Chat"],"Peer to peer":[null,"Peer-to-peer"],"Close chat":[null,"Chat schließen"],"is typing...":[null," schreibt gerade..."],"has stopped typing...":[null," schreibt nicht mehr..."],"Type here to chat...":[null,"Nachricht hier eingeben..."],"Send":[null,"Senden"],"File sharing":[null,"Datei-Austausch"],"File is no longer available":[null,"Datei ist nicht mehr verfügbar"],"Download":[null,"Laden"],"Open":[null,"Öffnen"],"Cancel":[null,"Abbrechen"],"Unshare":[null,"Zurückziehen"],"Retry":[null,"Nochmal versuchen"],"Download failed.":[null,"Fehler beim Download."],"Change room":[null,"Raum wechseln"],"Room":[null,"Raum"],"Main":[null,"Standard"],"Leave room":[null,"Raum verlassen"],"Current room":[null,"Raum"],"Screen sharing options":[null,"Optionen für Bildschirmfreigabe"],"Fit screen.":[null,"Bildschirm einpassen."],"Your picture":[null,"Ihr Bild"],"Take picture":[null,"Bild machen"],"Waiting for camera":[null,"Warte auf die Kamera"],"Your name":[null,"Ihr Name"],"Name":[null,"Name"],"Your picture and name are visible to others.":[null,"Ihr Bild und Name werden anderen Benutzern angezeigt."],"Microphone":[null,"Mikrofon"],"Camera":[null,"Kamera"],"Video quality":[null,"Video-Qualität"],"Low":[null,"Gering"],"High":[null,"Hoch"],"HD":[null,"HD"],"Language":[null,"Sprache"],"Language changes become active on reload.":[null,"Sie müssen die Seite neu laden, um die Spracheinstellung zu übernehmen."],"Default room":[null,"Standard Raum"],"Set alternative room to join at start.":[null," Raum wird beim Start automatisch betreten."],"Desktop notification":[null,"Desktop-Benachrichtigung"],"Enable":[null,"Aktivieren"],"Denied - check your browser settings":[null,"Verweigert - prüfen Sie die Browser-Einstellungen"],"Allowed":[null,"Aktiviert"],"Advanced settings":[null,"Erweiterte Einstellungen"],"Stereo audio":[null,"Stereo-Audio"],"Max video frame rate":[null,"Max. Bildwiederholrate"],"auto":[null,"auto"],"Experimental settings":[null,"Experimentelle Einstellungen"],"Show advanced settings":[null,"Erweiterte Einstellungen anzeigen"],"Hide advanced settings":[null,"Erweiterte Einstellungen ausblenden"],"Remember settings":[null,"Einstellungen merken"],"Apply":[null,"Übernehmen"],"Share by Email":[null,"Per E-Mail teilen"],"Share on Facebook":[null,"Auf Facebook teilen"],"Share on Twitter":[null,"Auf Twitter teilen"],"Share on Google Plus":[null,"Auf Google Plus teilen"],"Share on XING":[null,"Auf XING teilen"],"Initializing":[null,"Initialisiere"],"Online":[null,"Online"],"Calling":[null,"Verbinde mit"],"Hangup":[null,"Auflegen"],"In call with":[null,"Verbunden mit"],"Conference with":[null,"Konferenz mit"],"Your are offline":[null,"Sie sind offline"],"Go online":[null,"Online gehen"],"Connection interrupted":[null,"Verbindung unterbrochen"],"An error occured":[null,"Ein Fehler ist aufgetreten"],"Incoming call":[null,"Eingehender Anruf"],"from":[null,"von"],"Accept call":[null,"Anruf annehmen"],"Reject":[null,"Abweisen"],"Waiting for camera/microphone access":[null,"Warte auf Kamera/Mikrofon Freigabe"],"Please wait":[null,"Bitte warten"],"Checking camera and microphone access.":[null,"Prüfe Zugriff auf Kamera und Mikrofon."],"Please allow access to your camera and microphone.":[null,"Bitte gestatten Sie den Zugriff auf Ihre Kamera und Mikrofon."],"Camera / microphone access required.":[null,"Kamera / Mikrofon Zugriff wird benötigt."],"Please check your browser settings and allow camera and microphone access for this site.":[null,"Bitte prüfen Sie Ihre Browser-Einstellungen und gestatten Sie den Zugriff auf Kamera und Mikrofon für diese Seite."],"Skip check":[null,"Überspringen"],"Click here for help (Google Chrome).":[null,"Hier klicken für weitere Infos (Google Chrome)."],"Please set your user details and settings.":[null,"Bitte vervollständigen Sie Ihre Daten und Einstellungen."],"Please note that some settings require you to reload or to make a new call to become effective.":[null,"Bitte beachten Sie, dass einige Einstellungen erst nach einem Reload oder einem neuen Anruf aktiv werden."],"Create your room":[null,"Erstellen Sie Ihren Raum"],"This is your room link:":[null,"Ihre Raum-Addresse:"],"Creating room link ...":[null,"Raum-Link wird erstellt ..."],"Start":[null,"Start"],"Just click start":[null,"Klicken Sie auf Start"],"Share this URL with the people you want to meet.":[null,"Teilen Sie die Raum-Adresse mit anderen Kontakten."],"You can use and re-use this room as many times as you want.":[null,"Sie können diesen Raum so oft wieder benutzen wie Sie möchten."],"Peer to peer chat active.":[null,"Peer-to-peer Chat ist aktiv."],"Peer to peer chat is now off.":[null,"Peer-to-peer Chat ist nicht mehr aktiv."]," is now offline.":[null," ist jetzt offline."]," is now online.":[null," ist jetzt online."],"You share file:":[null,"Sie geben eine Datei frei:"],"Incoming file:":[null,"Eingehende Datei:"],"Quit from Spreed Speak Freely?":[null,"Spreed Speak Freely beenden?"],"Restart required to apply updates. Click ok to restart now.":[null,"Es stehen Updates zur Verfügung. Klicken Sie Ok um die Anwendung neu zu starten."],"Failed to access camera/microphone.":[null,"Fehler beim Zugriff auf die Kamera / das Mikrofon."],"Failed to establish peer connection.":[null,"Fehler beim Verbindungsaufbau."],"We are sorry but something went wrong. Boo boo.":[null,"Leider ist ein Fehler aufgetreten. Buhuhu."],"Oops":[null,"Hoppla"],"Peer connection failed. Check your settings.":[null,"Verbindung fehlgeschlagen. Überprüfen Sie Ihre Einstellungen."],"User hung up because of error.":[null,"Teilnehmer hat aufgelegt, da ein Fehler aufgetreten ist."]," is busy. Try again later.":[null," ist in einem Gespräch. Probieren Sie es später."]," rejected your call.":[null," hat Ihren Anruf abgelehnt."]," does not pick up.":[null," nimmt nicht ab."]," tried to call you.":[null," hat versucht Sie anzurufen."]," called you.":[null," hat Sie angerufen."],"Your browser does not support WebRTC. No calls possible.":[null,"Ihr Browser unterstützt kein WebRTC. Keine Anrufe möglich."],"Chat with":[null,"Chat mit"],"Message from ":[null,"Nachricht von "],"You are now in room %s ...":[null,"Sie sind nun im Raum %s ..."],"Your browser does not support file transfer.":[null,"Mit Ihrem Browser können keine Dateien übertragen werden."],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":[null,"Die Berechtigung für die Bildschirmaufzeichnung wurde verweigert. Bitte stellen Sie sicher die Unterstützung für Bildschimaufzeichnung in Ihrem Browser aktiviert ist. Kopieren Sie dazu chrome://flags/#enable-usermedia-screen-capture und öffnen Sie diese Adresse in Ihrem Browser. Aktivieren Sie die oberste Einstellung und starten dann den Browser neu. Anschließend können Sie die Bildschirmfreigabe benutzen."],"Use browser language":[null,"Browsereinstellung"],"Meet with me here:":[null,"Meeting:"],"Error":[null,"Fehler"],"Hint":[null,"Hinweis"],"Please confirm":[null,"Bitte bestätigen"],"More information required":[null,"Weitere Informationen nötig"],"Ok":[null,"Ok"],"Close":[null,"Schließen"],"Access code required":[null,"Bitte Zugriffscode eingeben"],"Access denied":[null,"Zugriff verweigert"],"Please provide a valid access code.":[null,"Bitte geben Sie einen gültigen Zugriffscode ein."],"Failed to verify access code. Check your Internet connection and try again.":[null,"Der Zugriffscode konnte nicht überprueft werden. Bitte prüfen Sie Ihre Internetverbindung."],"and %s":[null,"und %s"],"and %d others":[null,"und %d weiteren"],"User":[null,"Teilnehmer"],"Someone":[null,"Unbekannt"],"Me":[null,"Ich"]}}} \ No newline at end of file +{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=2; plural=(n != 1)"},"Share your screen":[null,"Bildschirm freigeben"],"Chat":[null,"Chat"],"Mute microphone":[null,"Mikrofon abschalten"],"Turn camera off":[null,"Kamera abschalten"],"Settings":[null,"Einstellungen"],"Your audio level":[null,"Ihr Audio-Pegel"],"Standard view":[null,"Standardansicht"],"Kiosk view":[null,"Kiosk-Ansicht"],"Start chat":[null,"Chat starten"],"Start video call":[null,"Video-Anruf starten"],"Start audio conference":[null,"Audio-Konferenz starten"],"No other users online":[null,"Niemand sonst online"],"Chat sessions":[null,"Chat-Sitzungen"],"Room chat":[null,"Raum-Chat"],"Peer to peer":[null,"Peer-to-peer"],"Close chat":[null,"Chat schließen"],"is typing...":[null," schreibt gerade..."],"has stopped typing...":[null," schreibt nicht mehr..."],"Type here to chat...":[null,"Nachricht hier eingeben..."],"Send":[null,"Senden"],"File sharing":[null,"Datei-Austausch"],"File is no longer available":[null,"Datei ist nicht mehr verfügbar"],"Download":[null,"Laden"],"Open":[null,"Öffnen"],"Cancel":[null,"Abbrechen"],"Unshare":[null,"Zurückziehen"],"Retry":[null,"Nochmal versuchen"],"Download failed.":[null,"Fehler beim Download."],"Change room":[null,"Raum wechseln"],"Room":[null,"Raum"],"Main":[null,"Standard"],"Leave room":[null,"Raum verlassen"],"Current room":[null,"Raum"],"Screen sharing options":[null,"Optionen für Bildschirmfreigabe"],"Fit screen.":[null,"Bildschirm einpassen."],"Your picture":[null,"Ihr Bild"],"Take picture":[null,"Bild machen"],"Waiting for camera":[null,"Warte auf die Kamera"],"Your name":[null,"Ihr Name"],"Name":[null,"Name"],"Your picture and name are visible to others.":[null,"Ihr Bild und Name werden anderen Benutzern angezeigt."],"Your ID":[null,"Ihre ID"],"Register":[null,"Registrieren"],"Only register an ID if this is your private browser.":[null,"Sie sollten sich nur registrieren wenn dies ein privater Browser ist den nur Sie benutzen."],"Log out":[null,"Ausloggen"],"Microphone":[null,"Mikrofon"],"Camera":[null,"Kamera"],"Video quality":[null,"Video-Qualität"],"Low":[null,"Gering"],"High":[null,"Hoch"],"HD":[null,"HD"],"Language":[null,"Sprache"],"Language changes become active on reload.":[null,"Sie müssen die Seite neu laden, um die Spracheinstellung zu übernehmen."],"Default room":[null,"Standard Raum"],"Set alternative room to join at start.":[null," Raum wird beim Start automatisch betreten."],"Desktop notification":[null,"Desktop-Benachrichtigung"],"Enable":[null,"Aktivieren"],"Denied - check your browser settings":[null,"Verweigert - prüfen Sie die Browser-Einstellungen"],"Allowed":[null,"Aktiviert"],"Advanced settings":[null,"Erweiterte Einstellungen"],"Stereo audio":[null,"Stereo-Audio"],"Max video frame rate":[null,"Max. Bildwiederholrate"],"auto":[null,"auto"],"Experimental settings":[null,"Experimentelle Einstellungen"],"Show advanced settings":[null,"Erweiterte Einstellungen anzeigen"],"Hide advanced settings":[null,"Erweiterte Einstellungen ausblenden"],"Remember settings":[null,"Einstellungen merken"],"Your ID will still be kept - press the log out button above to delete the ID.":[null,"Ihre ID bleibt dennoch gespeichert. Klicken Sie Ausloggen weiter oben um die ID zu löschen."],"Close":[null,"Schließen"],"Share by Email":[null,"Per E-Mail teilen"],"Share on Facebook":[null,"Auf Facebook teilen"],"Share on Twitter":[null,"Auf Twitter teilen"],"Share on Google Plus":[null,"Auf Google Plus teilen"],"Share on XING":[null,"Auf XING teilen"],"Initializing":[null,"Initialisiere"],"Online":[null,"Online"],"Calling":[null,"Verbinde mit"],"Hangup":[null,"Auflegen"],"In call with":[null,"Verbunden mit"],"Conference with":[null,"Konferenz mit"],"Your are offline":[null,"Sie sind offline"],"Go online":[null,"Online gehen"],"Connection interrupted":[null,"Verbindung unterbrochen"],"An error occured":[null,"Ein Fehler ist aufgetreten"],"Incoming call":[null,"Eingehender Anruf"],"from":[null,"von"],"Accept call":[null,"Anruf annehmen"],"Reject":[null,"Abweisen"],"Waiting for camera/microphone access":[null,"Warte auf Kamera/Mikrofon Freigabe"],"Please wait":[null,"Bitte warten"],"Checking camera and microphone access.":[null,"Prüfe Zugriff auf Kamera und Mikrofon."],"Please allow access to your camera and microphone.":[null,"Bitte gestatten Sie den Zugriff auf Ihre Kamera und Mikrofon."],"Camera / microphone access required.":[null,"Kamera / Mikrofon Zugriff wird benötigt."],"Please check your browser settings and allow camera and microphone access for this site.":[null,"Bitte prüfen Sie Ihre Browser-Einstellungen und gestatten Sie den Zugriff auf Kamera und Mikrofon für diese Seite."],"Skip check":[null,"Überspringen"],"Click here for help (Google Chrome).":[null,"Hier klicken für weitere Infos (Google Chrome)."],"Please set your user details and settings.":[null,"Bitte vervollständigen Sie Ihre Daten und Einstellungen."],"Please note that some settings require you to reload or to make a new call to become effective.":[null,"Bitte beachten Sie, dass einige Einstellungen erst nach einem Reload oder einem neuen Anruf aktiv werden."],"Create your room":[null,"Erstellen Sie Ihren Raum"],"This is your room link:":[null,"Ihre Raum-Addresse:"],"Creating room link ...":[null,"Raum-Link wird erstellt ..."],"Start":[null,"Start"],"Just click start":[null,"Klicken Sie auf Start"],"Share this URL with the people you want to meet.":[null,"Teilen Sie die Raum-Adresse mit anderen Kontakten."],"You can use and re-use this room as many times as you want.":[null,"Sie können diesen Raum so oft wieder benutzen wie Sie möchten."],"Peer to peer chat active.":[null,"Peer-to-peer Chat ist aktiv."],"Peer to peer chat is now off.":[null,"Peer-to-peer Chat ist nicht mehr aktiv."]," is now offline.":[null," ist jetzt offline."]," is now online.":[null," ist jetzt online."],"You share file:":[null,"Sie geben eine Datei frei:"],"Incoming file:":[null,"Eingehende Datei:"],"Quit from Spreed Speak Freely?":[null,"Spreed Speak Freely beenden?"],"Restart required to apply updates. Click ok to restart now.":[null,"Es stehen Updates zur Verfügung. Klicken Sie Ok um die Anwendung neu zu starten."],"Failed to access camera/microphone.":[null,"Fehler beim Zugriff auf die Kamera / das Mikrofon."],"Failed to establish peer connection.":[null,"Fehler beim Verbindungsaufbau."],"We are sorry but something went wrong. Boo boo.":[null,"Leider ist ein Fehler aufgetreten. Buhuhu."],"Oops":[null,"Hoppla"],"Peer connection failed. Check your settings.":[null,"Verbindung fehlgeschlagen. Überprüfen Sie Ihre Einstellungen."],"User hung up because of error.":[null,"Teilnehmer hat aufgelegt, da ein Fehler aufgetreten ist."]," is busy. Try again later.":[null," ist in einem Gespräch. Probieren Sie es später."]," rejected your call.":[null," hat Ihren Anruf abgelehnt."]," does not pick up.":[null," nimmt nicht ab."]," tried to call you.":[null," hat versucht Sie anzurufen."]," called you.":[null," hat Sie angerufen."],"Your browser does not support WebRTC. No calls possible.":[null,"Ihr Browser unterstützt kein WebRTC. Keine Anrufe möglich."],"Chat with":[null,"Chat mit"],"Message from ":[null,"Nachricht von "],"You are now in room %s ...":[null,"Sie sind nun im Raum %s ..."],"Your browser does not support file transfer.":[null,"Mit Ihrem Browser können keine Dateien übertragen werden."],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":[null,"Die Berechtigung für die Bildschirmaufzeichnung wurde verweigert. Bitte stellen Sie sicher die Unterstützung für Bildschimaufzeichnung in Ihrem Browser aktiviert ist. Kopieren Sie dazu chrome://flags/#enable-usermedia-screen-capture und öffnen Sie diese Adresse in Ihrem Browser. Aktivieren Sie die oberste Einstellung und starten dann den Browser neu. Anschließend können Sie die Bildschirmfreigabe benutzen."],"Use browser language":[null,"Browsereinstellung"],"Meet with me here:":[null,"Meeting:"],"Error":[null,"Fehler"],"Hint":[null,"Hinweis"],"Please confirm":[null,"Bitte bestätigen"],"More information required":[null,"Weitere Informationen nötig"],"Ok":[null,"Ok"],"Access code required":[null,"Bitte Zugriffscode eingeben"],"Access denied":[null,"Zugriff verweigert"],"Please provide a valid access code.":[null,"Bitte geben Sie einen gültigen Zugriffscode ein."],"Failed to verify access code. Check your Internet connection and try again.":[null,"Der Zugriffscode konnte nicht überprueft werden. Bitte prüfen Sie Ihre Internetverbindung."],"and %s":[null,"und %s"],"and %d others":[null,"und %d weiteren"],"User":[null,"Teilnehmer"],"Someone":[null,"Unbekannt"],"Me":[null,"Ich"]}}} \ No newline at end of file diff --git a/static/translation/messages-ja.json b/static/translation/messages-ja.json index 71f1d2e6..ebaf7605 100644 --- a/static/translation/messages-ja.json +++ b/static/translation/messages-ja.json @@ -1 +1 @@ -{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=1; plural=0"},"Share your screen":[null,"画面を共有する."],"Chat":[null,"チャット"],"Mute microphone":[null,"消音"],"Turn camera off":[null,"カメラをオフにする"],"Settings":[null,"設定"],"Your audio level":[null,"あなたの音量"],"Start chat":[null,"チャットを始める"],"Start video call":[null,"テレビ電話を始める"],"Start audio conference":[null,"音声会議を始める"],"No other users online":[null,"オンラインのユーザーはいません"],"Chat sessions":[null,"チャットのセッション"],"Room chat":[null,"ルームチャット"],"Peer to peer":[null,"ピア・ツー・ピア"],"Close chat":[null,"チャットを終える"],"is typing...":[null,"は入力中です..."],"has stopped typing...":[null,"は入力を止めました..."],"Type here to chat...":[null,"ここに入力してチャット開始します..."],"Send":[null,"送信"],"File sharing":[null,"ファイル共有"],"File is no longer available":[null,"ファイルは有効ではありません"],"Download":[null,"ダウンロード"],"Open":[null,"開く"],"Cancel":[null,"キャンセル"],"Unshare":[null,"共有取り消し"],"Retry":[null,"リトライ"],"Download failed.":[null,"ダウンロード失敗."],"Change room":[null,"ルームチェンジ"],"Room":[null,"ルーム"],"Main":[null,"メイン"],"Leave room":[null,"ルームを出る"],"Current room":[null,"現在のルーム"],"Screen sharing options":[null,"画面共有オプション"],"Fit screen.":[null,"画面に合わせる"],"Your picture":[null,"あなたの写真"],"Take picture":[null,"写真を取る"],"Waiting for camera":[null,"カメラ待ち"],"Your name":[null,"あなたの名前"],"Name":[null,"名前"],"Your picture and name are visible to others.":[null,"あなたの写真と名前は公開されています."],"Microphone":[null,"マイク"],"Camera":[null,"カメラ"],"Video quality":[null,"ビデオ画質"],"Low":[null,"低い"],"High":[null,"高い"],"HD":[null,"HD"],"Language":[null,"言語"],"Language changes become active on reload.":[null,"言語の変更は再読み込み時に適用となります."],"Default room":[null,"デフォルト・ルーム"],"Set alternative room to join at start.":[null,"スタート時に別のルームに参加する."],"Desktop notification":[null,"デスクトップ通知"],"Enable":[null,"有効にする"],"Denied - check your browser settings":[null,"拒否 - ブラウザ設定を確認して下さい"],"Allowed":[null,"許可"],"Advanced settings":[null,"詳細設定"],"Stereo audio":[null,"ステレオ・オーディオ"],"Max video frame rate":[null,"ビデオ最高フレームレート"],"auto":[null,"自動"],"Experimental settings":[null,"試験的に設定"],"Show advanced settings":[null,"詳細設定を表示"],"Hide advanced settings":[null,"詳細設定を隠す"],"Remember settings":[null,"設定を保存"],"Apply":[null,"適用"],"Share by Email":[null,"Eメールでシェア"],"Share on Facebook":[null,"フェイスブックでシェア"],"Share on Twitter":[null,"ツィッターでシェア"],"Share on Google Plus":[null,"Google+でシェア"],"Share on XING":[null,"XINGでシェア"],"Initializing":[null,"初期化中"],"Online":[null,"オンライン"],"Calling":[null,"発信中"],"Hangup":[null,"切断"],"In call with":[null,"と会話中"],"Conference with":[null,"と会議中"],"Your are offline":[null,"オフラインです"],"Go online":[null,"オンラインにする"],"Connection interrupted":[null,"接続は中断されました"],"An error occured":[null,"エラーが発生しました"],"Incoming call":[null,"着信中"],"from":[null,"から"],"Accept call":[null,"通話"],"Reject":[null,"拒否"],"Waiting for camera/microphone access":[null,"カメラ・マイクの接続待ち."],"Please wait":[null,"お待ちください"],"Checking camera and microphone access.":[null,"カメラ・マイクの接続確認中."],"Please allow access to your camera and microphone.":[null,"カメラとマイクの接続を許可してください."],"Camera / microphone access required.":[null,"カメラ・マイクの接続が必要です."],"Please check your browser settings and allow camera and microphone access for this site.":[null,"ブラウザ設定で、このサイトへのカメラ・マイクの接続を許可してください."],"Skip check":[null,"チェックをスキップ"],"Click here for help (Google Chrome).":[null,"ここをクリックしてヘルプ表示(Google Chrome)"],"Please set your user details and settings.":[null,"あなたのプロフィールとアプリの動作を設定してください."],"Please note that some settings require you to reload or to make a new call to become effective.":[null,"いくつかの設定は、再読み込みもしくは次回の発信から有効です."],"Create your room":[null,"自分のルームを作成する"],"This is your room link:":[null,"あなたのルームへのリンク:"],"Creating room link ...":[null,"ルームへのリンクを作る..."],"Start":[null,"開始"],"Just click start":[null,"クリックして開始"],"Share this URL with the people you want to meet.":[null,"会いたい人とURLをシェアする."],"You can use and re-use this room as many times as you want.":[null,"ルームは何回でも好きなだけ使えます."],"Peer to peer chat active.":[null,"ピア・ツー・ピア・チャットがアクティブです."],"Peer to peer chat is now off.":[null,"ピア・ツー・ピア・チャットがオフです."]," is now offline.":[null,"は今オフラインです"]," is now online.":[null,"は今オンラインです"],"You share file:":[null,"あなたの共有ファイル:"],"Incoming file:":[null,"受信中ファイル:"],"Quit from Spreed Speak Freely?":[null,"Spreed Speak Freelyを終了しますか?"],"Restart required to apply updates. Click ok to restart now.":[null,"アップデート適用のため再起動してください.ここをクリックして再起動する."],"Failed to access camera/microphone.":[null,"カメラ・マイクへの接続に失敗しました."],"Failed to establish peer connection.":[null,"ピアとの接続に失敗しました."],"We are sorry but something went wrong. Boo boo.":[null,"申し訳ないのですが、不具合が生じました。"],"Oops":[null,"しまった"],"Peer connection failed. Check your settings.":[null,"ピア接続に失敗しました.設定を確認してください."],"User hung up because of error.":[null,"エラーのため切断しました."]," is busy. Try again later.":[null,"は話中です.後で掛けなおしてください."]," rejected your call.":[null,"着信拒否されました."]," does not pick up.":[null,"は電話にでません."]," tried to call you.":[null,"は電話しようとしました."]," called you.":[null,"から電話がありました."],"Your browser does not support WebRTC. No calls possible.":[null,"ブラウザがWebRTCをサポートしていない為通話はできません."],"Chat with":[null,"とチャットする"],"Message from ":[null,"からのメッセージ"],"You are now in room %s ...":[null,"あなたは%sのルームにいます..."],"Your browser does not support file transfer.":[null,"ブラウザがファイル転送をサポートしていません."],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":[null,"画面共有は拒否されました.ブラウザの画面共有の設定を確認して下さい. Chromeのアドレスバーに chrome://flags/#enable-usermedia-screen-capture を入力して開き、スクリーンキャプチャのサポートを有効にしてください。その後ブラウザを再起動してください。"],"Use browser language":[null,"ブラウザの言語を使用"],"Meet with me here:":[null,"ここで私と会う:"],"Error":[null,"エラー"],"Hint":[null,"ヒント"],"Please confirm":[null,"確認して下さい"],"More information required":[null,"さらなる情報が必要です"],"Ok":[null,"OK"],"Close":[null,"閉じる"],"Access code required":[null,"アクセスコードが必要です"],"Access denied":[null,"アクセスが拒否されました"],"Please provide a valid access code.":[null,"有効なアクセスコードを入力してください."],"Failed to verify access code. Check your Internet connection and try again.":[null,"アクセスコードの確認に失敗しました.インターネット接続を確認してリトライしてください."],"and %s":[null,"と %2"],"and %d others":[null,""],"User":[null,"ユーザー"],"Someone":[null,"誰か"],"Me":[null,"私"]}}} \ No newline at end of file +{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=1; plural=0"},"Share your screen":[null,"画面を共有する."],"Chat":[null,"チャット"],"Mute microphone":[null,"消音"],"Turn camera off":[null,"カメラをオフにする"],"Settings":[null,"設定"],"Your audio level":[null,"あなたの音量"],"Standard view":[null,""],"Kiosk view":[null,""],"Start chat":[null,"チャットを始める"],"Start video call":[null,"テレビ電話を始める"],"Start audio conference":[null,"音声会議を始める"],"No other users online":[null,"オンラインのユーザーはいません"],"Chat sessions":[null,"チャットのセッション"],"Room chat":[null,"ルームチャット"],"Peer to peer":[null,"ピア・ツー・ピア"],"Close chat":[null,"チャットを終える"],"is typing...":[null,"は入力中です..."],"has stopped typing...":[null,"は入力を止めました..."],"Type here to chat...":[null,"ここに入力してチャット開始します..."],"Send":[null,"送信"],"File sharing":[null,"ファイル共有"],"File is no longer available":[null,"ファイルは有効ではありません"],"Download":[null,"ダウンロード"],"Open":[null,"開く"],"Cancel":[null,"キャンセル"],"Unshare":[null,"共有取り消し"],"Retry":[null,"リトライ"],"Download failed.":[null,"ダウンロード失敗."],"Change room":[null,"ルームチェンジ"],"Room":[null,"ルーム"],"Main":[null,"メイン"],"Leave room":[null,"ルームを出る"],"Current room":[null,"現在のルーム"],"Screen sharing options":[null,"画面共有オプション"],"Fit screen.":[null,"画面に合わせる"],"Your picture":[null,"あなたの写真"],"Take picture":[null,"写真を取る"],"Waiting for camera":[null,"カメラ待ち"],"Your name":[null,"あなたの名前"],"Name":[null,"名前"],"Your picture and name are visible to others.":[null,"あなたの写真と名前は公開されています."],"Your ID":[null,""],"Register":[null,""],"Only register an ID if this is your private browser.":[null,""],"Log out":[null,""],"Microphone":[null,"マイク"],"Camera":[null,"カメラ"],"Video quality":[null,"ビデオ画質"],"Low":[null,"低い"],"High":[null,"高い"],"HD":[null,"HD"],"Language":[null,"言語"],"Language changes become active on reload.":[null,"言語の変更は再読み込み時に適用となります."],"Default room":[null,"デフォルト・ルーム"],"Set alternative room to join at start.":[null,"スタート時に別のルームに参加する."],"Desktop notification":[null,"デスクトップ通知"],"Enable":[null,"有効にする"],"Denied - check your browser settings":[null,"拒否 - ブラウザ設定を確認して下さい"],"Allowed":[null,"許可"],"Advanced settings":[null,"詳細設定"],"Stereo audio":[null,"ステレオ・オーディオ"],"Max video frame rate":[null,"ビデオ最高フレームレート"],"auto":[null,"自動"],"Experimental settings":[null,"試験的に設定"],"Show advanced settings":[null,"詳細設定を表示"],"Hide advanced settings":[null,"詳細設定を隠す"],"Remember settings":[null,"設定を保存"],"Your ID will still be kept - press the log out button above to delete the ID.":[null,""],"Close":[null,"閉じる"],"Share by Email":[null,"Eメールでシェア"],"Share on Facebook":[null,"フェイスブックでシェア"],"Share on Twitter":[null,"ツィッターでシェア"],"Share on Google Plus":[null,"Google+でシェア"],"Share on XING":[null,"XINGでシェア"],"Initializing":[null,"初期化中"],"Online":[null,"オンライン"],"Calling":[null,"発信中"],"Hangup":[null,"切断"],"In call with":[null,"と会話中"],"Conference with":[null,"と会議中"],"Your are offline":[null,"オフラインです"],"Go online":[null,"オンラインにする"],"Connection interrupted":[null,"接続は中断されました"],"An error occured":[null,"エラーが発生しました"],"Incoming call":[null,"着信中"],"from":[null,"から"],"Accept call":[null,"通話"],"Reject":[null,"拒否"],"Waiting for camera/microphone access":[null,"カメラ・マイクの接続待ち."],"Please wait":[null,"お待ちください"],"Checking camera and microphone access.":[null,"カメラ・マイクの接続確認中."],"Please allow access to your camera and microphone.":[null,"カメラとマイクの接続を許可してください."],"Camera / microphone access required.":[null,"カメラ・マイクの接続が必要です."],"Please check your browser settings and allow camera and microphone access for this site.":[null,"ブラウザ設定で、このサイトへのカメラ・マイクの接続を許可してください."],"Skip check":[null,"チェックをスキップ"],"Click here for help (Google Chrome).":[null,"ここをクリックしてヘルプ表示(Google Chrome)"],"Please set your user details and settings.":[null,"あなたのプロフィールとアプリの動作を設定してください."],"Please note that some settings require you to reload or to make a new call to become effective.":[null,"いくつかの設定は、再読み込みもしくは次回の発信から有効です."],"Create your room":[null,"自分のルームを作成する"],"This is your room link:":[null,"あなたのルームへのリンク:"],"Creating room link ...":[null,"ルームへのリンクを作る..."],"Start":[null,"開始"],"Just click start":[null,"クリックして開始"],"Share this URL with the people you want to meet.":[null,"会いたい人とURLをシェアする."],"You can use and re-use this room as many times as you want.":[null,"ルームは何回でも好きなだけ使えます."],"Peer to peer chat active.":[null,"ピア・ツー・ピア・チャットがアクティブです."],"Peer to peer chat is now off.":[null,"ピア・ツー・ピア・チャットがオフです."]," is now offline.":[null,"は今オフラインです"]," is now online.":[null,"は今オンラインです"],"You share file:":[null,"あなたの共有ファイル:"],"Incoming file:":[null,"受信中ファイル:"],"Quit from Spreed Speak Freely?":[null,"Spreed Speak Freelyを終了しますか?"],"Restart required to apply updates. Click ok to restart now.":[null,"アップデート適用のため再起動してください.ここをクリックして再起動する."],"Failed to access camera/microphone.":[null,"カメラ・マイクへの接続に失敗しました."],"Failed to establish peer connection.":[null,"ピアとの接続に失敗しました."],"We are sorry but something went wrong. Boo boo.":[null,"申し訳ないのですが、不具合が生じました。"],"Oops":[null,"しまった"],"Peer connection failed. Check your settings.":[null,"ピア接続に失敗しました.設定を確認してください."],"User hung up because of error.":[null,"エラーのため切断しました."]," is busy. Try again later.":[null,"は話中です.後で掛けなおしてください."]," rejected your call.":[null,"着信拒否されました."]," does not pick up.":[null,"は電話にでません."]," tried to call you.":[null,"は電話しようとしました."]," called you.":[null,"から電話がありました."],"Your browser does not support WebRTC. No calls possible.":[null,"ブラウザがWebRTCをサポートしていない為通話はできません."],"Chat with":[null,"とチャットする"],"Message from ":[null,"からのメッセージ"],"You are now in room %s ...":[null,"あなたは%sのルームにいます..."],"Your browser does not support file transfer.":[null,"ブラウザがファイル転送をサポートしていません."],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":[null,"画面共有は拒否されました.ブラウザの画面共有の設定を確認して下さい. Chromeのアドレスバーに chrome://flags/#enable-usermedia-screen-capture を入力して開き、スクリーンキャプチャのサポートを有効にしてください。その後ブラウザを再起動してください。"],"Use browser language":[null,"ブラウザの言語を使用"],"Meet with me here:":[null,"ここで私と会う:"],"Error":[null,"エラー"],"Hint":[null,"ヒント"],"Please confirm":[null,"確認して下さい"],"More information required":[null,"さらなる情報が必要です"],"Ok":[null,"OK"],"Access code required":[null,"アクセスコードが必要です"],"Access denied":[null,"アクセスが拒否されました"],"Please provide a valid access code.":[null,"有効なアクセスコードを入力してください."],"Failed to verify access code. Check your Internet connection and try again.":[null,"アクセスコードの確認に失敗しました.インターネット接続を確認してリトライしてください."],"and %s":[null,"と %2"],"and %d others":[null,""],"User":[null,"ユーザー"],"Someone":[null,"誰か"],"Me":[null,"私"]}}} \ No newline at end of file diff --git a/static/translation/messages-ko.json b/static/translation/messages-ko.json index d9059c9c..418b7acc 100644 --- a/static/translation/messages-ko.json +++ b/static/translation/messages-ko.json @@ -1 +1 @@ -{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=1; plural=0"},"Share your screen":[null,"화면 공유하기"],"Chat":[null,"대화"],"Mute microphone":[null,"음성제거"],"Turn camera off":[null,"카메라꺼짐"],"Settings":[null,"설정"],"Your audio level":[null,"음성크기"],"Start chat":[null,"대화시작"],"Start video call":[null,"화상회의 시작"],"Start audio conference":[null,"음성회의 시작"],"No other users online":[null,"온라인에 다른 대화상대 없음"],"Chat sessions":[null,"대화 세션"],"Room chat":[null,"대화 방"],"Peer to peer":[null,"일대일"],"Close chat":[null,"대화 종료"],"is typing...":[null,"입력중"],"has stopped typing...":[null,"입력 종료"],"Type here to chat...":[null,"대화 입력"],"Send":[null,"전송"],"File sharing":[null,"회일 공유"],"File is no longer available":[null,"화일이 유효하지 않습니다"],"Download":[null,"다운로드"],"Open":[null,"열기"],"Cancel":[null,"취소"],"Unshare":[null,"비공유"],"Retry":[null,"재시도"],"Download failed.":[null,"다운로드실패"],"Change room":[null,"방 변경"],"Room":[null,"방"],"Main":[null,"메인"],"Leave room":[null,"방 이동"],"Current room":[null,"현재 방"],"Screen sharing options":[null,"화면 공유 옵션"],"Fit screen.":[null,"화면에 맟춤"],"Your picture":[null,"사용자 사진"],"Take picture":[null,"사진 찍음"],"Waiting for camera":[null,"카메라 대기중"],"Your name":[null,"사용자 이름"],"Name":[null,"이름"],"Your picture and name are visible to others.":[null,"사용자의 사진과 이름이 다른사람에게 보일수 있습니다."],"Microphone":[null,"마이크"],"Camera":[null,"카메라"],"Video quality":[null,"영상 수준"],"Low":[null,"낮음"],"High":[null,"높음"],"HD":[null,"고화질"],"Language":[null,"언어"],"Language changes become active on reload.":[null,"언어 변경이 재로드 되고 있습니다"],"Default room":[null,"기본 방"],"Set alternative room to join at start.":[null,"시작시에 다른 방에 합류하도록 설정 되었습니다"],"Desktop notification":[null,"데스크탑에 통보"],"Enable":[null,"활성화"],"Denied - check your browser settings":[null,"거부됨 - 브라우저 설정을 확인하세요"],"Allowed":[null,"허락됨"],"Advanced settings":[null,"고급 설정"],"Stereo audio":[null,"스테레오 음성"],"Max video frame rate":[null,"비디오프레임 비율 최대화"],"auto":[null,"자동"],"Experimental settings":[null,"실험 설정"],"Show advanced settings":[null,"고급 설정 보기"],"Hide advanced settings":[null,"고급 설정 감추기"],"Remember settings":[null,"설정 기억"],"Apply":[null,"적용"],"Share by Email":[null,"이메일로 공유"],"Share on Facebook":[null,"Facebook에서 공유"],"Share on Twitter":[null,"Twitter에서 공유"],"Share on Google Plus":[null,"구글 플러스에서 공유"],"Share on XING":[null,"Xing에서 공유"],"Initializing":[null,"초기화"],"Online":[null,"온라인"],"Calling":[null,"전화걸기"],"Hangup":[null,"전화끊기"],"In call with":[null,"전화중"],"Conference with":[null,"회의중"],"Your are offline":[null,"오프라인 입니다"],"Go online":[null,"온라인에 연결합니다"],"Connection interrupted":[null,"연결이 중단"],"An error occured":[null,"에러 발생"],"Incoming call":[null,"전화 걸려옴"],"from":[null,"부터"],"Accept call":[null,"전화 받음"],"Reject":[null,"거부"],"Waiting for camera/microphone access":[null,"카메라/마이크 사용을 기다림"],"Please wait":[null,"기다리세요"],"Checking camera and microphone access.":[null,"카메라와 마이크의 사용을 확인 하세요"],"Please allow access to your camera and microphone.":[null,"카메라와 마이크의 사용을 허용 하세요"],"Camera / microphone access required.":[null,"카메라/마이크 사용이 필요합니다"],"Please check your browser settings and allow camera and microphone access for this site.":[null,"이 사이트에 대하여 브라우저의 설정을 확인하고 카메라와 마이크의 사용을 허용 하세요"],"Skip check":[null,"확인 넘어가기"],"Click here for help (Google Chrome).":[null,"도움말을 원하면 여기를 클릭 하세요 (구글 크롬)"],"Please set your user details and settings.":[null,"사용자의 세부상세와 설정을 지정하세요 "],"Please note that some settings require you to reload or to make a new call to become effective.":[null,"일부 설정은 다시로드 하거나 새로운 전화가 연결시에 유효할수 있습니다."],"Create your room":[null,"새로운 방 만들기"],"This is your room link:":[null,"당신의 방 링크:"],"Creating room link ...":[null,"방 링크 만들기..."],"Start":[null,"시작"],"Just click start":[null,"시작하기 클릭"],"Share this URL with the people you want to meet.":[null,"이 URL을 만나고 싶은 사람과 공유하기"],"You can use and re-use this room as many times as you want.":[null,"당신은 이 방을 원하는 횟수 만큼 사용할 수 있습니다"],"Peer to peer chat active.":[null,"일대일 대화 활성화"],"Peer to peer chat is now off.":[null,"일대일 대화 꺼짐"]," is now offline.":[null,"현재 오프라인 상태"]," is now online.":[null,"현재 온라인 상태"],"You share file:":[null,"공유 화일:"],"Incoming file:":[null,"도착하는 화일:"],"Quit from Spreed Speak Freely?":[null,"Spreed Speak Freely사용을 중지 하시겠습니까?"],"Restart required to apply updates. Click ok to restart now.":[null,"업데이트를 적용하려면 재시작이 필요 합니다. 지금 재시작 하려면 ok를 클릭 하십시오"],"Failed to access camera/microphone.":[null,"카메라/마이크 사용 실패"],"Failed to establish peer connection.":[null,"상대연결 설정이 실패 하였습니다"],"We are sorry but something went wrong. Boo boo.":[null,"죄송합니다만 현재 문제가 있습니다."],"Oops":[null,"이런"],"Peer connection failed. Check your settings.":[null,"상대연결이 실패 했습니다. 설정을 확인 하십시오"],"User hung up because of error.":[null,"오류로 인해 사용자 끊어짐"]," is busy. Try again later.":[null,"통화중. 다시 시도 하세요."]," rejected your call.":[null,"전화가 거부 되었습니다."]," does not pick up.":[null,"전화를 받지 않습니다."]," tried to call you.":[null,"연결을 시도 중입니다"]," called you.":[null,"전화 드렸습니다."],"Your browser does not support WebRTC. No calls possible.":[null,"브라우저가 WebRTC를 지원하지 않습니다. 전화걸기가 불가능 합니다."],"Chat with":[null,"대화하기"],"Message from ":[null,"로 부터 메시지"],"You are now in room %s ...":[null,"당신은 현재 방%s ...에 있습니다"],"Your browser does not support file transfer.":[null,"당신의 브라우저가 회일전송을 지원하지 않습니다."],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":[null,"화면공유가 거절되었습니다. 사용하시는 브라우저에서 화면공유를 가능하도록 하여 주십시오. chrome://flags/#enable-usermedia-screen-capture를 복사하여 브라우저에서 수행하시고 상단의 프래그를 가능으로 변경 하십시오. 브라우저를 다시 수행시키면 사용하실수 있습니다."],"Use browser language":[null,"브라우저 언어 사용"],"Meet with me here:":[null,"나를 여기서 만납니다:"],"Error":[null,"오류"],"Hint":[null,"도움말"],"Please confirm":[null,"확인하십시오"],"More information required":[null,"더 많은 정보가 필요함"],"Ok":[null,"오케이"],"Close":[null,"닫음"],"Access code required":[null,"접속코드 필요함"],"Access denied":[null,"접속 거부"],"Please provide a valid access code.":[null,"유효한 접속코드가 필요합니다."],"Failed to verify access code. Check your Internet connection and try again.":[null,"접속코드 확인이 실패 했습니다. 인터넷 연결을 확인하고 다시 시도해 주십시오. "],"and %s":[null,"그리고 %2$s"],"and %d others":[null,""],"User":[null,"사용자"],"Someone":[null,"어떤 사람"],"Me":[null,"나"]}}} \ No newline at end of file +{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=1; plural=0"},"Share your screen":[null,"화면 공유하기"],"Chat":[null,"대화"],"Mute microphone":[null,"음성제거"],"Turn camera off":[null,"카메라꺼짐"],"Settings":[null,"설정"],"Your audio level":[null,"음성크기"],"Standard view":[null,""],"Kiosk view":[null,""],"Start chat":[null,"대화시작"],"Start video call":[null,"화상회의 시작"],"Start audio conference":[null,"음성회의 시작"],"No other users online":[null,"온라인에 다른 대화상대 없음"],"Chat sessions":[null,"대화 세션"],"Room chat":[null,"대화 방"],"Peer to peer":[null,"일대일"],"Close chat":[null,"대화 종료"],"is typing...":[null,"입력중"],"has stopped typing...":[null,"입력 종료"],"Type here to chat...":[null,"대화 입력"],"Send":[null,"전송"],"File sharing":[null,"회일 공유"],"File is no longer available":[null,"화일이 유효하지 않습니다"],"Download":[null,"다운로드"],"Open":[null,"열기"],"Cancel":[null,"취소"],"Unshare":[null,"비공유"],"Retry":[null,"재시도"],"Download failed.":[null,"다운로드실패"],"Change room":[null,"방 변경"],"Room":[null,"방"],"Main":[null,"메인"],"Leave room":[null,"방 이동"],"Current room":[null,"현재 방"],"Screen sharing options":[null,"화면 공유 옵션"],"Fit screen.":[null,"화면에 맟춤"],"Your picture":[null,"사용자 사진"],"Take picture":[null,"사진 찍음"],"Waiting for camera":[null,"카메라 대기중"],"Your name":[null,"사용자 이름"],"Name":[null,"이름"],"Your picture and name are visible to others.":[null,"사용자의 사진과 이름이 다른사람에게 보일수 있습니다."],"Your ID":[null,""],"Register":[null,""],"Only register an ID if this is your private browser.":[null,""],"Log out":[null,""],"Microphone":[null,"마이크"],"Camera":[null,"카메라"],"Video quality":[null,"영상 수준"],"Low":[null,"낮음"],"High":[null,"높음"],"HD":[null,"고화질"],"Language":[null,"언어"],"Language changes become active on reload.":[null,"언어 변경이 재로드 되고 있습니다"],"Default room":[null,"기본 방"],"Set alternative room to join at start.":[null,"시작시에 다른 방에 합류하도록 설정 되었습니다"],"Desktop notification":[null,"데스크탑에 통보"],"Enable":[null,"활성화"],"Denied - check your browser settings":[null,"거부됨 - 브라우저 설정을 확인하세요"],"Allowed":[null,"허락됨"],"Advanced settings":[null,"고급 설정"],"Stereo audio":[null,"스테레오 음성"],"Max video frame rate":[null,"비디오프레임 비율 최대화"],"auto":[null,"자동"],"Experimental settings":[null,"실험 설정"],"Show advanced settings":[null,"고급 설정 보기"],"Hide advanced settings":[null,"고급 설정 감추기"],"Remember settings":[null,"설정 기억"],"Your ID will still be kept - press the log out button above to delete the ID.":[null,""],"Close":[null,"닫음"],"Share by Email":[null,"이메일로 공유"],"Share on Facebook":[null,"Facebook에서 공유"],"Share on Twitter":[null,"Twitter에서 공유"],"Share on Google Plus":[null,"구글 플러스에서 공유"],"Share on XING":[null,"Xing에서 공유"],"Initializing":[null,"초기화"],"Online":[null,"온라인"],"Calling":[null,"전화걸기"],"Hangup":[null,"전화끊기"],"In call with":[null,"전화중"],"Conference with":[null,"회의중"],"Your are offline":[null,"오프라인 입니다"],"Go online":[null,"온라인에 연결합니다"],"Connection interrupted":[null,"연결이 중단"],"An error occured":[null,"에러 발생"],"Incoming call":[null,"전화 걸려옴"],"from":[null,"부터"],"Accept call":[null,"전화 받음"],"Reject":[null,"거부"],"Waiting for camera/microphone access":[null,"카메라/마이크 사용을 기다림"],"Please wait":[null,"기다리세요"],"Checking camera and microphone access.":[null,"카메라와 마이크의 사용을 확인 하세요"],"Please allow access to your camera and microphone.":[null,"카메라와 마이크의 사용을 허용 하세요"],"Camera / microphone access required.":[null,"카메라/마이크 사용이 필요합니다"],"Please check your browser settings and allow camera and microphone access for this site.":[null,"이 사이트에 대하여 브라우저의 설정을 확인하고 카메라와 마이크의 사용을 허용 하세요"],"Skip check":[null,"확인 넘어가기"],"Click here for help (Google Chrome).":[null,"도움말을 원하면 여기를 클릭 하세요 (구글 크롬)"],"Please set your user details and settings.":[null,"사용자의 세부상세와 설정을 지정하세요 "],"Please note that some settings require you to reload or to make a new call to become effective.":[null,"일부 설정은 다시로드 하거나 새로운 전화가 연결시에 유효할수 있습니다."],"Create your room":[null,"새로운 방 만들기"],"This is your room link:":[null,"당신의 방 링크:"],"Creating room link ...":[null,"방 링크 만들기..."],"Start":[null,"시작"],"Just click start":[null,"시작하기 클릭"],"Share this URL with the people you want to meet.":[null,"이 URL을 만나고 싶은 사람과 공유하기"],"You can use and re-use this room as many times as you want.":[null,"당신은 이 방을 원하는 횟수 만큼 사용할 수 있습니다"],"Peer to peer chat active.":[null,"일대일 대화 활성화"],"Peer to peer chat is now off.":[null,"일대일 대화 꺼짐"]," is now offline.":[null,"현재 오프라인 상태"]," is now online.":[null,"현재 온라인 상태"],"You share file:":[null,"공유 화일:"],"Incoming file:":[null,"도착하는 화일:"],"Quit from Spreed Speak Freely?":[null,"Spreed Speak Freely사용을 중지 하시겠습니까?"],"Restart required to apply updates. Click ok to restart now.":[null,"업데이트를 적용하려면 재시작이 필요 합니다. 지금 재시작 하려면 ok를 클릭 하십시오"],"Failed to access camera/microphone.":[null,"카메라/마이크 사용 실패"],"Failed to establish peer connection.":[null,"상대연결 설정이 실패 하였습니다"],"We are sorry but something went wrong. Boo boo.":[null,"죄송합니다만 현재 문제가 있습니다."],"Oops":[null,"이런"],"Peer connection failed. Check your settings.":[null,"상대연결이 실패 했습니다. 설정을 확인 하십시오"],"User hung up because of error.":[null,"오류로 인해 사용자 끊어짐"]," is busy. Try again later.":[null,"통화중. 다시 시도 하세요."]," rejected your call.":[null,"전화가 거부 되었습니다."]," does not pick up.":[null,"전화를 받지 않습니다."]," tried to call you.":[null,"연결을 시도 중입니다"]," called you.":[null,"전화 드렸습니다."],"Your browser does not support WebRTC. No calls possible.":[null,"브라우저가 WebRTC를 지원하지 않습니다. 전화걸기가 불가능 합니다."],"Chat with":[null,"대화하기"],"Message from ":[null,"로 부터 메시지"],"You are now in room %s ...":[null,"당신은 현재 방%s ...에 있습니다"],"Your browser does not support file transfer.":[null,"당신의 브라우저가 회일전송을 지원하지 않습니다."],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":[null,"화면공유가 거절되었습니다. 사용하시는 브라우저에서 화면공유를 가능하도록 하여 주십시오. chrome://flags/#enable-usermedia-screen-capture를 복사하여 브라우저에서 수행하시고 상단의 프래그를 가능으로 변경 하십시오. 브라우저를 다시 수행시키면 사용하실수 있습니다."],"Use browser language":[null,"브라우저 언어 사용"],"Meet with me here:":[null,"나를 여기서 만납니다:"],"Error":[null,"오류"],"Hint":[null,"도움말"],"Please confirm":[null,"확인하십시오"],"More information required":[null,"더 많은 정보가 필요함"],"Ok":[null,"오케이"],"Access code required":[null,"접속코드 필요함"],"Access denied":[null,"접속 거부"],"Please provide a valid access code.":[null,"유효한 접속코드가 필요합니다."],"Failed to verify access code. Check your Internet connection and try again.":[null,"접속코드 확인이 실패 했습니다. 인터넷 연결을 확인하고 다시 시도해 주십시오. "],"and %s":[null,"그리고 %2$s"],"and %d others":[null,""],"User":[null,"사용자"],"Someone":[null,"어떤 사람"],"Me":[null,"나"]}}} \ No newline at end of file diff --git a/static/translation/messages-zh-cn.json b/static/translation/messages-zh-cn.json index 1bfa978d..f85c8163 100644 --- a/static/translation/messages-zh-cn.json +++ b/static/translation/messages-zh-cn.json @@ -1 +1 @@ -{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=1; plural=0"},"Share your screen":[null,"共享您的屏幕"],"Chat":[null,"聊天"],"Mute microphone":[null,"关闭麦克风"],"Turn camera off":[null,"关闭摄像头"],"Settings":[null,"系统设置"],"Your audio level":[null,"您的通话音量"],"Start chat":[null,"开始聊天"],"Start video call":[null,"开始视频通话"],"Start audio conference":[null,"开始语音会议"],"No other users online":[null,"无其他联系人在线"],"Chat sessions":[null,"会话"],"Room chat":[null,"房间聊天"],"Peer to peer":[null,"P2P"],"Close chat":[null,"关闭聊天"],"is typing...":[null,"正在输入..."],"has stopped typing...":[null,"停止输入..."],"Type here to chat...":[null,"在此输入开始聊天..."],"Send":[null,"发送"],"File sharing":[null,"分享文件"],"File is no longer available":[null,"文件已不存在"],"Download":[null,"下载"],"Open":[null,"打开"],"Cancel":[null,"取消"],"Unshare":[null,"停止分享"],"Retry":[null,"重试"],"Download failed.":[null,"下载失败"],"Change room":[null,"更换房间"],"Room":[null,"房间"],"Main":[null,"主房间"],"Leave room":[null,"离开房间"],"Current room":[null,"當前房间"],"Fit screen.":[null,""],"Your picture":[null,"您的图片"],"Take picture":[null,"拍照"],"Waiting for camera":[null,"等待启动摄像头"],"Your name":[null,"您的名字"],"Name":[null,"名字"],"Your picture and name are visible to others.":[null,"别人能看到您的图片及名字"],"Microphone":[null,"麦克风"],"Camera":[null,"摄像头"],"Video quality":[null,"视频质量"],"Low":[null,"低"],"High":[null,"高"],"HD":[null,""],"Language":[null,"语言"],"Language changes become active on reload.":[null,"转换语言需重启程序"],"Default room":[null,"系统默认房间"],"Set alternative room to join at start.":[null,"重设初始默认房间"],"Desktop notification":[null,"桌面提醒"],"Enable":[null,"开启"],"Denied - check your browser settings":[null,"被拒绝--请检查浏览器设置"],"Allowed":[null,"启用"],"Advanced settings":[null,"高级设置"],"Stereo audio":[null,"立体声"],"Max video frame rate":[null,"最大视频帧速率"],"auto":[null,"自动"],"Experimental settings":[null,"实验设置"],"Show advanced settings":[null,"展开高级设置"],"Hide advanced settings":[null,"隐藏高级设置"],"Remember settings":[null,"记住设置"],"Apply":[null,"适用"],"Share by Email":[null,"电子邮件共享"],"Share on Facebook":[null,"Facebook共享"],"Share on Twitter":[null,"Twitter共享"],"Share on Google Plus":[null,"Google Plus共享"],"Share on XING":[null,"XING共享"],"Initializing":[null,"初始化"],"Online":[null,"在线"],"Calling":[null,"呼叫中"],"Hangup":[null,"挂断"],"In call with":[null,"正在和**通话"],"Conference with":[null,"和**会议通话"],"Your are offline":[null,"您不在线"],"Go online":[null,"上线"],"Connection interrupted":[null,"连接已中断"],"An error occured":[null,"出现错误"],"Incoming call":[null,"来电"],"from":[null,"来自"],"Accept call":[null,"接受通话"],"Reject":[null,"拒绝"],"Waiting for camera/microphone access":[null,"等待摄像头/麦克风连接"],"Please wait":[null,"请等候"],"Checking camera and microphone access.":[null,"正在检查摄像头及麦克风连接"],"Please allow access to your camera and microphone.":[null,"请允许连接您的摄像头及麦克风"],"Camera / microphone access required.":[null,"需连接摄像头/麦克风"],"Please check your browser settings and allow camera and microphone access for this site.":[null,"请检查浏览器设置并允许摄像头及麦克风连接此网站"],"Skip check":[null,"越过检查"],"Click here for help (Google Chrome).":[null,"点击这里获取帮助 (Google Chrome)"],"Please set your user details and settings.":[null,"请设定您的用户信息及设置"],"Please note that some settings require you to reload or to make a new call to become effective.":[null,"请注意,有些设置需要重新加载或开始新的通话后才能生效。"],"Create your room":[null,"创建您的房间"],"This is your room link:":[null,"这是您的房间链接"],"Creating room link ...":[null,"创建房间链接"],"Start":[null,"开始"],"Just click start":[null,"直接点击开始"],"Share this URL with the people you want to meet.":[null,"请与您需要联系的人分享此URL "],"You can use and re-use this room as many times as you want.":[null,"您可使用或反复多次使用此房间"],"Peer to peer chat active.":[null,"P2P聊天已启动"],"Peer to peer chat is now off.":[null,"P2P现在未启动"]," is now offline.":[null," 不在线"]," is now online.":[null," 现在在线"],"You share file:":[null,"分享文件:"],"Incoming file:":[null,"发来文件:"],"Quit from Spreed Speak Freely?":[null,"退出 Spreed Speak Freely?"],"Restart required to apply updates. Click ok to restart now.":[null,"适用更新需重启,现在点击Ok重新启动。"],"Failed to access camera/microphone.":[null,"摄像头/麦克风连接失败"],"Failed to establish peer connection.":[null,"对等连接建立失败"],"We are sorry but something went wrong. Boo boo.":[null,"很抱歉,有错误发生。"],"Oops":[null,"Oops"],"Peer connection failed. Check your settings.":[null,"对等连接失败,请检查设置。"],"User hung up because of error.":[null,"用户因错误挂断"]," is busy. Try again later.":[null," 正在通话,请稍后再试。"]," rejected your call.":[null," 拒绝了您的呼叫。"]," does not pick up.":[null," 不接听呼叫。"]," tried to call you.":[null," 曾呼叫您。"]," called you.":[null," 曾与您通话。"],"Your browser does not support WebRTC. No calls possible.":[null,"您的浏览器不支持WebRTC。不能进行通话。"],"Chat with":[null,"与**聊天"],"Message from ":[null,"来自于**的信息"],"You are now in room %s ...":[null,"您在 %s 房间"],"Your browser does not support file transfer.":[null,"您的浏览器不支持文件传输"],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":[null,""],"Use browser language":[null,"使用浏览器语言"],"Meet with me here:":[null,"我们这里见:"],"Error":[null,"错误"],"Hint":[null,"提示"],"Please confirm":[null,"请确认"],"More information required":[null,"需要更多信息"],"Ok":[null,"Ok"],"Close":[null,"关闭"],"Access code required":[null,"需要接入码"],"Access denied":[null,"连接被拒绝"],"Please provide a valid access code.":[null,"请提供有效接入码"],"Failed to verify access code. Check your Internet connection and try again.":[null,"接入码认证失败。请检查您的网络连接并重试。"],"and %s":[null,""],"and %d others":[null,""],"User":[null,""],"Someone":[null,""]}}} \ No newline at end of file +{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=1; plural=0"},"Share your screen":[null,"共享您的屏幕"],"Chat":[null,"聊天"],"Mute microphone":[null,"关闭麦克风"],"Turn camera off":[null,"关闭摄像头"],"Settings":[null,"系统设置"],"Your audio level":[null,"您的通话音量"],"Standard view":[null,""],"Kiosk view":[null,""],"Start chat":[null,"开始聊天"],"Start video call":[null,"开始视频通话"],"Start audio conference":[null,"开始语音会议"],"No other users online":[null,"无其他联系人在线"],"Chat sessions":[null,"会话"],"Room chat":[null,"房间聊天"],"Peer to peer":[null,"P2P"],"Close chat":[null,"关闭聊天"],"is typing...":[null,"正在输入..."],"has stopped typing...":[null,"停止输入..."],"Type here to chat...":[null,"在此输入开始聊天..."],"Send":[null,"发送"],"File sharing":[null,"分享文件"],"File is no longer available":[null,"文件已不存在"],"Download":[null,"下载"],"Open":[null,"打开"],"Cancel":[null,"取消"],"Unshare":[null,"停止分享"],"Retry":[null,"重试"],"Download failed.":[null,"下载失败"],"Change room":[null,"更换房间"],"Room":[null,"房间"],"Main":[null,"主房间"],"Leave room":[null,"离开房间"],"Current room":[null,"當前房间"],"Fit screen.":[null,""],"Your picture":[null,"您的图片"],"Take picture":[null,"拍照"],"Waiting for camera":[null,"等待启动摄像头"],"Your name":[null,"您的名字"],"Name":[null,"名字"],"Your picture and name are visible to others.":[null,"别人能看到您的图片及名字"],"Your ID":[null,""],"Register":[null,""],"Only register an ID if this is your private browser.":[null,""],"Log out":[null,""],"Microphone":[null,"麦克风"],"Camera":[null,"摄像头"],"Video quality":[null,"视频质量"],"Low":[null,"低"],"High":[null,"高"],"HD":[null,""],"Language":[null,"语言"],"Language changes become active on reload.":[null,"转换语言需重启程序"],"Default room":[null,"系统默认房间"],"Set alternative room to join at start.":[null,"重设初始默认房间"],"Desktop notification":[null,"桌面提醒"],"Enable":[null,"开启"],"Denied - check your browser settings":[null,"被拒绝--请检查浏览器设置"],"Allowed":[null,"启用"],"Advanced settings":[null,"高级设置"],"Stereo audio":[null,"立体声"],"Max video frame rate":[null,"最大视频帧速率"],"auto":[null,"自动"],"Experimental settings":[null,"实验设置"],"Show advanced settings":[null,"展开高级设置"],"Hide advanced settings":[null,"隐藏高级设置"],"Remember settings":[null,"记住设置"],"Your ID will still be kept - press the log out button above to delete the ID.":[null,""],"Close":[null,"关闭"],"Share by Email":[null,"电子邮件共享"],"Share on Facebook":[null,"Facebook共享"],"Share on Twitter":[null,"Twitter共享"],"Share on Google Plus":[null,"Google Plus共享"],"Share on XING":[null,"XING共享"],"Initializing":[null,"初始化"],"Online":[null,"在线"],"Calling":[null,"呼叫中"],"Hangup":[null,"挂断"],"In call with":[null,"正在和**通话"],"Conference with":[null,"和**会议通话"],"Your are offline":[null,"您不在线"],"Go online":[null,"上线"],"Connection interrupted":[null,"连接已中断"],"An error occured":[null,"出现错误"],"Incoming call":[null,"来电"],"from":[null,"来自"],"Accept call":[null,"接受通话"],"Reject":[null,"拒绝"],"Waiting for camera/microphone access":[null,"等待摄像头/麦克风连接"],"Please wait":[null,"请等候"],"Checking camera and microphone access.":[null,"正在检查摄像头及麦克风连接"],"Please allow access to your camera and microphone.":[null,"请允许连接您的摄像头及麦克风"],"Camera / microphone access required.":[null,"需连接摄像头/麦克风"],"Please check your browser settings and allow camera and microphone access for this site.":[null,"请检查浏览器设置并允许摄像头及麦克风连接此网站"],"Skip check":[null,"越过检查"],"Click here for help (Google Chrome).":[null,"点击这里获取帮助 (Google Chrome)"],"Please set your user details and settings.":[null,"请设定您的用户信息及设置"],"Please note that some settings require you to reload or to make a new call to become effective.":[null,"请注意,有些设置需要重新加载或开始新的通话后才能生效。"],"Create your room":[null,"创建您的房间"],"This is your room link:":[null,"这是您的房间链接"],"Creating room link ...":[null,"创建房间链接"],"Start":[null,"开始"],"Just click start":[null,"直接点击开始"],"Share this URL with the people you want to meet.":[null,"请与您需要联系的人分享此URL "],"You can use and re-use this room as many times as you want.":[null,"您可使用或反复多次使用此房间"],"Peer to peer chat active.":[null,"P2P聊天已启动"],"Peer to peer chat is now off.":[null,"P2P现在未启动"]," is now offline.":[null," 不在线"]," is now online.":[null," 现在在线"],"You share file:":[null,"分享文件:"],"Incoming file:":[null,"发来文件:"],"Quit from Spreed Speak Freely?":[null,"退出 Spreed Speak Freely?"],"Restart required to apply updates. Click ok to restart now.":[null,"适用更新需重启,现在点击Ok重新启动。"],"Failed to access camera/microphone.":[null,"摄像头/麦克风连接失败"],"Failed to establish peer connection.":[null,"对等连接建立失败"],"We are sorry but something went wrong. Boo boo.":[null,"很抱歉,有错误发生。"],"Oops":[null,"Oops"],"Peer connection failed. Check your settings.":[null,"对等连接失败,请检查设置。"],"User hung up because of error.":[null,"用户因错误挂断"]," is busy. Try again later.":[null," 正在通话,请稍后再试。"]," rejected your call.":[null," 拒绝了您的呼叫。"]," does not pick up.":[null," 不接听呼叫。"]," tried to call you.":[null," 曾呼叫您。"]," called you.":[null," 曾与您通话。"],"Your browser does not support WebRTC. No calls possible.":[null,"您的浏览器不支持WebRTC。不能进行通话。"],"Chat with":[null,"与**聊天"],"Message from ":[null,"来自于**的信息"],"You are now in room %s ...":[null,"您在 %s 房间"],"Your browser does not support file transfer.":[null,"您的浏览器不支持文件传输"],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":[null,""],"Use browser language":[null,"使用浏览器语言"],"Meet with me here:":[null,"我们这里见:"],"Error":[null,"错误"],"Hint":[null,"提示"],"Please confirm":[null,"请确认"],"More information required":[null,"需要更多信息"],"Ok":[null,"Ok"],"Access code required":[null,"需要接入码"],"Access denied":[null,"连接被拒绝"],"Please provide a valid access code.":[null,"请提供有效接入码"],"Failed to verify access code. Check your Internet connection and try again.":[null,"接入码认证失败。请检查您的网络连接并重试。"],"and %s":[null,""],"and %d others":[null,""],"User":[null,""],"Someone":[null,""]}}} \ No newline at end of file diff --git a/static/translation/messages-zh-tw.json b/static/translation/messages-zh-tw.json index 36d67beb..5803c38d 100644 --- a/static/translation/messages-zh-tw.json +++ b/static/translation/messages-zh-tw.json @@ -1 +1 @@ -{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=1; plural=0"},"Share your screen":[null,"共享您的屏幕"],"Chat":[null,"聊天"],"Mute microphone":[null,"關閉麥克風"],"Turn camera off":[null,"關閉攝像頭"],"Settings":[null,"系統設置"],"Your audio level":[null,"您的通話音量"],"Start chat":[null,"開始聊天"],"Start video call":[null,"開始視頻通話"],"Start audio conference":[null,"開始語音會議"],"No other users online":[null,"無其他聯繫人在線"],"Chat sessions":[null,"會話"],"Room chat":[null,"房間聊天"],"Peer to peer":[null,"P2P"],"Close chat":[null,"關閉聊天"],"is typing...":[null,"正在輸入..."],"has stopped typing...":[null,"停止輸入..."],"Type here to chat...":[null,"在此輸入開始聊天..."],"Send":[null,"發送"],"File sharing":[null,"分享文件"],"File is no longer available":[null,"文件已不存在"],"Download":[null,"下載"],"Open":[null,"打開"],"Cancel":[null,"取消"],"Unshare":[null,"停止分享"],"Retry":[null,"重試"],"Download failed.":[null,"下載失敗"],"Change room":[null,"更換房間"],"Room":[null,"房間"],"Main":[null,"住房間"],"Leave room":[null,"離開房間"],"Current room":[null,"當前房間"],"Fit screen.":[null,""],"Your picture":[null,"您的圖片"],"Take picture":[null,"拍照"],"Waiting for camera":[null,"等待啟動攝像頭"],"Your name":[null,"您的名字"],"Name":[null,"名字"],"Your picture and name are visible to others.":[null,"別人能看到您的圖片及名字"],"Microphone":[null,"麥克風"],"Camera":[null,"攝像頭"],"Video quality":[null,"視頻質量"],"Low":[null,"低"],"High":[null,"高"],"HD":[null,""],"Language":[null,"語言"],"Language changes become active on reload.":[null,"轉換語言需要重啟程序"],"Default room":[null,"系統默認房間"],"Set alternative room to join at start.":[null,"重設初始默認房間"],"Desktop notification":[null,"桌面提醒"],"Enable":[null,"開啟"],"Denied - check your browser settings":[null,"被拒絕﹣請檢查瀏覽器設置"],"Allowed":[null,"啟用"],"Advanced settings":[null,"高級設置"],"Stereo audio":[null,"立體聲"],"Max video frame rate":[null,"最大視頻幀速率"],"auto":[null,"自動"],"Experimental settings":[null,"試驗設置"],"Show advanced settings":[null,"展開高級設置"],"Hide advanced settings":[null,"隐藏高级设置"],"Remember settings":[null,"記住設置"],"Apply":[null,"適用"],"Share by Email":[null,"電子郵件共享"],"Share on Facebook":[null,"Facebook共享"],"Share on Twitter":[null,"Twitter共享"],"Share on Google Plus":[null,"Google Plus共享"],"Share on XING":[null,"XING共享"],"Initializing":[null,"初始化"],"Online":[null,"在線"],"Calling":[null,"呼叫中"],"Hangup":[null,"掛斷"],"In call with":[null,"正在和**通電話"],"Conference with":[null,"和**會議通話"],"Your are offline":[null,"您不在線"],"Go online":[null,"上線"],"Connection interrupted":[null,"連接已終端"],"An error occured":[null,"出現錯誤"],"Incoming call":[null,"來電"],"from":[null,"來自"],"Accept call":[null,"接受通話"],"Reject":[null,"拒絕"],"Waiting for camera/microphone access":[null,"等待攝像頭/麥克風連接"],"Please wait":[null,"請等候"],"Checking camera and microphone access.":[null,"正在檢查攝像頭及麥克風連接"],"Please allow access to your camera and microphone.":[null,"請允許連接您的攝像頭及麥克風"],"Camera / microphone access required.":[null,"需連接攝像頭/麥克風"],"Please check your browser settings and allow camera and microphone access for this site.":[null,"請檢查瀏覽器設置並允許攝像頭及麥克風連接此網站"],"Skip check":[null,"越过检查"],"Click here for help (Google Chrome).":[null,"點擊這裡獲取幫助 (Google Chrome)"],"Please set your user details and settings.":[null,"請設定您的用戶信息及設置"],"Please note that some settings require you to reload or to make a new call to become effective.":[null,"請注意,有些設置需要重新加載或開始新的通話後才能生效。"],"Create your room":[null,"穿件您的房间"],"This is your room link:":[null,"這是您的房間鏈接:"],"Creating room link ...":[null,"創建房間連接 ..."],"Start":[null,"開始"],"Just click start":[null,"直接點擊開始"],"Share this URL with the people you want to meet.":[null,"請與您需要聯繫的人分享此URL"],"You can use and re-use this room as many times as you want.":[null,"您可使用或反復多次使用此房間"],"Peer to peer chat active.":[null,"P2P聊天啟動"],"Peer to peer chat is now off.":[null,"P2P現在未啟動"]," is now offline.":[null," 不在線"]," is now online.":[null," 現在在線"],"You share file:":[null,"分享文件:"],"Incoming file:":[null,"發來文件:"],"Quit from Spreed Speak Freely?":[null,"退出 Spreed Speak Freely?"],"Restart required to apply updates. Click ok to restart now.":[null,"適用更新需重啟,現在點擊Ok重新啟動。"],"Failed to access camera/microphone.":[null,"攝像頭/麥克風連接失敗"],"Failed to establish peer connection.":[null,"對等連接建立失敗"],"We are sorry but something went wrong. Boo boo.":[null,"很抱歉,有序哦嗚發生......"],"Oops":[null,"Oops"],"Peer connection failed. Check your settings.":[null,"對等連接失敗,請檢查設置。"],"User hung up because of error.":[null,"用戶因錯誤掛斷"]," is busy. Try again later.":[null," 正在通話,請您稍後。"]," rejected your call.":[null," 拒絕了您的呼叫"]," does not pick up.":[null," 不接聽呼叫"]," tried to call you.":[null," 曾呼叫您"]," called you.":[null," 曾與您通話"],"Your browser does not support WebRTC. No calls possible.":[null,"您的遊覽器不支持WebRTC。不能進行通話。"],"Chat with":[null,"于**聊天"],"Message from ":[null,"來自於**的信息"],"You are now in room %s ...":[null,"您在 %s 房間"],"Your browser does not support file transfer.":[null,"您的遊覽器不支持文件傳輸"],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":[null,""],"Use browser language":[null,"使用瀏覽器語言"],"Meet with me here:":[null,"我們這裡見:"],"Error":[null,"錯誤"],"Hint":[null,"提示"],"Please confirm":[null,"請確認"],"More information required":[null,"需要更多信息"],"Ok":[null,"Ok"],"Close":[null,"關閉"],"Access code required":[null,"需要接入碼"],"Access denied":[null,"連接被拒絕"],"Please provide a valid access code.":[null,"請提供有效接入碼"],"Failed to verify access code. Check your Internet connection and try again.":[null,"接入碼認證錯誤。請檢查您的網絡連接并重試。"],"and %s":[null,""],"and %d others":[null,""],"User":[null,""],"Someone":[null,""]}}} \ No newline at end of file +{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=1; plural=0"},"Share your screen":[null,"共享您的屏幕"],"Chat":[null,"聊天"],"Mute microphone":[null,"關閉麥克風"],"Turn camera off":[null,"關閉攝像頭"],"Settings":[null,"系統設置"],"Your audio level":[null,"您的通話音量"],"Standard view":[null,""],"Kiosk view":[null,""],"Start chat":[null,"開始聊天"],"Start video call":[null,"開始視頻通話"],"Start audio conference":[null,"開始語音會議"],"No other users online":[null,"無其他聯繫人在線"],"Chat sessions":[null,"會話"],"Room chat":[null,"房間聊天"],"Peer to peer":[null,"P2P"],"Close chat":[null,"關閉聊天"],"is typing...":[null,"正在輸入..."],"has stopped typing...":[null,"停止輸入..."],"Type here to chat...":[null,"在此輸入開始聊天..."],"Send":[null,"發送"],"File sharing":[null,"分享文件"],"File is no longer available":[null,"文件已不存在"],"Download":[null,"下載"],"Open":[null,"打開"],"Cancel":[null,"取消"],"Unshare":[null,"停止分享"],"Retry":[null,"重試"],"Download failed.":[null,"下載失敗"],"Change room":[null,"更換房間"],"Room":[null,"房間"],"Main":[null,"住房間"],"Leave room":[null,"離開房間"],"Current room":[null,"當前房間"],"Fit screen.":[null,""],"Your picture":[null,"您的圖片"],"Take picture":[null,"拍照"],"Waiting for camera":[null,"等待啟動攝像頭"],"Your name":[null,"您的名字"],"Name":[null,"名字"],"Your picture and name are visible to others.":[null,"別人能看到您的圖片及名字"],"Your ID":[null,""],"Register":[null,""],"Only register an ID if this is your private browser.":[null,""],"Log out":[null,""],"Microphone":[null,"麥克風"],"Camera":[null,"攝像頭"],"Video quality":[null,"視頻質量"],"Low":[null,"低"],"High":[null,"高"],"HD":[null,""],"Language":[null,"語言"],"Language changes become active on reload.":[null,"轉換語言需要重啟程序"],"Default room":[null,"系統默認房間"],"Set alternative room to join at start.":[null,"重設初始默認房間"],"Desktop notification":[null,"桌面提醒"],"Enable":[null,"開啟"],"Denied - check your browser settings":[null,"被拒絕﹣請檢查瀏覽器設置"],"Allowed":[null,"啟用"],"Advanced settings":[null,"高級設置"],"Stereo audio":[null,"立體聲"],"Max video frame rate":[null,"最大視頻幀速率"],"auto":[null,"自動"],"Experimental settings":[null,"試驗設置"],"Show advanced settings":[null,"展開高級設置"],"Hide advanced settings":[null,"隐藏高级设置"],"Remember settings":[null,"記住設置"],"Your ID will still be kept - press the log out button above to delete the ID.":[null,""],"Close":[null,"關閉"],"Share by Email":[null,"電子郵件共享"],"Share on Facebook":[null,"Facebook共享"],"Share on Twitter":[null,"Twitter共享"],"Share on Google Plus":[null,"Google Plus共享"],"Share on XING":[null,"XING共享"],"Initializing":[null,"初始化"],"Online":[null,"在線"],"Calling":[null,"呼叫中"],"Hangup":[null,"掛斷"],"In call with":[null,"正在和**通電話"],"Conference with":[null,"和**會議通話"],"Your are offline":[null,"您不在線"],"Go online":[null,"上線"],"Connection interrupted":[null,"連接已終端"],"An error occured":[null,"出現錯誤"],"Incoming call":[null,"來電"],"from":[null,"來自"],"Accept call":[null,"接受通話"],"Reject":[null,"拒絕"],"Waiting for camera/microphone access":[null,"等待攝像頭/麥克風連接"],"Please wait":[null,"請等候"],"Checking camera and microphone access.":[null,"正在檢查攝像頭及麥克風連接"],"Please allow access to your camera and microphone.":[null,"請允許連接您的攝像頭及麥克風"],"Camera / microphone access required.":[null,"需連接攝像頭/麥克風"],"Please check your browser settings and allow camera and microphone access for this site.":[null,"請檢查瀏覽器設置並允許攝像頭及麥克風連接此網站"],"Skip check":[null,"越过检查"],"Click here for help (Google Chrome).":[null,"點擊這裡獲取幫助 (Google Chrome)"],"Please set your user details and settings.":[null,"請設定您的用戶信息及設置"],"Please note that some settings require you to reload or to make a new call to become effective.":[null,"請注意,有些設置需要重新加載或開始新的通話後才能生效。"],"Create your room":[null,"穿件您的房间"],"This is your room link:":[null,"這是您的房間鏈接:"],"Creating room link ...":[null,"創建房間連接 ..."],"Start":[null,"開始"],"Just click start":[null,"直接點擊開始"],"Share this URL with the people you want to meet.":[null,"請與您需要聯繫的人分享此URL"],"You can use and re-use this room as many times as you want.":[null,"您可使用或反復多次使用此房間"],"Peer to peer chat active.":[null,"P2P聊天啟動"],"Peer to peer chat is now off.":[null,"P2P現在未啟動"]," is now offline.":[null," 不在線"]," is now online.":[null," 現在在線"],"You share file:":[null,"分享文件:"],"Incoming file:":[null,"發來文件:"],"Quit from Spreed Speak Freely?":[null,"退出 Spreed Speak Freely?"],"Restart required to apply updates. Click ok to restart now.":[null,"適用更新需重啟,現在點擊Ok重新啟動。"],"Failed to access camera/microphone.":[null,"攝像頭/麥克風連接失敗"],"Failed to establish peer connection.":[null,"對等連接建立失敗"],"We are sorry but something went wrong. Boo boo.":[null,"很抱歉,有序哦嗚發生......"],"Oops":[null,"Oops"],"Peer connection failed. Check your settings.":[null,"對等連接失敗,請檢查設置。"],"User hung up because of error.":[null,"用戶因錯誤掛斷"]," is busy. Try again later.":[null," 正在通話,請您稍後。"]," rejected your call.":[null," 拒絕了您的呼叫"]," does not pick up.":[null," 不接聽呼叫"]," tried to call you.":[null," 曾呼叫您"]," called you.":[null," 曾與您通話"],"Your browser does not support WebRTC. No calls possible.":[null,"您的遊覽器不支持WebRTC。不能進行通話。"],"Chat with":[null,"于**聊天"],"Message from ":[null,"來自於**的信息"],"You are now in room %s ...":[null,"您在 %s 房間"],"Your browser does not support file transfer.":[null,"您的遊覽器不支持文件傳輸"],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":[null,""],"Use browser language":[null,"使用瀏覽器語言"],"Meet with me here:":[null,"我們這裡見:"],"Error":[null,"錯誤"],"Hint":[null,"提示"],"Please confirm":[null,"請確認"],"More information required":[null,"需要更多信息"],"Ok":[null,"Ok"],"Access code required":[null,"需要接入碼"],"Access denied":[null,"連接被拒絕"],"Please provide a valid access code.":[null,"請提供有效接入碼"],"Failed to verify access code. Check your Internet connection and try again.":[null,"接入碼認證錯誤。請檢查您的網絡連接并重試。"],"and %s":[null,""],"and %d others":[null,""],"User":[null,""],"Someone":[null,""]}}} \ No newline at end of file From 9a1b3c9548e00a88c963c83132fd9fa4e5af69d6 Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Mon, 28 Apr 2014 00:10:38 +0200 Subject: [PATCH 23/48] Only show logout and warning if there is something stored. --- static/js/controllers/mediastreamcontroller.js | 1 + static/js/directives/settings.js | 2 ++ static/partials/settings.html | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/static/js/controllers/mediastreamcontroller.js b/static/js/controllers/mediastreamcontroller.js index f90dd9f4..277ab654 100644 --- a/static/js/controllers/mediastreamcontroller.js +++ b/static/js/controllers/mediastreamcontroller.js @@ -394,6 +394,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function // Check if we can load a user. var login = mediaStream.users.load(); if (login !== null) { + $scope.loadedUserlogin = true; console.log("Trying to authorize with stored credentials ..."); mediaStream.users.authorize(login, function(data) { console.info("Retrieved nonce - authenticating as user:", data.userid); diff --git a/static/js/directives/settings.js b/static/js/directives/settings.js index 92b2c504..3d8cb4b5 100644 --- a/static/js/directives/settings.js +++ b/static/js/directives/settings.js @@ -135,6 +135,8 @@ define(['underscore', 'text!partials/settings.html'], function(_, template) { if (data.nonce) { // If the server provided us a nonce, we can do everthing on our own. mediaStream.users.store(data); + $scope.loadedUserlogin = true; + safeApply($scope); // Directly authenticate ourselves with the provided nonce. mediaStream.api.requestAuthentication(data.userid, data.nonce); delete data.nonce; diff --git a/static/partials/settings.html b/static/partials/settings.html index a5691973..05f7f55c 100644 --- a/static/partials/settings.html +++ b/static/partials/settings.html @@ -34,7 +34,7 @@ {{_('Only register an ID if this is your private browser.')}}
{{userid}}
-