configuring cloudlare turn service

This commit is contained in:
Roland Osborne 2024-05-29 23:43:16 -07:00
parent 2804de9972
commit 0f51f7b5d4
14 changed files with 150 additions and 35 deletions

View File

@ -4119,6 +4119,8 @@ components:
type: boolean
enableIce:
type: boolean
iceService:
type: boolean
iceUrl:
type: string
iceUsername:
@ -4130,6 +4132,7 @@ components:
openAccessLimit:
type: integer
format: int64
Seal:
type: object
@ -4899,12 +4902,28 @@ components:
keepAlive:
type: integer
format: int32
iceService:
type: boolean
iceUrl:
type: string
iceUsername:
type: string
icePassword:
type: string
IceUrl:
tyle: object
required:
- urls
- username
- credential
properties:
urls:
type: string
username:
type: string
credential:
type: string
Ring:
type: object
@ -4920,6 +4939,10 @@ components:
index:
type: integer
format: int32
ice:
type: array
items:
$ref: '#/components/schemas/IceUrl'
iceUrl:
type: string
iceUsername:

View File

@ -61,6 +61,7 @@ func AddCall(w http.ResponseWriter, r *http.Request) {
// allocate bridge
callerToken := hex.EncodeToString(callerBin);
calleeToken := hex.EncodeToString(calleeBin);
iceService := getBoolConfigValue(CNFIceService, false);
iceUrl := getStrConfigValue(CNFIceUrl, "")
iceUsername := getStrConfigValue(CNFIceUsername, "")
icePassword := getStrConfigValue(CNFIcePassword, "")
@ -72,6 +73,7 @@ func AddCall(w http.ResponseWriter, r *http.Request) {
CardId: cardId,
CallerToken: callerToken,
CalleeToken: calleeToken,
IceService: iceService,
IceUrl: iceUrl,
IceUsername: iceUsername,
IcePassword: icePassword,

View File

@ -25,6 +25,7 @@ func GetNodeConfig(w http.ResponseWriter, r *http.Request) {
config.KeyType = getStrConfigValue(CNFKeyType, APPRSA2048)
config.PushSupported = getBoolConfigValue(CNFPushSupported, true)
config.EnableIce = getBoolConfigValue(CNFEnableIce, false)
config.IceService = getBoolConfigValue(CNFIceService, false)
config.IceUrl = getStrConfigValue(CNFIceUrl, "")
config.IceUsername = getStrConfigValue(CNFIceUsername, "")
config.IcePassword = getStrConfigValue(CNFIcePassword, "")

View File

@ -101,7 +101,7 @@ func SetNodeConfig(w http.ResponseWriter, r *http.Request) {
return res
}
// upsert push supported
// upsert ice supported
if res := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "config_id"}},
DoUpdates: clause.AssignmentColumns([]string{"bool_value"}),
@ -109,6 +109,14 @@ func SetNodeConfig(w http.ResponseWriter, r *http.Request) {
return res
}
// upsert ice service used
if res := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "config_id"}},
DoUpdates: clause.AssignmentColumns([]string{"bool_value"}),
}).Create(&store.Config{ConfigID: CNFIceService, BoolValue: config.IceService}).Error; res != nil {
return res
}
// upsert key type
if res := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "config_id"}},

View File

@ -155,6 +155,7 @@ func SetRing(card *store.Card, ring Ring) {
var phone Phone
phone.CallID = ring.CallID
phone.CalleeToken = ring.CalleeToken
phone.Ice = ring.Ice
phone.IceUrl = ring.IceUrl
phone.IceUsername = ring.IceUsername
phone.IcePassword = ring.IcePassword

View File

@ -54,6 +54,9 @@ const CNFKeyType = "key_type"
//CNFEnableIce specifies whether webrtc is enabled
const CNFEnableIce = "enable_ice"
//CNFIceMode specifies if turn service is used
const CNFIceService = "ice_service"
//CNFIceUrl specifies the ice candidate url
const CNFIceUrl = "ice_url"

View File

@ -375,6 +375,8 @@ type NodeConfig struct {
EnableIce bool `json:"enableIce"`
IceService bool `json:"iceService"`
IceUrl string `json:"iceUrl"`
IceUsername string `json:"iceUsername"`
@ -458,6 +460,8 @@ type Phone struct {
CalleeToken string `json:"calleeToken"`
Ice []IceUrl `json:"ice,omitEmpty"`
IceUrl string `json:"iceUrl"`
IceUsername string `json:"iceUsername"`
@ -563,6 +567,8 @@ type Call struct {
KeepAlive int32 `json:"keepAlive"`
IceService bool `json:"iceService"`
IceUrl string `json:"iceUrl"`
IceUsername string `json:"iceUsername"`
@ -570,6 +576,15 @@ type Call struct {
IcePassword string `json:"icePassword"`
}
type IceUrl struct {
URLs string `json:"urls"`
Username string `json:"username"`
Credential string `json:"credential"`
}
type Ring struct {
CallID string `json:"callId"`
@ -578,6 +593,8 @@ type Ring struct {
Index int32 `json:"index"`
Ice []IceUrl `json:"ice,omitEmpty"`
IceUrl string `json:"iceUrl"`
IceUsername string `json:"iceUsername"`

View File

@ -144,6 +144,8 @@ export const en = {
binaryHint: 'Allow binary files to be posted in topics',
enableWeb: 'Enable WebRTC Calls',
webHint: 'Enable audio and video calls to contacts',
enableService: 'Cloudflare Service',
serviceHint: 'Enable Cloudflare Service',
serverUrl: 'WebRTC Server URL',
urlHint: 'turn:ip:port?transport=udp',
webUsername: 'WebRTC Username',
@ -350,6 +352,8 @@ export const fr = {
binaryHint: 'Autoriser la publication de fichiers binaires dans les sujets',
enableWeb: 'Activer les Appels WebRTC',
webHint: 'Autoriser les appels audio et vidéo aux contacts',
enableService: 'Service Cloudflare',
serviceHint: 'Activer le Service Cloudflare',
serverUrl: 'URL du Serveur WebRTC',
urlHint: 'turn:ip:port?transport=udp',
webUsername: "Nom d'Utilisateur WebRTC",
@ -557,6 +561,8 @@ export const sp = {
binaryHint: 'Permitir que se publiquen archivos binarios en temas',
enableWeb: 'Activar llamadas WebRTC',
webHint: 'Permitir llamadas de audio y video a contactos',
enableService: 'Servicio Cloudflare',
serviceHint: 'Habilitar el Servicio Cloudflare',
serverUrl: 'URL del servidor WebRTC',
urlHint: 'turn:ip:puerto?transporte=udp',
webUsername: 'Nombre de usuario WebRTC',
@ -763,6 +769,8 @@ export const pt = {
binaryHint: 'Permitir que arquivos binários sejam postados em tópicos',
enableWeb: 'Ativar chamadas WebRTC',
webHint: 'Permitir chamadas de áudio e vídeo para contatos',
enableService: 'Serviço Cloudflare',
serviceHint: 'Habilitar serviço Cloudflare',
serverUrl: 'URL do servidor WebRTC',
urlHint: 'turn:ip:port?transport=udp',
webUsername: 'Nome de usuário WebRTC',
@ -969,6 +977,8 @@ export const de = {
binaryHint: 'Erlauben Sie die Veröffentlichung von Binärdateien in Themen',
enableWeb: 'WebRTC-Anrufe aktivieren',
webHint: 'Audio- und Videoanrufe an Kontakte zulassen',
enableService: 'Cloudflare-Dienst',
serviceHint: 'Aktivieren Sie den Cloudflare-Dienst',
serverUrl: 'URL des WebRTC-Servers',
urlHint: 'turn:ip:port?transport=udp',
webUsername: 'WebRTC-Benutzername',
@ -1175,6 +1185,8 @@ export const ru = {
binaryHint: 'Разрешить публикацию двоичных файлов в темах',
enableWeb: 'Включить WebRTC-звонки',
webHint: 'Разрешить аудио- и видеозвонки контактам',
enableService: 'Сервис Cloudflare',
serviceHint: 'Включить службу Cloudflare',
serverUrl: 'URL сервера WebRTC',
urlHint: 'turn:ip:port?transport=udp',
webUsername: 'Имя пользователя WebRTC',

View File

@ -185,8 +185,9 @@ export function useAppContext(websocket) {
setAppRevision(activity.revision);
}
else if (activity.ring) {
const { cardId, callId, calleeToken, iceUrl, iceUsername, icePassword } = activity.ring;
ringContext.actions.ring(cardId, callId, calleeToken, iceUrl, iceUsername, icePassword);
const { cardId, callId, calleeToken, ice, iceUrl, iceUsername, icePassword } = activity.ring;
const config = ice ? ice : [{ urls: iceUrl, username: iceUsername, credential: icePassword }];
ringContext.actions.ring(cardId, callId, calleeToken, ice);
}
else {
setAppRevision(activity);

View File

@ -134,6 +134,28 @@ export function useRingContext() {
processing.current = false;
}
const getIce = async (service, urls, username, credential) => {
if (!service) {
return [{ urls, username, credential }];
}
const params = await fetch(urls.replace('%%TURN_KEY_ID%%', username), {
method: 'POST',
body: '{"ttl": 86400}',
headers: new Headers({
'Authorization': `Bearer ${credential}`,
'Content-Type': 'application/json'
})
});
const server = await params.json();
const ice = [];
server.iceServers.urls.forEach(urls => {
const { username, credential } = server.iceServers;
ice.push({ urls, username, credential });
});
return ice;
}
const getAudioStream = async (audioId) => {
try {
if (audioId) {
@ -300,9 +322,9 @@ export function useRingContext() {
clearToken: () => {
access.current = null;
},
ring: (cardId, callId, calleeToken, iceUrl, iceUsername, icePassword) => {
ring: (cardId, callId, calleeToken, ice) => {
const key = `${cardId}:${callId}`
const call = ringing.current.get(key) || { cardId, calleeToken, callId, iceUrl, iceUsername, icePassword }
const call = ringing.current.get(key) || { cardId, calleeToken, callId, ice }
call.expires = Date.now() + EXPIRE;
ringing.current.set(key, call);
updateState({ ringing: ringing.current });
@ -334,13 +356,11 @@ export function useRingContext() {
}
}
},
accept: async (cardId, callId, contactNode, contactToken, calleeToken, iceUrl, iceUsername, icePassword, audioId) => {
accept: async (cardId, callId, contactNode, contactToken, calleeToken, ice, audioId) => {
if (calling.current) {
throw new Error("active session");
}
const ice = [{ urls: iceUrl, username: iceUsername, credential: icePassword }];
const key = `${cardId}:${callId}`
const call = ringing.current.get(key);
if (call) {
@ -389,6 +409,7 @@ export function useRingContext() {
// create call
let call;
let ice;
try {
call = await addCall(access.current, cardId);
}
@ -398,9 +419,11 @@ export function useRingContext() {
}
let index = 0;
const { id, keepAlive, callerToken, calleeToken, iceUrl, iceUsername, icePassword } = call;
const { id, keepAlive, callerToken, calleeToken, iceService, iceUrl, iceUsername, icePassword } = call;
try {
await addContactRing(contactNode, contactToken, { index, callId: id, calleeToken, iceUrl, iceUsername, icePassword });
ice = await getIce(iceService, iceUrl, iceUsername, icePassword);
const turn = ice[ice.length - 1];
await addContactRing(contactNode, contactToken, { index, callId: id, calleeToken, ice, iceUrl: turn.urls, iceUsername: turn.username, icePassword: turn.credential });
}
catch (err) {
console.log(err);
@ -421,7 +444,8 @@ export function useRingContext() {
}
}
else {
await addContactRing(contactNode, contactToken, { index, callId: id, calleeToken, iceUrl, iceUsername, icePassword });
const turn = ice[ice.length - 1];
await addContactRing(contactNode, contactToken, { index, callId: id, calleeToken, ice, iceUrl: turn.urls, iceUsername: turn.username, icePassword: turn.credential });
index += 1;
}
}
@ -432,7 +456,6 @@ export function useRingContext() {
updateState({ callStatus: "ringing" });
calling.current = { callId: id, host: true };
const ice = [{ urls: iceUrl, username: iceUsername, credential: icePassword }];
await connect('polite', audioId, window.location.host, callerToken, () => clearInterval(ringInterval), () => clearInterval(aliveInterval), ice);
},
enableVideo: async (videoId) => {

View File

@ -265,21 +265,34 @@ export function Dashboard() {
defaultChecked={false} checked={state.enableIce} />
</div>
</Tooltip>
<div className="field">
<div>{state.strings.serverUrl}</div>
<Input placeholder={state.strings.urlHint} onChange={(e) => actions.setIceUrl(e.target.value)}
disabled={!state.enableIce} value={state.iceUrl} />
</div>
<div className="field">
<div>{state.strings.webUsername}</div>
<Input placeholder={state.strings.username} onChange={(e) => actions.setIceUsername(e.target.value)}
disabled={!state.enableIce} value={state.iceUsername} />
</div>
<div className="field">
<div>{state.strings.webPassword}</div>
<Input placeholder={state.strings.password} onChange={(e) => actions.setIcePassword(e.target.value)}
disabled={!state.enableIce} value={state.icePassword} />
</div>
{ state.enableIce && (
<div className="iceInput">
<Tooltip placement="topLeft" title={state.strings.serviceHint}>
<div className="field">
<div>{state.strings.enableService}</div>
<Switch onChange={(e) => actions.setIceService(e)} size="small"
defaultChecked={false} checked={state.iceService} />
</div>
</Tooltip>
{ !state.iceService && (
<div className="field">
<div>{state.strings.serverUrl}</div>
<Input placeholder={state.strings.urlHint} onChange={(e) => actions.setIceUrl(e.target.value)}
value={state.iceUrl} />
</div>
)}
<div className="field">
<div>{state.strings.webUsername}</div>
<Input placeholder={state.strings.username} onChange={(e) => actions.setIceUsername(e.target.value)}
value={state.iceUsername} />
</div>
<div className="field">
<div>{state.strings.webPassword}</div>
<Input placeholder={state.strings.password} onChange={(e) => actions.setIcePassword(e.target.value)}
value={state.icePassword} />
</div>
</div>
)}
<div className="control">
<Button key="back" onClick={() => actions.setShowSettings(false)}>{state.strings.cancel}</Button>
<Button key="save" type="primary" onClick={() => actions.setSettings()} loading={state.busy}>{state.strings.save}</Button>

View File

@ -111,6 +111,12 @@ export const SettingsLayout = styled(Space)`
min-height: 32px;
}
.iceInput {
display: flex;
flex-direction: column;
gap: 8px;
}
.field {
white-space: nowrap;
display: flex;

View File

@ -27,6 +27,7 @@ export function useDashboard(token) {
enableVideo: null,
enableBinary: null,
enableIce: null,
iceService: null,
iceUrl: null,
iceUsername: null,
icePassword: null,
@ -128,6 +129,10 @@ export function useDashboard(token) {
setEnableIce: (enableIce) => {
updateState({ enableIce });
},
setIceService: (iceService) => {
const iceUrl = iceService ? 'https://rtc.live.cloudflare.com/v1/turn/keys/%%TURN_KEY_ID%%/credentials/generate' : '';
updateState({ iceService, iceUrl });
},
setIceUrl: (iceUrl) => {
updateState({ iceUrl });
},
@ -181,9 +186,9 @@ export function useDashboard(token) {
if (!state.busy) {
updateState({ busy: true });
try {
const { domain, keyType, accountStorage, pushSupported, transformSupported, allowUnsealed, enableImage, enableAudio, enableVideo, enableBinary, enableIce, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit } = state;
const { domain, keyType, accountStorage, pushSupported, transformSupported, allowUnsealed, enableImage, enableAudio, enableVideo, enableBinary, enableIce, iceService, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit } = state;
const storage = accountStorage * 1073741824;
const config = { domain, accountStorage: storage, keyType, enableImage, enableAudio, enableVideo, enableBinary, pushSupported, transformSupported, allowUnsealed, enableIce, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit };
const config = { domain, accountStorage: storage, keyType, enableImage, enableAudio, enableVideo, enableBinary, pushSupported, transformSupported, allowUnsealed, enableIce, iceService, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit };
await setNodeConfig(app.state.adminToken, config);
updateState({ busy: false, showSettings: false });
}
@ -200,9 +205,9 @@ export function useDashboard(token) {
try {
const enabled = await getAdminMFAuth(app.state.adminToken);
const config = await getNodeConfig(app.state.adminToken);
const { accountStorage, domain, keyType, pushSupported, transformSupported, allowUnsealed, enableImage, enableAudio, enableVideo, enableBinary, enableIce, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit } = config;
const { accountStorage, domain, keyType, pushSupported, transformSupported, allowUnsealed, enableImage, enableAudio, enableVideo, enableBinary, enableIce, iceService, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit } = config;
const storage = Math.ceil(accountStorage / 1073741824);
updateState({ mfAuthSet: true, mfaAuthEnabled: enabled, configError: false, domain, accountStorage: storage, keyType, enableImage, enableAudio, enableVideo, enableBinary, pushSupported, transformSupported, allowUnsealed, enableIce, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit });
updateState({ mfAuthSet: true, mfaAuthEnabled: enabled, configError: false, domain, accountStorage: storage, keyType, enableImage, enableAudio, enableVideo, enableBinary, pushSupported, transformSupported, allowUnsealed, enableIce, iceService, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit });
}
catch(err) {
console.log(err);

View File

@ -58,14 +58,14 @@ export function useSession() {
const expired = Date.now();
ring.state.ringing.forEach(call => {
if (call.expires > expired && !call.status) {
const { callId, cardId, calleeToken, iceUrl, iceUsername, icePassword } = call;
const { callId, cardId, calleeToken, ice } = call;
const contact = card.state.cards.get(cardId);
if (contact) {
const { imageSet, name, handle, node, guid } = contact.data.cardProfile || {};
const { token } = contact.data.cardDetail;
const contactToken = `${guid}.${token}`;
const img = imageSet ? card.actions.getCardImageUrl(cardId) : null;
ringing.push({ cardId, img, name, handle, contactNode: node, callId, contactToken, calleeToken, iceUrl, iceUsername, icePassword });
ringing.push({ cardId, img, name, handle, contactNode: node, callId, contactToken, calleeToken, ice });
}
}
});
@ -180,9 +180,9 @@ export function useSession() {
await ring.actions.decline(cardId, node, contactToken, callId);
},
accept: async (call) => {
const { cardId, callId, contactNode, contactToken, calleeToken, iceUrl, iceUsername, icePassword } = call;
const { cardId, callId, contactNode, contactToken, calleeToken, ice } = call;
const node = contactNode ? contactNode : window.location.host;
await ring.actions.accept(cardId, callId, node, contactToken, calleeToken, iceUrl, iceUsername, icePassword, state.audioId);
await ring.actions.accept(cardId, callId, node, contactToken, calleeToken, ice, state.audioId);
},
end: async () => {
await ring.actions.end();