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 @@ -
+
-
{{user.Id|displayName}}
-
{{user.Ua}}
+
{{session.Id|displayName}}
+
{{session.Ua}}
diff --git a/static/partials/buddyactions.html b/static/partials/buddyactions.html index e4574374..6fdb45e8 100644 --- a/static/partials/buddyactions.html +++ b/static/partials/buddyactions.html @@ -1,4 +1,4 @@
- +
diff --git a/static/partials/buddyactionsforaudiomixer.html b/static/partials/buddyactionsforaudiomixer.html index 23cd32ad..5f59ae93 100644 --- a/static/partials/buddyactionsforaudiomixer.html +++ b/static/partials/buddyactionsforaudiomixer.html @@ -1,3 +1,3 @@
- +
diff --git a/static/partials/settings.html b/static/partials/settings.html index 4c3d0b0c..d525d49b 100644 --- a/static/partials/settings.html +++ b/static/partials/settings.html @@ -1,13 +1,13 @@
{{version}}
-
{{_('Settings')}}
-
+
+ +
+ +
+ + + +
+
+
{{userid}}
+ {{_('Authenticated by certificate. To log out you have to remove your certificate from the browser.')}} +
+ +
+ +
{{userid}}
+ +
+
+
+ {{_('Only register an ID if this is your private browser.')}} +
+

@@ -131,12 +161,13 @@
+ +

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

- {{_('Apply')}} {{_('Cancel')}} + {{_('Close')}}
- +
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