Browse Source
To provide peer to peer support for users with a firewall, a TURN service might be required. Not everyone can/wants to setup a self-hosted TURN server. This change adds support to consume a remote TURN service which usually requires authentication. Spreed WebRTC received TURN credentials using this service in regular intervals and provides them to all Spreed WebRTC sessions. If the remote TURN service provides multiple zones and a GEO location endpoint, the web client will also directly connect to that TURN service GEO endpoint to let the TURN service select preferred zones based on the clients information. The advanced settings provide a way to control the TURN service zone directly and to disable the client side GEO call. By default the selection is auto if the TURN service provides a GEO endpoint. If no such endpoint is required, the zone with the highest priority is used by default (as sent by the TURN credentials service).pull/346/head
22 changed files with 493 additions and 41 deletions
|
@ -0,0 +1,125 @@
@@ -0,0 +1,125 @@
|
||||
/* |
||||
* Spreed WebRTC. |
||||
* Copyright (C) 2013-2016 struktur AG |
||||
* |
||||
* This file is part of Spreed WebRTC. |
||||
* |
||||
* 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 channelling |
||||
|
||||
import ( |
||||
"log" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/strukturag/spreed-turnservicecli/turnservicecli" |
||||
) |
||||
|
||||
type TURNServiceManager interface { |
||||
TurnDataCreator |
||||
} |
||||
|
||||
type turnServiceManager struct { |
||||
sync.Mutex |
||||
pleaders map[uint64]Sender // Mapping of clients waiting to receive TURN data.
|
||||
|
||||
uri string |
||||
accessToken string |
||||
clientID string |
||||
turnService *turnservicecli.TURNService |
||||
} |
||||
|
||||
func NewTURNServiceManager(uri string, accessToken string, clientID string) TURNServiceManager { |
||||
turnService := turnservicecli.NewTURNService(uri, 0, nil) |
||||
mgr := &turnServiceManager{ |
||||
uri: uri, |
||||
accessToken: accessToken, |
||||
clientID: clientID, |
||||
|
||||
turnService: turnService, |
||||
pleaders: make(map[uint64]Sender), |
||||
} |
||||
|
||||
turnService.Open(accessToken, clientID, "") |
||||
turnService.BindOnCredentials(mgr.onCredentials) |
||||
log.Println("Fetching TURN credentials from service") |
||||
go func() { |
||||
//time.Sleep(10000 * time.Millisecond)
|
||||
turnService.Autorefresh(true) |
||||
}() |
||||
// Wait a bit, to give TURN service some time to populate credentials, so
|
||||
// we avoid to have send them as an update for fast reconnecting clients.
|
||||
time.Sleep(500 * time.Millisecond) |
||||
if mgr.turnService.Credentials(false) == nil { |
||||
log.Println("No TURN credentials from service on startup - extra traffic for clients connecting before credentials have been received") |
||||
} |
||||
|
||||
return mgr |
||||
} |
||||
|
||||
func (mgr *turnServiceManager) CreateTurnData(sender Sender, session *Session) *DataTurn { |
||||
credentials := mgr.turnService.Credentials(false) |
||||
turn, err := mgr.turnData(credentials) |
||||
if err != nil || turn.Ttl == 0 { |
||||
// When no data was return from service, refresh quickly.
|
||||
mgr.Lock() |
||||
mgr.pleaders[sender.Index()] = sender |
||||
mgr.Unlock() |
||||
|
||||
// Have client come back early.
|
||||
turn.Ttl = 300 |
||||
} |
||||
|
||||
return turn |
||||
} |
||||
|
||||
func (mgr *turnServiceManager) turnData(credentials *turnservicecli.CachedCredentialsData) (*DataTurn, error) { |
||||
turn := &DataTurn{} |
||||
if credentials != nil { |
||||
ttl := credentials.TTL() |
||||
if ttl > 0 { |
||||
turn.Username = credentials.Turn.Username |
||||
turn.Password = credentials.Turn.Password |
||||
turn.Servers = credentials.Turn.Servers |
||||
turn.Ttl = int(ttl) |
||||
turn.GeoURI = credentials.Turn.GeoURI |
||||
} |
||||
} |
||||
|
||||
return turn, nil |
||||
} |
||||
|
||||
func (mgr *turnServiceManager) onCredentials(credentials *turnservicecli.CachedCredentialsData, err error) { |
||||
if err != nil { |
||||
log.Printf("TURN credentials service error: %s\n", err.Error()) |
||||
return |
||||
} |
||||
|
||||
log.Println("Received TURN credentials from service", credentials.Turn.Username) |
||||
|
||||
mgr.Lock() |
||||
for _, sender := range mgr.pleaders { |
||||
if turn, err := mgr.turnData(credentials); err == nil { |
||||
sender.Outgoing(&DataTurnUpdate{ |
||||
Type: "TurnUpdate", |
||||
Turn: turn, |
||||
}) |
||||
} |
||||
} |
||||
mgr.pleaders = make(map[uint64]Sender) // Clear.
|
||||
mgr.Unlock() |
||||
} |
||||
@ -0,0 +1,180 @@
@@ -0,0 +1,180 @@
|
||||
/* |
||||
* Spreed WebRTC. |
||||
* Copyright (C) 2013-2016 struktur AG |
||||
* |
||||
* This file is part of Spreed WebRTC. |
||||
* |
||||
* 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/>.
|
||||
* |
||||
*/ |
||||
|
||||
"use strict"; |
||||
define(["jquery"], function($) { |
||||
var geoRequestTimeout = 30000; // Timeout for geo requests in milliseconds.
|
||||
var geoFastRetryTimeout = 45000; // Refresh timer in milliseconds, after which GEO requests should be retried if failed before.
|
||||
var refreshPercentile = 90; // Percent of the TTL when TURN credentials should be refreshed.
|
||||
|
||||
// turnData
|
||||
return ["$timeout", "$http", "api", "randomGen", "appData", function($timeout, $http, api, randomGen, appData) { |
||||
var ttlTimeout = null; |
||||
var geoRefresh = null; |
||||
var geoPreferred = null; |
||||
|
||||
var service = this; |
||||
service.e = $({}); |
||||
service.data = {}; |
||||
|
||||
service.apply = function() { |
||||
var turn = service.data; |
||||
var turnData = { |
||||
"username": turn.username, |
||||
"password": turn.password, |
||||
"ttl": turn.ttl |
||||
}; |
||||
if (turn && turn.servers) { |
||||
// Multiple options, need to sort and use settings.
|
||||
if (!turn.serverMap) { |
||||
var servers = {}; |
||||
turn.servers.sort(function(a, b) { |
||||
servers[a.id] = a; |
||||
servers[b.id] = b; |
||||
return (a.prio > b.prio) ? 1 : ((a.prio < b.prio) ? -1 : 0); |
||||
}); |
||||
turn.first = turn.servers[0]; |
||||
if (turn.geo_uri) { |
||||
turn.servers.unshift({ |
||||
"id": "auto" |
||||
}) |
||||
} |
||||
turn.serverMap = servers; |
||||
} |
||||
var urls; |
||||
if (turn.preferred) { |
||||
for (var i=0; i<turn.preferred.length; i++) { |
||||
if (turn.serverMap.hasOwnProperty(turn.preferred[i])) { |
||||
urls = turn.serverMap[turn.preferred[i]].urns; |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
if (!urls && turn.first) { |
||||
urls = turn.first.urns; |
||||
} |
||||
turnData.urls = urls; |
||||
} else if (turn && turn.urls) { |
||||
// Simple case, single region.
|
||||
turnData.urls = turn.urls |
||||
} else { |
||||
// Unknown data.
|
||||
turnData.urls = []; |
||||
} |
||||
console.log("TURN servers selected: ", turnData.urls, turn.preferred || null); |
||||
service.e.triggerHandler("apply", [turnData]); |
||||
|
||||
return turnData; |
||||
}; |
||||
|
||||
service.refresh = function(withGeo) { |
||||
$timeout.cancel(geoRefresh); |
||||
|
||||
var turn = service.data; |
||||
service.e.triggerHandler("refresh", [turn]); |
||||
if (turn.selected === "auto" && turn.geo_uri) { |
||||
if (geoPreferred !== null) { |
||||
// Use existing data.
|
||||
turn.preferred = geoPreferred; |
||||
|
||||
} else { |
||||
if (!withGeo) { |
||||
// Avoid triggering spurious GEO request for fast updates.
|
||||
geoRefresh = $timeout(function() { |
||||
service.refresh(true); |
||||
}, 1000); |
||||
return; |
||||
} |
||||
|
||||
// Run Geo request.
|
||||
var nonce = randomGen.random({hex: true}); |
||||
$http({ |
||||
method: "POST", |
||||
url: turn.geo_uri, |
||||
headers: {"Content-Type": "application/x-www-form-urlencoded"}, |
||||
data: "nonce="+encodeURIComponent(nonce)+"&username="+encodeURIComponent(turn.username)+"&password="+encodeURIComponent(turn.password), |
||||
timeout: geoRequestTimeout |
||||
}).then(function(response) { |
||||
// success
|
||||
if (turn !== service.data) { |
||||
// No longer our data.
|
||||
return; |
||||
} |
||||
if (response.status === 200) { |
||||
var data = response.data; |
||||
if (data.success && data.nonce === nonce) { |
||||
geoPreferred = turn.preferred = data.geo.prefer; |
||||
console.log("TURN GEO auto selected: ", turn.preferred); |
||||
service.apply(); |
||||
} |
||||
} |
||||
}, function(response) { |
||||
// failed
|
||||
if (turn !== service.data) { |
||||
// No longer our data.
|
||||
return; |
||||
} |
||||
console.warn("TURN GEO failed:", response.status, response); |
||||
$timeout.cancel(ttlTimeout); |
||||
ttlTimeout = $timeout(function() { |
||||
// Fast retry.
|
||||
console.warn("TURN GEO failed - refreshing early.") |
||||
api.sendSelf(); |
||||
}, geoFastRetryTimeout) |
||||
}) |
||||
} |
||||
} else { |
||||
// Set directly.
|
||||
turn.preferred = []; |
||||
if (turn.selected) { |
||||
turn.preferred.push(turn.selected); |
||||
} |
||||
} |
||||
service.apply(); |
||||
}; |
||||
|
||||
service.update = function(turn) { |
||||
$timeout.cancel(ttlTimeout); |
||||
if (service.data && service.data.preferred) { |
||||
// Keep preferred list if there is one.
|
||||
turn.preferred = service.data.preferred; |
||||
} |
||||
service.data = turn; |
||||
service.refresh() |
||||
|
||||
// Support to refresh TURN data when ttl was reached.
|
||||
if (turn.ttl) { |
||||
ttlTimeout = $timeout(function() { |
||||
console.log("TURN TTL reached - sending refresh request."); |
||||
api.sendSelf(); |
||||
}, turn.ttl * 0.01 * refreshPercentile * 1000); |
||||
} |
||||
}; |
||||
|
||||
service.cancel = function() { |
||||
$timeout.cancel(ttlTimeout); |
||||
} |
||||
|
||||
appData.e.on("userSettingsLoaded", service.refresh); |
||||
|
||||
return service; |
||||
}] |
||||
}) |
||||
Loading…
Reference in new issue