diff --git a/Makefile b/Makefile
index 0141e825..107448bb 100644
--- a/Makefile
+++ b/Makefile
@@ -63,6 +63,10 @@ gopath:
get:
GOPATH=$(GOPATH) go get $(PKG)
+getupdate:
+ rm -rf vendor/*
+ GOPATH=$(GOPATH) go get $(PKG)
+
binary:
GOPATH=$(GOPATH) go build -o $(OUTPUT)/$(EXENAME) -ldflags '$(LDFLAGS)' $(PKG)
@@ -167,4 +171,4 @@ tarball: distclean release install
echo -n $(VERSION) > $(TARPATH)/version.txt
tar czf $(DIST)/$(PACKAGE_NAME).tar.gz -C $(DIST) $(PACKAGE_NAME)
-.PHONY: clean distclean pristine get build styles javascript release releasetest dist_gopath install gopath binary binaryrace binaryall tarball assets
+.PHONY: clean distclean pristine get getupdate build styles javascript release releasetest dist_gopath install gopath binary binaryrace binaryall tarball assets
diff --git a/doc/CHANNELING-API.txt b/doc/CHANNELING-API.txt
index 64f193cf..b8c027fe 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,8 @@ Special purpose documents for channling
{
"Type": "Self",
"Id": "4",
+ "Sid": "5157",
+ "Userid": "",
"Token": "some-very-long-string",
"Version": "server-version-number",
"Turn": {
@@ -107,7 +109,9 @@ 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.
Version: Server version number. Use this to detect server upgrades.
@@ -253,15 +257,13 @@ 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
{
"Type": "Left",
- "Id": "5",
- "Ua": "",
- "Status": null
+ "Id": "5"
}
Joined
@@ -269,10 +271,13 @@ Additional types for user 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
{
@@ -291,6 +296,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)
{
@@ -309,26 +318,30 @@ Additional types for user 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
{
@@ -346,6 +359,32 @@ Additional types for user 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
@@ -531,7 +570,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 +584,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 +636,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
diff --git a/doc/REST-API.txt b/doc/REST-API.txt
new file mode 100644
index 00000000..9a9be2a8
--- /dev/null
+++ b/doc/REST-API.txt
@@ -0,0 +1,159 @@
+
+ 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.
+
+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:
+
+ /api/v1/config
+
+ The config end points returns the server configuration. As it is available
+ to the Web client.
+
+ GET application/x-www-form-urlencoded
+ No parameters.
+ Response 200:
+ {
+ "Title": "Spreed WebRTC",
+ "S": "static/ver=1399302670",
+ "B": "/",
+ "Token": "i-did-not-change-the-public-token-boo",
+ "StunURIs": [],
+ "TurnURIs": [
+ "turn:myturnserver:443?transport=udp",
+ "turn:myturnserver:443?transport=tcp"
+ ],
+ "Tokens": false,
+ "Version": "0.17.5",
+ "UsersEnabled": true,
+ "UsersAllowRegistration": true,
+ "UsersMode": "certificate",
+ "Plugin": ""
+ }
+
+
+ /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 403, 413:
+ {
+ "success": false,
+ "code": "error-code",
+ "message": "error-message"
+ }
+
+
+ /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.
+
+ /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"
+ }
+ Response 200:
+ {
+ "success": true,
+ "userid": "user-id",
+ "useridcombo": "authorization-id",
+ "timestamp": 1430688014,
+ "secret": "authorization-secret-for-authorization-id",
+ "nonce": "authorization-nonce"
+ }
+ 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
+
+ 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
diff --git a/doc/plugin-test-authorize.js b/doc/plugin-test-authorize.js
new file mode 100644
index 00000000..039693f4
--- /dev/null
+++ b/doc/plugin-test-authorize.js
@@ -0,0 +1,108 @@
+/*
+ * 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 lastData = 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.testCreateSuseridLocal = 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);
+ var data = {
+ useridcombo: useridCombo,
+ secret: sjcl.codec.base64.fromBits(secret)
+ }
+ lastData = data;
+ return data;
+
+ };
+
+ $window.testCreateSuseridServer = function() {
+ mediaStream.users.register(null, function(data) {
+ console.log("Retrieved user", data);
+ lastData = data;
+ }, function() {
+ console.log("Register error", arguments);
+ });
+ };
+
+ $window.testAuthorize = function(data) {
+ console.log("Testing authorize with data", data);
+ mediaStream.users.authorize(data, function(data) {
+ lastNonce = data.nonce;
+ lastUserid = data.userid;
+ console.log("Retrieved nonce", lastNonce, lastUserid);
+ }, 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 (lastData === null) {
+ console.log("Run testCreateSuseridServer fist.");
+ return
+ }
+ $window.testAuthorize(lastData);
+ };
+
+ }]);
+
+ }
+
+ }
+
+});
\ No newline at end of file
diff --git a/server.conf.in b/server.conf.in
index 8c8388f9..4f87d273 100644
--- a/server.conf.in
+++ b/server.conf.in
@@ -1,35 +1,156 @@
-# 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
-#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
+; 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
+; 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
+; 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
+; 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 users authorization mode.
+; sharedsecret:
+; Validates the userid with a HMAC authentication secret.
+; The format goes like this:
+; BASE64(HMAC-SHA-256(secret, expirationTimestampInSeconds:userid))
+; httpheader:
+; The userid is provided as an HTTP header. The server does not do any
+; validation. This usually only makes sense with a front end HTTPS proxy which
+; does the authentication and injects the user id as HTTP header for sessions
+; REST requests. In mode httpheader, allowRegistration should be false.
+; certificate:
+; The userid is provided as CommonName with a certificate provided with TLS
+; client authentication. When you use this with a front end proxy for TLS
+; termination, that proxy has to validate the certificate and inject certain
+; headers into the proxy connection. In certificate mode the server can act as
+; a signing CA to sign incoming user certificate requests with a private key
+; when allowRegistration is true. While certificate mode offers the highest
+; security it is currently considered experimental and the user experience
+; varies between browsers and platforms.
+;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
+; The HTTP header name where to find the userid in "httpheader" mode.
+;httpheader_header = x-userid
+; Full path to PEM encoded private key to use for user creation in "certificate"
+; mode. Keep this commented if you do not want the server to sign certificate
+; requests.
+;certificate_key = userskey.key
+; Full path to PEM encoded certificate to use for user validation in
+; "certificate" mode. When allowRegistration is true and certificate_key is also
+; set then the server will act as a CA and sign incoming user registrations and
+; return certificates to users as registration.
+;certificate_certificate = usersca.crt
+; The HTTP header name where to find if the TLS client authentication was
+; successfull. The value of this header is matched to
+; certificate_verifiedHeaderValue and only if there is a match, the proxy
+; handled TLS client authentication is accepted as success. Make sure to secure
+; these headers with your front end proxy (always set them). Do not use these
+; settings when not using a front end proxy.
+;certificate_verifiedHeader = x-verified
+;certificate_verifiedHeaderValue = SUCCESS
+; The HTTP header name where to find the PEM encoded certificate authenticated
+; by a front end proxy. With Nginx the required value is in $ssl_client_cert.
+;certificate_certificateHeader = x-certificate
+; The valid duration of generated certificates created in certificate mode when
+; allowRegistration is enabled.
+;certificate_validForDays = 365
+; Organization to set into the created user certificates. Use a readable public
+; name to make the certificate easily recognizable as certificate for your
+; server so users can choose the correct certificate when prompted.
+;certificate_organization= = My Spreed Server
+; If enabled the server can create new userids. Set allowRegistration to true to
+; enable userid creation/registration. Users are created according the settings
+; of the currently configured mode (see above).
+;allowRegistration = false
diff --git a/src/app/spreed-speakfreely-server/wsdata.go b/src/app/spreed-speakfreely-server/channeling.go
similarity index 75%
rename from src/app/spreed-speakfreely-server/wsdata.go
rename to src/app/spreed-speakfreely-server/channeling.go
index fe2691a8..9f153813 100644
--- a/src/app/spreed-speakfreely-server/wsdata.go
+++ b/src/app/spreed-speakfreely-server/channeling.go
@@ -48,6 +48,8 @@ type DataAnswer struct {
type DataSelf struct {
Type string
Id string
+ Sid string
+ Userid string
Token string
Version string
Turn *DataTurn
@@ -61,13 +63,14 @@ type DataTurn struct {
Urls []string `json:"urls"`
}
-type DataUser struct {
+type DataSession struct {
Type string
Id string
- Ua string
- Token string
- Version string
- Rev uint64
+ Userid string `json:"Userid,omitempty"`
+ Ua string `json:"Ua,omitempty"`
+ Token string `json:"Token,omitempty"`
+ Version string `json:"Version,omitempty"`
+ Rev uint64 `json:"Rev,omitempty"`
Status interface{}
}
@@ -102,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 {
@@ -120,9 +124,9 @@ type DataOutgoing struct {
To string
}
-type DataUsers struct {
+type DataSessions struct {
Type string
- Users []*DataUser
+ Users []*DataSession
Index uint64
Batch uint64
}
@@ -137,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/config.go b/src/app/spreed-speakfreely-server/config.go
index b5516a55..313d0603 100644
--- a/src/app/spreed-speakfreely-server/config.go
+++ b/src/app/spreed-speakfreely-server/config.go
@@ -23,23 +23,48 @@ package main
import (
"fmt"
+ "net/http"
)
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
+ UsersMode string // Users mode string
+ 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, usersMode, 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,
+ UsersMode: usersMode,
+ Plugin: plugin,
+ globalRoomid: globalRoomid,
+ defaultRoomEnabled: defaultRoomEnabled,
+ }
+}
+
+func (config *Config) Get(request *http.Request) (int, interface{}, http.Header) {
+ return 200, config, http.Header{"Content-Type": {"application/json; charset=utf-8"}}
}
diff --git a/src/app/spreed-speakfreely-server/connection.go b/src/app/spreed-speakfreely-server/connection.go
index cd667c73..d50a254f 100644
--- a/src/app/spreed-speakfreely-server/connection.go
+++ b/src/app/spreed-speakfreely-server/connection.go
@@ -27,6 +27,7 @@ import (
"github.com/gorilla/websocket"
"io"
"log"
+ "net/http"
"sync"
"time"
)
@@ -55,8 +56,9 @@ const (
type Connection struct {
// References.
- h *Hub
- ws *websocket.Conn
+ h *Hub
+ ws *websocket.Conn
+ request *http.Request
// Data handling.
condition *sync.Cond
@@ -66,21 +68,20 @@ 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
- RemoteAddr string
}
-func NewConnection(h *Hub, ws *websocket.Conn, remoteAddr string) *Connection {
+func NewConnection(h *Hub, ws *websocket.Conn, request *http.Request) *Connection {
c := &Connection{
- h: h,
- ws: ws,
- RemoteAddr: remoteAddr,
+ h: h,
+ ws: ws,
+ request: request,
}
c.condition = sync.NewCond(&c.mutex)
@@ -93,7 +94,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()
@@ -112,24 +113,18 @@ func (c *Connection) close() {
func (c *Connection) register() error {
- id, err := c.h.EncodeTicket("id", "")
- if err != nil {
- log.Println("Failed to create new Id while register", err)
- return err
- }
- c.Id = id
- //log.Println("Created new id", id)
- c.h.registerHandler(c)
+ s := c.h.CreateSession(c.request, nil)
+ c.h.registerHandler(c, s)
return nil
}
func (c *Connection) reregister(token string) error {
- if id, err := c.h.DecodeTicket("token", token); err == nil {
- c.Id = id
- c.h.registerHandler(c)
+ if st, err := c.h.DecodeSessionToken(token); err == nil {
+ s := c.h.CreateSession(c.request, st)
+ c.h.registerHandler(c, s)
} 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 0c865495..879f60db 100644
--- a/src/app/spreed-speakfreely-server/hub.go
+++ b/src/app/spreed-speakfreely-server/hub.go
@@ -28,9 +28,11 @@ import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
+ "errors"
"fmt"
"github.com/gorilla/securecookie"
"log"
+ "net/http"
"strings"
"sync"
"sync/atomic"
@@ -51,21 +53,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
@@ -78,23 +80,32 @@ type Hub struct {
broadcastChatMessages uint64
unicastChatMessages uint64
buddyImages ImageCache
+ realm string
+ tokenName string
+ useridRetriever func(*http.Request) (string, error)
}
-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),
- userTable: make(map[string]*User),
+ sessionTable: make(map[string]*Session),
roomTable: make(map[string]*RoomWorker),
version: version,
config: config,
sessionSecret: []byte(sessionSecret),
turnSecret: []byte(turnSecret),
+ realm: realm,
+ }
+
+ 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()
+ h.tokenName = fmt.Sprintf("token@%s", h.realm)
return h
}
@@ -105,7 +116,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 +124,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
@@ -155,21 +166,66 @@ func (h *Hub) CreateTurnData(id string) *DataTurn {
}
-func (h *Hub) EncodeTicket(key, value string) (string, error) {
+func (h *Hub) CreateSession(request *http.Request, 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?
- if value == "" {
- // Create new id.
- value = fmt.Sprintf("%s", securecookie.GenerateRandomKey(16))
+ var session *Session
+ var userid string
+ usersEnabled := h.config.UsersEnabled
+
+ if usersEnabled && h.useridRetriever != nil {
+ userid, _ = h.useridRetriever(request)
}
- return h.tickets.Encode(key, value)
+
+ if st == nil {
+ sid := NewRandomString(32)
+ id, _ := h.tickets.Encode("id", sid)
+ session = NewSession(id, sid, userid)
+ log.Println("Created new session id", len(id), id, sid, userid)
+ } else {
+ if userid == "" {
+ userid = st.Userid
+ }
+ if !usersEnabled {
+ userid = ""
+ }
+ session = NewSession(st.Id, st.Sid, userid)
+ }
+
+ return 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(h.tokenName, 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(h.tokenName, token, st)
+ return st, err
}
@@ -180,8 +236,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)
@@ -244,34 +300,33 @@ 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 user instance.
+ // Set flags.
h.count++
c.Idx = h.count
- u := &User{Id: c.Id}
- h.userTable[c.Id] = u
- c.User = u
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)
+ //log.Printf("Register (%d) from %s: %s (existing)\n", c.Idx, c.Id)
}
+ 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.Id)
+ h.server.OnRegister(c)
+
}
func (h *Hub) unregisterHandler(c *Connection) {
@@ -281,16 +336,16 @@ func (h *Hub) unregisterHandler(c *Connection) {
h.mutex.Unlock()
return
}
- user := c.User
- c.close()
+ session := c.Session
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)
+ c.close()
}
@@ -322,21 +377,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,8 +399,27 @@ 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
}
+
+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(h.realm, st)
+ if err != nil {
+ return "", err
+ }
+
+ return nonce, nil
+
+}
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/main.go b/src/app/spreed-speakfreely-server/main.go
index 5310362e..366aa0d4 100644
--- a/src/app/spreed-speakfreely-server/main.go
+++ b/src/app/spreed-speakfreely-server/main.go
@@ -263,6 +263,31 @@ func runner(runtime phoenix.Runtime) error {
defaultRoomEnabled = defaultRoomEnabledString == "true"
}
+ 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"
+ }
+
+ 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"
+ }
+
+ usersMode, _ := runtime.GetString("users", "mode")
+
// Create token provider.
var tokenProvider TokenProvider
if tokenFile != "" {
@@ -271,7 +296,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, usersMode, plugin)
// Load templates.
tt := template.New("")
@@ -295,8 +320,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 {
@@ -330,6 +358,12 @@ func runner(runtime phoenix.Runtime) error {
// Create router.
router := mux.NewRouter()
r := router.PathPrefix(basePath).Subrouter().StrictSlash(true)
+
+ // Prepare listeners.
+ runtime.DefaultHTTPHandler(r)
+ runtime.DefaultHTTPSHandler(r)
+
+ // Add handlers.
r.HandleFunc("/", httputils.MakeGzipHandler(mainHandler))
r.Handle("/static/img/buddy/{flags}/{imageid}/{idx:.*}", http.StripPrefix(basePath, makeImageHandler(hub, time.Duration(24)*time.Hour)))
r.Handle("/static/{path:.*}", http.StripPrefix(basePath, httputils.FileStaticServer(http.Dir(rootFolder))))
@@ -342,7 +376,16 @@ func runner(runtime phoenix.Runtime) error {
api := sleepy.NewAPI()
api.SetMux(r.PathPrefix("/api/v1/").Subrouter())
api.AddResource(&Rooms{}, "/rooms")
+ api.AddResource(config, "/config")
api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens")
+ if usersEnabled {
+ // Create Users handler.
+ users := NewUsers(hub, usersMode, serverRealm, runtime)
+ api.AddResource(&Sessions{hub: hub, users: users}, "/sessions/{id}/")
+ if usersAllowRegistration {
+ api.AddResource(users, "/users")
+ }
+ }
if statsEnabled {
api.AddResourceWithWrapper(&Stats{hub: hub}, httputils.MakeGzipHandler, "/stats")
log.Println("Stats are enabled!")
@@ -357,9 +400,6 @@ func runner(runtime phoenix.Runtime) error {
}
}
- runtime.DefaultHTTPHandler(r)
- runtime.DefaultHTTPSHandler(r)
-
return runtime.Start()
}
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..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,9 +34,9 @@ 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 := 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/roomworker.go b/src/app/spreed-speakfreely-server/roomworker.go
index c95da059..568546ea 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
+ sessions := &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,17 +164,17 @@ func (r *RoomWorker) usersHandler(c *Connection) {
break
}
}
- users.Users = ul
- 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()
}
@@ -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..cb949985 100644
--- a/src/app/spreed-speakfreely-server/server.go
+++ b/src/app/spreed-speakfreely-server/server.go
@@ -37,9 +37,19 @@ 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 {
+ 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,
+ Sid: c.Session.Sid,
+ 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)
}
@@ -49,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, &DataUser{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)
}
@@ -72,17 +82,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, 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, &DataUser{Type: "Joined", Id: c.Id, Ua: msg.Hello.Ua})
+ s.Broadcast(c, c.Session.DataSessionJoined())
} else {
c.Hello = false
}
@@ -99,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.UpdateUser(c, &UserUpdate{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, &DataUser{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.
@@ -170,10 +187,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)
}
@@ -209,9 +226,22 @@ func (s *Server) Users(c *Connection) {
}
+func (s *Server) Authenticate(c *Connection, st *SessionToken) bool {
+
+ err := c.Session.Authenticate(c.h.realm, 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.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/session.go b/src/app/spreed-speakfreely-server/session.go
new file mode 100644
index 00000000..889ae2f6
--- /dev/null
+++ b/src/app/spreed-speakfreely-server/session.go
@@ -0,0 +1,219 @@
+/*
+ * 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 (
+ "errors"
+ "fmt"
+ "github.com/gorilla/securecookie"
+ "sync"
+)
+
+var sessionNonces *securecookie.SecureCookie
+
+type Session struct {
+ Id string
+ Sid string
+ Userid string
+ Roomid string
+ Ua string
+ UpdateRev uint64
+ Status interface{}
+ Nonce string
+ 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()
+ defer s.mutex.Unlock()
+
+ for _, key := range update.Types {
+
+ //fmt.Println("type update", key)
+ switch key {
+ case "Roomid":
+ s.Roomid = update.Roomid
+ case "Ua":
+ s.Ua = update.Ua
+ case "Status":
+ s.Status = update.Status
+ }
+
+ }
+
+ s.UpdateRev++
+ return s.UpdateRev
+
+}
+
+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(realm string, 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(fmt.Sprintf("%s@%s", s.Sid, realm), st.Userid)
+
+ return s.Nonce, err
+
+}
+
+func (s *Session) Authenticate(realm string, 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(fmt.Sprintf("%s@%s", s.Sid, realm), 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 {
+ return &SessionToken{Id: s.Id, Sid: s.Sid, Userid: s.Userid}
+}
+
+func (s *Session) Data() *DataSession {
+
+ s.mutex.RLock()
+ defer s.mutex.RUnlock()
+
+ return &DataSession{
+ Id: s.Id,
+ Userid: s.Userid,
+ Ua: s.Ua,
+ Status: s.Status,
+ Rev: s.UpdateRev,
+ }
+
+}
+
+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
+ Roomid string
+ Ua string
+ Status interface{}
+}
+
+type SessionToken struct {
+ 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() {
+ // 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..c7ecca89
--- /dev/null
+++ b/src/app/spreed-speakfreely-server/sessions.go
@@ -0,0 +1,121 @@
+/*
+ * 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"
+ "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
+ users *Users
+}
+
+// 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 snr SessionNonceRequest
+ err := decoder.Decode(&snr)
+ if err != nil {
+ error = true
+ }
+
+ vars := mux.Vars(request)
+ id, ok := vars["id"]
+ if !ok {
+ error = true
+ }
+
+ // Make sure data matches request.
+ if id != snr.Id {
+ error = true
+ log.Println("Session patch failed - request id mismatch.")
+ }
+
+ // Make sure that we have a Sid.
+ if snr.Sid == "" {
+ error = true
+ 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
+ }
+
+ var userid string
+ // Validate with users handler.
+ if sessions.users.handler != nil {
+ userid, err = sessions.users.handler.Validate(&snr, request)
+ if err != nil {
+ error = true
+ log.Println("Session patch failed - users validation failed.", err)
+ }
+ // Make sure that we have a user.
+ if userid == "" {
+ error = true
+ log.Println("Session patch failed - userid empty.")
+ }
+ } else {
+ log.Println("Session patch failed - no handler.")
+ error = true
+ }
+
+ var nonce string
+ if !error {
+ // 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
+ }
+ }
+
+ if error {
+ return 403, NewApiError("session_patch_failed", "Failed to patch session"), 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/sleepy/core.go b/src/app/spreed-speakfreely-server/sleepy/core.go
index b5d726c7..465e497b 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,9 +146,28 @@ func (api *API) requestHandler(resource interface{}) http.HandlerFunc {
return
}
- code, data, header := handler(request.Form, request.Header)
+ code, data, header := handler(request)
+
+ var content []byte
+ var err error
+
+ switch data.(type) {
+ case string:
+ content = []byte(data.(string))
+ case []byte:
+ content = data.([]byte)
+ default:
+ // Encode JSON.
+ content, err = json.MarshalIndent(data, "", " ")
+ if err != nil {
+ if header == nil {
+ header = http.Header{"Content-Type": {"application/json"}}
+ } else if header.Get("Content-Type") == "" {
+ header.Set("Content-Type", "application/json")
+ }
+ }
+ }
- content, err := json.MarshalIndent(data, "", " ")
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
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/tls.go b/src/app/spreed-speakfreely-server/tls.go
new file mode 100644
index 00000000..92a082c4
--- /dev/null
+++ b/src/app/spreed-speakfreely-server/tls.go
@@ -0,0 +1,110 @@
+/*
+ * TLS helpers for Go based on crypto/tls package.
+ *
+ * Copyright (C) 2014 struktur AG. All rights reserved.
+ * Copyright 2011 The Go Authors. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+package main
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/rsa"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "io/ioutil"
+ "strings"
+)
+
+func loadX509PrivateKey(keyFile string) (privateKey crypto.PrivateKey, err error) {
+ keyPEMBlock, err := ioutil.ReadFile(keyFile)
+ if err != nil {
+ return
+ }
+ var keyDERBlock *pem.Block
+ for {
+ keyDERBlock, keyPEMBlock = pem.Decode(keyPEMBlock)
+ if keyDERBlock == nil {
+ err = errors.New("failed to parse key PEM data")
+ return
+ }
+ if keyDERBlock.Type == "PRIVATE KEY" || strings.HasSuffix(keyDERBlock.Type, " PRIVATE KEY") {
+ break
+ }
+ }
+ privateKey, err = parsePrivateKey(keyDERBlock.Bytes)
+ return
+}
+
+func loadX509Certificate(certFile string) (cert tls.Certificate, err error) {
+ certPEMBlock, err := ioutil.ReadFile(certFile)
+ if err != nil {
+ return
+ }
+ var certDERBlock *pem.Block
+ for {
+ certDERBlock, certPEMBlock = pem.Decode(certPEMBlock)
+ if certDERBlock == nil {
+ break
+ }
+ if certDERBlock.Type == "CERTIFICATE" {
+ cert.Certificate = append(cert.Certificate, certDERBlock.Bytes)
+ }
+ }
+
+ if len(cert.Certificate) == 0 {
+ err = errors.New("failed to parse certificate PEM data")
+ }
+ return
+}
+
+// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
+// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
+// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
+func parsePrivateKey(der []byte) (crypto.PrivateKey, error) {
+ if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
+ return key, nil
+ }
+ if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
+ switch key := key.(type) {
+ case *rsa.PrivateKey, *ecdsa.PrivateKey:
+ return key, nil
+ default:
+ return nil, errors.New("found unknown private key type in PKCS#8 wrapping")
+ }
+ }
+ if key, err := x509.ParseECPrivateKey(der); err == nil {
+ return key, nil
+ }
+ return nil, errors.New("failed to parse private key")
+}
diff --git a/src/app/spreed-speakfreely-server/tokens.go b/src/app/spreed-speakfreely-server/tokens.go
index 7f3f550b..70c0d406 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,24 +36,22 @@ 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
+ 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/src/app/spreed-speakfreely-server/user.go b/src/app/spreed-speakfreely-server/user.go
deleted file mode 100644
index ac40823e..00000000
--- a/src/app/spreed-speakfreely-server/user.go
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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 (
- "sync"
-)
-
-type User struct {
- Id string
- Roomid string
- Ua string
- UpdateRev uint64
- Status interface{}
- mutex sync.RWMutex
-}
-
-func (u *User) Update(update *UserUpdate) uint64 {
-
- //user := reflect.ValueOf(&u).Elem()
- u.mutex.Lock()
- defer u.mutex.Unlock()
-
- for _, key := range update.Types {
-
- //fmt.Println("type update", key)
- switch key {
- case "Roomid":
- u.Roomid = update.Roomid
- case "Ua":
- u.Ua = update.Ua
- case "Status":
- u.Status = update.Status
- }
-
- }
-
- u.UpdateRev++
- return u.UpdateRev
-
-}
-
-func (u *User) Data() *DataUser {
-
- u.mutex.RLock()
- defer u.mutex.RUnlock()
-
- return &DataUser{
- Id: u.Id,
- Ua: u.Ua,
- Status: u.Status,
- Rev: u.UpdateRev,
- }
-
-}
-
-type UserUpdate struct {
- Id string
- Types []string
- Roomid string
- Ua string
- Status interface{}
-}
diff --git a/src/app/spreed-speakfreely-server/users.go b/src/app/spreed-speakfreely-server/users.go
new file mode 100644
index 00000000..5ed00dda
--- /dev/null
+++ b/src/app/spreed-speakfreely-server/users.go
@@ -0,0 +1,465 @@
+/*
+ * 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"
+ "crypto/hmac"
+ "crypto/rand"
+ "crypto/sha256"
+ "crypto/tls"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/base64"
+ "encoding/json"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "github.com/longsleep/pkac"
+ "github.com/satori/go.uuid"
+ "github.com/strukturag/phoenix"
+ "log"
+ "math/big"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+)
+
+var (
+ serialNumberLimit *big.Int = new(big.Int).Lsh(big.NewInt(1), 128)
+)
+
+type UsersHandler interface {
+ Get(request *http.Request) (string, error)
+ Validate(snr *SessionNonceRequest, request *http.Request) (string, error)
+ Create(snr *UserNonce, request *http.Request) (*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) Get(request *http.Request) (userid string, err error) {
+ return
+}
+
+func (uh *UsersSharedsecretHandler) Validate(snr *SessionNonceRequest, request *http.Request) (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")
+ }
+
+ secret := uh.createHMAC(snr.UseridCombo)
+ if snr.Secret != secret {
+ return "", errors.New("invalid secret")
+ }
+
+ return userid, nil
+
+}
+
+func (uh *UsersSharedsecretHandler) Create(un *UserNonce, request *http.Request) (*UserNonce, error) {
+
+ // TODO(longsleep): Make this configureable - One year for now ...
+ expiration := time.Now().Add(time.Duration(1) * time.Hour * 24 * 31 * 12)
+ un.Timestamp = expiration.Unix()
+ un.UseridCombo = fmt.Sprintf("%d:%s", un.Timestamp, un.Userid)
+ un.Secret = uh.createHMAC(un.UseridCombo)
+
+ return un, nil
+
+}
+
+type UsersHTTPHeaderHandler struct {
+ headerName string
+}
+
+func (uh *UsersHTTPHeaderHandler) Get(request *http.Request) (userid string, err error) {
+ userid = request.Header.Get(uh.headerName)
+ if userid == "" {
+ err = errors.New("no userid provided")
+ }
+ return
+}
+
+func (uh *UsersHTTPHeaderHandler) Validate(snr *SessionNonceRequest, request *http.Request) (string, error) {
+ return uh.Get(request)
+}
+
+func (uh *UsersHTTPHeaderHandler) Create(un *UserNonce, request *http.Request) (*UserNonce, error) {
+ return nil, errors.New("create is not possible in httpheader mode")
+}
+
+type UsersCertificateHandler struct {
+ validFor time.Duration
+ privateKey crypto.PrivateKey
+ certificate *x509.Certificate
+ verifiedHeader string
+ verifiedHeaderValue string
+ certificateHeader string
+ organization []string
+}
+
+func (uh *UsersCertificateHandler) makeTemplate(commonName string) (*x509.Certificate, error) {
+
+ notBefore := time.Now()
+ notAfter := notBefore.Add(uh.validFor)
+
+ serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+ if err != nil {
+ return nil, err
+ }
+
+ return &x509.Certificate{
+ SerialNumber: serialNumber,
+ Subject: pkix.Name{
+ CommonName: commonName,
+ Organization: uh.organization,
+ },
+ NotBefore: notBefore,
+ NotAfter: notAfter,
+ KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
+ BasicConstraintsValid: true,
+ }, nil
+
+}
+
+func (uh *UsersCertificateHandler) Get(request *http.Request) (userid string, err error) {
+ if uh.verifiedHeader != "" && uh.verifiedHeaderValue != "" {
+ // Use incoming HTTP headers.
+ if request.Header.Get(uh.verifiedHeader) != uh.verifiedHeaderValue {
+ // Verify header does not match - ignore incoming userid.
+ return
+ }
+ if uh.certificateHeader != "" {
+ // Read userid from certificate in header if configured.
+ var cert tls.Certificate
+ var certDERBlock *pem.Block
+ // Whuahah this is an evil fix to get back valid PEM data from Nginx $ssl_client_cert values.
+ certString := strings.Replace(request.Header.Get(uh.certificateHeader), " ", "\n", -1)
+ certString = strings.Replace(certString, "BEGIN\n", "BEGIN ", 1)
+ certString = strings.Replace(certString, "END\n", "END ", 1)
+ certPEMBlock := []byte(certString)
+ for {
+ certDERBlock, certPEMBlock = pem.Decode(certPEMBlock)
+ if certDERBlock == nil {
+ break
+ }
+ if certDERBlock.Type == "CERTIFICATE" {
+ cert.Certificate = append(cert.Certificate, certDERBlock.Bytes)
+ }
+ }
+ if len(cert.Certificate) == 0 {
+ err = errors.New("failed to parse certificate PEM data")
+ return
+ }
+ var certificates []*x509.Certificate
+ if certificates, err = x509.ParseCertificates(cert.Certificate[0]); err == nil {
+ userid = certificates[0].Subject.CommonName
+ }
+ }
+ } else {
+ // Direct TLS termination and authentication.
+ if request.TLS == nil || len(request.TLS.VerifiedChains) == 0 {
+ return
+ }
+ chain := request.TLS.VerifiedChains[0]
+ if len(chain) == 0 {
+ return
+ }
+ cert := chain[0]
+ userid = cert.Subject.CommonName
+ }
+ log.Printf("Client certificate found for user: %s\n", userid)
+ return
+}
+
+func (uh *UsersCertificateHandler) Validate(snr *SessionNonceRequest, request *http.Request) (string, error) {
+ return uh.Get(request)
+}
+
+func (uh *UsersCertificateHandler) Create(un *UserNonce, request *http.Request) (*UserNonce, error) {
+
+ spkac := request.Form.Get("pubkey")
+ if spkac == "" {
+ return nil, errors.New("no spkac provided")
+ }
+ spkacDerBytes, err := base64.StdEncoding.DecodeString(spkac)
+ if err != nil {
+ return nil, errors.New(fmt.Sprintf("spkac invalid: %s", err))
+ }
+
+ publicKey, err := pkac.ParseSPKAC(spkacDerBytes)
+ if err != nil {
+ return nil, errors.New(fmt.Sprintf("unable to parse spkac: %s", err))
+ }
+
+ template, err := uh.makeTemplate(un.Userid)
+ if err != nil {
+ return nil, err
+ }
+
+ certDerBytes, err := x509.CreateCertificate(rand.Reader, template, uh.certificate, publicKey, uh.privateKey)
+ if err != nil {
+ return nil, errors.New(fmt.Sprintf("failed to create certificate: %s", err))
+ }
+
+ log.Println("Generated new certificate", un.Userid)
+ un.SetResponse(certDerBytes, "application/x-x509-user-cert", http.Header{
+ "Content-Length": {strconv.Itoa(len(certDerBytes))},
+ "Accept-Ranges": {"bytes"},
+ "Last-Modified": {time.Now().UTC().Format(http.TimeFormat)},
+ })
+
+ return un, nil
+
+}
+
+type UserNonce struct {
+ Nonce string `json:"nonce"`
+ Userid string `json:"userid"`
+ UseridCombo string `json:"useridcombo"`
+ Secret string `json:"secret"`
+ Timestamp int64 `json:"timestamp"`
+ Success bool `json:"success"`
+ raw []byte
+ contentType string
+ header http.Header
+}
+
+func (un *UserNonce) SetResponse(raw []byte, contentType string, header http.Header) {
+ un.raw = raw
+ un.contentType = contentType
+ un.header = header
+}
+
+func (un *UserNonce) Response() (int, interface{}, http.Header) {
+ header := un.header
+ if header == nil {
+ header = http.Header{}
+ }
+ if un.contentType != "" {
+ header.Set("Content-Type", un.contentType)
+ return 200, un.raw, header
+ } else {
+ return 200, un, header
+ }
+}
+
+type Users struct {
+ hub *Hub
+ realm string
+ handler UsersHandler
+}
+
+func NewUsers(hub *Hub, mode, realm string, runtime phoenix.Runtime) *Users {
+
+ var users = &Users{
+ hub: hub,
+ realm: realm,
+ }
+
+ var handler UsersHandler
+ var err error
+
+ // Create handler based on mode.
+ if handler, err = users.createHandler(mode, runtime); handler != nil && err == nil {
+ users.handler = handler
+ // Register handler Get at the hub.
+ users.hub.useridRetriever = func(request *http.Request) (userid string, err error) {
+ userid, err = handler.Get(request)
+ if err != nil {
+ log.Printf("Failed to get userid from handler: %s", err)
+ } else {
+ if userid != "" {
+ log.Printf("Users handler get success: %s\n", userid)
+ }
+ }
+ return
+ }
+ log.Printf("Enabled users handler '%s'\n", mode)
+ } else if err != nil {
+ log.Printf("Failed to enable handler '%s': %s\n", mode, err)
+ }
+
+ return users
+}
+
+func (users *Users) createHandler(mode string, runtime phoenix.Runtime) (handler UsersHandler, err error) {
+
+ switch mode {
+ case "sharedsecret":
+ secret, _ := runtime.GetString("users", "sharedsecret_secret")
+ if secret != "" {
+ handler = &UsersSharedsecretHandler{secret: []byte(secret)}
+ } else {
+ err = errors.New("Cannot enable sharedsecret users handler: No secret.")
+ }
+ case "httpheader":
+ headerName, _ := runtime.GetString("users", "httpheader_header")
+ if headerName == "" {
+ headerName = "x-users"
+ }
+ handler = &UsersHTTPHeaderHandler{headerName: headerName}
+ case "certificate":
+ var err2 error
+ verifiedHeader, _ := runtime.GetString("users", "certificate_verifiedHeader")
+ verifiedHeaderValue, _ := runtime.GetString("users", "certificate_verifiedHeaderValue")
+ certificateHeader, _ := runtime.GetString("users", "certificate_certificateHeader")
+ validForDays, _ := runtime.GetInt("users", "certificate_validForDays")
+ if validForDays == 0 {
+ validForDays = 365
+ }
+ organization, _ := runtime.GetString("users", "certificate_organization")
+ if organization == "" {
+ organization = "My Spreed Server"
+ }
+ uh := &UsersCertificateHandler{
+ verifiedHeader: verifiedHeader,
+ verifiedHeaderValue: verifiedHeaderValue,
+ certificateHeader: certificateHeader,
+ validFor: time.Duration(validForDays) * 24 * time.Hour,
+ organization: []string{organization},
+ }
+ keyFn, _ := runtime.GetString("users", "certificate_key")
+ certificateFn, _ := runtime.GetString("users", "certificate_certificate")
+ if keyFn != "" && certificateFn != "" {
+ // Load private key from file and use it for signing,
+ if uh.privateKey, err2 = loadX509PrivateKey(keyFn); err2 == nil {
+ log.Printf("Users certificate private key loaded from %s\n", keyFn)
+ } else {
+ log.Printf("Failed to load certificate private key: %s\n", err2)
+ }
+ }
+ if certificateFn != "" {
+ // Load Certificate from file.
+ var certificate tls.Certificate
+ if certificate, err = loadX509Certificate(certificateFn); err == nil {
+ // Parse first certificate in file.
+ var certificates []*x509.Certificate
+ if certificates, err = x509.ParseCertificates(certificate.Certificate[0]); err == nil {
+ // Use first parsed certificate as CA.
+ uh.certificate = certificates[0]
+ log.Printf("Users certificate loaded from %s\n", certificateFn)
+ handler = uh
+ // Get TLS config if the server has one.
+ if tlsConfig, err2 := runtime.TLSConfig(); err2 == nil {
+ // Enable TLS client certificate authentication.
+ tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven
+ // Create cert pool.
+ pool := x509.NewCertPool()
+ // Add CA certificate to pool for TLS client authentication.
+ for _, derCert := range certificate.Certificate {
+ cert, err2 := x509.ParseCertificate(derCert)
+ if err2 != nil {
+ continue
+ }
+ pool.AddCert(cert)
+ }
+ // Add pool to config.
+ tlsConfig.ClientCAs = pool
+ log.Printf("Initialized TLS auth pool with %d certificates.", len(pool.Subjects()))
+ }
+ }
+ }
+ } else {
+ err = errors.New("Cannot enable certificate users handler: No certificate.")
+ }
+ }
+
+ return
+
+}
+
+// Post is used to create new userids for this server.
+func (users *Users) Post(request *http.Request) (int, interface{}, http.Header) {
+
+ if users.handler == nil {
+ return 404, "No handler found", http.Header{"Content-Type": {"text/plain"}}
+ }
+
+ var snr *SessionNonceRequest
+
+ switch request.Header.Get("Content-Type") {
+ case "application/json":
+ snr = &SessionNonceRequest{}
+ decoder := json.NewDecoder(request.Body)
+ err := decoder.Decode(snr)
+ if err != nil {
+ return 400, NewApiError("users_bad_request", "Failed to parse request"), http.Header{"Content-Type": {"application/json"}}
+ }
+ case "application/x-www-form-urlencoded":
+ snr = &SessionNonceRequest{
+ Id: request.Form.Get("id"),
+ Sid: request.Form.Get("sid"),
+ }
+ default:
+ return 400, NewApiError("users_invalid_request", "Invalid request type"), 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 := 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) {
+ 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}, request)
+ 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 un.Response()
+
+}
diff --git a/src/app/spreed-speakfreely-server/ws.go b/src/app/spreed-speakfreely-server/ws.go
index df8ab61c..0df83a76 100644
--- a/src/app/spreed-speakfreely-server/ws.go
+++ b/src/app/spreed-speakfreely-server/ws.go
@@ -71,15 +71,9 @@ func makeWsHubHandler(h *Hub) http.HandlerFunc {
// Read request details.
r.ParseForm()
token := r.FormValue("t")
- remoteAddr := r.RemoteAddr
- if remoteAddr == "@" || remoteAddr == "127.0.0.1" {
- if r.Header["X-Forwarded-For"][0] != "" {
- remoteAddr = r.Header["X-Forwarded-For"][0]
- }
- }
// Create a new connection instance.
- c := NewConnection(h, ws, remoteAddr)
+ c := NewConnection(h, ws, r)
if token != "" {
if err := c.reregister(token); err != nil {
log.Println(err)
diff --git a/src/i18n/messages-de.po b/src/i18n/messages-de.po
index 7ebf9bdb..da7c7dc0 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-14 16:16+0200\n"
-"PO-Revision-Date: 2014-04-14 16:17+0100\n"
+"POT-Creation-Date: 2014-05-05 16:53+0200\n"
+"PO-Revision-Date: 2014-05-05 16:54+0100\n"
"Last-Translator: Simon Eisenmann \n"
"Language-Team: struktur AG \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
@@ -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"
@@ -135,6 +141,27 @@ 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 ""
+"Authenticated by certificate. To log out you have to remove your "
+"certificate from the browser."
+msgstr ""
+"Mit Zertifikat angemeldet. Melden Sie sich ab indem Sie das Zertifikat "
+"aus dem Browser entfernen."
+
+msgid "Log out"
+msgstr "Ausloggen"
+
+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 "Microphone"
msgstr "Mikrofon"
@@ -201,8 +228,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"
@@ -429,9 +463,6 @@ msgstr "Weitere Informationen nötig"
msgid "Ok"
msgstr "Ok"
-msgid "Close"
-msgstr "Schließen"
-
msgid "Access code required"
msgstr "Bitte Zugriffscode eingeben"
diff --git a/src/i18n/messages-ja.po b/src/i18n/messages-ja.po
index a59cb35c..7beae5f8 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-05-05 16:53+0200\n"
"PO-Revision-Date: 2014-04-23 22:25+0100\n"
"Last-Translator: Curt Frisemo \n"
"Language-Team: Curt Frisemo \n"
@@ -36,6 +36,12 @@ msgstr "設定"
msgid "Your audio level"
msgstr "あなたの音量"
+msgid "Standard view"
+msgstr ""
+
+msgid "Kiosk view"
+msgstr ""
+
msgid "Start chat"
msgstr "チャットを始める"
@@ -135,6 +141,23 @@ msgstr "名前"
msgid "Your picture and name are visible to others."
msgstr "あなたの写真と名前は公開されています."
+msgid "Your ID"
+msgstr ""
+
+msgid "Register"
+msgstr ""
+
+msgid ""
+"Authenticated by certificate. To log out you have to remove your "
+"certificate from the browser."
+msgstr ""
+
+msgid "Log out"
+msgstr ""
+
+msgid "Only register an ID if this is your private browser."
+msgstr ""
+
msgid "Microphone"
msgstr "マイク"
@@ -201,8 +224,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メールでシェア"
@@ -420,9 +448,6 @@ msgstr "さらなる情報が必要です"
msgid "Ok"
msgstr "OK"
-msgid "Close"
-msgstr "閉じる"
-
msgid "Access code required"
msgstr "アクセスコードが必要です"
diff --git a/src/i18n/messages-ko.po b/src/i18n/messages-ko.po
index 5aff519e..a5bed9f7 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-05-05 16:53+0200\n"
"PO-Revision-Date: 2014-04-13 20:30+0900\n"
"Last-Translator: FULL NAME \n"
"Language-Team: Curt Frisemo \n"
@@ -36,6 +36,12 @@ msgstr "설정"
msgid "Your audio level"
msgstr "음성크기"
+msgid "Standard view"
+msgstr ""
+
+msgid "Kiosk view"
+msgstr ""
+
msgid "Start chat"
msgstr "대화시작"
@@ -135,6 +141,23 @@ msgstr "이름"
msgid "Your picture and name are visible to others."
msgstr "사용자의 사진과 이름이 다른사람에게 보일수 있습니다."
+msgid "Your ID"
+msgstr ""
+
+msgid "Register"
+msgstr ""
+
+msgid ""
+"Authenticated by certificate. To log out you have to remove your "
+"certificate from the browser."
+msgstr ""
+
+msgid "Log out"
+msgstr ""
+
+msgid "Only register an ID if this is your private browser."
+msgstr ""
+
msgid "Microphone"
msgstr "마이크"
@@ -201,8 +224,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 "이메일로 공유"
@@ -420,9 +448,6 @@ msgstr "더 많은 정보가 필요함"
msgid "Ok"
msgstr "오케이"
-msgid "Close"
-msgstr "닫음"
-
msgid "Access code required"
msgstr "접속코드 필요함"
diff --git a/src/i18n/messages-zh-cn.po b/src/i18n/messages-zh-cn.po
index 9d2218e7..b6c6c252 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-05-05 16:53+0200\n"
"PO-Revision-Date: 2014-03-31 23:26+0100\n"
"Last-Translator: Michael P.\n"
"Language-Team: Curt Frisemo \n"
@@ -36,6 +36,12 @@ msgstr "系统设置"
msgid "Your audio level"
msgstr "您的通话音量"
+msgid "Standard view"
+msgstr ""
+
+msgid "Kiosk view"
+msgstr ""
+
msgid "Start chat"
msgstr "开始聊天"
@@ -136,6 +142,23 @@ msgstr "名字"
msgid "Your picture and name are visible to others."
msgstr "别人能看到您的图片及名字"
+msgid "Your ID"
+msgstr ""
+
+msgid "Register"
+msgstr ""
+
+msgid ""
+"Authenticated by certificate. To log out you have to remove your "
+"certificate from the browser."
+msgstr ""
+
+msgid "Log out"
+msgstr ""
+
+msgid "Only register an ID if this is your private browser."
+msgstr ""
+
msgid "Microphone"
msgstr "麦克风"
@@ -202,8 +225,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 "电子邮件共享"
@@ -418,9 +446,6 @@ msgstr "需要更多信息"
msgid "Ok"
msgstr "Ok"
-msgid "Close"
-msgstr "关闭"
-
msgid "Access code required"
msgstr "需要接入码"
diff --git a/src/i18n/messages-zh-tw.po b/src/i18n/messages-zh-tw.po
index e23b5835..2a15fff1 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-05-05 16:53+0200\n"
"PO-Revision-Date: 2014-04-07 18:09+0800\n"
"Last-Translator: Michael P.\n"
"Language-Team: Curt Frisemo \n"
@@ -36,6 +36,12 @@ msgstr "系統設置"
msgid "Your audio level"
msgstr "您的通話音量"
+msgid "Standard view"
+msgstr ""
+
+msgid "Kiosk view"
+msgstr ""
+
msgid "Start chat"
msgstr "開始聊天"
@@ -136,6 +142,23 @@ msgstr "名字"
msgid "Your picture and name are visible to others."
msgstr "別人能看到您的圖片及名字"
+msgid "Your ID"
+msgstr ""
+
+msgid "Register"
+msgstr ""
+
+msgid ""
+"Authenticated by certificate. To log out you have to remove your "
+"certificate from the browser."
+msgstr ""
+
+msgid "Log out"
+msgstr ""
+
+msgid "Only register an ID if this is your private browser."
+msgstr ""
+
msgid "Microphone"
msgstr "麥克風"
@@ -202,8 +225,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 "電子郵件共享"
@@ -418,9 +446,6 @@ msgstr "需要更多信息"
msgid "Ok"
msgstr "Ok"
-msgid "Close"
-msgstr "關閉"
-
msgid "Access code required"
msgstr "需要接入碼"
diff --git a/src/i18n/messages.pot b/src/i18n/messages.pot
index c6aaf9e7..59986fa5 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-14 16:16+0200\n"
+"POT-Creation-Date: 2014-05-05 16:53+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -36,6 +36,12 @@ msgstr ""
msgid "Your audio level"
msgstr ""
+msgid "Standard view"
+msgstr ""
+
+msgid "Kiosk view"
+msgstr ""
+
msgid "Start chat"
msgstr ""
@@ -135,6 +141,23 @@ msgstr ""
msgid "Your picture and name are visible to others."
msgstr ""
+msgid "Your ID"
+msgstr ""
+
+msgid "Register"
+msgstr ""
+
+msgid ""
+"Authenticated by certificate. To log out you have to remove your "
+"certificate from the browser."
+msgstr ""
+
+msgid "Log out"
+msgstr ""
+
+msgid "Only register an ID if this is your private browser."
+msgstr ""
+
msgid "Microphone"
msgstr ""
@@ -201,7 +224,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"
@@ -417,9 +445,6 @@ msgstr ""
msgid "Ok"
msgstr ""
-msgid "Close"
-msgstr ""
-
msgid "Access code required"
msgstr ""
diff --git a/src/styles/components/_audiovideo.scss b/src/styles/components/_audiovideo.scss
index 9d804296..6322dc5b 100644
--- a/src/styles/components/_audiovideo.scss
+++ b/src/styles/components/_audiovideo.scss
@@ -300,6 +300,9 @@
max-height: none;
right: 0;
}
+ .overlayActions {
+ display: none;
+ }
}
.renderer-onepeople {
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/controllers/mediastreamcontroller.js b/static/js/controllers/mediastreamcontroller.js
index b97e5193..277ab654 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;
@@ -365,9 +366,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 +388,24 @@ define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigS
}, 300);
}
}
+
+ // Support authentication from localStorage.
+ if (!data.Userid && mediaStream.config.UsersEnabled) {
+ // 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);
+ mediaStream.api.requestAuthentication(data.userid, data.nonce);
+ delete data.nonce;
+ }, function(data, status) {
+ console.error("Failed to authorize session", status, data);
+ });
+ }
+ }
+
// Support to upgrade stuff when ttl was reached.
if (data.Turn.ttl) {
ttlTimeout = $timeout(function() {
@@ -392,6 +413,7 @@ define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigS
mediaStream.api.sendSelf();
}, data.Turn.ttl / 100 * 90 * 1000);
}
+
// Support resurrection shrine.
if (resurrect) {
var resurrection = resurrect;
@@ -404,6 +426,7 @@ define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigS
}
}, 0);
}
+
});
mediaStream.webrtc.e.on("peercall", function(event, peercall) {
@@ -519,6 +542,7 @@ define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigS
if (opts.soft) {
return;
}
+ $scope.userid = null;
break;
case "error":
if (reconnecting || connected) {
@@ -593,6 +617,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/directives/settings.js b/static/js/directives/settings.js
index 8ba1a538..3b51c132 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,10 @@ define(['underscore', 'text!partials/settings.html'], function(_, template) {
name: translation._("Use browser language")
}
];
+ $scope.withUsers = mediaStream.config.UsersEnabled;
+ $scope.withUsersRegistration = mediaStream.config.UsersAllowRegistration;
+ $scope.withUsersMode = mediaStream.config.UsersMode;
+
_.each(availableLanguages, function(name, code) {
$scope.availableLanguages.push({
code: code,
@@ -46,7 +50,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) {
@@ -120,9 +125,45 @@ define(['underscore', 'text!partials/settings.html'], function(_, template) {
$scope.showTakePicture = false;
safeApply($scope);
}
-
}
};
+
+ $scope.registerUserid = function(btn) {
+
+ var successHandler = function(data) {
+ console.info("Created new userid:", data.userid);
+ // 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;
+ };
+
+ console.log("No userid - creating one ...");
+ mediaStream.users.register(btn.form, function(data) {
+ if (data.nonce) {
+ successHandler(data);
+ } 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.
+ mediaStream.users.authorize(data, successHandler, function(data, status) {
+ console.error("Failed to get nonce after create", status, data);
+ });
+ }
+ }, 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;
@@ -144,7 +185,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) {
@@ -163,6 +204,8 @@ define(['underscore', 'text!partials/settings.html'], function(_, template) {
}
});
});
+ } else if (!showSettings && oldValue) {
+ $scope.saveSettings();
}
});
}];
diff --git a/static/js/libs/sjcl.js b/static/js/libs/sjcl.js
new file mode 100644
index 00000000..1d5110d1
--- /dev/null
+++ b/static/js/libs/sjcl.js
@@ -0,0 +1,2419 @@
+// http://bitwiseshiftleft.github.io/sjcl/
+// ./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.
+
+/** @fileOverview Javascript cryptography implementation.
+ *
+ * Crush to remove comments, shorten variable names and
+ * generally reduce transmission size.
+ *
+ * @author Emily Stark
+ * @author Mike Hamburg
+ * @author Dan Boneh
+ */
+
+"use strict";
+/*jslint indent: 2, bitwise: false, nomen: false, plusplus: false, white: false, regexp: false */
+/*global document, window, escape, unescape, module, require, Uint32Array */
+
+/** @namespace The Stanford Javascript Crypto Library, top-level namespace. */
+var sjcl = {
+ /** @namespace Symmetric ciphers. */
+ cipher: {},
+
+ /** @namespace Hash functions. Right now only SHA256 is implemented. */
+ hash: {},
+
+ /** @namespace Key exchange functions. Right now only SRP is implemented. */
+ keyexchange: {},
+
+ /** @namespace Block cipher modes of operation. */
+ mode: {},
+
+ /** @namespace Miscellaneous. HMAC and PBKDF2. */
+ misc: {},
+
+ /**
+ * @namespace Bit array encoders and decoders.
+ *
+ * @description
+ * The members of this namespace are functions which translate between
+ * SJCL's bitArrays and other objects (usually strings). Because it
+ * isn't always clear which direction is encoding and which is decoding,
+ * the method names are "fromBits" and "toBits".
+ */
+ codec: {},
+
+ /** @namespace Exceptions. */
+ exception: {
+ /** @constructor Ciphertext is corrupt. */
+ corrupt: function(message) {
+ this.toString = function() { return "CORRUPT: "+this.message; };
+ this.message = message;
+ },
+
+ /** @constructor Invalid parameter. */
+ invalid: function(message) {
+ this.toString = function() { return "INVALID: "+this.message; };
+ this.message = message;
+ },
+
+ /** @constructor Bug or missing feature in SJCL. @constructor */
+ bug: function(message) {
+ this.toString = function() { return "BUG: "+this.message; };
+ this.message = message;
+ },
+
+ /** @constructor Something isn't ready. */
+ notReady: function(message) {
+ this.toString = function() { return "NOT READY: "+this.message; };
+ this.message = message;
+ }
+ }
+};
+
+if(typeof module !== 'undefined' && module.exports){
+ module.exports = sjcl;
+}
+/** @fileOverview Low-level AES implementation.
+ *
+ * This file contains a low-level implementation of AES, optimized for
+ * size and for efficiency on several browsers. It is based on
+ * OpenSSL's aes_core.c, a public-domain implementation by Vincent
+ * Rijmen, Antoon Bosselaers and Paulo Barreto.
+ *
+ * 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.
+ *
+ * @author Emily Stark
+ * @author Mike Hamburg
+ * @author Dan Boneh
+ */
+
+/**
+ * Schedule out an AES key for both encryption and decryption. This
+ * is a low-level class. Use a cipher mode to do bulk encryption.
+ *
+ * @constructor
+ * @param {Array} key The key as an array of 4, 6 or 8 words.
+ *
+ * @class Advanced Encryption Standard (low-level interface)
+ */
+sjcl.cipher.aes = function (key) {
+ if (!this._tables[0][0][0]) {
+ this._precompute();
+ }
+
+ var i, j, tmp,
+ encKey, decKey,
+ sbox = this._tables[0][4], decTable = this._tables[1],
+ keyLen = key.length, rcon = 1;
+
+ if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) {
+ throw new sjcl.exception.invalid("invalid aes key size");
+ }
+
+ this._key = [encKey = key.slice(0), decKey = []];
+
+ // schedule encryption keys
+ for (i = keyLen; i < 4 * keyLen + 28; i++) {
+ tmp = encKey[i-1];
+
+ // apply sbox
+ if (i%keyLen === 0 || (keyLen === 8 && i%keyLen === 4)) {
+ tmp = sbox[tmp>>>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= status.Rev) {
- console.warn("Received old status update in status", status.Rev, scope.status.Rev);
+ // Update session.
+ scope.session.Userid = data.Userid;
+ scope.session.Rev = data.Rev;
+ // Update status.
+ if (scope.status && scope.status.Rev >= data.Rev) {
+ console.warn("Received old status update in status", data.Rev, scope.status.Rev);
} else {
- scope.status = status.Status;
+ scope.status = data.Status;
this.updateBuddyPicture(scope.status);
var displayName = scope.displayName;
if (scope.status.displayName) {
@@ -355,18 +364,25 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
};
- Buddylist.prototype.onJoined = function(user) {
+ Buddylist.prototype.onJoined = function(data) {
- //console.log("Joined", user);
- var id = user.Id;
+ //console.log("Joined", data);
+ var id = data.Id;
var scope = buddyData.get(id, this.$scope, _.bind(this.onBuddyScope, this));
- scope.user = user;
+ // Create session.
+ scope.session = {
+ Id: data.Id,
+ Userid: data.Userid,
+ Ua: data.Ua,
+ Rev: 0
+ };
+ // Add status.
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 (data.Status) {
+ if (scope.status && scope.status.Rev >= data.Status.Rev) {
+ console.warn("Received old status update in join", data.Status.Rev, scope.status.Rev);
} else {
- scope.status = user.Status;
+ scope.status = data.Status;
scope.displayName = scope.status.displayName;
this.updateBuddyPicture(scope.status);
}
@@ -380,14 +396,14 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
};
- Buddylist.prototype.onLeft = function(user) {
+ Buddylist.prototype.onLeft = function(data) {
- //console.log("Left", user);
- var id = user.Id;
+ //console.log("Left", session);
+ var id = data.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/js/services/mediastream.js b/static/js/services/mediastream.js
index fddfea61..76643cb4 100644
--- a/static/js/services/mediastream.js
+++ b/static/js/services/mediastream.js
@@ -21,12 +21,13 @@
define([
'jquery',
'underscore',
+ 'ua-parser',
'mediastream/connector',
'mediastream/api',
'mediastream/webrtc',
'mediastream/tokens'
-], function($, _, Connector, Api, WebRTC, tokens) {
+], function($, _, uaparser, Connector, Api, WebRTC, tokens) {
return ["globalContext", "$route", "$location", "$window", "visibility", "alertify", "$http", "safeApply", "$timeout", "$sce", function(context, $route, $location, $window, visibility, alertify, $http, safeApply, $timeout, $sce) {
@@ -40,20 +41,8 @@ define([
var api = new Api(connector);
var webrtc = new WebRTC(api);
- var td = null;
- $window.testDisconnect = function() {
- if (td) {
- $window.clearInterval(td);
- td = null;
- console.info("Stopped disconnector.");
- return;
- }
- td = $window.setInterval(function() {
- console.info("Test disconnect!");
- connector.conn.close();
- }, 10000);
- console.info("Started disconnector.");
- };
+ // Create encryption key from server token and browser name.
+ var secureKey = sjcl.codec.base64.fromBits(sjcl.hash.sha256.hash(context.Cfg.Token + uaparser().browser.name));
var mediaStream = {
version: version,
@@ -75,6 +64,131 @@ define([
return (context.Cfg.B || "/") + "api/v1/" + path;
}
},
+ users: {
+ register: function(form, success_cb, error_cb) {
+ var url = mediaStream.url.api("users");
+ if (form) {
+ // Form submit mode.
+ $(form).attr("action", url).attr("method", "POST");
+ var idE = $('');
+ idE.val(mediaStream.api.id);
+ var sidE = $('');
+ sidE.val(mediaStream.api.sid);
+ $(form).append(idE);
+ $(form).append(sidE);
+ var iframe = $(form).find("iframe");
+ form.submit();
+ $timeout(function() {
+ idE.remove();
+ sidE.remove();
+ idE=null;
+ sidE=null;
+ }, 0);
+ var retries = 0;
+ var authorize = function() {
+ mediaStream.users.authorize({
+ count: retries
+ }, success_cb, function(data, status) {
+ // Error handler retry.
+ retries++;
+ if (retries <= 10) {
+ $timeout(authorize, 2000);
+ } else {
+ console.error("Failed to authorize session", status, data);
+ if (error_cb) {
+ error_cb(data, status)
+ }
+ }
+ });
+ };
+ $timeout(authorize, 1500);
+ } else {
+ // AJAX mode.
+ var data = {
+ id: mediaStream.api.id,
+ sid: mediaStream.api.sid
+ }
+ $http({
+ method: "POST",
+ url: url,
+ data: JSON.stringify(data),
+ headers: {'Content-Type': 'application/json'}
+ }).
+ success(function(data, status) {
+ if (data.userid !== "" && data.success) {
+ success_cb(data, status);
+ } else {
+ if (error_cb) {
+ error_cb(data, status);
+ }
+ }
+ }).
+ error(function(data, status) {
+ if (error_cb) {
+ error_cb(data, status)
+ }
+ });
+ }
+ },
+ authorize: function(data, success_cb, error_cb) {
+ var url = mediaStream.url.api("sessions") + "/" + mediaStream.api.id + "/";
+ var login = _.clone(data);
+ login.id = mediaStream.api.id;
+ login.sid = mediaStream.api.sid;
+ $http({
+ method: "PATCH",
+ url: url,
+ data: JSON.stringify(login),
+ headers: {'Content-Type': 'application/json'}
+ }).
+ success(function(data, status) {
+ if (data.nonce !== "" && data.success) {
+ success_cb(data, status);
+ } else {
+ if (error_cb) {
+ error_cb(data, status);
+ }
+ }
+ }).
+ error(function(data, status) {
+ if (error_cb) {
+ 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-"+context.Cfg.UsersMode, login);
+ return login;
+ },
+ load: function() {
+ // Check if we have something in store.
+ var login = localStorage.getItem("mediastream-login-"+context.Cfg.UsersMode);
+ 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-"+context.Cfg.UsersMode);
+ }
+ },
initialize: function($rootScope, translation) {
var cont = false;
@@ -203,10 +317,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) {
diff --git a/static/partials/buddy.html b/static/partials/buddy.html
index 69aa87d8..e103cf66 100644
--- a/static/partials/buddy.html
+++ b/static/partials/buddy.html
@@ -1,5 +1,5 @@
-
diff --git a/static/translation/messages-de.json b/static/translation/messages-de.json
index 9155d882..fc4e058c 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"],"Authenticated by certificate. To log out you have to remove your certificate from the browser.":[null,"Mit Zertifikat angemeldet. Melden Sie sich ab indem Sie das Zertifikat aus dem Browser entfernen."],"Log out":[null,"Ausloggen"],"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."],"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..e16ba5c6 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,""],"Authenticated by certificate. To log out you have to remove your certificate from the browser.":[null,""],"Log out":[null,""],"Only register an ID if this is your private browser.":[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..71ce4552 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,""],"Authenticated by certificate. To log out you have to remove your certificate from the browser.":[null,""],"Log out":[null,""],"Only register an ID if this is your private browser.":[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..26bf6e54 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,""],"Authenticated by certificate. To log out you have to remove your certificate from the browser.":[null,""],"Log out":[null,""],"Only register an ID if this is your private browser.":[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..e8b7c4f3 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,""],"Authenticated by certificate. To log out you have to remove your certificate from the browser.":[null,""],"Log out":[null,""],"Only register an ID if this is your private browser.":[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