Browse Source
Conflicts: src/styles/components/_audiovideo.scss static/partials/buddy.html Fixed conflicts in: src/styles/components/_audiovideo.scss static/partials/buddy.htmlpull/29/head^2
49 changed files with 4753 additions and 432 deletions
@ -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 |
||||
@ -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); |
||||
}; |
||||
|
||||
}]); |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
}); |
||||
@ -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 |
||||
|
||||
@ -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) |
||||
} |
||||
@ -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"}} |
||||
|
||||
} |
||||
@ -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") |
||||
} |
||||
@ -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{} |
||||
} |
||||
@ -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() |
||||
|
||||
} |
||||
@ -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> |
||||
|
||||
@ -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> |
||||
|
||||
@ -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> |
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue