proper iq tracing (handling of errors); responding to all iqs

This commit is contained in:
Daniel Gultsch 2020-04-09 15:22:03 +02:00
parent 15a2491d7b
commit 268eedad89
9 changed files with 174 additions and 37 deletions

View file

@ -169,6 +169,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
case CONNECTIVITY_ERROR:
binding.status.setText(R.string.rtp_state_connectivity_error);
break;
case APPLICATION_ERROR:
binding.status.setText(R.string.rtp_state_application_failure);
break;
}
}
@ -191,7 +194,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
this.binding.endCall.setImageResource(R.drawable.ic_clear_white_48dp);
this.binding.endCall.show();
this.binding.acceptCall.hide();
} else if (state == RtpEndUserState.CONNECTIVITY_ERROR) {
} else if (state == RtpEndUserState.CONNECTIVITY_ERROR || state == RtpEndUserState.APPLICATION_ERROR) {
this.binding.rejectCall.setOnClickListener(this::exit);
this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
this.binding.rejectCall.show();

View file

@ -95,6 +95,7 @@ public abstract class AbstractJingleConnection {
TERMINATED_SUCCESS, //equal to 'ENDED' (after successful call) ui will just close
TERMINATED_DECLINED_OR_BUSY, //equal to 'ENDED' (after other party declined the call)
TERMINATED_CONNECTIVITY_ERROR, //equal to 'ENDED' (but after network failures; ui will display retry button)
TERMINATED_CANCEL_OR_TIMEOUT //more or less the same as retracted; caller pressed end call before session was accepted
TERMINATED_CANCEL_OR_TIMEOUT, //more or less the same as retracted; caller pressed end call before session was accepted
TERMINATED_APPLICATION_FAILURE
}
}

View file

@ -55,22 +55,27 @@ public class JingleConnectionManager extends AbstractConnectionManager {
} else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)) {
connection = new JingleRtpConnection(this, id, from);
} else {
//TODO return feature-not-implemented
respondWithJingleError(account, packet, "unsupported-info", "feature-not-implemented", "cancel");
return;
}
connections.put(id, connection);
connection.deliverPacket(packet);
} else {
Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet);
final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
final Element error = response.addChild("error");
error.setAttribute("type", "cancel");
error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas");
error.addChild("unknown-session", "urn:xmpp:jingle:errors:1");
account.getXmppConnection().sendIqPacket(response, null);
respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel");
}
}
public void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) {
final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR);
final Element error = response.addChild("error");
error.setAttribute("type", conditionType);
error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas");
error.addChild(jingleCondition, "urn:xmpp:jingle:errors:1");
account.getXmppConnection().sendIqPacket(response, null);
}
public void deliverMessage(final Account account, final Jid to, final Jid from, final Element message) {
Preconditions.checkArgument(Namespace.JINGLE_MESSAGE.equals(message.getNamespace()));
final String sessionId = message.getAttribute("id");

View file

@ -34,12 +34,40 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
static {
final ImmutableMap.Builder<State, Collection<State>> transitionBuilder = new ImmutableMap.Builder<>();
transitionBuilder.put(State.NULL, ImmutableList.of(State.PROPOSED, State.SESSION_INITIALIZED));
transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED, State.REJECTED, State.RETRACTED));
transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED_PRE_APPROVED, State.TERMINATED_SUCCESS));
transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of(State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT, State.TERMINATED_DECLINED_OR_BUSY));
transitionBuilder.put(State.SESSION_INITIALIZED_PRE_APPROVED, ImmutableList.of(State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT, State.TERMINATED_DECLINED_OR_BUSY));
transitionBuilder.put(State.SESSION_ACCEPTED, ImmutableList.of(State.TERMINATED_SUCCESS, State.TERMINATED_CONNECTIVITY_ERROR));
transitionBuilder.put(State.NULL, ImmutableList.of(
State.PROPOSED,
State.SESSION_INITIALIZED,
State.TERMINATED_APPLICATION_FAILURE
));
transitionBuilder.put(State.PROPOSED, ImmutableList.of(
State.ACCEPTED,
State.PROCEED,
State.REJECTED,
State.RETRACTED,
State.TERMINATED_APPLICATION_FAILURE
));
transitionBuilder.put(State.PROCEED, ImmutableList.of(
State.SESSION_INITIALIZED_PRE_APPROVED,
State.TERMINATED_SUCCESS,
State.TERMINATED_APPLICATION_FAILURE
));
transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of(
State.SESSION_ACCEPTED,
State.TERMINATED_CANCEL_OR_TIMEOUT,
State.TERMINATED_DECLINED_OR_BUSY,
State.TERMINATED_APPLICATION_FAILURE
));
transitionBuilder.put(State.SESSION_INITIALIZED_PRE_APPROVED, ImmutableList.of(
State.SESSION_ACCEPTED,
State.TERMINATED_CANCEL_OR_TIMEOUT,
State.TERMINATED_DECLINED_OR_BUSY,
State.TERMINATED_APPLICATION_FAILURE
));
transitionBuilder.put(State.SESSION_ACCEPTED, ImmutableList.of(
State.TERMINATED_SUCCESS,
State.TERMINATED_CONNECTIVITY_ERROR,
State.TERMINATED_APPLICATION_FAILURE
));
VALID_TRANSITIONS = transitionBuilder.build();
}
@ -64,6 +92,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
case CANCEL:
case TIMEOUT:
return State.TERMINATED_CANCEL_OR_TIMEOUT;
case FAILED_APPLICATION:
return State.TERMINATED_APPLICATION_FAILURE;
default:
return State.TERMINATED_CONNECTIVITY_ERROR;
}
@ -86,12 +116,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
receiveSessionTerminate(jinglePacket);
break;
default:
respondOk(jinglePacket);
Log.d(Config.LOGTAG, String.format("%s: received unhandled jingle action %s", id.account.getJid().asBareJid(), jinglePacket.getAction()));
break;
}
}
private void receiveSessionTerminate(final JinglePacket jinglePacket) {
respondOk(jinglePacket);
final Reason reason = jinglePacket.getReason();
final State previous = this.state;
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session terminate reason=" + reason + " while in state " + previous);
@ -105,11 +137,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void receiveTransportInfo(final JinglePacket jinglePacket) {
if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) {
respondOk(jinglePacket);
final RtpContentMap contentMap;
try {
contentMap = RtpContentMap.of(jinglePacket);
} catch (IllegalArgumentException | NullPointerException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e);
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents; ignoring", e);
return;
}
final RtpContentMap rtpContentMap = isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap;
@ -136,13 +169,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
}
} else {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received transport info while in state=" + this.state);
respondWithOutOfOrder(jinglePacket);
}
}
private void receiveSessionInitiate(final JinglePacket jinglePacket) {
if (isInitiator()) {
Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid()));
//TODO respond with out-of-order
respondWithOutOfOrder(jinglePacket);
return;
}
final RtpContentMap contentMap;
@ -150,6 +184,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
contentMap = RtpContentMap.of(jinglePacket);
contentMap.requireContentDescriptions();
} catch (IllegalArgumentException | IllegalStateException | NullPointerException e) {
respondOk(jinglePacket);
sendSessionTerminate(Reason.FAILED_APPLICATION);
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e);
return;
}
@ -161,6 +197,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
target = State.SESSION_INITIALIZED;
}
if (transition(target)) {
respondOk(jinglePacket);
this.initiatorRtpContentMap = contentMap;
if (target == State.SESSION_INITIALIZED_PRE_APPROVED) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate");
@ -171,13 +208,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
}
} else {
Log.d(Config.LOGTAG, String.format("%s: received session-initiate while in state %s", id.account.getJid().asBareJid(), state));
respondWithOutOfOrder(jinglePacket);
}
}
private void receiveSessionAccept(final JinglePacket jinglePacket) {
if (!isInitiator()) {
Log.d(Config.LOGTAG, String.format("%s: received session-accept even though we were responding", id.account.getJid().asBareJid()));
//TODO respond with out-of-order
respondWithOutOfOrder(jinglePacket);
return;
}
final RtpContentMap contentMap;
@ -185,28 +223,43 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
contentMap = RtpContentMap.of(jinglePacket);
contentMap.requireContentDescriptions();
} catch (IllegalArgumentException | IllegalStateException | NullPointerException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e);
respondOk(jinglePacket);
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents in session-accept", e);
webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION);
return;
}
Log.d(Config.LOGTAG, "processing session-accept with " + contentMap.contents.size() + " contents");
if (transition(State.SESSION_ACCEPTED)) {
respondOk(jinglePacket);
receiveSessionAccept(contentMap);
} else {
Log.d(Config.LOGTAG, String.format("%s: received session-accept while in state %s", id.account.getJid().asBareJid(), state));
//TODO out-of-order
respondOk(jinglePacket);
}
}
private void receiveSessionAccept(final RtpContentMap contentMap) {
this.responderRtpContentMap = contentMap;
final SessionDescription sessionDescription;
try {
sessionDescription = SessionDescription.of(contentMap);
} catch (IllegalArgumentException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-accept to SDP", e);
webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION);
return;
}
org.webrtc.SessionDescription answer = new org.webrtc.SessionDescription(
org.webrtc.SessionDescription.Type.ANSWER,
SessionDescription.of(contentMap).toString()
sessionDescription.toString()
);
try {
this.webRTCWrapper.setRemoteDescription(answer).get();
} catch (Exception e) {
Log.d(Config.LOGTAG, "unable to receive session accept", e);
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", e);
webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION);
}
}
@ -219,8 +272,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
try {
offer = SessionDescription.of(rtpContentMap);
} catch (final IllegalArgumentException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to process offer", e);
//TODO terminate session with application error
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-initiate to SDP", e);
webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION);
;
return;
}
sendSessionAccept(offer);
@ -228,7 +283,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void sendSessionAccept(SessionDescription offer) {
discoverIceServers(iceServers -> {
setupWebRTC(iceServers);
try {
setupWebRTC(iceServers);
} catch (WebRTCWrapper.InitializationException e) {
sendSessionTerminate(Reason.FAILED_APPLICATION);
return;
}
final org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription(
org.webrtc.SessionDescription.Type.OFFER,
offer.toString()
@ -351,7 +411,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
this.jingleConnectionManager.finishConnection(this);
}
} else {
//TODO a carbon copied proceed from another client of mine has the same logic as `accept`
Log.d(Config.LOGTAG, String.format("%s: ignoring proceed from %s. was expected from %s", id.account.getJid().asBareJid(), from, id.with));
}
}
@ -374,7 +433,13 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void sendSessionInitiate(final State targetState) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate");
discoverIceServers(iceServers -> {
setupWebRTC(iceServers);
try {
setupWebRTC(iceServers);
} catch (WebRTCWrapper.InitializationException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize webrtc");
transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE);
return;
}
try {
org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get();
final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
@ -382,8 +447,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
sendSessionInitiate(rtpContentMap, targetState);
this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
} catch (Exception e) {
Log.d(Config.LOGTAG, "unable to sendSessionInitiate", e);
} catch (final Exception e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", e);
webRTCWrapper.close();
if (isInState(targetState)) {
sendSessionTerminate(Reason.FAILED_APPLICATION);
} else {
transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE);
}
}
});
}
@ -422,8 +493,43 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void send(final JinglePacket jinglePacket) {
jinglePacket.setTo(id.with);
//TODO track errors
xmppConnectionService.sendIqPacket(id.account, jinglePacket, null);
xmppConnectionService.sendIqPacket(id.account, jinglePacket, (account, response) -> {
if (response.getType() == IqPacket.TYPE.ERROR) {
final String errorCondition = response.getErrorCondition();
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition);
this.webRTCWrapper.close();
final State target;
if (Arrays.asList(
"service-unavailable",
"recipient-unavailable",
"remote-server-not-found",
"remote-server-timeout"
).contains(errorCondition)) {
target = State.TERMINATED_CONNECTIVITY_ERROR;
} else {
target = State.TERMINATED_APPLICATION_FAILURE;
}
if (transition(target)) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": terminated session with " + id.with);
} else {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not transitioning because already at state=" + this.state);
}
} else if (response.getType() == IqPacket.TYPE.TIMEOUT) {
this.webRTCWrapper.close();
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error");
transition(State.TERMINATED_CONNECTIVITY_ERROR);
this.jingleConnectionManager.finishConnection(this);
}
});
}
private void respondWithOutOfOrder(final JinglePacket jinglePacket) {
jingleConnectionManager.respondWithJingleError(id.account, jinglePacket, "out-of-order", "unexpected-request", "wait");
}
private void respondOk(final JinglePacket jinglePacket) {
xmppConnectionService.sendIqPacket(id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null);
}
public RtpEndUserState getEndUserState() {
@ -474,6 +580,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
return RtpEndUserState.ENDED;
case TERMINATED_CONNECTIVITY_ERROR:
return RtpEndUserState.CONNECTIVITY_ERROR;
case TERMINATED_APPLICATION_FAILURE:
return RtpEndUserState.APPLICATION_ERROR;
}
throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state));
}
@ -526,7 +634,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
throw new IllegalStateException("called 'endCall' while in state " + this.state);
}
private void setupWebRTC(final List<PeerConnection.IceServer> iceServers) {
private void setupWebRTC(final List<PeerConnection.IceServer> iceServers) throws WebRTCWrapper.InitializationException {
this.webRTCWrapper.setup(this.xmppConnectionService);
this.webRTCWrapper.initializePeerConnection(iceServers);
}

View file

@ -6,10 +6,10 @@ public enum RtpEndUserState {
CONNECTED, //session-accepted and webrtc peer connection is connected
FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet
RINGING, //'propose' has been sent out and it has been 184 acked
ACCEPTED_ON_OTHER_DEVICE, //received 'accept' from one of our own devices
ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received
ENDING_CALL, //libwebrt says 'closed' but session-terminate hasnt gone through
ENDED, //close UI
DECLINED_OR_BUSY, //other party declined; no retry button
CONNECTIVITY_ERROR //network error; retry button
CONNECTIVITY_ERROR, //network error; retry button
APPLICATION_ERROR //something rather bad happened; libwebrtc failed or we got in IQ-error
}

View file

@ -132,7 +132,7 @@ public class WebRTCWrapper {
);
}
public void initializePeerConnection(final List<PeerConnection.IceServer> iceServers) {
public void initializePeerConnection(final List<PeerConnection.IceServer> iceServers) throws InitializationException {
PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory();
CameraVideoCapturer capturer = null;
@ -195,7 +195,7 @@ public class WebRTCWrapper {
final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, peerConnectionObserver);
if (peerConnection == null) {
throw new IllegalStateException("Unable to create PeerConnection");
throw new InitializationException("Unable to create PeerConnection");
}
peerConnection.addStream(stream);
peerConnection.setAudioPlayout(true);
@ -344,6 +344,13 @@ public class WebRTCWrapper {
}
}
public static class InitializationException extends Exception {
private InitializationException(String message) {
super(message);
}
}
public interface EventCallback {
void onIceCandidate(IceCandidate iceCandidate);

View file

@ -5,7 +5,7 @@ import android.support.annotation.NonNull;
import com.google.common.base.CaseFormat;
public enum Reason {
SUCCESS, DECLINE, BUSY, CANCEL, CONNECTIVITY_ERROR, FAILED_TRANSPORT, TIMEOUT, UNKNOWN;
SUCCESS, DECLINE, BUSY, CANCEL, CONNECTIVITY_ERROR, FAILED_TRANSPORT, FAILED_APPLICATION, TIMEOUT, UNKNOWN;
public static Reason of(final String value) {
try {

View file

@ -31,6 +31,18 @@ abstract public class AbstractAcknowledgeableStanza extends AbstractStanza {
return null;
}
public String getErrorCondition() {
Element error = findChild("error");
if (error != null) {
for(Element element : error.getChildren()) {
if (!element.getName().equals("text")) {
return element.getName();
}
}
}
return null;
}
public boolean valid() {
return InvalidJid.isValid(getFrom()) && InvalidJid.isValid(getTo());
}

View file

@ -898,6 +898,7 @@
<string name="rtp_state_ringing">Ringing</string>
<string name="rtp_state_declined_or_busy">Busy</string>
<string name="rtp_state_connectivity_error">Unable to connect call</string>
<string name="rtp_state_application_failure">Application failure</string>
<plurals name="view_users">
<item quantity="one">View %1$d Participant</item>
<item quantity="other">View %1$d Participants</item>