22 changed files with 493 additions and 41 deletions
|
@ -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 @@ |
|||||||
|
/* |
||||||
|
* 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