plugin XEP-0167, XEP-0176: transport-info and ICE candidate sending are delayed if session is not active yet

This commit is contained in:
Goffi 2023-08-21 15:19:45 +02:00
commit d60b79b18f
4 changed files with 125 additions and 108 deletions

View file

@ -55,9 +55,6 @@ IQ_SET : Final = '/iq[@type="set"]'
NS_JINGLE : Final = "urn:xmpp:jingle:1"
NS_JINGLE_ERROR : Final = "urn:xmpp:jingle:errors:1"
JINGLE_REQUEST : Final = f'{IQ_SET}/jingle[@xmlns="{NS_JINGLE}"]'
STATE_PENDING : Final = "PENDING"
STATE_ACTIVE : Final = "ACTIVE"
STATE_ENDED : Final = "ENDED"
CONFIRM_TXT : Final = D_(
"{entity} want to start a jingle session with you, do you accept ?"
)
@ -89,6 +86,10 @@ class XEP_0166:
REASON_FAILED_TRANSPORT : Final = "failed-transport"
REASON_CONNECTIVITY_ERROR : Final = "connectivity-error"
STATE_PENDING : Final = "PENDING"
STATE_ACTIVE : Final = "ACTIVE"
STATE_ENDED : Final = "ENDED"
# standard actions
A_SESSION_INITIATE : Final = "session-initiate"
@ -194,7 +195,7 @@ class XEP_0166:
session_data = {
"id": sid,
"state": STATE_PENDING,
"state": XEP_0166.STATE_PENDING,
"initiator": client.jid if role == XEP_0166.ROLE_INITIATOR else peer_jid,
"role": role,
"local_jid": local_jid or client.jid,
@ -502,7 +503,8 @@ class XEP_0166:
content_elt["creator"] = content_data["creator"]
if context_elt is not None:
pass
if context_elt.parent is None:
content_elt.addChild(context_elt)
elif action == XEP_0166.A_TRANSPORT_INFO:
context_elt = transport_elt = content_elt.addElement(
"transport", content_data["transport"].namespace
@ -1191,7 +1193,7 @@ class XEP_0166:
d_list.addCallback(lambda __: iq_elt.send())
def change_state(__, session):
session["state"] = STATE_ACTIVE
session["state"] = XEP_0166.STATE_ACTIVE
d_list.addCallback(change_state, session)
d_list.addCallback(
@ -1279,7 +1281,7 @@ class XEP_0166:
# at this point we can send the <iq/> result to confirm reception of the request
client.send(xmlstream.toResponse(request, "result"))
# and change the state
session["state"] = STATE_ACTIVE
session["state"] = XEP_0166.STATE_ACTIVE
session["jingle_elt"] = jingle_elt
await defer.DeferredList(self._call_plugins(

View file

@ -115,19 +115,13 @@ class XEP_0167(BaseApplicationHandler):
# args: session_id, serialised setup data (dict with keys "role" and "sdp"),
# profile
host.bridge.add_signal(
"call_setup", ".plugin", signature="sss"
)
host.bridge.add_signal("call_setup", ".plugin", signature="sss")
# args: session_id, data, profile
host.bridge.add_signal(
"call_ended", ".plugin", signature="sss"
)
host.bridge.add_signal("call_ended", ".plugin", signature="sss")
# args: session_id, info_type, extra, profile
host.bridge.add_signal(
"call_info", ".plugin", signature="ssss"
)
host.bridge.add_signal("call_info", ".plugin", signature="ssss")
def get_handler(self, client):
return XEP_0167_handler()
@ -194,7 +188,9 @@ class XEP_0167(BaseApplicationHandler):
except KeyError:
log.warning(f"no media ID found for {media_type}: {media_data}")
try:
call_data[media_type]["ice-candidates"] = transport_data["candidates"]
call_data[media_type]["ice-candidates"] = transport_data.get(
"candidates", []
)
metadata["ice-ufrag"] = transport_data["ufrag"]
metadata["ice-pwd"] = transport_data["pwd"]
except KeyError:
@ -250,12 +246,7 @@ class XEP_0167(BaseApplicationHandler):
)
return sid
def _call_answer_sdp(
self,
session_id: str,
answer_sdp: str,
profile: str
) -> None:
def _call_answer_sdp(self, session_id: str, answer_sdp: str, profile: str) -> None:
client = self.host.get_client(profile)
session = self._j.get_session(client, session_id)
try:
@ -274,9 +265,7 @@ class XEP_0167(BaseApplicationHandler):
):
client = self.host.get_client(profile_key)
return defer.ensureDeferred(
self.call_end(
client, session_id, data_format.deserialise(data_s)
)
self.call_end(client, session_id, data_format.deserialise(data_s))
)
async def call_end(
@ -297,10 +286,7 @@ class XEP_0167(BaseApplicationHandler):
# jingle callbacks
async def confirm_incoming_call(
self,
client: SatXMPPEntity,
session: dict,
call_type: str
self, client: SatXMPPEntity, session: dict, call_type: str
) -> bool:
"""Prompt the user for a call confirmation.
@ -341,10 +327,7 @@ class XEP_0167(BaseApplicationHandler):
return accepted
async def jingle_preflight(
self,
client: SatXMPPEntity,
session: dict,
description_elt: domish.Element
self, client: SatXMPPEntity, session: dict, description_elt: domish.Element
) -> None:
"""Perform preflight checks for an incoming call session.
@ -380,7 +363,7 @@ class XEP_0167(BaseApplicationHandler):
self.host.bridge.call_ended(
session["id"],
data_format.serialise({"reason": "retracted"}),
client.profile
client.profile,
)
raise e
@ -392,13 +375,11 @@ class XEP_0167(BaseApplicationHandler):
client: SatXMPPEntity,
session: dict,
info_type: str,
info_data: dict|None = None
info_data: dict | None = None,
) -> None:
if info_type == "ringing":
if not session.get("ringing", False):
self.host.bridge.call_info(
session["id"], "ringing", "", client.profile
)
self.host.bridge.call_info(session["id"], "ringing", "", client.profile)
# we indicate that the ringing has started, to avoid sending several times
# the signal
session["ringing"] = True
@ -406,10 +387,7 @@ class XEP_0167(BaseApplicationHandler):
log.warning(f"Unknown preflight info type: {info_type!r}")
async def jingle_preflight_cancel(
self,
client: SatXMPPEntity,
session: dict,
cancel_error: exceptions.CancelError
self, client: SatXMPPEntity, session: dict, cancel_error: exceptions.CancelError
) -> None:
"""The call has been rejected"""
# call_ended is use to send the signal only once even if there are audio and video
@ -417,20 +395,15 @@ class XEP_0167(BaseApplicationHandler):
call_ended = session.get("call_ended", False)
if call_ended:
return
data = {
"reason": getattr(cancel_error, "reason", "cancelled")
}
data = {"reason": getattr(cancel_error, "reason", "cancelled")}
text = getattr(cancel_error, "text", None)
if text:
data["text"] = text
self.host.bridge.call_ended(
session["id"],
data_format.serialise(data),
client.profile
session["id"], data_format.serialise(data), client.profile
)
session["call_ended"] = True
def jingle_session_init(
self,
client: SatXMPPEntity,
@ -507,11 +480,13 @@ class XEP_0167(BaseApplicationHandler):
self.host.bridge.call_setup(
session["id"],
data_format.serialise({
"role": session["role"],
"sdp": sdp,
}),
client.profile
data_format.serialise(
{
"role": session["role"],
"sdp": sdp,
}
),
client.profile,
)
answer_sdp = await answer_sdp_d
@ -559,11 +534,13 @@ class XEP_0167(BaseApplicationHandler):
answer_sdp = mapping.generate_sdp_from_session(session)
self.host.bridge.call_setup(
session["id"],
data_format.serialise({
"role": session["role"],
"sdp": answer_sdp,
}),
client.profile
data_format.serialise(
{
"role": session["role"],
"sdp": answer_sdp,
}
),
client.profile,
)
else:
log.warning(f"FIXME: unmanaged action {action}")
@ -635,9 +612,7 @@ class XEP_0167(BaseApplicationHandler):
reason_elt: domish.Element,
) -> None:
reason, text = self._j.parse_reason_elt(reason_elt)
data = {
"reason": reason
}
data = {"reason": reason}
if text:
data["text"] = text
self.host.bridge.call_ended(

View file

@ -439,12 +439,11 @@ def parse_sdp(sdp: str) -> dict:
try:
ice_candidates = first_content["transport_data"]["candidates"]
except KeyError:
log.warning("missing candidates in SDP")
else:
for idx, content in enumerate(all_media.values()):
if idx == 0:
continue
content["transport_data"].setdefault("candidates", ice_candidates)
ice_candidates = []
for idx, content in enumerate(all_media.values()):
if idx == 0:
continue
content["transport_data"].setdefault("candidates", ice_candidates)
return call_data

View file

@ -36,7 +36,7 @@ from .plugin_xep_0166 import BaseTransportHandler
log = getLogger(__name__)
NS_JINGLE_ICE_UDP= "urn:xmpp:jingle:transports:ice-udp:1"
NS_JINGLE_ICE_UDP = "urn:xmpp:jingle:transports:ice-udp:1"
PLUGIN_INFO = {
C.PI_NAME: "Jingle ICE-UDP Transport Method",
@ -53,7 +53,6 @@ PLUGIN_INFO = {
class XEP_0176(BaseTransportHandler):
def __init__(self, host):
log.info(f"plugin {PLUGIN_INFO[C.PI_NAME]!r} initialization")
self.host = host
@ -86,11 +85,13 @@ class XEP_0176(BaseTransportHandler):
profile_key: str,
):
client = self.host.get_client(profile_key)
return defer.ensureDeferred(self.ice_candidates_add(
client,
session_id,
data_format.deserialise(media_ice_data_s),
))
return defer.ensureDeferred(
self.ice_candidates_add(
client,
session_id,
data_format.deserialise(media_ice_data_s),
)
)
def build_transport(self, ice_data: dict) -> domish.Element:
"""Generate <transport> element from ICE data
@ -119,14 +120,13 @@ class XEP_0176(BaseTransportHandler):
try:
ufrag: str = ice_data["ufrag"]
pwd: str = ice_data["pwd"]
candidates: List[dict] = ice_data["candidates"]
except KeyError as e:
raise exceptions.DataError(f"ICE {e} must be provided")
candidates: List[dict] = ice_data.get("candidates", [])
candidates.sort(key=lambda c: int(c.get("priority", 0)), reverse=True)
transport_elt = domish.Element(
(NS_JINGLE_ICE_UDP, "transport"),
attribs={"ufrag": ufrag, "pwd": pwd}
(NS_JINGLE_ICE_UDP, "transport"), attribs={"ufrag": ufrag, "pwd": pwd}
)
for candidate in candidates:
@ -162,10 +162,7 @@ class XEP_0176(BaseTransportHandler):
@return: ICE data (as in [build_transport])
"""
try:
ice_data = {
"ufrag": transport_elt["ufrag"],
"pwd": transport_elt["pwd"]
}
ice_data = {"ufrag": transport_elt["ufrag"], "pwd": transport_elt["pwd"]}
except KeyError as e:
raise exceptions.DataError(
f"<transport> is missing mandatory attribute {e}: {transport_elt.toXml()}"
@ -256,11 +253,36 @@ class XEP_0176(BaseTransportHandler):
peer_ice_data = self.parse_transport(transport_elt)
transport_data["peer_ice_data"] = peer_ice_data
elif action in (self._j.A_ACCEPTED_ACK, self._j.A_PREPARE_RESPONDER):
elif action == self._j.A_ACCEPTED_ACK:
buffer = session.pop("XEP-0176_handler_buffer", None)
if buffer:
log.debug("replaying buffered events")
for args in buffer:
await self.jingle_handler(*args)
elif action == self._j.A_PREPARE_RESPONDER:
pass
elif action == self._j.A_SESSION_ACCEPT:
pass
# we check if we have any buffered ICE candidates, and send them if it's the
# case
media_type = content_data["application_data"].get("media")
try:
buffer = session["XEP-0176_buffer"]
buffered_ice_data = buffer.pop(media_type)
except KeyError:
pass
else:
if not buffer:
del session["XEP-0176_buffer"]
transport_elt = self.build_transport(buffered_ice_data)
iq_elt, __ = self._j.build_action(
client,
self._j.A_TRANSPORT_INFO,
session,
content_name,
context_elt=transport_elt,
)
await iq_elt.send()
elif action == self._j.A_START:
pass
@ -269,6 +291,13 @@ class XEP_0176(BaseTransportHandler):
# responder side, we give our candidates
transport_elt = self.build_transport(transport_data["local_ice_data"])
elif action == self._j.A_TRANSPORT_INFO:
if session["state"] == self._j.STATE_PENDING:
# Session is not yet active; we buffer the arguments to replay them
# when the session becomes active. This makes the frontend's life easier.
log.debug("session is not active yet, buffering transport-info element")
buffer = session.setdefault("XEP-0176_handler_buffer", [])
buffer.append([client, action, session, content_name, transport_elt])
return transport_elt
media_type = content_data["application_data"].get("media")
new_ice_data = self.parse_transport(transport_elt)
@ -283,10 +312,10 @@ class XEP_0176(BaseTransportHandler):
self.host.bridge.ice_candidates_new(
session["id"],
data_format.serialise({media_type: new_ice_data}),
client.profile
client.profile,
)
elif action == self._j.A_DESTROY:
pass
pass
else:
log.warning("FIXME: unmanaged action {}".format(action))
@ -303,10 +332,7 @@ class XEP_0176(BaseTransportHandler):
log.debug("ICE-UDP session terminated")
def update_candidates(
self,
transport_data: dict,
new_ice_data: dict,
local: bool
self, transport_data: dict, new_ice_data: dict, local: bool
) -> bool:
"""Update ICE candidates when new one are received
@ -320,9 +346,7 @@ class XEP_0176(BaseTransportHandler):
try:
ice_data = transport_data[key]
except KeyError:
log.warning(
f"no {key} available"
)
log.warning(f"no {key} available")
transport_data[key] = new_ice_data
else:
if (
@ -336,28 +360,45 @@ class XEP_0176(BaseTransportHandler):
return False
async def ice_candidates_add(
self,
client: SatXMPPEntity,
session_id: str,
media_ice_data: Dict[str, dict]
self, client: SatXMPPEntity, session_id: str, media_ice_data: Dict[str, dict]
) -> None:
"""Called when a new ICE candidates are available for a session
@param session_id: Session ID
@param candidates: a map from media type (audio, video) to ICE data
@param media_ice_data: a map from media type (audio, video) to ICE data
ICE data must be in the same format as in [self.parse_transport]
"""
session = self._j.get_session(client, session_id)
iq_elt: Optional[domish.Element] = None
for media_type, new_ice_data in media_ice_data.items():
if session["state"] == self._j.STATE_PENDING:
log.debug(f"session not active, buffering")
buffer = session.setdefault("XEP-0176_buffer", {})
media_buffer = buffer.setdefault(media_type, {})
for key in ["ufrag", "pwd"]:
if key not in media_buffer:
media_buffer[key] = new_ice_data[key]
else:
if media_buffer[key] != new_ice_data[key]:
log.warning(
f"{key} conflict, new value will replace old one\n"
f"buffer={media_buffer[key]!r}\n"
f"new={new_ice_data[key]!r}"
)
media_buffer[key] = new_ice_data[key]
media_buffer.setdefault("candidates", []).extend(
new_ice_data["candidates"]
)
continue
for content_name, content_data in session["contents"].items():
if content_data["application_data"].get("media") == media_type:
break
else:
log.warning(
"no media of type {media_type} has been found"
)
log.warning(f"no media of type {media_type} has been found")
continue
restart = self.update_candidates(
content_data["transport_data"], new_ice_data, True
@ -370,8 +411,12 @@ class XEP_0176(BaseTransportHandler):
self.host.bridge.ice_restart(session["id"], "local", client.profile)
transport_elt = self.build_transport(new_ice_data)
iq_elt, __ = self._j.build_action(
client, self._j.A_TRANSPORT_INFO, session, content_name, iq_elt=iq_elt,
transport_elt=transport_elt
client,
self._j.A_TRANSPORT_INFO,
session,
content_name,
iq_elt=iq_elt,
context_elt=transport_elt,
)
if iq_elt is not None:
@ -380,13 +425,9 @@ class XEP_0176(BaseTransportHandler):
except Exception as e:
log.warning(f"Could not send new ICE candidates: {e}")
else:
log.error("Could not find any content to apply new ICE candidates")
@implementer(iwokkel.IDisco)
class XEP_0176_handler(XMPPHandler):
def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
return [disco.DiscoFeature(NS_JINGLE_ICE_UDP)]