Browse Source

Merge remote-tracking branch 'upstream/master' into theurere-css-to-scss-branch

Conflicts:
	src/styles/components/_audiovideo.scss
	static/partials/buddy.html

Fixed conflicts in:
src/styles/components/_audiovideo.scss
static/partials/buddy.html
pull/29/head^2
Evan Theurer 12 years ago
parent
commit
5bcd207c78
  1. 6
      Makefile
  2. 75
      doc/CHANNELING-API.txt
  3. 159
      doc/REST-API.txt
  4. 108
      doc/plugin-test-authorize.js
  5. 173
      server.conf.in
  6. 23
      src/app/spreed-speakfreely-server/channeling.go
  7. 33
      src/app/spreed-speakfreely-server/config.go
  8. 31
      src/app/spreed-speakfreely-server/connection.go
  9. 170
      src/app/spreed-speakfreely-server/hub.go
  10. 26
      src/app/spreed-speakfreely-server/images.go
  11. 50
      src/app/spreed-speakfreely-server/main.go
  12. 17
      src/app/spreed-speakfreely-server/random.go
  13. 5
      src/app/spreed-speakfreely-server/rooms.go
  14. 40
      src/app/spreed-speakfreely-server/roomworker.go
  15. 54
      src/app/spreed-speakfreely-server/server.go
  16. 219
      src/app/spreed-speakfreely-server/session.go
  17. 121
      src/app/spreed-speakfreely-server/sessions.go
  18. 56
      src/app/spreed-speakfreely-server/sleepy/core.go
  19. 5
      src/app/spreed-speakfreely-server/stats.go
  20. 110
      src/app/spreed-speakfreely-server/tls.go
  21. 13
      src/app/spreed-speakfreely-server/tokens.go
  22. 82
      src/app/spreed-speakfreely-server/user.go
  23. 465
      src/app/spreed-speakfreely-server/users.go
  24. 8
      src/app/spreed-speakfreely-server/ws.go
  25. 45
      src/i18n/messages-de.po
  26. 37
      src/i18n/messages-ja.po
  27. 37
      src/i18n/messages-ko.po
  28. 37
      src/i18n/messages-zh-cn.po
  29. 37
      src/i18n/messages-zh-tw.po
  30. 35
      src/i18n/messages.pot
  31. 3
      src/styles/components/_audiovideo.scss
  32. 3
      static/js/base.js
  33. 32
      static/js/controllers/mediastreamcontroller.js
  34. 49
      static/js/directives/settings.js
  35. 2419
      static/js/libs/sjcl.js
  36. 26
      static/js/main.js
  37. 16
      static/js/mediastream/api.js
  38. 9
      static/js/mediastream/connector.js
  39. 60
      static/js/services/buddylist.js
  40. 152
      static/js/services/mediastream.js
  41. 6
      static/partials/buddy.html
  42. 2
      static/partials/buddyactions.html
  43. 2
      static/partials/buddyactionsforaudiomixer.html
  44. 39
      static/partials/settings.html
  45. 2
      static/translation/messages-de.json
  46. 2
      static/translation/messages-ja.json
  47. 2
      static/translation/messages-ko.json
  48. 2
      static/translation/messages-zh-cn.json
  49. 2
      static/translation/messages-zh-tw.json

6
Makefile

@ -63,6 +63,10 @@ gopath: @@ -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 @@ -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

75
doc/CHANNELING-API.txt

@ -1,5 +1,5 @@ @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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

159
doc/REST-API.txt

@ -0,0 +1,159 @@ @@ -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

108
doc/plugin-test-authorize.js

@ -0,0 +1,108 @@ @@ -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 <http://www.gnu.org/licenses/>.
*
*/
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);
};
}]);
}
}
});

173
server.conf.in

@ -1,35 +1,156 @@ @@ -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

23
src/app/spreed-speakfreely-server/wsdata.go → src/app/spreed-speakfreely-server/channeling.go

@ -48,6 +48,8 @@ type DataAnswer struct { @@ -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 { @@ -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{}
}
@ -112,6 +115,7 @@ type DataIncoming struct { @@ -112,6 +115,7 @@ type DataIncoming struct {
Chat *DataChat
Conference *DataConference
Alive *DataAlive
Authentication *DataAuthentication
}
type DataOutgoing struct {
@ -120,9 +124,9 @@ 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 { @@ -137,3 +141,8 @@ type DataAlive struct {
Type string
Alive uint64
}
type DataAuthentication struct {
Type string
Authentication *SessionToken
}

33
src/app/spreed-speakfreely-server/config.go

@ -23,6 +23,7 @@ package main @@ -23,6 +23,7 @@ package main
import (
"fmt"
"net/http"
)
type Config struct {
@ -30,16 +31,40 @@ type Config struct { @@ -30,16 +31,40 @@ type Config struct {
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
globalRoomid string // Id of the global room (not exported to Javascript)
defaultRoomEnabled bool // Flag to enable default room ("")
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"}}
}

31
src/app/spreed-speakfreely-server/connection.go

@ -27,6 +27,7 @@ import ( @@ -27,6 +27,7 @@ import (
"github.com/gorilla/websocket"
"io"
"log"
"net/http"
"sync"
"time"
)
@ -57,6 +58,7 @@ type Connection struct { @@ -57,6 +58,7 @@ type Connection struct {
// References.
h *Hub
ws *websocket.Conn
request *http.Request
// Data handling.
condition *sync.Cond
@ -66,21 +68,20 @@ type Connection struct { @@ -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,
request: request,
}
c.condition = sync.NewCond(&c.mutex)
@ -93,7 +94,7 @@ func (c *Connection) close() { @@ -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() { @@ -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

170
src/app/spreed-speakfreely-server/hub.go

@ -28,9 +28,11 @@ import ( @@ -28,9 +28,11 @@ import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/gorilla/securecookie"
"log"
"net/http"
"strings"
"sync"
"sync/atomic"
@ -53,19 +55,19 @@ type MessageRequest struct { @@ -53,19 +55,19 @@ type MessageRequest struct {
type HubStat struct {
Rooms int `json:"rooms"`
Connections int `json:"connections"`
Users int `json:"users"`
Sessions int `json:"sessions"`
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"`
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 { @@ -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 { @@ -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 { @@ -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 { @@ -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?
var session *Session
var userid string
usersEnabled := h.config.UsersEnabled
if usersEnabled && h.useridRetriever != nil {
userid, _ = h.useridRetriever(request)
}
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 {
if value == "" {
// Create new id.
value = fmt.Sprintf("%s", securecookie.GenerateRandomKey(16))
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 h.tickets.Encode(key, value)
return true
}
func (h *Hub) DecodeTicket(key, value string) (string, error) {
func (h *Hub) EncodeSessionToken(st *SessionToken) (string, error) {
result := ""
err := h.tickets.Decode(key, value, &result)
return result, err
return h.tickets.Encode(h.tokenName, st)
}
func (h *Hub) DecodeSessionToken(token string) (*SessionToken, error) {
st := &SessionToken{}
err := h.tickets.Decode(h.tokenName, token, st)
return st, err
}
@ -180,8 +236,8 @@ func (h *Hub) GetRoom(id string) *RoomWorker { @@ -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,33 +300,32 @@ func (h *Hub) isDefaultRoomid(id string) bool { @@ -244,33 +300,32 @@ 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()
//log.Printf("Register (%d) from %s: %s (existing)\n", c.Idx, c.Id)
}
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
h.sessionTable[c.Id] = s
//fmt.Println("registered", c.Id)
h.mutex.Unlock()
//log.Printf("Register (%d) from %s: %s\n", c.Idx, c.RemoteAddr, c.Id)
//log.Printf("Register (%d) from %s: %s\n", c.Idx, c.Id)
h.server.OnRegister(c)
}
}
@ -281,16 +336,16 @@ 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) { @@ -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 { @@ -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
}

26
src/app/spreed-speakfreely-server/images.go

@ -38,29 +38,29 @@ type Image struct { @@ -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
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 { @@ -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 { @@ -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 { @@ -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 { @@ -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()

50
src/app/spreed-speakfreely-server/main.go

@ -263,6 +263,31 @@ func runner(runtime phoenix.Runtime) error { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -357,9 +400,6 @@ func runner(runtime phoenix.Runtime) error {
}
}
runtime.DefaultHTTPHandler(r)
runtime.DefaultHTTPSHandler(r)
return runtime.Start()
}

17
src/app/spreed-speakfreely-server/random.go

@ -23,7 +23,6 @@ package main @@ -23,7 +23,6 @@ package main
import (
"crypto/rand"
"encoding/base64"
pseudoRand "math/rand"
"time"
)
@ -32,7 +31,7 @@ const ( @@ -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 { @@ -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())

5
src/app/spreed-speakfreely-server/rooms.go

@ -24,7 +24,6 @@ package main @@ -24,7 +24,6 @@ package main
import (
"fmt"
"net/http"
"net/url"
)
type Room struct {
@ -35,9 +34,9 @@ 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"}}
}

40
src/app/spreed-speakfreely-server/roomworker.go

@ -35,7 +35,7 @@ const ( @@ -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 { @@ -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) { @@ -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) { @@ -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) { @@ -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)
}
}
}

54
src/app/spreed-speakfreely-server/server.go

@ -37,9 +37,19 @@ type Server struct { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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)

219
src/app/spreed-speakfreely-server/session.go

@ -0,0 +1,219 @@ @@ -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 <http://www.gnu.org/licenses/>.
*
*/
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)
}

121
src/app/spreed-speakfreely-server/sessions.go

@ -0,0 +1,121 @@ @@ -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 <http://www.gnu.org/licenses/>.
*
*/
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"}}
}

56
src/app/spreed-speakfreely-server/sleepy/core.go

@ -35,7 +35,6 @@ import ( @@ -35,7 +35,6 @@ import (
"fmt"
"github.com/gorilla/mux"
"net/http"
"net/url"
)
const (
@ -43,30 +42,44 @@ 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 { @@ -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 { @@ -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 { @@ -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

5
src/app/spreed-speakfreely-server/stats.go

@ -23,7 +23,6 @@ package main @@ -23,7 +23,6 @@ package main
import (
"net/http"
"net/url"
"runtime"
"time"
)
@ -73,9 +72,9 @@ type Stats struct { @@ -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": {"*"}}
}

110
src/app/spreed-speakfreely-server/tls.go

@ -0,0 +1,110 @@ @@ -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")
}

13
src/app/spreed-speakfreely-server/tokens.go

@ -24,7 +24,6 @@ package main @@ -24,7 +24,6 @@ package main
import (
"log"
"net/http"
"net/url"
"strings"
)
@ -37,24 +36,22 @@ type Tokens struct { @@ -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"}}
}

82
src/app/spreed-speakfreely-server/user.go

@ -1,82 +0,0 @@ @@ -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 <http://www.gnu.org/licenses/>.
*
*/
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{}
}

465
src/app/spreed-speakfreely-server/users.go

@ -0,0 +1,465 @@ @@ -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 <http://www.gnu.org/licenses/>.
*
*/
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()
}

8
src/app/spreed-speakfreely-server/ws.go

@ -71,15 +71,9 @@ func makeWsHubHandler(h *Hub) http.HandlerFunc { @@ -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)

45
src/i18n/messages-de.po

@ -8,8 +8,8 @@ msgid "" @@ -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 <simon@struktur.de>\n"
"Language-Team: struktur AG <opensource@struktur.de>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
@ -36,6 +36,12 @@ msgstr "Einstellungen" @@ -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" @@ -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" @@ -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" @@ -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"

37
src/i18n/messages-ja.po

@ -8,7 +8,7 @@ msgid "" @@ -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 <curt.frisemo@spreed.com>\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -36,6 +36,12 @@ msgstr "設定" @@ -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 "名前" @@ -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 "詳細設定を隠す" @@ -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 "さらなる情報が必要です" @@ -420,9 +448,6 @@ msgstr "さらなる情報が必要です"
msgid "Ok"
msgstr "OK"
msgid "Close"
msgstr "閉じる"
msgid "Access code required"
msgstr "アクセスコードが必要です"

37
src/i18n/messages-ko.po

@ -8,7 +8,7 @@ msgid "" @@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -36,6 +36,12 @@ msgstr "설정" @@ -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 "이름" @@ -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 "고급 설정 감추기" @@ -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 "더 많은 정보가 필요함" @@ -420,9 +448,6 @@ msgstr "더 많은 정보가 필요함"
msgid "Ok"
msgstr "오케이"
msgid "Close"
msgstr "닫음"
msgid "Access code required"
msgstr "접속코드 필요함"

37
src/i18n/messages-zh-cn.po

@ -8,7 +8,7 @@ msgid "" @@ -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 <curt.frisemo@spreed.com>\n"
@ -36,6 +36,12 @@ msgstr "系统设置" @@ -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 "名字" @@ -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 "隐藏高级设置" @@ -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 "需要更多信息" @@ -418,9 +446,6 @@ msgstr "需要更多信息"
msgid "Ok"
msgstr "Ok"
msgid "Close"
msgstr "关闭"
msgid "Access code required"
msgstr "需要接入码"

37
src/i18n/messages-zh-tw.po

@ -8,7 +8,7 @@ msgid "" @@ -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 <curt.frisemo@spreed.com>\n"
@ -36,6 +36,12 @@ msgstr "系統設置" @@ -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 "名字" @@ -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 "隐藏高级设置" @@ -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 "需要更多信息" @@ -418,9 +446,6 @@ msgstr "需要更多信息"
msgid "Ok"
msgstr "Ok"
msgid "Close"
msgstr "關閉"
msgid "Access code required"
msgstr "需要接入碼"

35
src/i18n/messages.pot

@ -9,7 +9,7 @@ msgid "" @@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -36,6 +36,12 @@ msgstr "" @@ -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 "" @@ -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 "" @@ -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 "" @@ -417,9 +445,6 @@ msgstr ""
msgid "Ok"
msgstr ""
msgid "Close"
msgstr ""
msgid "Access code required"
msgstr ""

3
src/styles/components/_audiovideo.scss

@ -300,6 +300,9 @@ @@ -300,6 +300,9 @@
max-height: none;
right: 0;
}
.overlayActions {
display: none;
}
}
.renderer-onepeople {

3
static/js/base.js

@ -31,5 +31,6 @@ define([ @@ -31,5 +31,6 @@ define([
'audiocontext',
'rAF',
'humanize',
'sha'
'sha',
'sjcl'
], function(){});

32
static/js/controllers/mediastreamcontroller.js

@ -18,7 +18,7 @@ @@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) {

49
static/js/directives/settings.js

@ -20,7 +20,7 @@ @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -163,6 +204,8 @@ define(['underscore', 'text!partials/settings.html'], function(_, template) {
}
});
});
} else if (!showSettings && oldValue) {
$scope.saveSettings();
}
});
}];

2419
static/js/libs/sjcl.js

File diff suppressed because it is too large Load Diff

26
static/js/main.js

@ -49,6 +49,7 @@ require.config({ @@ -49,6 +49,7 @@ require.config({
'humanize': 'libs/humanize',
'sha': 'libs/sha',
'dialogs': 'libs/angular/dialogs.min',
'sjcl': 'libs/sjcl',
'partials': '../partials',
'sounds': '../sounds',
@ -110,10 +111,35 @@ require.config({ @@ -110,10 +111,35 @@ require.config({
'dialogs': {
deps: ['angular', 'angular-sanitize'],
exports: 'angular'
},
'sjcl': {
exports: 'sjcl'
}
}
});
(function() {
var debugDefault = window.location.href.match(/(\?|&)debug($|&|=)/);
// Overwrite console to not log stuff per default.
// Write debug(true) in console to enable or start with ?debug parameter.
window.consoleBackup = window.console;
window.debug = function(flag) {
if (!flag) {
window.console = {
log: function() {},
info: function() {},
warn: function() {},
error: function() {},
debug: function() {},
trace: function() {}
}
} else {
window.console = consoleBackup;
}
};
window.debug(debugDefault && true);
}());
require.onError = (function() {
var retrying = false;
return function(err) {

16
static/js/mediastream/api.js

@ -26,6 +26,7 @@ define(['jquery', 'underscore'], function($, _) { @@ -26,6 +26,7 @@ define(['jquery', 'underscore'], function($, _) {
var Api = function(connector) {
this.id = null;
this.sid = null;
this.session = {};
this.connector = connector;
@ -112,6 +113,7 @@ define(['jquery', 'underscore'], function($, _) { @@ -112,6 +113,7 @@ define(['jquery', 'underscore'], function($, _) {
this.connector.token = data.Token;
}
this.id = data.Id;
this.sid = data.Sid;
this.e.triggerHandler("received.self", [data]);
break;
case "Offer":
@ -225,6 +227,20 @@ define(['jquery', 'underscore'], function($, _) { @@ -225,6 +227,20 @@ define(['jquery', 'underscore'], function($, _) {
};
Api.prototype.requestAuthentication = function(userid, nonce) {
var data = {
Type: "Authentication",
Authentication: {
Userid: userid,
Nonce: nonce
}
}
return this.send("Authentication", data);
};
Api.prototype.updateStatus = function(status) {
var data = {

9
static/js/mediastream/connector.js

@ -111,6 +111,15 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) { @@ -111,6 +111,15 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) {
};
Connector.prototype.forgetAndReconnect = function() {
this.token = null;
if (this.conn && this.connected) {
this.conn.close();
}
};
Connector.prototype.room = function(roomid, cb) {
var was_connected = this.connected;

60
static/js/services/buddylist.js

@ -31,7 +31,7 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! @@ -31,7 +31,7 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
BuddyTree.prototype.create = function(id, scope) {
var sort = scope.displayName ? scope.displayName : "user "+scope.buddyIndexSortable+" "+id;
var sort = scope.displayName ? scope.displayName : "session "+scope.buddyIndexSortable+" "+id;
var data = {
id: id,
sort: sort + "z" + id
@ -138,13 +138,17 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! @@ -138,13 +138,17 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
$element.on("mouseenter mouseleave", ".buddy", _.bind(function(event) {
// Hover handler for on Buddy actions.
var buddyElement = $(event.currentTarget);
this.hover(buddyElement, event.type === "mouseenter" ? true : false, buddyElement.scope().user.Id);
this.hover(buddyElement, event.type === "mouseenter" ? true : false, buddyElement.scope().session.Id);
}, this));
$element.on("click", ".buddy", _.bind(function(event) {
var buddyElement = $(event.currentTarget);
buddyElement.scope().doDefault();
}, this));
$element.attr("data-xthreshold", "10");
$element.on("swipeleft", ".buddy", _.bind(function(event) {
event.preventDefault();
var buddyElement = $(event.currentTarget);
this.hover(buddyElement, !buddyElement.hasClass("hovered"), buddyElement.scope().user.Id);
this.hover(buddyElement, !buddyElement.hasClass("hovered"), buddyElement.scope().session.Id);
}, this));
$window.setInterval(_.bind(this.soundLoop, this), 500);
@ -184,7 +188,8 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! @@ -184,7 +188,8 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
Buddylist.prototype.onBuddyScope = function(scope) {
scope.element = null;
scope.doDefault = function(id) {
scope.doDefault = function() {
var id = scope.session.Id;
if (scope.status.isMixer) {
return scope.doAudioConference(id);
}
@ -319,7 +324,7 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! @@ -319,7 +324,7 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
Buddylist.prototype.updateBuddyPicture = function(status) {
url = status.buddyPicture
url = status.buddyPicture;
if (!url) {
return;
}
@ -330,15 +335,19 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! @@ -330,15 +335,19 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
};
Buddylist.prototype.onStatus = function(status) {
Buddylist.prototype.onStatus = function(data) {
//console.log("onStatus", status);
var id = status.Id;
var id = data.Id;
var scope = buddyData.get(id, this.$scope, _.bind(this.onBuddyScope, this));
if (scope.status && scope.status.Rev >= 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! @@ -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! @@ -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) {

152
static/js/services/mediastream.js

@ -21,12 +21,13 @@ @@ -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([ @@ -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([ @@ -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 = $('<input name="id" type="hidden">');
idE.val(mediaStream.api.id);
var sidE = $('<input name="sid" type="hidden">');
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([ @@ -203,10 +317,16 @@ define([
});
}
}).
error(function() {
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) {

6
static/partials/buddy.html

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
<div class="buddy withSubline" ng-click="doDefault(user.Id)">
<div class="buddy withSubline">
<div class="avatar"><i class="fa fa-user fa-3x"/><img ng-show="status.buddyPicture" alt ng-src="{{status.buddyPicture}}" width="46" height="46"/></div>
<div class="display-name">{{user.Id|displayName}}</div>
<div class="browser">{{user.Ua}}</div>
<div class="display-name">{{session.Id|displayName}}</div>
<div class="browser"><i ng-show="session.Userid" class="fa fa-star-o"></i> {{session.Ua}}</div>
</div>

2
static/partials/buddyactions.html

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
<div class="buddyactions active">
<a class="btn btn-info" title="{{_('Start video call')}}"><i class="fa fa-eye"></i></a>
<a class="btn btn-info" title="{{_('Start chat')}}" ng-click="doChat(user.Id); $event.stopPropagation()"><i class="fa fa-comments-o"></i></a>
<a class="btn btn-info" title="{{_('Start chat')}}" ng-click="doChat(session.Id); $event.stopPropagation()"><i class="fa fa-comments-o"></i></a>
</div>

2
static/partials/buddyactionsforaudiomixer.html

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
<div class="buddyactions active">
<a class="btn btn-info" title="{{_('Start audio conference')}}" ng-click="doAudioConference(user.Id); ; $event.stopPropagation()"><i class="fa fa-users"></i></a>
<a class="btn btn-info" title="{{_('Start audio conference')}}" ng-click="doAudioConference(session.Id); ; $event.stopPropagation()"><i class="fa fa-users"></i></a>
</div>

39
static/partials/settings.html

@ -1,13 +1,13 @@ @@ -1,13 +1,13 @@
<div class="settings nicescroll">
<div class="version">{{version}}</div>
<form class="form-horizontal" on-enter="saveSettings(user)" on-escape="reset()"
<div class="form-horizontal" on-enter="saveSettings(user)" on-escape="reset()"
<fieldset>
<legend>{{_('Settings')}}</legend>
<div class="form-group">
<label class="col-xs-4 control-label">{{_('Your picture')}}</label>
<div class="col-xs-8">
<label style="padding-top:5px">
<label>
<div style="margin-bottom:5px">
<img ng-show="user.buddyPicture" ng-src="{{user.buddyPicture}}" alt="" />
</div>
@ -26,6 +26,36 @@ @@ -26,6 +26,36 @@
<span class="help-block">{{_('Your picture and name are visible to others.')}}</span>
</div>
</div>
<div class="form-group" ng-if="withUsers || userid">
<label class="col-xs-4 control-label">{{_('Your ID')}}</label>
<div ng-switch="withUsersMode">
<form ng-switch-when="certificate" class="col-xs-8" target="users_registration_certificate_iframe">
<div ng-if="!userid">
<keygen style="display:none" name="pubkey"/>
<label ng-if="!userid && withUsersRegistration">
<button class="btn btn-small btn-primary" ng-click="registerUserid($event.target)">{{_('Register')}}</button>
</label>
<iframe style="display:none" name="users_registration_certificate_iframe"></iframe>
</div>
<div ng-if="userid">
<pre class="small">{{userid}}</pre>
<span class-"help-block">{{_('Authenticated by certificate. To log out you have to remove your certificate from the browser.')}}</span>
</div>
</form>
<div ng-switch-default class="col-xs-8">
<label ng-if="!userid && withUsersRegistration">
<button class="btn btn-small btn-primary" ng-click="registerUserid($event.target)">{{_('Register')}}</button>
</label>
<pre class="small" ng-if="userid">{{userid}}</pre>
<label ng-if="userid && loadedUserlogin">
<button class="btn btn-small btn-default" ng-click="forgetUserid()">{{_('Log out')}}</button>
</label>
</div>
</div>
<div class="col-xs-8 col-xs-offset-4" ng-if="!userid && withUsersRegistration">
<span class="help-block">{{_('Only register an ID if this is your private browser.')}}</span>
</div>
</div>
<hr/>
<div ng-show="mediaSources.supported" class="form-group">
<label class="col-xs-4 control-label">{{_('Microphone')}}</label>
@ -132,11 +162,12 @@ @@ -132,11 +162,12 @@
<label>
<input type="checkbox" ng-model="rememberSettings"> {{_('Remember settings')}}
</label>
<p class="text-warning" style="margin-top:4px" ng-show="userid &&!rememberSettings && loadedUserlogin"><strong>{{_('Your ID will still be kept - press the log out button above to delete the ID.')}}</strong></p>
</div>
<a ng-click="saveSettings(user)" class="btn btn-primary">{{_('Apply')}}</a> <a ng-click="cancelSettings()" class="btn btn-default">{{_('Cancel')}}</a>
<a ng-click="layout.settings=false" class="btn btn-default">{{_('Close')}}</a>
</div>
</div>
</fieldset>
</form>
</div>
</div>

2
static/translation/messages-de.json

File diff suppressed because one or more lines are too long

2
static/translation/messages-ja.json

File diff suppressed because one or more lines are too long

2
static/translation/messages-ko.json

File diff suppressed because one or more lines are too long

2
static/translation/messages-zh-cn.json

File diff suppressed because one or more lines are too long

2
static/translation/messages-zh-tw.json

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save