From 148cf48d2b68354881066e2587e2673c91d2619a Mon Sep 17 00:00:00 2001 From: hrxi Date: Sat, 28 Dec 2019 03:11:51 +0100 Subject: Add libnice and listen for direct connections in Jingle SOCKS5 (#608) Add libnice as a plugin. If it is present, use libnice to enumerate local IP addresses and listen on them to support direct connections for Jingle SOCKS5. Tested with Conversations and Gajim. Created the nice.vapi file using ``` vapigen --library nice --pkg gio-2.0 --metadatadir metadata /usr/share/gir-1.0/Nice-0.1.gir ``` --- .../module/xep/0260_jingle_socks5_bytestreams.vala | 217 +++++++++++++++++++-- 1 file changed, 206 insertions(+), 11 deletions(-) (limited to 'xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala') diff --git a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala index ea7ef375..bb31cadc 100644 --- a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala +++ b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala @@ -5,6 +5,7 @@ using Xmpp.Xep; namespace Xmpp.Xep.JingleSocks5Bytestreams { private const string NS_URI = "urn:xmpp:jingle:transports:s5b:1"; +private const int NEGOTIATION_TIMEOUT = 3; public class Module : Jingle.Transport, XmppStreamModule { public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0260_jingle_socks5_bytestreams"); @@ -33,7 +34,7 @@ public class Module : Jingle.Transport, XmppStreamModule { public int transport_priority() { return 1; } - private Gee.List get_local_candidates(XmppStream stream) { + private Gee.List get_proxies(XmppStream stream) { Gee.List result = new ArrayList(); int i = 1 << 15; foreach (Socks5Bytestreams.Proxy proxy in stream.get_module(Socks5Bytestreams.Module.IDENTITY).get_proxies(stream)) { @@ -42,18 +43,59 @@ public class Module : Jingle.Transport, XmppStreamModule { } return result; } + private Gee.List start_local_listeners(XmppStream stream, Jid local_full_jid, string dstaddr, out LocalListener? local_listener) { + Gee.List result = new ArrayList(); + SocketListener listener = new SocketListener(); + int i = 1 << 15; + foreach (string ip_address in stream.get_module(Socks5Bytestreams.Module.IDENTITY).get_local_ip_addresses()) { + InetSocketAddress addr = new InetSocketAddress.from_string(ip_address, 0); + SocketAddress effective_any; + string cid = random_uuid(); + try { + listener.add_address(addr, SocketType.STREAM, SocketProtocol.DEFAULT, new StringWrapper(cid), out effective_any); + } catch (Error e) { + continue; + } + InetSocketAddress effective = (InetSocketAddress)effective_any; + result.add(new Candidate.build(cid, ip_address, local_full_jid, (int)effective.port, i, CandidateType.DIRECT)); + i -= 1; + } + if (!result.is_empty) { + local_listener = new LocalListener(listener, dstaddr); + local_listener.start(); + } else { + local_listener = new LocalListener.empty(); + } + return result; + } + private void select_candidates(XmppStream stream, Jid local_full_jid, string dstaddr, Parameters result) { + result.local_candidates.add_all(get_proxies(stream)); + result.local_candidates.add_all(start_local_listeners(stream, local_full_jid, dstaddr, out result.listener)); + result.local_candidates.sort((c1, c2) => { + if (c1.priority < c2.priority) { return 1; } + if (c1.priority > c2.priority) { return -1; } + return 0; + }); + } public Jingle.TransportParameters create_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid) { Parameters result = new Parameters.create(local_full_jid, peer_full_jid, random_uuid()); - result.local_candidates.add_all(get_local_candidates(stream)); + string dstaddr = calculate_dstaddr(result.sid, local_full_jid, peer_full_jid); + select_candidates(stream, local_full_jid, dstaddr, result); return result; } public Jingle.TransportParameters parse_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws Jingle.IqError { Parameters result = Parameters.parse(local_full_jid, peer_full_jid, transport); - result.local_candidates.add_all(get_local_candidates(stream)); + string dstaddr = calculate_dstaddr(result.sid, local_full_jid, peer_full_jid); + select_candidates(stream, local_full_jid, dstaddr, result); return result; } } +private string calculate_dstaddr(string sid, Jid first_jid, Jid second_jid) { + string hashed = sid + first_jid.to_string() + second_jid.to_string(); + return Checksum.compute_for_string(ChecksumType.SHA1, hashed); +} + public enum CandidateType { ASSISTED, DIRECT, @@ -156,6 +198,142 @@ bool bytes_equal(uint8[] a, uint8[] b) { return true; } +class StringWrapper : GLib.Object { + public string str { get; set; } + + public StringWrapper(string str) { + this.str = str; + } +} + +class LocalListener { + SocketListener? inner; + string dstaddr; + HashMap connections = new HashMap(); + + public LocalListener(SocketListener inner, string dstaddr) { + this.inner = inner; + this.dstaddr = dstaddr; + } + public LocalListener.empty() { + this.inner = null; + this.dstaddr = ""; + } + + public void start() { + if (inner == null) { + return; + } + run.begin(); + } + async void run() { + while (true) { + Object cid; + SocketConnection conn; + try { + conn = yield inner.accept_async(null, out cid); + } catch (Error e) { + break; + } + handle_conn.begin(((StringWrapper)cid).str, conn); + } + } + async void handle_conn(string cid, SocketConnection conn) { + conn.socket.timeout = NEGOTIATION_TIMEOUT; + size_t read; + size_t written; + uint8[] read_buffer = new uint8[1024]; + ByteArray write_buffer = new ByteArray(); + + try { + // 05 SOCKS version 5 + // ?? number of authentication methods + yield conn.input_stream.read_all_async(read_buffer[0:2], GLib.Priority.DEFAULT, null, out read); + if (read != 2) { + throw new IOError.PROXY_FAILED("wanted client hello message consisting of 2 bytes, only got %d bytes".printf((int)read)); + } + if (read_buffer[0] != 0x05 || read_buffer[1] == 0) { + throw new IOError.PROXY_FAILED("wanted 05 xx, got %02x %02x".printf(read_buffer[0], read_buffer[1])); + } + int num_auth_methods = read_buffer[1]; + // ?? authentication method (num_auth_methods times) + yield conn.input_stream.read_all_async(read_buffer[0:num_auth_methods], GLib.Priority.DEFAULT, null, out read); + bool found_null_auth = false; + for (int i = 0; i < read; i++) { + if (read_buffer[i] == 0x00) { + found_null_auth = true; + break; + } + } + if (read != num_auth_methods || !found_null_auth) { + throw new IOError.PROXY_FAILED("peer didn't offer null auth"); + } + // 05 SOCKS version 5 + // 00 nop authentication + yield conn.output_stream.write_all_async({0x05, 0x00}, GLib.Priority.DEFAULT, null, out written); + + // 05 SOCKS version 5 + // 01 connect + // 00 reserved + // 03 address type: domain name + // ?? length of the domain + // .. domain + // 00 port 0 (upper half) + // 00 port 0 (lower half) + yield conn.input_stream.read_all_async(read_buffer[0:4], GLib.Priority.DEFAULT, null, out read); + if (read != 4) { + throw new IOError.PROXY_FAILED("wanted connect message consisting of 4 bytes, only got %d bytes".printf((int)read)); + } + if (read_buffer[0] != 0x05 || read_buffer[1] != 0x01 || read_buffer[3] != 0x03) { + throw new IOError.PROXY_FAILED("wanted 05 00 ?? 03, got %02x %02x %02x %02x".printf(read_buffer[0], read_buffer[1], read_buffer[2], read_buffer[3])); + } + yield conn.input_stream.read_all_async(read_buffer[0:1], GLib.Priority.DEFAULT, null, out read); + if (read != 1) { + throw new IOError.PROXY_FAILED("wanted length of dstaddr consisting of 1 byte, only got %d bytes".printf((int)read)); + } + int dstaddr_len = read_buffer[0]; + yield conn.input_stream.read_all_async(read_buffer[0:dstaddr_len+2], GLib.Priority.DEFAULT, null, out read); + if (read != dstaddr_len + 2) { + throw new IOError.PROXY_FAILED("wanted dstaddr and port consisting of %d bytes, got %d bytes".printf(dstaddr_len + 2, (int)read)); + } + if (!bytes_equal(read_buffer[0:dstaddr_len], dstaddr.data)) { + string repr = ((string)read_buffer[0:dstaddr.length]).make_valid().escape(); + throw new IOError.PROXY_FAILED(@"wanted dstaddr $(dstaddr), got $(repr)"); + } + if (read_buffer[dstaddr_len] != 0x00 || read_buffer[dstaddr_len + 1] != 0x00) { + throw new IOError.PROXY_FAILED("wanted 00 00, got %02x %02x".printf(read_buffer[dstaddr_len], read_buffer[dstaddr_len + 1])); + } + + // 05 SOCKS version 5 + // 00 success + // 00 reserved + // 03 address type: domain name + // ?? length of the domain + // .. domain + // 00 port 0 (upper half) + // 00 port 0 (lower half) + write_buffer.append({0x05, 0x00, 0x00, 0x03}); + write_buffer.append({(uint8)dstaddr.length}); + write_buffer.append(dstaddr.data); + write_buffer.append({0x00, 0x00}); + yield conn.output_stream.write_all_async(write_buffer.data, GLib.Priority.DEFAULT, null, out written); + + conn.socket.timeout = 0; + if (!connections.has_key(cid)) { + connections[cid] = conn; + } + } catch (Error e) { + } + } + + public SocketConnection? get_connection(string cid) { + if (!connections.has_key(cid)) { + return null; + } + return connections[cid]; + } +} + class Parameters : Jingle.TransportParameters, Object { public Jingle.Role role { get; private set; } public string sid { get; private set; } @@ -163,6 +341,7 @@ class Parameters : Jingle.TransportParameters, Object { public string local_dstaddr { get; private set; } public Gee.List local_candidates = new ArrayList(); public Gee.List remote_candidates = new ArrayList(); + public LocalListener? listener = null; Jid local_full_jid; Jid peer_full_jid; @@ -179,10 +358,6 @@ class Parameters : Jingle.TransportParameters, Object { SourceFunc waiting_for_activation_callback; bool waiting_for_activation_error = false; - private static string calculate_dstaddr(string sid, Jid first_jid, Jid second_jid) { - string hashed = sid + first_jid.to_string() + second_jid.to_string(); - return Checksum.compute_for_string(ChecksumType.SHA1, hashed); - } private Parameters(Jingle.Role role, string sid, Jid local_full_jid, Jid peer_full_jid, string? remote_dstaddr) { this.role = role; this.sid = sid; @@ -355,7 +530,21 @@ class Parameters : Jingle.TransportParameters, Object { wait_for_remote_activation.begin(local_selected_candidate, local_selected_candidate_conn); } } else { - connect_to_local_candidate.begin(remote_selected_candidate); + if (remote_selected_candidate.type_ == CandidateType.DIRECT) { + Jingle.Session? strong = session; + if (strong == null) { + return; + } + SocketConnection? conn = listener.get_connection(remote_selected_candidate.cid); + if (conn == null) { + // Remote hasn't actually connected to us?! + strong.set_transport_connection(hack, null); + return; + } + strong.set_transport_connection(hack, conn); + } else { + connect_to_local_candidate.begin(remote_selected_candidate); + } } } public async void wait_for_remote_activation(Candidate candidate, SocketConnection conn) { @@ -425,7 +614,7 @@ class Parameters : Jingle.TransportParameters, Object { } } public async SocketConnection connect_to_socks5(Candidate candidate, string dstaddr) throws Error { - SocketClient socket_client = new SocketClient() { timeout=3 }; + SocketClient socket_client = new SocketClient() { timeout=NEGOTIATION_TIMEOUT }; string address = @"[$(candidate.host)]:$(candidate.port)"; debug("Connecting to SOCKS5 server at %s", address); @@ -444,7 +633,10 @@ class Parameters : Jingle.TransportParameters, Object { yield conn.input_stream.read_all_async(read_buffer[0:2], GLib.Priority.DEFAULT, null, out read); // 05 SOCKS version 5 - // 01 success + // 00 nop authentication + if (read != 2) { + throw new IOError.PROXY_FAILED("wanted 05 00, only got %d bytes".printf((int)read)); + } if (read_buffer[0] != 0x05 || read_buffer[1] != 0x00) { throw new IOError.PROXY_FAILED("wanted 05 00, got %02x %02x".printf(read_buffer[0], read_buffer[1])); } @@ -472,6 +664,9 @@ class Parameters : Jingle.TransportParameters, Object { // .. domain // 00 port 0 (upper half) // 00 port 0 (lower half) + if (read != write_buffer.len) { + throw new IOError.PROXY_FAILED("wanted server success response consisting of %d bytes, only got %d bytes".printf((int)write_buffer.len, (int)read)); + } if (read_buffer[0] != 0x05 || read_buffer[1] != 0x00 || read_buffer[3] != 0x03) { throw new IOError.PROXY_FAILED("wanted 05 00 ?? 03, got %02x %02x %02x %02x".printf(read_buffer[0], read_buffer[1], read_buffer[2], read_buffer[3])); } @@ -486,7 +681,7 @@ class Parameters : Jingle.TransportParameters, Object { throw new IOError.PROXY_FAILED("wanted port 00 00, got %02x %02x".printf(read_buffer[5+dstaddr.length], read_buffer[5+dstaddr.length+1])); } - conn.get_socket().set_timeout(0); + conn.socket.timeout = 0; return conn; } -- cgit v1.2.3-54-g00ecf From 2b90fcc39a1079346d6c5e2bfff8987104da737a Mon Sep 17 00:00:00 2001 From: fiaxh Date: Fri, 19 Mar 2021 22:46:39 +0100 Subject: Improve & refactor Jingle base implementation Co-authored-by: Marvin W --- xmpp-vala/CMakeLists.txt | 29 +- xmpp-vala/src/core/xmpp_log.vala | 4 +- xmpp-vala/src/module/iq/module.vala | 2 + xmpp-vala/src/module/xep/0166_jingle.vala | 1061 -------------------- .../src/module/xep/0166_jingle/component.vala | 52 + xmpp-vala/src/module/xep/0166_jingle/content.vala | 236 +++++ .../xep/0166_jingle/content_description.vala | 27 + .../src/module/xep/0166_jingle/content_node.vala | 112 +++ .../module/xep/0166_jingle/content_security.vala | 18 + .../module/xep/0166_jingle/content_transport.vala | 29 + .../src/module/xep/0166_jingle/jingle_flag.vala | 38 + .../src/module/xep/0166_jingle/jingle_module.vala | 235 +++++ .../src/module/xep/0166_jingle/jingle_structs.vala | 73 ++ .../src/module/xep/0166_jingle/reason_element.vala | 29 + xmpp-vala/src/module/xep/0166_jingle/session.vala | 559 +++++++++++ .../src/module/xep/0166_jingle/session_info.vala | 12 + .../src/module/xep/0234_jingle_file_transfer.vala | 162 ++- .../module/xep/0260_jingle_socks5_bytestreams.vala | 100 +- .../xep/0261_jingle_in_band_bytestreams.vala | 50 +- 19 files changed, 1671 insertions(+), 1157 deletions(-) delete mode 100644 xmpp-vala/src/module/xep/0166_jingle.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/component.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/content.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/content_description.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/content_node.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/content_security.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/content_transport.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/jingle_flag.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/jingle_structs.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/reason_element.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/session.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/session_info.vala (limited to 'xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala') diff --git a/xmpp-vala/CMakeLists.txt b/xmpp-vala/CMakeLists.txt index fcc74fdc..48f411c7 100644 --- a/xmpp-vala/CMakeLists.txt +++ b/xmpp-vala/CMakeLists.txt @@ -61,15 +61,18 @@ SOURCES "src/module/xep/0048_conference.vala" "src/module/xep/0402_bookmarks2.vala" "src/module/xep/0004_data_forms.vala" + "src/module/xep/0030_service_discovery/flag.vala" "src/module/xep/0030_service_discovery/identity.vala" "src/module/xep/0030_service_discovery/info_result.vala" "src/module/xep/0030_service_discovery/item.vala" "src/module/xep/0030_service_discovery/items_result.vala" "src/module/xep/0030_service_discovery/module.vala" + "src/module/xep/0045_muc/flag.vala" "src/module/xep/0045_muc/module.vala" "src/module/xep/0045_muc/status_code.vala" + "src/module/xep/0047_in_band_bytestreams.vala" "src/module/xep/0049_private_xml_storage.vala" "src/module/xep/0054_vcard/module.vala" @@ -81,7 +84,31 @@ SOURCES "src/module/xep/0084_user_avatars.vala" "src/module/xep/0085_chat_state_notifications.vala" "src/module/xep/0115_entity_capabilities.vala" - "src/module/xep/0166_jingle.vala" + + "src/module/xep/0166_jingle/content.vala" + "src/module/xep/0166_jingle/content_description.vala" + "src/module/xep/0166_jingle/content_node.vala" + "src/module/xep/0166_jingle/content_security.vala" + "src/module/xep/0166_jingle/content_transport.vala" + "src/module/xep/0166_jingle/component.vala" + "src/module/xep/0166_jingle/jingle_flag.vala" + "src/module/xep/0166_jingle/jingle_module.vala" + "src/module/xep/0166_jingle/jingle_structs.vala" + "src/module/xep/0166_jingle/reason_element.vala" + "src/module/xep/0166_jingle/session.vala" + "src/module/xep/0166_jingle/session_info.vala" + + "src/module/xep/0167_jingle_rtp/content_parameters.vala" + "src/module/xep/0167_jingle_rtp/content_type.vala" + "src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala" + "src/module/xep/0167_jingle_rtp/payload_type.vala" + "src/module/xep/0167_jingle_rtp/session_info_type.vala" + "src/module/xep/0167_jingle_rtp/stream.vala" + + "src/module/xep/0176_jingle_ice_udp/candidate.vala" + "src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala" + "src/module/xep/0176_jingle_ice_udp/transport_parameters.vala" + "src/module/xep/0184_message_delivery_receipts.vala" "src/module/xep/0191_blocking_command.vala" "src/module/xep/0198_stream_management.vala" diff --git a/xmpp-vala/src/core/xmpp_log.vala b/xmpp-vala/src/core/xmpp_log.vala index 4790a8ab..3d5693ef 100644 --- a/xmpp-vala/src/core/xmpp_log.vala +++ b/xmpp-vala/src/core/xmpp_log.vala @@ -110,13 +110,13 @@ public class XmppLog { public void node(string what, StanzaNode node, XmppStream stream) { if (should_log_node(node)) { - stderr.printf("%sXMPP %s [%s %p %s]%s\n%s\n", use_ansi ? ANSI_COLOR_WHITE : "", what, ident, stream, new DateTime.now_local().to_string(), use_ansi ? ANSI_COLOR_END : "", use_ansi ? node.to_ansi_string(hide_ns) : node.to_string()); + stderr.printf("%sXMPP %s [%s stream:%p thread:%p %s]%s\n%s\n", use_ansi ? ANSI_COLOR_WHITE : "", what, ident, stream, Thread.self(), new DateTime.now_local().to_string(), use_ansi ? ANSI_COLOR_END : "", use_ansi ? node.to_ansi_string(hide_ns) : node.to_string()); } } public void str(string what, string str, XmppStream stream) { if (should_log_str(str)) { - stderr.printf("%sXMPP %s [%s %p %s]%s\n%s\n", use_ansi ? ANSI_COLOR_WHITE : "", what, ident, stream, new DateTime.now_local().to_string(), use_ansi ? ANSI_COLOR_END : "", str); + stderr.printf("%sXMPP %s [%s stream:%p thread:%p %s]%s\n%s\n", use_ansi ? ANSI_COLOR_WHITE : "", what, ident, stream, Thread.self(), new DateTime.now_local().to_string(), use_ansi ? ANSI_COLOR_END : "", str); } } diff --git a/xmpp-vala/src/module/iq/module.vala b/xmpp-vala/src/module/iq/module.vala index 9deb0422..56605d01 100644 --- a/xmpp-vala/src/module/iq/module.vala +++ b/xmpp-vala/src/module/iq/module.vala @@ -10,6 +10,8 @@ namespace Xmpp.Iq { private HashMap> namespaceRegistrants = new HashMap>(); public async Iq.Stanza send_iq_async(XmppStream stream, Iq.Stanza iq) { + assert(iq.type_ == Iq.Stanza.TYPE_GET || iq.type_ == Iq.Stanza.TYPE_SET); + Iq.Stanza? return_stanza = null; send_iq(stream, iq, (_, result_iq) => { return_stanza = result_iq; diff --git a/xmpp-vala/src/module/xep/0166_jingle.vala b/xmpp-vala/src/module/xep/0166_jingle.vala deleted file mode 100644 index 3a634222..00000000 --- a/xmpp-vala/src/module/xep/0166_jingle.vala +++ /dev/null @@ -1,1061 +0,0 @@ -using Gee; -using Xmpp.Xep; -using Xmpp; - -namespace Xmpp.Xep.Jingle { - -private const string NS_URI = "urn:xmpp:jingle:1"; -private const string ERROR_NS_URI = "urn:xmpp:jingle:errors:1"; - -public errordomain IqError { - BAD_REQUEST, - NOT_ACCEPTABLE, - NOT_IMPLEMENTED, - UNSUPPORTED_INFO, - OUT_OF_ORDER, - RESOURCE_CONSTRAINT, -} - -void send_iq_error(IqError iq_error, XmppStream stream, Iq.Stanza iq) { - ErrorStanza error; - if (iq_error is IqError.BAD_REQUEST) { - error = new ErrorStanza.bad_request(iq_error.message); - } else if (iq_error is IqError.NOT_ACCEPTABLE) { - error = new ErrorStanza.not_acceptable(iq_error.message); - } else if (iq_error is IqError.NOT_IMPLEMENTED) { - error = new ErrorStanza.feature_not_implemented(iq_error.message); - } else if (iq_error is IqError.UNSUPPORTED_INFO) { - StanzaNode unsupported_info = new StanzaNode.build("unsupported-info", ERROR_NS_URI).add_self_xmlns(); - error = new ErrorStanza.build(ErrorStanza.TYPE_CANCEL, ErrorStanza.CONDITION_FEATURE_NOT_IMPLEMENTED, iq_error.message, unsupported_info); - } else if (iq_error is IqError.OUT_OF_ORDER) { - StanzaNode out_of_order = new StanzaNode.build("out-of-order", ERROR_NS_URI).add_self_xmlns(); - error = new ErrorStanza.build(ErrorStanza.TYPE_MODIFY, ErrorStanza.CONDITION_UNEXPECTED_REQUEST, iq_error.message, out_of_order); - } else if (iq_error is IqError.RESOURCE_CONSTRAINT) { - error = new ErrorStanza.resource_constraint(iq_error.message); - } else { - assert_not_reached(); - } - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.error(iq, error) { to=iq.from }); -} - -public errordomain Error { - GENERAL, - BAD_REQUEST, - INVALID_PARAMETERS, - UNSUPPORTED_TRANSPORT, - UNSUPPORTED_SECURITY, - NO_SHARED_PROTOCOLS, - TRANSPORT_ERROR, -} - -StanzaNode? get_single_node_anyns(StanzaNode parent, string? node_name = null) throws IqError { - StanzaNode? result = null; - foreach (StanzaNode child in parent.get_all_subnodes()) { - if (node_name == null || child.name == node_name) { - if (result != null) { - if (node_name != null) { - throw new IqError.BAD_REQUEST(@"multiple $(node_name) nodes"); - } else { - throw new IqError.BAD_REQUEST(@"expected single subnode"); - } - } - result = child; - } - } - return result; -} - -class ContentNode { - public Role creator; - public string name; - public StanzaNode? description; - public StanzaNode? transport; - public StanzaNode? security; -} - -ContentNode get_single_content_node(StanzaNode jingle) throws IqError { - Gee.List contents = jingle.get_subnodes("content"); - if (contents.size == 0) { - throw new IqError.BAD_REQUEST("missing content node"); - } - if (contents.size > 1) { - throw new IqError.NOT_IMPLEMENTED("can't process multiple content nodes"); - } - StanzaNode content = contents[0]; - string? creator_str = content.get_attribute("creator"); - // Vala can't typecheck the ternary operator here. - Role? creator = null; - if (creator_str != null) { - creator = Role.parse(creator_str); - } else { - // TODO(hrxi): now, is the creator attribute optional or not (XEP-0166 - // Jingle)? - creator = Role.INITIATOR; - } - - string? name = content.get_attribute("name"); - StanzaNode? description = get_single_node_anyns(content, "description"); - StanzaNode? transport = get_single_node_anyns(content, "transport"); - StanzaNode? security = get_single_node_anyns(content, "security"); - if (name == null || creator == null) { - throw new IqError.BAD_REQUEST("missing name or creator"); - } - - return new ContentNode() { - creator=creator, - name=name, - description=description, - transport=transport, - security=security - }; -} - -// This module can only be attached to one stream at a time. -public class Module : XmppStreamModule, Iq.Handler { - public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0166_jingle"); - - private HashMap content_types = new HashMap(); - private HashMap transports = new HashMap(); - private HashMap security_preconditions = new HashMap(); - - private XmppStream? current_stream = null; - - public override void attach(XmppStream stream) { - stream.add_flag(new Flag()); - stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); - stream.get_module(Iq.Module.IDENTITY).register_for_namespace(NS_URI, this); - current_stream = stream; - } - public override void detach(XmppStream stream) { - stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI); - stream.get_module(Iq.Module.IDENTITY).unregister_from_namespace(NS_URI, this); - } - - public void register_content_type(ContentType content_type) { - content_types[content_type.content_type_ns_uri()] = content_type; - } - public ContentType? get_content_type(string ns_uri) { - if (!content_types.has_key(ns_uri)) { - return null; - } - return content_types[ns_uri]; - } - public void register_transport(Transport transport) { - transports[transport.transport_ns_uri()] = transport; - } - public Transport? get_transport(string ns_uri) { - if (!transports.has_key(ns_uri)) { - return null; - } - return transports[ns_uri]; - } - public async Transport? select_transport(XmppStream stream, TransportType type, Jid receiver_full_jid, Set blacklist) { - Transport? result = null; - foreach (Transport transport in transports.values) { - if (transport.transport_type() != type) { - continue; - } - if (transport.transport_ns_uri() in blacklist) { - continue; - } - if (yield transport.is_transport_available(stream, receiver_full_jid)) { - if (result != null) { - if (result.transport_priority() >= transport.transport_priority()) { - continue; - } - } - result = transport; - } - } - return result; - } - public void register_security_precondition(SecurityPrecondition precondition) { - security_preconditions[precondition.security_ns_uri()] = precondition; - } - public SecurityPrecondition? get_security_precondition(string? ns_uri) { - if (ns_uri == null) return null; - if (!security_preconditions.has_key(ns_uri)) { - return null; - } - return security_preconditions[ns_uri]; - } - - private async bool is_jingle_available(XmppStream stream, Jid full_jid) { - bool? has_jingle = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI); - return has_jingle != null && has_jingle; - } - - public async bool is_available(XmppStream stream, TransportType type, Jid full_jid) { - return (yield is_jingle_available(stream, full_jid)) && (yield select_transport(stream, type, full_jid, Set.empty())) != null; - } - - public async Session create_session(XmppStream stream, TransportType type, Jid receiver_full_jid, Senders senders, string content_name, StanzaNode description, string? precondition_name = null, Object? precondation_options = null) throws Error { - if (!yield is_jingle_available(stream, receiver_full_jid)) { - throw new Error.NO_SHARED_PROTOCOLS("No Jingle support"); - } - Transport? transport = yield select_transport(stream, type, receiver_full_jid, Set.empty()); - if (transport == null) { - throw new Error.NO_SHARED_PROTOCOLS("No suitable transports"); - } - SecurityPrecondition? precondition = get_security_precondition(precondition_name); - if (precondition_name != null && precondition == null) { - throw new Error.UNSUPPORTED_SECURITY("No suitable security precondiiton found"); - } - Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; - if (my_jid == null) { - throw new Error.GENERAL("Couldn't determine own JID"); - } - TransportParameters transport_params = transport.create_transport_parameters(stream, my_jid, receiver_full_jid); - SecurityParameters? security_params = precondition != null ? precondition.create_security_parameters(stream, my_jid, receiver_full_jid, precondation_options) : null; - Session session = new Session.initiate_sent(random_uuid(), type, transport_params, security_params, my_jid, receiver_full_jid, content_name, send_terminate_and_remove_session); - StanzaNode content = new StanzaNode.build("content", NS_URI) - .put_attribute("creator", "initiator") - .put_attribute("name", content_name) - .put_attribute("senders", senders.to_string()) - .put_node(description) - .put_node(transport_params.to_transport_stanza_node()); - if (security_params != null) { - content.put_node(security_params.to_security_stanza_node(stream, my_jid, receiver_full_jid)); - } - StanzaNode jingle = new StanzaNode.build("jingle", NS_URI) - .add_self_xmlns() - .put_attribute("action", "session-initiate") - .put_attribute("initiator", my_jid.to_string()) - .put_attribute("sid", session.sid) - .put_node(content); - Iq.Stanza iq = new Iq.Stanza.set(jingle) { to=receiver_full_jid }; - - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq, (stream, iq) => { - // TODO(hrxi): handle errors - stream.get_flag(Flag.IDENTITY).add_session(session); - }); - - return session; - } - - public void handle_session_initiate(XmppStream stream, string sid, StanzaNode jingle, Iq.Stanza iq) throws IqError { - ContentNode content = get_single_content_node(jingle); - if (content.description == null || content.transport == null) { - throw new IqError.BAD_REQUEST("missing description or transport node"); - } - Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; - if (my_jid == null) { - throw new IqError.RESOURCE_CONSTRAINT("Couldn't determine own JID"); - } - Transport? transport = get_transport(content.transport.ns_uri); - TransportParameters? transport_params = null; - if (transport != null) { - transport_params = transport.parse_transport_parameters(stream, my_jid, iq.from, content.transport); - } else { - // terminate the session below - } - - ContentType? content_type = get_content_type(content.description.ns_uri); - if (content_type == null) { - // TODO(hrxi): how do we signal an unknown content type? - throw new IqError.NOT_IMPLEMENTED("unknown content type"); - } - ContentParameters content_params = content_type.parse_content_parameters(content.description); - - SecurityPrecondition? precondition = content.security != null ? get_security_precondition(content.security.ns_uri) : null; - SecurityParameters? security_params = null; - if (precondition != null) { - debug("Using precondition %s", precondition.security_ns_uri()); - security_params = precondition.parse_security_parameters(stream, my_jid, iq.from, content.security); - } else if (content.security != null) { - throw new IqError.NOT_IMPLEMENTED("unknown security precondition"); - } - - TransportType type = content_type.content_type_transport_type(); - Session session = new Session.initiate_received(sid, type, transport_params, security_params, my_jid, iq.from, content.name, send_terminate_and_remove_session); - stream.get_flag(Flag.IDENTITY).add_session(session); - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); - - if (transport == null || transport.transport_type() != type) { - StanzaNode reason = new StanzaNode.build("reason", NS_URI) - .put_node(new StanzaNode.build("unsupported-transports", NS_URI)); - session.terminate(reason, "unsupported transports"); - return; - } - - content_params.on_session_initiate(stream, session); - } - - private void send_terminate_and_remove_session(Jid to, string sid, StanzaNode reason) { - StanzaNode jingle = new StanzaNode.build("jingle", NS_URI) - .add_self_xmlns() - .put_attribute("action", "session-terminate") - .put_attribute("sid", sid) - .put_node(reason); - Iq.Stanza iq = new Iq.Stanza.set(jingle) { to=to }; - current_stream.get_module(Iq.Module.IDENTITY).send_iq(current_stream, iq); - - // Immediately remove the session from the open sessions as per the - // XEP, don't wait for confirmation. - current_stream.get_flag(Flag.IDENTITY).remove_session(sid); - } - - public async void on_iq_set(XmppStream stream, Iq.Stanza iq) { - try { - handle_iq_set(stream, iq); - } catch (IqError e) { - send_iq_error(e, stream, iq); - } - } - - public void handle_iq_set(XmppStream stream, Iq.Stanza iq) throws IqError { - StanzaNode? jingle = iq.stanza.get_subnode("jingle", NS_URI); - string? sid = jingle != null ? jingle.get_attribute("sid") : null; - string? action = jingle != null ? jingle.get_attribute("action") : null; - if (jingle == null || sid == null || action == null) { - throw new IqError.BAD_REQUEST("missing jingle node, sid or action"); - } - Session? session = stream.get_flag(Flag.IDENTITY).get_session(sid); - if (action == "session-initiate") { - if (session != null) { - // TODO(hrxi): Info leak if other clients use predictable session IDs? - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.error(iq, new ErrorStanza.build(ErrorStanza.TYPE_MODIFY, ErrorStanza.CONDITION_CONFLICT, "session ID already in use", null)) { to=iq.from }); - return; - } - handle_session_initiate(stream, sid, jingle, iq); - return; - } - if (session == null) { - StanzaNode unknown_session = new StanzaNode.build("unknown-session", ERROR_NS_URI).add_self_xmlns(); - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.error(iq, new ErrorStanza.item_not_found(unknown_session)) { to=iq.from }); - return; - } - session.handle_iq_set(stream, action, jingle, iq); - } - - public override string get_ns() { return NS_URI; } - public override string get_id() { return IDENTITY.id; } -} - -public enum TransportType { - DATAGRAM, - STREAMING, -} - -public enum Senders { - BOTH, - INITIATOR, - NONE, - RESPONDER; - - public string to_string() { - switch (this) { - case BOTH: return "both"; - case INITIATOR: return "initiator"; - case NONE: return "none"; - case RESPONDER: return "responder"; - } - assert_not_reached(); - } -} - -public delegate void SessionTerminate(Jid to, string sid, StanzaNode reason); - -public interface Transport : Object { - public abstract string transport_ns_uri(); - public async abstract bool is_transport_available(XmppStream stream, Jid full_jid); - public abstract TransportType transport_type(); - public abstract int transport_priority(); - public abstract TransportParameters create_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid) throws Error; - public abstract TransportParameters parse_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws IqError; -} - - -// Gets a null `stream` if connection setup was unsuccessful and another -// transport method should be tried. -public interface TransportParameters : Object { - public abstract string transport_ns_uri(); - public abstract StanzaNode to_transport_stanza_node(); - public abstract void on_transport_accept(StanzaNode transport) throws IqError; - public abstract void on_transport_info(StanzaNode transport) throws IqError; - public abstract void create_transport_connection(XmppStream stream, Session session); -} - -public enum Role { - INITIATOR, - RESPONDER; - - public string to_string() { - switch (this) { - case INITIATOR: return "initiator"; - case RESPONDER: return "responder"; - } - assert_not_reached(); - } - - public static Role parse(string role) throws IqError { - switch (role) { - case "initiator": return INITIATOR; - case "responder": return RESPONDER; - } - throw new IqError.BAD_REQUEST(@"invalid role $(role)"); - } -} - -public interface ContentType : Object { - public abstract string content_type_ns_uri(); - public abstract TransportType content_type_transport_type(); - public abstract ContentParameters parse_content_parameters(StanzaNode description) throws IqError; - public abstract void handle_content_session_info(XmppStream stream, Session session, StanzaNode info, Iq.Stanza iq) throws IqError; -} - -public interface ContentParameters : Object { - public abstract void on_session_initiate(XmppStream stream, Session session); -} - -public interface SecurityPrecondition : Object { - public abstract string security_ns_uri(); - public abstract SecurityParameters? create_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Object options) throws Jingle.Error; - public abstract SecurityParameters? parse_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws IqError; -} - -public interface SecurityParameters : Object { - public abstract string security_ns_uri(); - public abstract StanzaNode to_security_stanza_node(XmppStream stream, Jid local_full_jid, Jid peer_full_jid); - public abstract IOStream wrap_stream(IOStream stream); -} - -public class Session { - // INITIATE_SENT -> CONNECTING -> [REPLACING_TRANSPORT -> CONNECTING ->]... ACTIVE -> ENDED - // INITIATE_RECEIVED -> CONNECTING -> [WAITING_FOR_TRANSPORT_REPLACE -> CONNECTING ->].. ACTIVE -> ENDED - public enum State { - INITIATE_SENT, - REPLACING_TRANSPORT, - INITIATE_RECEIVED, - WAITING_FOR_TRANSPORT_REPLACE, - CONNECTING, - ACTIVE, - ENDED, - } - - public State state { get; private set; } - - public Role role { get; private set; } - public string sid { get; private set; } - public TransportType type_ { get; private set; } - public Jid local_full_jid { get; private set; } - public Jid peer_full_jid { get; private set; } - public Role content_creator { get; private set; } - public string content_name { get; private set; } - public SecurityParameters? security { get; private set; } - - private Connection connection; - public IOStream conn { get { return connection; } } - - public bool terminate_on_connection_close { get; set; } - - // INITIATE_SENT | INITIATE_RECEIVED | CONNECTING - Set tried_transport_methods = new HashSet(); - TransportParameters? transport = null; - - SessionTerminate session_terminate_handler; - - public Session.initiate_sent(string sid, TransportType type, TransportParameters transport, SecurityParameters? security, Jid local_full_jid, Jid peer_full_jid, string content_name, owned SessionTerminate session_terminate_handler) { - this.state = State.INITIATE_SENT; - this.role = Role.INITIATOR; - this.sid = sid; - this.type_ = type; - this.local_full_jid = local_full_jid; - this.peer_full_jid = peer_full_jid; - this.content_creator = Role.INITIATOR; - this.content_name = content_name; - this.tried_transport_methods = new HashSet(); - this.tried_transport_methods.add(transport.transport_ns_uri()); - this.transport = transport; - this.security = security; - this.connection = new Connection(this); - this.session_terminate_handler = (owned)session_terminate_handler; - this.terminate_on_connection_close = true; - } - - public Session.initiate_received(string sid, TransportType type, TransportParameters? transport, SecurityParameters? security, Jid local_full_jid, Jid peer_full_jid, string content_name, owned SessionTerminate session_terminate_handler) { - this.state = State.INITIATE_RECEIVED; - this.role = Role.RESPONDER; - this.sid = sid; - this.type_ = type; - this.local_full_jid = local_full_jid; - this.peer_full_jid = peer_full_jid; - this.content_creator = Role.INITIATOR; - this.content_name = content_name; - this.transport = transport; - this.security = security; - this.tried_transport_methods = new HashSet(); - if (transport != null) { - this.tried_transport_methods.add(transport.transport_ns_uri()); - } - this.connection = new Connection(this); - this.session_terminate_handler = (owned)session_terminate_handler; - this.terminate_on_connection_close = true; - } - - public void handle_iq_set(XmppStream stream, string action, StanzaNode jingle, Iq.Stanza iq) throws IqError { - // Validate action. - switch (action) { - case "session-accept": - case "session-info": - case "session-terminate": - case "transport-accept": - case "transport-info": - case "transport-reject": - case "transport-replace": - break; - case "content-accept": - case "content-add": - case "content-modify": - case "content-reject": - case "content-remove": - case "description-info": - case "security-info": - throw new IqError.NOT_IMPLEMENTED(@"$(action) is not implemented"); - default: - throw new IqError.BAD_REQUEST("invalid action"); - } - ContentNode? content = null; - StanzaNode? transport = null; - // Do some pre-processing. - if (action != "session-info" && action != "session-terminate") { - content = get_single_content_node(jingle); - verify_content(content); - switch (action) { - case "transport-accept": - case "transport-reject": - case "transport-replace": - case "transport-info": - switch (state) { - case State.INITIATE_SENT: - case State.REPLACING_TRANSPORT: - case State.INITIATE_RECEIVED: - case State.WAITING_FOR_TRANSPORT_REPLACE: - case State.CONNECTING: - break; - default: - throw new IqError.OUT_OF_ORDER("transport-* unsupported after connection setup"); - } - // TODO(hrxi): What to do with description nodes? - if (content.transport == null) { - throw new IqError.BAD_REQUEST("missing transport node"); - } - transport = content.transport; - break; - } - } - switch (action) { - case "session-accept": - if (state != State.INITIATE_SENT) { - throw new IqError.OUT_OF_ORDER("got session-accept while not waiting for one"); - } - handle_session_accept(stream, content, jingle, iq); - break; - case "session-info": - handle_session_info(stream, jingle, iq); - break; - case "session-terminate": - handle_session_terminate(stream, jingle, iq); - break; - case "transport-accept": - handle_transport_accept(stream, transport, jingle, iq); - break; - case "transport-reject": - handle_transport_reject(stream, jingle, iq); - break; - case "transport-replace": - handle_transport_replace(stream, transport, jingle, iq); - break; - case "transport-info": - handle_transport_info(stream, transport, jingle, iq); - break; - } - } - void handle_session_accept(XmppStream stream, ContentNode content, StanzaNode jingle, Iq.Stanza iq) throws IqError { - string? responder_str = jingle.get_attribute("responder"); - Jid responder = iq.from; - if (responder_str != null) { - try { - responder = new Jid(responder_str); - } catch (InvalidJidError e) { - warning("Received invalid session accept: %s", e.message); - } - } - // TODO(hrxi): more sanity checking, perhaps replace who we're talking to - if (!responder.is_full()) { - throw new IqError.BAD_REQUEST("invalid responder JID"); - } - if (content.description == null || content.transport == null) { - throw new IqError.BAD_REQUEST("missing description or transport node"); - } - if (content.transport.ns_uri != transport.transport_ns_uri()) { - throw new IqError.BAD_REQUEST("session-accept with unnegotiated transport method"); - } - transport.on_transport_accept(content.transport); - // TODO(hrxi): handle content.description :) - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); - - state = State.CONNECTING; - transport.create_transport_connection(stream, this); - } - void connection_created(XmppStream stream, IOStream? conn) { - if (state != State.CONNECTING) { - return; - } - if (conn != null) { - state = State.ACTIVE; - tried_transport_methods.clear(); - if (security != null) { - connection.set_inner(security.wrap_stream(conn)); - } else { - connection.set_inner(conn); - } - transport = null; - } else { - if (role == Role.INITIATOR) { - select_new_transport.begin(stream); - } else { - state = State.WAITING_FOR_TRANSPORT_REPLACE; - } - } - } - void handle_session_terminate(XmppStream stream, StanzaNode jingle, Iq.Stanza iq) throws IqError { - connection.on_terminated_by_jingle("remote terminated jingle session"); - state = State.ENDED; - stream.get_flag(Flag.IDENTITY).remove_session(sid); - - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); - // TODO(hrxi): also handle presence type=unavailable - } - void handle_session_info(XmppStream stream, StanzaNode jingle, Iq.Stanza iq) throws IqError { - StanzaNode? info = get_single_node_anyns(jingle); - if (info == null) { - // Jingle session ping - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); - return; - } - ContentType? content_type = stream.get_module(Module.IDENTITY).get_content_type(info.ns_uri); - if (content_type == null) { - throw new IqError.UNSUPPORTED_INFO("unknown session-info namespace"); - } - content_type.handle_content_session_info(stream, this, info, iq); - } - async void select_new_transport(XmppStream stream) { - Transport? new_transport = yield stream.get_module(Module.IDENTITY).select_transport(stream, type_, peer_full_jid, tried_transport_methods); - if (new_transport == null) { - StanzaNode reason = new StanzaNode.build("reason", NS_URI) - .put_node(new StanzaNode.build("failed-transport", NS_URI)); - terminate(reason, "failed transport"); - return; - } - tried_transport_methods.add(new_transport.transport_ns_uri()); - transport = new_transport.create_transport_parameters(stream, local_full_jid, peer_full_jid); - StanzaNode jingle = new StanzaNode.build("jingle", NS_URI) - .add_self_xmlns() - .put_attribute("action", "transport-replace") - .put_attribute("sid", sid) - .put_node(new StanzaNode.build("content", NS_URI) - .put_attribute("creator", "initiator") - .put_attribute("name", content_name) - .put_node(transport.to_transport_stanza_node()) - ); - Iq.Stanza iq = new Iq.Stanza.set(jingle) { to=peer_full_jid }; - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); - state = State.REPLACING_TRANSPORT; - } - void handle_transport_accept(XmppStream stream, StanzaNode transport_node, StanzaNode jingle, Iq.Stanza iq) throws IqError { - if (state != State.REPLACING_TRANSPORT) { - throw new IqError.OUT_OF_ORDER("no outstanding transport-replace request"); - } - if (transport_node.ns_uri != transport.transport_ns_uri()) { - throw new IqError.BAD_REQUEST("transport-accept with unnegotiated transport method"); - } - transport.on_transport_accept(transport_node); - state = State.CONNECTING; - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); - transport.create_transport_connection(stream, this); - } - void handle_transport_reject(XmppStream stream, StanzaNode jingle, Iq.Stanza iq) throws IqError { - if (state != State.REPLACING_TRANSPORT) { - throw new IqError.OUT_OF_ORDER("no outstanding transport-replace request"); - } - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); - select_new_transport.begin(stream); - } - void handle_transport_replace(XmppStream stream, StanzaNode transport_node, StanzaNode jingle, Iq.Stanza iq) throws IqError { - Transport? transport = stream.get_module(Module.IDENTITY).get_transport(transport_node.ns_uri); - TransportParameters? parameters = null; - if (transport != null) { - // Just parse the transport info for the errors. - parameters = transport.parse_transport_parameters(stream, local_full_jid, peer_full_jid, transport_node); - } - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); - if (state != State.WAITING_FOR_TRANSPORT_REPLACE || transport == null) { - StanzaNode jingle_response = new StanzaNode.build("jingle", NS_URI) - .add_self_xmlns() - .put_attribute("action", "transport-reject") - .put_attribute("sid", sid) - .put_node(new StanzaNode.build("content", NS_URI) - .put_attribute("creator", "initiator") - .put_attribute("name", content_name) - .put_node(transport_node) - ); - Iq.Stanza iq_response = new Iq.Stanza.set(jingle_response) { to=peer_full_jid }; - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq_response); - return; - } - this.transport = parameters; - StanzaNode jingle_response = new StanzaNode.build("jingle", NS_URI) - .add_self_xmlns() - .put_attribute("action", "transport-accept") - .put_attribute("sid", sid) - .put_node(new StanzaNode.build("content", NS_URI) - .put_attribute("creator", "initiator") - .put_attribute("name", content_name) - .put_node(this.transport.to_transport_stanza_node()) - ); - Iq.Stanza iq_response = new Iq.Stanza.set(jingle_response) { to=peer_full_jid }; - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq_response); - - state = State.CONNECTING; - this.transport.create_transport_connection(stream, this); - } - void handle_transport_info(XmppStream stream, StanzaNode transport, StanzaNode jingle, Iq.Stanza iq) throws IqError { - this.transport.on_transport_info(transport); - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); - } - void verify_content(ContentNode content) throws IqError { - if (content.name != content_name || content.creator != content_creator) { - throw new IqError.BAD_REQUEST("unknown content"); - } - } - public void set_transport_connection(XmppStream stream, IOStream? conn) { - if (state != State.CONNECTING) { - return; - } - connection_created(stream, conn); - } - public void send_transport_info(XmppStream stream, StanzaNode transport) { - if (state != State.CONNECTING) { - return; - } - StanzaNode jingle = new StanzaNode.build("jingle", NS_URI) - .add_self_xmlns() - .put_attribute("action", "transport-info") - .put_attribute("sid", sid) - .put_node(new StanzaNode.build("content", NS_URI) - .put_attribute("creator", "initiator") - .put_attribute("name", content_name) - .put_node(transport) - ); - Iq.Stanza iq = new Iq.Stanza.set(jingle) { to=peer_full_jid }; - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); - } - public void accept(XmppStream stream, StanzaNode description) { - if (state != State.INITIATE_RECEIVED) { - return; // TODO(hrxi): what to do? - } - StanzaNode jingle = new StanzaNode.build("jingle", NS_URI) - .add_self_xmlns() - .put_attribute("action", "session-accept") - .put_attribute("sid", sid) - .put_node(new StanzaNode.build("content", NS_URI) - .put_attribute("creator", "initiator") - .put_attribute("name", content_name) - .put_node(description) - .put_node(transport.to_transport_stanza_node()) - ); - Iq.Stanza iq = new Iq.Stanza.set(jingle) { to=peer_full_jid }; - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); - - state = State.CONNECTING; - transport.create_transport_connection(stream, this); - } - - public void reject(XmppStream stream) { - if (state != State.INITIATE_RECEIVED) { - return; // TODO(hrxi): what to do? - } - StanzaNode reason = new StanzaNode.build("reason", NS_URI) - .put_node(new StanzaNode.build("decline", NS_URI)); - terminate(reason, "declined"); - } - - public void set_application_error(XmppStream stream, StanzaNode? application_reason = null) { - StanzaNode reason = new StanzaNode.build("reason", NS_URI) - .put_node(new StanzaNode.build("failed-application", NS_URI)); - if (application_reason != null) { - reason.put_node(application_reason); - } - terminate(reason, "application error"); - } - - public void on_connection_error(IOError error) { - // TODO(hrxi): where can we get an XmppStream from? - StanzaNode reason = new StanzaNode.build("reason", NS_URI) - .put_node(new StanzaNode.build("failed-transport", NS_URI)) - .put_node(new StanzaNode.build("text", NS_URI) - .put_node(new StanzaNode.text(error.message)) - ); - terminate(reason, @"transport error: $(error.message)"); - } - public void on_connection_close() { - if (terminate_on_connection_close) { - StanzaNode reason = new StanzaNode.build("reason", NS_URI) - .put_node(new StanzaNode.build("success", NS_URI)); - terminate(reason, "success"); - } - } - - public void terminate(StanzaNode reason, string? local_reason) { - if (state == State.ENDED) { - return; - } - if (state == State.ACTIVE) { - if (local_reason != null) { - connection.on_terminated_by_jingle(@"local session-terminate: $(local_reason)"); - } else { - connection.on_terminated_by_jingle("local session-terminate"); - } - } - - session_terminate_handler(peer_full_jid, sid, reason); - state = State.ENDED; - } -} - -public class Connection : IOStream { - public class Input : InputStream { - private weak Connection connection; - public Input(Connection connection) { - this.connection = connection; - } - public override ssize_t read(uint8[] buffer, Cancellable? cancellable = null) throws IOError { - throw new IOError.NOT_SUPPORTED("can't do non-async reads on jingle connections"); - } - public override async ssize_t read_async(uint8[]? buffer, int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { - return yield connection.read_async(buffer, io_priority, cancellable); - } - public override bool close(Cancellable? cancellable = null) throws IOError { - return connection.close_read(cancellable); - } - public override async bool close_async(int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { - return yield connection.close_read_async(io_priority, cancellable); - } - } - public class Output : OutputStream { - private weak Connection connection; - public Output(Connection connection) { - this.connection = connection; - } - public override ssize_t write(uint8[] buffer, Cancellable? cancellable = null) throws IOError { - throw new IOError.NOT_SUPPORTED("can't do non-async writes on jingle connections"); - } - public override async ssize_t write_async(uint8[]? buffer, int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { - return yield connection.write_async(buffer, io_priority, cancellable); - } - public override bool close(Cancellable? cancellable = null) throws IOError { - return connection.close_write(cancellable); - } - public override async bool close_async(int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { - return yield connection.close_write_async(io_priority, cancellable); - } - } - - private Input input; - private Output output; - public override InputStream input_stream { get { return input; } } - public override OutputStream output_stream { get { return output; } } - - private weak Session session; - private IOStream? inner = null; - private string? error = null; - - private bool read_closed = false; - private bool write_closed = false; - - private class OnSetInnerCallback { - public SourceFunc callback; - public int io_priority; - } - - Gee.List callbacks = new ArrayList(); - - public Connection(Session session) { - this.input = new Input(this); - this.output = new Output(this); - this.session = session; - } - - public void set_inner(IOStream inner) { - assert(this.inner == null); - this.inner = inner; - foreach (OnSetInnerCallback c in callbacks) { - Idle.add((owned) c.callback, c.io_priority); - } - callbacks = null; - } - - public void on_terminated_by_jingle(string reason) { - if (error == null) { - close_async.begin(); - error = reason; - } - } - - private void check_for_errors() throws IOError { - if (error != null) { - throw new IOError.CLOSED(error); - } - } - private async void wait_and_check_for_errors(int io_priority, Cancellable? cancellable = null) throws IOError { - while (true) { - check_for_errors(); - if (inner != null) { - return; - } - SourceFunc callback = wait_and_check_for_errors.callback; - ulong id = 0; - if (cancellable != null) { - id = cancellable.connect(() => callback()); - } - callbacks.add(new OnSetInnerCallback() { callback=(owned)callback, io_priority=io_priority}); - yield; - if (cancellable != null) { - cancellable.disconnect(id); - } - } - } - private void handle_connection_error(IOError error) { - Session? strong = session; - if (strong != null) { - strong.on_connection_error(error); - } - } - private void handle_connection_close() { - Session? strong = session; - if (strong != null) { - strong.on_connection_close(); - } - } - - public async ssize_t read_async(uint8[]? buffer, int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { - yield wait_and_check_for_errors(io_priority, cancellable); - try { - return yield inner.input_stream.read_async(buffer, io_priority, cancellable); - } catch (IOError e) { - handle_connection_error(e); - throw e; - } - } - public async ssize_t write_async(uint8[]? buffer, int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { - yield wait_and_check_for_errors(io_priority, cancellable); - try { - return yield inner.output_stream.write_async(buffer, io_priority, cancellable); - } catch (IOError e) { - handle_connection_error(e); - throw e; - } - } - public bool close_read(Cancellable? cancellable = null) throws IOError { - check_for_errors(); - if (read_closed) { - return true; - } - close_read_async.begin(GLib.Priority.DEFAULT, cancellable); - return true; - } - public async bool close_read_async(int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { - debug("Closing Jingle input stream"); - yield wait_and_check_for_errors(io_priority, cancellable); - if (read_closed) { - return true; - } - read_closed = true; - IOError error = null; - bool result = true; - try { - result = yield inner.input_stream.close_async(io_priority, cancellable); - } catch (IOError e) { - if (error == null) { - error = e; - } - } - try { - result = (yield close_if_both_closed(io_priority, cancellable)) && result; - } catch (IOError e) { - if (error == null) { - error = e; - } - } - if (error != null) { - handle_connection_error(error); - throw error; - } - return result; - } - public bool close_write(Cancellable? cancellable = null) throws IOError { - check_for_errors(); - if (write_closed) { - return true; - } - close_write_async.begin(GLib.Priority.DEFAULT, cancellable); - return true; - } - public async bool close_write_async(int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { - yield wait_and_check_for_errors(io_priority, cancellable); - if (write_closed) { - return true; - } - write_closed = true; - IOError error = null; - bool result = true; - try { - result = yield inner.output_stream.close_async(io_priority, cancellable); - } catch (IOError e) { - if (error == null) { - error = e; - } - } - try { - result = (yield close_if_both_closed(io_priority, cancellable)) && result; - } catch (IOError e) { - if (error == null) { - error = e; - } - } - if (error != null) { - handle_connection_error(error); - throw error; - } - return result; - } - private async bool close_if_both_closed(int io_priority, Cancellable? cancellable = null) throws IOError { - if (read_closed && write_closed) { - handle_connection_close(); - //return yield inner.close_async(io_priority, cancellable); - } - return true; - } -} - -public class Flag : XmppStreamFlag { - public static FlagIdentity IDENTITY = new FlagIdentity(NS_URI, "jingle"); - - private HashMap sessions = new HashMap(); - - public void add_session(Session session) { - sessions[session.sid] = session; - } - public Session? get_session(string sid) { - return sessions.has_key(sid) ? sessions[sid] : null; - } - public void remove_session(string sid) { - sessions.unset(sid); - } - - public override string get_ns() { return NS_URI; } - public override string get_id() { return IDENTITY.id; } -} - -} diff --git a/xmpp-vala/src/module/xep/0166_jingle/component.vala b/xmpp-vala/src/module/xep/0166_jingle/component.vala new file mode 100644 index 00000000..544bcd69 --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/component.vala @@ -0,0 +1,52 @@ +namespace Xmpp.Xep.Jingle { + + public abstract class ComponentConnection : Object { + public uint8 component_id { get; set; default = 0; } + public abstract async void terminate(bool we_terminated, string? reason_name = null, string? reason_text = null); + public signal void connection_closed(); + public signal void connection_error(IOError e); + } + + public abstract class DatagramConnection : ComponentConnection { + public bool ready { get; set; default = false; } + private string? terminate_reason_name = null; + private string? terminate_reason_text = null; + private bool terminated = false; + + public override async void terminate(bool we_terminated, string? reason_string = null, string? reason_text = null) { + if (!terminated) { + terminated = true; + terminate_reason_name = reason_string; + terminate_reason_text = reason_text; + connection_closed(); + } + } + + public signal void datagram_received(Bytes datagram); + public abstract void send_datagram(Bytes datagram); + } + + public class StreamingConnection : ComponentConnection { + public Gee.Future stream { get { return promise.future; } } + protected Gee.Promise promise = new Gee.Promise(); + private string? terminated = null; + + public async void init(IOStream stream) { + assert(!this.stream.ready); + promise.set_value(stream); + if (terminated != null) { + yield stream.close_async(); + } + } + + public override async void terminate(bool we_terminated, string? reason_name = null, string? reason_text = null) { + if (terminated == null) { + terminated = (reason_name ?? "") + " - " + (reason_text ?? "") + @"we terminated? $we_terminated"; + if (stream.ready) { + yield stream.value.close_async(); + } + } + } + } +} + diff --git a/xmpp-vala/src/module/xep/0166_jingle/content.vala b/xmpp-vala/src/module/xep/0166_jingle/content.vala new file mode 100644 index 00000000..67c13dd8 --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/content.vala @@ -0,0 +1,236 @@ +using Gee; +using Xmpp; + +public class Xmpp.Xep.Jingle.Content : Object { + + public signal void senders_modify_incoming(Senders proposed_senders); + + // INITIATE_SENT -> CONNECTING -> [REPLACING_TRANSPORT -> CONNECTING ->]... ACTIVE -> ENDED + // INITIATE_RECEIVED -> CONNECTING -> [WAITING_FOR_TRANSPORT_REPLACE -> CONNECTING ->].. ACTIVE -> ENDED + public enum State { + PENDING, + WANTS_TO_BE_ACCEPTED, + ACCEPTED, + REPLACING_TRANSPORT, + WAITING_FOR_TRANSPORT_REPLACE + } + + public State state { get; set; } + + public Role role { get; private set; } + public Jid local_full_jid { get; private set; } + public Jid peer_full_jid { get; private set; } + public Role content_creator { get; private set; } + public string content_name { get; private set; } + public Senders senders { get; private set; } + + public ContentType content_type; + public ContentParameters content_params; + public Transport transport; + public TransportParameters? transport_params; + public SecurityPrecondition security_precondition; + public SecurityParameters? security_params; + + public weak Session session; + public Map component_connections = new HashMap(); // TODO private + + // INITIATE_SENT | INITIATE_RECEIVED | CONNECTING + public Set tried_transport_methods = new HashSet(); + + + public Content.initiate_sent(string content_name, Senders senders, + ContentType content_type, ContentParameters content_params, + Transport transport, TransportParameters? transport_params, + SecurityPrecondition? security_precondition, SecurityParameters? security_params, + Jid local_full_jid, Jid peer_full_jid) { + this.content_name = content_name; + this.senders = senders; + this.role = Role.INITIATOR; + this.local_full_jid = local_full_jid; + this.peer_full_jid = peer_full_jid; + this.content_creator = Role.INITIATOR; + + this.content_type = content_type; + this.content_params = content_params; + this.transport = transport; + this.transport_params = transport_params; + this.security_precondition = security_precondition; + this.security_params = security_params; + + this.tried_transport_methods.add(transport.ns_uri); + + state = State.PENDING; + } + + public Content.initiate_received(string content_name, Senders senders, + ContentType content_type, ContentParameters content_params, + Transport transport, TransportParameters? transport_params, + SecurityPrecondition? security_precondition, SecurityParameters? security_params, + Jid local_full_jid, Jid peer_full_jid) throws Error { + this.content_name = content_name; + this.senders = senders; + this.role = Role.RESPONDER; + this.local_full_jid = local_full_jid; + this.peer_full_jid = peer_full_jid; + this.content_creator = Role.INITIATOR; + + this.content_type = content_type; + this.content_params = content_params; + this.transport = transport; + this.transport_params = transport_params; + this.security_precondition = security_precondition; + this.security_params = security_params; + + if (transport != null) { + this.tried_transport_methods.add(transport.ns_uri); + } + + state = State.PENDING; + } + + public void set_session(Session session) { + this.session = session; + this.transport_params.set_content(this); + } + + public void accept() { + state = State.WANTS_TO_BE_ACCEPTED; + + session.accept_content(this); + } + + public void reject() { + session.reject_content(this); + } + + public void terminate(bool we_terminated, string? reason_name, string? reason_text) { + content_params.terminate(we_terminated, reason_name, reason_text); + + foreach (ComponentConnection connection in component_connections.values) { + connection.terminate(we_terminated, reason_name, reason_text); + } + } + + public void modify(Senders new_sender) { + session.send_content_modify(this, new_sender); + this.senders = new_sender; + } + + public void accept_content_modify(Senders senders) { + this.senders = senders; + } + + internal void handle_content_modify(XmppStream stream, Senders proposed_senders) { + senders_modify_incoming(proposed_senders); + } + + internal void on_accept(XmppStream stream) { + this.transport_params.create_transport_connection(stream, this); + this.content_params.accept(stream, session, this); + } + + internal void handle_accept(XmppStream stream, ContentNode content_node) { + this.transport_params.handle_transport_accept(content_node.transport); + this.transport_params.create_transport_connection(stream, this); + this.content_params.handle_accept(stream, this.session, this, content_node.description); + } + + private async void select_new_transport() { + XmppStream stream = session.stream; + Transport? new_transport = yield stream.get_module(Module.IDENTITY).select_transport(stream, transport.type_, transport_params.components, peer_full_jid, tried_transport_methods); + if (new_transport == null) { + session.terminate(ReasonElement.FAILED_TRANSPORT, null, "failed transport"); + // TODO should we only terminate this content or really the whole session? + return; + } + tried_transport_methods.add(new_transport.ns_uri); + transport_params = new_transport.create_transport_parameters(stream, transport_params.components, local_full_jid, peer_full_jid); + set_transport_params(transport_params); + session.send_transport_replace(this, transport_params); + state = State.REPLACING_TRANSPORT; + } + + public void handle_transport_accept(XmppStream stream, StanzaNode transport_node, StanzaNode jingle, Iq.Stanza iq) throws IqError { + if (state != State.REPLACING_TRANSPORT) { + throw new IqError.OUT_OF_ORDER("no outstanding transport-replace request"); + } + if (transport_node.ns_uri != transport.ns_uri) { + throw new IqError.BAD_REQUEST("transport-accept with unnegotiated transport method"); + } + transport_params.handle_transport_accept(transport_node); + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + transport_params.create_transport_connection(stream, this); + } + + public void handle_transport_reject(XmppStream stream, StanzaNode jingle, Iq.Stanza iq) throws IqError { + if (state != State.REPLACING_TRANSPORT) { + throw new IqError.OUT_OF_ORDER("no outstanding transport-replace request"); + } + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + select_new_transport.begin(); + } + + public void handle_transport_replace(XmppStream stream, StanzaNode transport_node, StanzaNode jingle, Iq.Stanza iq) throws IqError { + Transport? transport = stream.get_module(Module.IDENTITY).get_transport(transport_node.ns_uri); + TransportParameters? parameters = null; + if (transport != null) { + // Just parse the transport info for the errors. + parameters = transport.parse_transport_parameters(stream, content_type.required_components, local_full_jid, peer_full_jid, transport_node); + } + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + if (state != State.WAITING_FOR_TRANSPORT_REPLACE || transport == null) { + session.send_transport_reject(this, transport_node); + return; + } + set_transport_params(parameters); + session.send_transport_accept(this, parameters); + + this.transport_params.create_transport_connection(stream, this); + } + + public void handle_transport_info(XmppStream stream, StanzaNode transport, StanzaNode jingle, Iq.Stanza iq) throws IqError { + this.transport_params.handle_transport_info(transport); + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + } + + public void on_description_info(XmppStream stream, StanzaNode description, StanzaNode jinglq, Iq.Stanza iq) throws IqError { + // TODO: do something. + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + } + + void verify_content(ContentNode content) throws IqError { + if (content.name != content_name || content.creator != content_creator) { + throw new IqError.BAD_REQUEST("unknown content"); + } + } + + public void set_transport_connection(ComponentConnection? conn, uint8 component = 1) { + debug(@"set_transport_connection: %s, %s, %i, %s, overwrites: %s", this.content_name, this.state.to_string(), component, (conn != null).to_string(), component_connections.has_key(component).to_string()); + + if (conn != null) { + component_connections[component] = conn; + if (transport_params.components == component) { + state = State.ACCEPTED; + tried_transport_methods.clear(); + } + } else { + if (role == Role.INITIATOR) { + select_new_transport.begin(); + } else { + state = State.WAITING_FOR_TRANSPORT_REPLACE; + } + } + } + + private void set_transport_params(TransportParameters transport_params) { + this.transport_params = transport_params; + } + + public ComponentConnection? get_transport_connection(uint8 component = 1) { + return component_connections[component]; + } + + public void send_transport_info(StanzaNode transport) { + session.send_transport_info(this, transport); + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/content_description.vala b/xmpp-vala/src/module/xep/0166_jingle/content_description.vala new file mode 100644 index 00000000..1a24e52e --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/content_description.vala @@ -0,0 +1,27 @@ +using Gee; +using Xmpp.Xep; +using Xmpp; + +namespace Xmpp.Xep.Jingle { + + public interface ContentType : Object { + public abstract string ns_uri { get; } + public abstract TransportType required_transport_type { get; } + public abstract uint8 required_components { get; } + public abstract ContentParameters parse_content_parameters(StanzaNode description) throws IqError; + } + + public interface ContentParameters : Object { + /** Called when the counterpart proposes the content */ + public abstract async void handle_proposed_content(XmppStream stream, Jingle.Session session, Content content); + + /** Called when we accept the content that was proposed by the counterpart */ + public abstract void accept(XmppStream stream, Jingle.Session session, Jingle.Content content); + /** Called when the counterpart accepts the content that was proposed by us*/ + public abstract void handle_accept(XmppStream stream, Jingle.Session session, Jingle.Content content, StanzaNode description_node); + + public abstract void terminate(bool we_terminated, string? reason_name, string? reason_text); + + public abstract StanzaNode get_description_node(); + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/content_node.vala b/xmpp-vala/src/module/xep/0166_jingle/content_node.vala new file mode 100644 index 00000000..7d8d56c8 --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/content_node.vala @@ -0,0 +1,112 @@ +using Gee; +using Xmpp.Xep; +using Xmpp; + +namespace Xmpp.Xep.Jingle { + + class ContentNode { + public Role creator; + public string name; + public Senders senders; + public StanzaNode? description; + public StanzaNode? transport; + public StanzaNode? security; + } + + [Version(deprecated = true)] + ContentNode get_single_content_node(StanzaNode jingle) throws IqError { + Gee.List contents = jingle.get_subnodes("content"); + if (contents.size == 0) { + throw new IqError.BAD_REQUEST("missing content node"); + } + if (contents.size > 1) { + throw new IqError.NOT_IMPLEMENTED("can't process multiple content nodes"); + } + StanzaNode content = contents[0]; + string? creator_str = content.get_attribute("creator"); + // Vala can't typecheck the ternary operator here. + Role? creator = null; + if (creator_str != null) { + creator = Role.parse(creator_str); + } else { + // TODO(hrxi): now, is the creator attribute optional or not (XEP-0166 + // Jingle)? + creator = Role.INITIATOR; + } + + string? name = content.get_attribute("name"); + + Senders senders = Senders.parse(content.get_attribute("senders")); + + StanzaNode? description = get_single_node_anyns(content, "description"); + StanzaNode? transport = get_single_node_anyns(content, "transport"); + StanzaNode? security = get_single_node_anyns(content, "security"); + if (name == null || creator == null) { + throw new IqError.BAD_REQUEST("missing name or creator"); + } + + return new ContentNode() { + creator=creator, + name=name, + senders=senders, + description=description, + transport=transport, + security=security + }; + } + + Gee.List get_content_nodes(StanzaNode jingle) throws IqError { + Gee.List contents = jingle.get_subnodes("content"); + if (contents.size == 0) { + throw new IqError.BAD_REQUEST("missing content node"); + } + Gee.List list = new ArrayList(); + foreach (StanzaNode content in contents) { + string? creator_str = content.get_attribute("creator"); + // Vala can't typecheck the ternary operator here. + Role? creator = null; + if (creator_str != null) { + creator = Role.parse(creator_str); + } else { + // TODO(hrxi): now, is the creator attribute optional or not (XEP-0166 + // Jingle)? + creator = Role.INITIATOR; + } + + string? name = content.get_attribute("name"); + Senders senders = Senders.parse(content.get_attribute("senders")); + StanzaNode? description = get_single_node_anyns(content, "description"); + StanzaNode? transport = get_single_node_anyns(content, "transport"); + StanzaNode? security = get_single_node_anyns(content, "security"); + if (name == null || creator == null) { + throw new IqError.BAD_REQUEST("missing name or creator"); + } + list.add(new ContentNode() { + creator=creator, + name=name, + senders=senders, + description=description, + transport=transport, + security=security + }); + } + return list; + } + + StanzaNode? get_single_node_anyns(StanzaNode parent, string? node_name = null) throws IqError { + StanzaNode? result = null; + foreach (StanzaNode child in parent.get_all_subnodes()) { + if (node_name == null || child.name == node_name) { + if (result != null) { + if (node_name != null) { + throw new IqError.BAD_REQUEST(@"multiple $(node_name) nodes"); + } else { + throw new IqError.BAD_REQUEST(@"expected single subnode"); + } + } + result = child; + } + } + return result; + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/content_security.vala b/xmpp-vala/src/module/xep/0166_jingle/content_security.vala new file mode 100644 index 00000000..0e10311d --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/content_security.vala @@ -0,0 +1,18 @@ +using Gee; +using Xmpp.Xep; +using Xmpp; + +namespace Xmpp.Xep.Jingle { + + public interface SecurityPrecondition : Object { + public abstract string security_ns_uri(); + public abstract SecurityParameters? create_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Object options) throws Jingle.Error; + public abstract SecurityParameters? parse_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws IqError; + } + + public interface SecurityParameters : Object { + public abstract string security_ns_uri(); + public abstract StanzaNode to_security_stanza_node(XmppStream stream, Jid local_full_jid, Jid peer_full_jid); + public abstract IOStream wrap_stream(IOStream stream); + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/content_transport.vala b/xmpp-vala/src/module/xep/0166_jingle/content_transport.vala new file mode 100644 index 00000000..cd74c836 --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/content_transport.vala @@ -0,0 +1,29 @@ +namespace Xmpp.Xep.Jingle { + + public interface Transport : Object { + public abstract string ns_uri { get; } + public async abstract bool is_transport_available(XmppStream stream, uint8 components, Jid full_jid); + public abstract TransportType type_ { get; } + public abstract int priority { get; } + public abstract TransportParameters create_transport_parameters(XmppStream stream, uint8 components, Jid local_full_jid, Jid peer_full_jid) throws Error; + public abstract TransportParameters parse_transport_parameters(XmppStream stream, uint8 components, Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws IqError; + } + + public enum TransportType { + DATAGRAM, + STREAMING, + } + + // Gets a null `stream` if connection setup was unsuccessful and another + // transport method should be tried. + public interface TransportParameters : Object { + public abstract string ns_uri { get; } + public abstract uint8 components { get; } + + public abstract void set_content(Content content); + public abstract StanzaNode to_transport_stanza_node(); + public abstract void handle_transport_accept(StanzaNode transport) throws IqError; + public abstract void handle_transport_info(StanzaNode transport) throws IqError; + public abstract void create_transport_connection(XmppStream stream, Content content); + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/jingle_flag.vala b/xmpp-vala/src/module/xep/0166_jingle/jingle_flag.vala new file mode 100644 index 00000000..9f0acd27 --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/jingle_flag.vala @@ -0,0 +1,38 @@ +using Gee; +using Xmpp; + +public class Xmpp.Xep.Jingle.Flag : XmppStreamFlag { + public static FlagIdentity IDENTITY = new FlagIdentity(NS_URI, "jingle"); + + public HashMap sessions = new HashMap(); + public HashMap> promises = new HashMap>(); + + // We might get transport-infos about a session before we finished fully creating the session. (e.g. telepathy outgoing calls) + // Thus, we "pre add" the session as soon as possible and can then await it. + public void pre_add_session(string sid) { + var promise = new Promise(); + promises[sid] = promise; + } + + public void add_session(Session session) { + if (promises.has_key(session.sid)) { + promises[session.sid].set_value(session); + promises.unset(session.sid); + } + sessions[session.sid] = session; + } + + public async Session? get_session(string sid) { + if (promises.has_key(sid)) { + return yield promises[sid].future.wait_async(); + } + return sessions[sid]; + } + + public void remove_session(string sid) { + sessions.unset(sid); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala b/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala new file mode 100644 index 00000000..1e8a36d1 --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala @@ -0,0 +1,235 @@ +using Gee; +using Xmpp; + +namespace Xmpp.Xep.Jingle { + + internal const string NS_URI = "urn:xmpp:jingle:1"; + private const string ERROR_NS_URI = "urn:xmpp:jingle:errors:1"; + + // This module can only be attached to one stream at a time. + public class Module : XmppStreamModule, Iq.Handler { + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0166_jingle"); + + public signal void session_initiate_received(XmppStream stream, Session session); + + private HashMap content_types = new HashMap(); + private HashMap session_info_types = new HashMap(); + private HashMap transports = new HashMap(); + private HashMap security_preconditions = new HashMap(); + + public override void attach(XmppStream stream) { + stream.add_flag(new Flag()); + stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); + stream.get_module(Iq.Module.IDENTITY).register_for_namespace(NS_URI, this); + + // TODO update stream in all sessions + } + + public override void detach(XmppStream stream) { + stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI); + stream.get_module(Iq.Module.IDENTITY).unregister_from_namespace(NS_URI, this); + } + + public void register_content_type(ContentType content_type) { + content_types[content_type.ns_uri] = content_type; + } + + public void register_session_info_type(SessionInfoNs info_ns) { + session_info_types[info_ns.ns_uri] = info_ns; + } + + public ContentType? get_content_type(string ns_uri) { + if (!content_types.has_key(ns_uri)) { + return null; + } + return content_types[ns_uri]; + } + + public SessionInfoNs? get_session_info_type(string ns_uri) { + return session_info_types[ns_uri]; + } + + public void register_transport(Transport transport) { + transports[transport.ns_uri] = transport; + } + + public Transport? get_transport(string ns_uri) { + if (!transports.has_key(ns_uri)) { + return null; + } + return transports[ns_uri]; + } + + public async Transport? select_transport(XmppStream stream, TransportType type, uint8 components, Jid receiver_full_jid, Set blacklist) { + Transport? result = null; + foreach (Transport transport in transports.values) { + if (transport.type_ != type) { + continue; + } + if (transport.ns_uri in blacklist) { + continue; + } + if (yield transport.is_transport_available(stream, components, receiver_full_jid)) { + if (result != null) { + if (result.priority >= transport.priority) { + continue; + } + } + result = transport; + } + } + return result; + } + + public void register_security_precondition(SecurityPrecondition precondition) { + security_preconditions[precondition.security_ns_uri()] = precondition; + } + + public SecurityPrecondition? get_security_precondition(string? ns_uri) { + if (ns_uri == null) return null; + if (!security_preconditions.has_key(ns_uri)) { + return null; + } + return security_preconditions[ns_uri]; + } + + private async bool is_jingle_available(XmppStream stream, Jid full_jid) { + bool? has_jingle = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI); + return has_jingle != null && has_jingle; + } + + public async bool is_available(XmppStream stream, TransportType type, uint8 components, Jid full_jid) { + return (yield is_jingle_available(stream, full_jid)) && (yield select_transport(stream, type, components, full_jid, Set.empty())) != null; + } + + public async Session create_session(XmppStream stream, Gee.List contents, Jid receiver_full_jid, string sid = random_uuid()) throws Error { + if (!yield is_jingle_available(stream, receiver_full_jid)) { + throw new Error.NO_SHARED_PROTOCOLS("No Jingle support"); + } + Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; + if (my_jid == null) { + throw new Error.GENERAL("Couldn't determine own JID"); + } + + Session session = new Session.initiate_sent(stream, sid, my_jid, receiver_full_jid); + session.terminated.connect((session, stream, _1, _2, _3) => { stream.get_flag(Flag.IDENTITY).remove_session(session.sid); }); + + foreach (Content content in contents) { + session.insert_content(content); + } + + // Build & send session-initiate iq stanza + StanzaNode initiate_jingle_iq = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "session-initiate") + .put_attribute("initiator", my_jid.to_string()) + .put_attribute("sid", session.sid); + + foreach (Content content in contents) { + StanzaNode content_node = new StanzaNode.build("content", NS_URI) + .put_attribute("creator", "initiator") + .put_attribute("name", content.content_name) + .put_attribute("senders", content.senders.to_string()) + .put_node(content.content_params.get_description_node()) + .put_node(content.transport_params.to_transport_stanza_node()); + if (content.security_params != null) { + content_node.put_node(content.security_params.to_security_stanza_node(stream, my_jid, receiver_full_jid)); + } + initiate_jingle_iq.put_node(content_node); + } + + Iq.Stanza iq = new Iq.Stanza.set(initiate_jingle_iq) { to=receiver_full_jid }; + + stream.get_flag(Flag.IDENTITY).add_session(session); + // We might get a follow-up before the ack => add_session before send_iq returns + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq, (stream, iq) => { + if (iq.is_error()) warning("Jingle session-initiate got error: %s", iq.stanza.to_string()); + }); + + return session; + } + + public async void handle_session_initiate(XmppStream stream, string sid, StanzaNode jingle, Iq.Stanza iq) throws IqError { + Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; + if (my_jid == null) { + throw new IqError.RESOURCE_CONSTRAINT("Couldn't determine own JID"); + } + + Session session = new Session.initiate_received(stream, sid, my_jid, iq.from); + session.terminated.connect((stream) => { stream.get_flag(Flag.IDENTITY).remove_session(sid); }); + + stream.get_flag(Flag.IDENTITY).pre_add_session(session.sid); + + foreach (ContentNode content_node in get_content_nodes(jingle)) { + yield session.insert_content_node(content_node, iq.from); + } + + stream.get_flag(Flag.IDENTITY).add_session(session); + + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + + session_initiate_received(stream, session); + } + + public async void on_iq_set(XmppStream stream, Iq.Stanza iq) { + try { + yield handle_iq_set(stream, iq); + } catch (IqError e) { + send_iq_error(e, stream, iq); + } + } + + public async void handle_iq_set(XmppStream stream, Iq.Stanza iq) throws IqError { + StanzaNode? jingle_node = iq.stanza.get_subnode("jingle", NS_URI); + if (jingle_node == null) { + throw new IqError.BAD_REQUEST("missing jingle node"); + } + string? sid = jingle_node.get_attribute("sid"); + string? action = jingle_node.get_attribute("action"); + if (sid == null || action == null) { + throw new IqError.BAD_REQUEST("missing jingle node, sid or action"); + } + + Session? session = yield stream.get_flag(Flag.IDENTITY).get_session(sid); + if (action == "session-initiate") { + if (session != null) { + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.error(iq, new ErrorStanza.build(ErrorStanza.TYPE_MODIFY, ErrorStanza.CONDITION_CONFLICT, "session ID already in use", null)) { to=iq.from }); + return; + } + yield handle_session_initiate(stream, sid, jingle_node, iq); + return; + } + if (session == null) { + StanzaNode unknown_session = new StanzaNode.build("unknown-session", ERROR_NS_URI).add_self_xmlns(); + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.error(iq, new ErrorStanza.item_not_found(unknown_session)) { to=iq.from }); + return; + } + session.handle_iq_set(action, jingle_node, iq); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } + } + + void send_iq_error(IqError iq_error, XmppStream stream, Iq.Stanza iq) { + ErrorStanza error; + if (iq_error is IqError.BAD_REQUEST) { + error = new ErrorStanza.bad_request(iq_error.message); + } else if (iq_error is IqError.NOT_ACCEPTABLE) { + error = new ErrorStanza.not_acceptable(iq_error.message); + } else if (iq_error is IqError.NOT_IMPLEMENTED) { + error = new ErrorStanza.feature_not_implemented(iq_error.message); + } else if (iq_error is IqError.UNSUPPORTED_INFO) { + StanzaNode unsupported_info = new StanzaNode.build("unsupported-info", ERROR_NS_URI).add_self_xmlns(); + error = new ErrorStanza.build(ErrorStanza.TYPE_CANCEL, ErrorStanza.CONDITION_FEATURE_NOT_IMPLEMENTED, iq_error.message, unsupported_info); + } else if (iq_error is IqError.OUT_OF_ORDER) { + StanzaNode out_of_order = new StanzaNode.build("out-of-order", ERROR_NS_URI).add_self_xmlns(); + error = new ErrorStanza.build(ErrorStanza.TYPE_MODIFY, ErrorStanza.CONDITION_UNEXPECTED_REQUEST, iq_error.message, out_of_order); + } else if (iq_error is IqError.RESOURCE_CONSTRAINT) { + error = new ErrorStanza.resource_constraint(iq_error.message); + } else { + assert_not_reached(); + } + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.error(iq, error) { to=iq.from }); + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/jingle_structs.vala b/xmpp-vala/src/module/xep/0166_jingle/jingle_structs.vala new file mode 100644 index 00000000..0f283e0e --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/jingle_structs.vala @@ -0,0 +1,73 @@ +using Gee; +using Xmpp; + +namespace Xmpp.Xep.Jingle { + + public errordomain IqError { + BAD_REQUEST, + NOT_ACCEPTABLE, + NOT_IMPLEMENTED, + UNSUPPORTED_INFO, + OUT_OF_ORDER, + RESOURCE_CONSTRAINT, + } + + public errordomain Error { + GENERAL, + BAD_REQUEST, + INVALID_PARAMETERS, + UNSUPPORTED_TRANSPORT, + UNSUPPORTED_SECURITY, + NO_SHARED_PROTOCOLS, + TRANSPORT_ERROR, + } + + public enum Senders { + BOTH, + INITIATOR, + NONE, + RESPONDER; + + public string to_string() { + switch (this) { + case BOTH: return "both"; + case INITIATOR: return "initiator"; + case NONE: return "none"; + case RESPONDER: return "responder"; + } + assert_not_reached(); + } + + public static Senders parse(string? senders) throws IqError { + if (senders == null) return Senders.BOTH; + switch (senders) { + case "initiator": return Senders.INITIATOR; + case "responder": return Senders.RESPONDER; + case "both": return Senders.BOTH; + } + throw new IqError.BAD_REQUEST(@"invalid role $(senders)"); + } + } + + public enum Role { + INITIATOR, + RESPONDER; + + public string to_string() { + switch (this) { + case INITIATOR: return "initiator"; + case RESPONDER: return "responder"; + } + assert_not_reached(); + } + + public static Role parse(string role) throws IqError { + switch (role) { + case "initiator": return INITIATOR; + case "responder": return RESPONDER; + } + throw new IqError.BAD_REQUEST(@"invalid role $(role)"); + } + } + +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/reason_element.vala b/xmpp-vala/src/module/xep/0166_jingle/reason_element.vala new file mode 100644 index 00000000..1cbdf936 --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/reason_element.vala @@ -0,0 +1,29 @@ +using Gee; +using Xmpp; + +namespace Xmpp.Xep.Jingle.ReasonElement { + public const string ALTERNATIVE_SESSION = "alternative-session"; + public const string BUSY = "busy"; + public const string CANCEL = "cancel"; + public const string CONNECTIVITY_ERROR = "connectivity-error"; + public const string DECLINE = "decline"; + public const string EXPIRED = "expired"; + public const string FAILED_APPLICATION = "failed_application"; + public const string FAILED_TRANSPORT = "failed_transport"; + public const string GENERAL_ERROR = "general-error"; + public const string GONE = "gone"; + public const string INCOMPATIBLE_PARAMETERS = "incompatible-parameters"; + public const string MEDIA_ERROR = "media-error"; + public const string SECURITY_ERROR = "security-error"; + public const string SUCCESS = "success"; + public const string TIMEOUT = "timeout"; + public const string UNSUPPORTED_APPLICATIONS = "unsupported-applications"; + public const string UNSUPPORTED_TRANSPORTS = "unsupported-transports"; + + public const string[] NORMAL_TERMINATE_REASONS = { + BUSY, + CANCEL, + DECLINE, + SUCCESS + }; +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/session.vala b/xmpp-vala/src/module/xep/0166_jingle/session.vala new file mode 100644 index 00000000..e9ad9169 --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/session.vala @@ -0,0 +1,559 @@ +using Gee; +using Xmpp; + + +public delegate void Xmpp.Xep.Jingle.SessionTerminate(Jid to, string sid, StanzaNode reason); + +public class Xmpp.Xep.Jingle.Session : Object { + + public signal void terminated(XmppStream stream, bool we_terminated, string? reason_name, string? reason_text); + public signal void additional_content_add_incoming(XmppStream stream, Content content); + + // INITIATE_SENT/INITIATE_RECEIVED -> CONNECTING -> PENDING -> ACTIVE -> ENDED + public enum State { + INITIATE_SENT, + INITIATE_RECEIVED, + ACTIVE, + ENDED, + } + + public XmppStream stream { get; set; } + public State state { get; set; } + public string sid { get; private set; } + public Jid local_full_jid { get; private set; } + public Jid peer_full_jid { get; private set; } + public bool we_initiated { get; private set; } + + public HashMap contents = new HashMap(); + + public SecurityParameters? security { get { return contents.values.to_array()[0].security_params; } } + + public Session.initiate_sent(XmppStream stream, string sid, Jid local_full_jid, Jid peer_full_jid) { + this.stream = stream; + this.sid = sid; + this.local_full_jid = local_full_jid; + this.peer_full_jid = peer_full_jid; + this.state = State.INITIATE_SENT; + this.we_initiated = true; + } + + public Session.initiate_received(XmppStream stream, string sid, Jid local_full_jid, Jid peer_full_jid) { + this.stream = stream; + this.sid = sid; + this.local_full_jid = local_full_jid; + this.peer_full_jid = peer_full_jid; + this.state = State.INITIATE_RECEIVED; + this.we_initiated = false; + } + + public void handle_iq_set(string action, StanzaNode jingle, Iq.Stanza iq) throws IqError { + + if (action.has_prefix("session-")) { + switch (action) { + case "session-accept": + Gee.List content_nodes = get_content_nodes(jingle); + + if (state != State.INITIATE_SENT) { + throw new IqError.OUT_OF_ORDER("got session-accept while not waiting for one"); + } + handle_session_accept(content_nodes, jingle, iq); + break; + case "session-info": + handle_session_info.begin(jingle, iq); + break; + case "session-terminate": + handle_session_terminate(jingle, iq); + break; + default: + throw new IqError.BAD_REQUEST("invalid action"); + } + + + } else if (action.has_prefix("content-")) { + switch (action) { + case "content-accept": + ContentNode content_node = get_single_content_node(jingle); + handle_content_accept(content_node); + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + break; + case "content-add": + ContentNode content_node = get_single_content_node(jingle); + insert_content_node.begin(content_node, peer_full_jid); + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + break; + case "content-modify": + handle_content_modify(stream, jingle, iq); + break; + case "content-reject": + case "content-remove": + throw new IqError.NOT_IMPLEMENTED(@"$(action) is not implemented"); + default: + throw new IqError.BAD_REQUEST("invalid action"); + } + + + } else if (action.has_prefix("transport-")) { + ContentNode content_node = get_single_content_node(jingle); + if (!contents.has_key(content_node.name)) { + throw new IqError.BAD_REQUEST("unknown content"); + } + + if (content_node.transport == null) { + throw new IqError.BAD_REQUEST("missing transport node"); + } + + Content content = contents[content_node.name]; + + if (content_node.creator != content.content_creator) { + throw new IqError.BAD_REQUEST("unknown content; creator"); + } + + switch (action) { + case "transport-accept": + content.handle_transport_accept(stream, content_node.transport, jingle, iq); + break; + case "transport-info": + content.handle_transport_info(stream, content_node.transport, jingle, iq); + break; + case "transport-reject": + content.handle_transport_reject(stream, jingle, iq); + break; + case "transport-replace": + content.handle_transport_replace(stream, content_node.transport, jingle, iq); + break; + default: + throw new IqError.BAD_REQUEST("invalid action"); + } + + + } else if (action == "description-info") { + ContentNode content_node = get_single_content_node(jingle); + if (!contents.has_key(content_node.name)) { + throw new IqError.BAD_REQUEST("unknown content"); + } + + Content content = contents[content_node.name]; + + if (content_node.creator != content.content_creator) { + throw new IqError.BAD_REQUEST("unknown content; creator"); + } + + content.on_description_info(stream, content_node.description, jingle, iq); + } else if (action == "security-info") { + throw new IqError.NOT_IMPLEMENTED(@"$(action) is not implemented"); + + + } else { + throw new IqError.BAD_REQUEST("invalid action"); + } + } + + internal void insert_content(Content content) { + this.contents[content.content_name] = content; + content.set_session(this); + } + + internal async void insert_content_node(ContentNode content_node, Jid peer_full_jid) throws IqError { + if (content_node.description == null || content_node.transport == null) { + throw new IqError.BAD_REQUEST("missing description or transport node"); + } + + Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; + + Transport? transport = stream.get_module(Jingle.Module.IDENTITY).get_transport(content_node.transport.ns_uri); + ContentType? content_type = stream.get_module(Jingle.Module.IDENTITY).get_content_type(content_node.description.ns_uri); + + if (content_type == null) { + // TODO(hrxi): how do we signal an unknown content type? + throw new IqError.NOT_IMPLEMENTED("unknown content type"); + } + + TransportParameters? transport_params = null; + if (transport != null) { + transport_params = transport.parse_transport_parameters(stream, content_type.required_components, my_jid, peer_full_jid, content_node.transport); + } else { + // terminate the session below + } + + ContentParameters content_params = content_type.parse_content_parameters(content_node.description); + + SecurityPrecondition? precondition = content_node.security != null ? stream.get_module(Jingle.Module.IDENTITY).get_security_precondition(content_node.security.ns_uri) : null; + SecurityParameters? security_params = null; + if (precondition != null) { + debug("Using precondition %s", precondition.security_ns_uri()); + security_params = precondition.parse_security_parameters(stream, my_jid, peer_full_jid, content_node.security); + } else if (content_node.security != null) { + throw new IqError.NOT_IMPLEMENTED("unknown security precondition"); + } + + TransportType type = content_type.required_transport_type; + + if (transport == null || transport.type_ != type) { + terminate(ReasonElement.UNSUPPORTED_TRANSPORTS, null, "unsupported transports"); + throw new IqError.NOT_IMPLEMENTED("unsupported transports"); + } + + Content content = new Content.initiate_received(content_node.name, content_node.senders, + content_type, content_params, + transport, transport_params, + precondition, security_params, + my_jid, peer_full_jid); + insert_content(content); + + yield content_params.handle_proposed_content(stream, this, content); + + if (this.state == State.ACTIVE) { + additional_content_add_incoming(stream, content); + } + } + + public async void add_content(Content content) { + content.session = this; + this.contents[content.content_name] = content; + + StanzaNode content_add_node = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "content-add") + .put_attribute("sid", sid) + .put_node(new StanzaNode.build("content", NS_URI) + .put_attribute("creator", "initiator") + .put_attribute("name", content.content_name) + .put_attribute("senders", content.senders.to_string()) + .put_node(content.content_params.get_description_node()) + .put_node(content.transport_params.to_transport_stanza_node())); + + Iq.Stanza iq = new Iq.Stanza.set(content_add_node) { to=peer_full_jid }; + yield stream.get_module(Iq.Module.IDENTITY).send_iq_async(stream, iq); + } + + private void handle_content_accept(ContentNode content_node) throws IqError { + if (content_node.description == null || content_node.transport == null) throw new IqError.BAD_REQUEST("missing description or transport node"); + if (!contents.has_key(content_node.name)) throw new IqError.BAD_REQUEST("unknown content"); + + Content content = contents[content_node.name]; + + if (content_node.creator != content.content_creator) warning("Counterpart accepts content with an unexpected `creator`"); + if (content_node.senders != content.senders) warning("Counterpart accepts content with an unexpected `senders`"); + if (content_node.transport.ns_uri != content.transport_params.ns_uri) throw new IqError.BAD_REQUEST("session-accept with unnegotiated transport method"); + + content.handle_accept(stream, content_node); + } + + private void handle_content_modify(XmppStream stream, StanzaNode jingle_node, Iq.Stanza iq) throws IqError { + ContentNode content_node = get_single_content_node(jingle_node); + + Content? content = contents[content_node.name]; + + if (content == null) throw new IqError.BAD_REQUEST("no such content"); + if (content_node.creator != content.content_creator) throw new IqError.BAD_REQUEST("mismatching creator"); + + Iq.Stanza result_iq = new Iq.Stanza.result(iq) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, result_iq); + + content.handle_content_modify(stream, content_node.senders); + } + + private void handle_session_accept(Gee.List content_nodes, StanzaNode jingle, Iq.Stanza iq) throws IqError { + string? responder_str = jingle.get_attribute("responder"); + Jid responder = iq.from; + if (responder_str != null) { + try { + responder = new Jid(responder_str); + } catch (InvalidJidError e) { + warning("Received invalid session accept: %s", e.message); + } + } + // TODO(hrxi): more sanity checking, perhaps replace who we're talking to + if (!responder.is_full()) { + throw new IqError.BAD_REQUEST("invalid responder JID"); + } + foreach (ContentNode content_node in content_nodes) { + handle_content_accept(content_node); + } + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + + state = State.ACTIVE; + } + + private void handle_session_terminate(StanzaNode jingle, Iq.Stanza iq) throws IqError { + string? reason_text = null; + string? reason_name = null; + StanzaNode? reason_node = iq.stanza.get_deep_subnode(NS_URI + ":jingle", NS_URI + ":reason"); + if (reason_node != null) { + if (reason_node.sub_nodes.size > 2) warning("Jingle session-terminate reason node w/ >2 subnodes: %s", iq.stanza.to_string()); + + StanzaNode? specific_reason_node = null; + StanzaNode? text_node = null; + foreach (StanzaNode node in reason_node.sub_nodes) { + if (node.name == "text") { + text_node = node; + } else if (node.ns_uri == NS_URI) { + specific_reason_node = node; + } + } + reason_name = specific_reason_node != null ? specific_reason_node.name : null; + reason_text = text_node != null ? text_node.get_string_content() : null; + + if (reason_name != null && !(specific_reason_node.name in ReasonElement.NORMAL_TERMINATE_REASONS)) { + warning("Jingle session terminated: %s : %s", reason_name ?? "", reason_text ?? ""); + } else { + debug("Jingle session terminated: %s : %s", reason_name ?? "", reason_text ?? ""); + } + } + + foreach (Content content in contents.values) { + content.terminate(false, reason_name, reason_text); + } + + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + // TODO(hrxi): also handle presence type=unavailable + + state = State.ENDED; + terminated(stream, false, reason_name, reason_text); + } + + private async void handle_session_info(StanzaNode jingle, Iq.Stanza iq) throws IqError { + StanzaNode? info = get_single_node_anyns(jingle); + if (info == null) { + // Jingle session ping + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + return; + } + SessionInfoNs? info_ns = stream.get_module(Module.IDENTITY).get_session_info_type(info.ns_uri); + if (info_ns == null) { + throw new IqError.UNSUPPORTED_INFO("unknown session-info namespace"); + } + info_ns.handle_content_session_info(stream, this, info, iq); + + Iq.Stanza result_iq = new Iq.Stanza.result(iq) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, result_iq); + } + + private void accept() { + if (state != State.INITIATE_RECEIVED) critical("Accepting a stream, but we're the initiator"); + + StanzaNode jingle = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "session-accept") + .put_attribute("sid", sid); + foreach (Content content in contents.values) { + StanzaNode content_node = new StanzaNode.build("content", NS_URI) + .put_attribute("creator", "initiator") + .put_attribute("name", content.content_name) + .put_attribute("senders", content.senders.to_string()) + .put_node(content.content_params.get_description_node()) + .put_node(content.transport_params.to_transport_stanza_node()); + jingle.put_node(content_node); + } + Iq.Stanza iq = new Iq.Stanza.set(jingle) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); + + + foreach (Content content in contents.values) { + content.on_accept(stream); + } + + state = State.ACTIVE; + } + + internal void accept_content(Content content) { + if (state == State.INITIATE_RECEIVED) { + bool all_accepted = true; + foreach (Content c in contents.values) { + if (c.state != Content.State.WANTS_TO_BE_ACCEPTED) { + all_accepted = false; + } + } + if (all_accepted) { + accept(); + } + } else if (state == State.ACTIVE) { + StanzaNode content_accept_node = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "content-accept") + .put_attribute("sid", sid) + .put_node(new StanzaNode.build("content", NS_URI) + .put_attribute("creator", "initiator") + .put_attribute("name", content.content_name) + .put_attribute("senders", content.senders.to_string()) + .put_node(content.content_params.get_description_node()) + .put_node(content.transport_params.to_transport_stanza_node())); + + Iq.Stanza iq = new Iq.Stanza.set(content_accept_node) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); + + content.on_accept(stream); + } + } + + private void reject() { + if (state != State.INITIATE_RECEIVED) critical("Accepting a stream, but we're the initiator"); + terminate(ReasonElement.DECLINE, null, "declined"); + } + + internal void reject_content(Content content) { + if (state == State.INITIATE_RECEIVED) { + reject(); + } else { + warning("not really handeling content rejects"); + } + } + + public void set_application_error(StanzaNode? application_reason = null) { + terminate(ReasonElement.FAILED_APPLICATION, null, "application error"); + } + + public void terminate(string? reason_name, string? reason_text, string? local_reason) { + if (state == State.ENDED) return; + + if (state == State.ACTIVE) { + string reason_str; + if (local_reason != null) { + reason_str = @"local session-terminate: $(local_reason)"; + } else { + reason_str = "local session-terminate"; + } + foreach (Content content in contents.values) { + content.terminate(true, reason_name, reason_text); + } + } + + StanzaNode terminate_iq = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "session-terminate") + .put_attribute("sid", sid); + if (reason_name != null || reason_text != null) { + StanzaNode reason_node = new StanzaNode.build("reason", NS_URI); + if (reason_name != null) { + reason_node.put_node(new StanzaNode.build(reason_name, NS_URI)); + } + if (reason_text != null) { + reason_node.put_node(new StanzaNode.text(reason_text)); + } + terminate_iq.put_node(reason_node); + } + Iq.Stanza iq = new Iq.Stanza.set(terminate_iq) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); + + state = State.ENDED; + terminated(stream, true, reason_name, reason_text); + } + + internal void send_session_info(StanzaNode child_node) { + if (state == State.ENDED) return; + + StanzaNode node = new StanzaNode.build("jingle", NS_URI).add_self_xmlns() + .put_attribute("action", "session-info") + .put_attribute("sid", sid) + // TODO put `initiator`? + .put_node(child_node); + Iq.Stanza iq = new Iq.Stanza.set(node) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); + } + + internal void send_content_modify(Content content, Senders senders) { + if (state == State.ENDED) return; + + StanzaNode node = new StanzaNode.build("jingle", NS_URI).add_self_xmlns() + .put_attribute("action", "content-modify") + .put_attribute("sid", sid) + .put_node(new StanzaNode.build("content", NS_URI) + .put_attribute("creator", content.content_creator.to_string()) + .put_attribute("name", content.content_name) + .put_attribute("senders", senders.to_string())); + Iq.Stanza iq = new Iq.Stanza.set(node) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); + } + + internal void send_transport_accept(Content content, TransportParameters transport_params) { + if (state == State.ENDED) return; + + StanzaNode jingle_response = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "transport-accept") + .put_attribute("sid", sid) + .put_node(new StanzaNode.build("content", NS_URI) + .put_attribute("creator", "initiator") + .put_attribute("name", content.content_name) + .put_node(transport_params.to_transport_stanza_node()) + ); + Iq.Stanza iq_response = new Iq.Stanza.set(jingle_response) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq_response); + } + + internal void send_transport_replace(Content content, TransportParameters transport_params) { + if (state == State.ENDED) return; + + StanzaNode jingle = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "transport-replace") + .put_attribute("sid", sid) + .put_node(new StanzaNode.build("content", NS_URI) + .put_attribute("creator", "initiator") + .put_attribute("name", content.content_name) + .put_node(transport_params.to_transport_stanza_node()) + ); + Iq.Stanza iq = new Iq.Stanza.set(jingle) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); + } + + internal void send_transport_reject(Content content, StanzaNode transport_node) { + if (state == State.ENDED) return; + + StanzaNode jingle_response = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "transport-reject") + .put_attribute("sid", sid) + .put_node(new StanzaNode.build("content", NS_URI) + .put_attribute("creator", "initiator") + .put_attribute("name", content.content_name) + .put_node(transport_node) + ); + Iq.Stanza iq_response = new Iq.Stanza.set(jingle_response) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq_response); + } + + internal void send_transport_info(Content content, StanzaNode transport) { + if (state == State.ENDED) return; + + StanzaNode jingle = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "transport-info") + .put_attribute("sid", sid) + .put_node(new StanzaNode.build("content", NS_URI) + .put_attribute("creator", "initiator") + .put_attribute("name", content.content_name) + .put_node(transport) + ); + Iq.Stanza iq = new Iq.Stanza.set(jingle) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); + } + + public bool senders_include_us(Senders senders) { + switch (senders) { + case Senders.BOTH: + return true; + case Senders.NONE: + return false; + case Senders.INITIATOR: + return we_initiated; + case Senders.RESPONDER: + return !we_initiated; + } + assert_not_reached(); + } + + public bool senders_include_counterpart(Senders senders) { + switch (senders) { + case Senders.BOTH: + return true; + case Senders.NONE: + return false; + case Senders.INITIATOR: + return !we_initiated; + case Senders.RESPONDER: + return we_initiated; + } + assert_not_reached(); + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/session_info.vala b/xmpp-vala/src/module/xep/0166_jingle/session_info.vala new file mode 100644 index 00000000..fcf7584f --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/session_info.vala @@ -0,0 +1,12 @@ +using Gee; +using Xmpp.Xep; +using Xmpp; + +namespace Xmpp.Xep.Jingle { + + public interface SessionInfoNs : Object { + public abstract string ns_uri { get; } + + public abstract void handle_content_session_info(XmppStream stream, Session session, StanzaNode info, Iq.Stanza iq) throws IqError; + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala b/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala index 1c0323be..07b158bc 100644 --- a/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala +++ b/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala @@ -7,50 +7,42 @@ namespace Xmpp.Xep.JingleFileTransfer { private const string NS_URI = "urn:xmpp:jingle:apps:file-transfer:5"; public class Module : Jingle.ContentType, XmppStreamModule { + + public signal void file_incoming(XmppStream stream, FileTransfer file_transfer); + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0234_jingle_file_transfer"); + public SessionInfoType session_info_type = new SessionInfoType(); public override void attach(XmppStream stream) { stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); stream.get_module(Jingle.Module.IDENTITY).register_content_type(this); + stream.get_module(Jingle.Module.IDENTITY).register_session_info_type(session_info_type); } public override void detach(XmppStream stream) { stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI); } - public string content_type_ns_uri() { - return NS_URI; - } - public Jingle.TransportType content_type_transport_type() { - return Jingle.TransportType.STREAMING; - } + public string ns_uri { get { return NS_URI; } } + public Jingle.TransportType required_transport_type { get { return Jingle.TransportType.STREAMING; } } + public uint8 required_components { get { return 1; } } + public Jingle.ContentParameters parse_content_parameters(StanzaNode description) throws Jingle.IqError { return Parameters.parse(this, description); } - public void handle_content_session_info(XmppStream stream, Jingle.Session session, StanzaNode info, Iq.Stanza iq) throws Jingle.IqError { - switch (info.name) { - case "received": - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); - break; - case "checksum": - // TODO(hrxi): handle hash - stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); - break; - default: - throw new Jingle.IqError.UNSUPPORTED_INFO(@"unsupported file transfer info $(info.name)"); - } - } - public signal void file_incoming(XmppStream stream, FileTransfer file_transfer); + public Jingle.ContentParameters create_content_parameters(Object object) throws Jingle.IqError { + assert_not_reached(); + } public async bool is_available(XmppStream stream, Jid full_jid) { bool? has_feature = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI); if (has_feature == null || !(!)has_feature) { return false; } - return yield stream.get_module(Jingle.Module.IDENTITY).is_available(stream, Jingle.TransportType.STREAMING, full_jid); + return yield stream.get_module(Jingle.Module.IDENTITY).is_available(stream, required_transport_type, required_components, full_jid); } - public async void offer_file_stream(XmppStream stream, Jid receiver_full_jid, InputStream input_stream, string basename, int64 size, string? precondition_name = null, Object? precondition_options = null) throws IOError { + public async void offer_file_stream(XmppStream stream, Jid receiver_full_jid, InputStream input_stream, string basename, int64 size, string? precondition_name = null, Object? precondition_options = null) throws Jingle.Error { StanzaNode file_node; StanzaNode description = new StanzaNode.build("description", NS_URI) .add_self_xmlns() @@ -64,25 +56,83 @@ public class Module : Jingle.ContentType, XmppStreamModule { warning("Sending file %s without size, likely going to cause problems down the road...", basename); } - Jingle.Session session; - try { - session = yield stream.get_module(Jingle.Module.IDENTITY) - .create_session(stream, Jingle.TransportType.STREAMING, receiver_full_jid, Jingle.Senders.INITIATOR, "a-file-offer", description, precondition_name, precondition_options); // TODO(hrxi): Why "a-file-offer"? - } catch (Jingle.Error e) { - throw new IOError.FAILED(@"couldn't create Jingle session: $(e.message)"); + Parameters parameters = Parameters.parse(this, description); + + Jingle.Module jingle_module = stream.get_module(Jingle.Module.IDENTITY); + + Jingle.Transport? transport = yield jingle_module.select_transport(stream, required_transport_type, required_components, receiver_full_jid, Set.empty()); + if (transport == null) { + throw new Jingle.Error.NO_SHARED_PROTOCOLS("No suitable transports"); + } + Jingle.SecurityPrecondition? precondition = jingle_module.get_security_precondition(precondition_name); + if (precondition_name != null && precondition == null) { + throw new Jingle.Error.UNSUPPORTED_SECURITY("No suitable security precondiiton found"); } - session.terminate_on_connection_close = false; + Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; + if (my_jid == null) { + throw new Jingle.Error.GENERAL("Couldn't determine own JID"); + } + Jingle.TransportParameters transport_params = transport.create_transport_parameters(stream, required_components, my_jid, receiver_full_jid); + Jingle.SecurityParameters? security_params = precondition != null ? precondition.create_security_parameters(stream, my_jid, receiver_full_jid, precondition_options) : null; + + Jingle.Content content = new Jingle.Content.initiate_sent("a-file-offer", Jingle.Senders.INITIATOR, + this, parameters, + transport, transport_params, + precondition, security_params, + my_jid, receiver_full_jid); + + ArrayList contents = new ArrayList(); + contents.add(content); + - yield session.conn.input_stream.close_async(); + Jingle.Session? session = null; + try { + session = yield jingle_module.create_session(stream, contents, receiver_full_jid); + + // Wait for the counterpart to accept our offer + ulong content_notify_id = 0; + content_notify_id = content.notify["state"].connect(() => { + if (content.state == Jingle.Content.State.ACCEPTED) { + Idle.add(offer_file_stream.callback); + content.disconnect(content_notify_id); + } + }); + yield; - // TODO(hrxi): catch errors - yield session.conn.output_stream.splice_async(input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE|OutputStreamSpliceFlags.CLOSE_TARGET); + // Send the file data + Jingle.StreamingConnection connection = content.component_connections.values.to_array()[0] as Jingle.StreamingConnection; + IOStream io_stream = yield connection.stream.wait_async(); + yield io_stream.input_stream.close_async(); + yield io_stream.output_stream.splice_async(input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE|OutputStreamSpliceFlags.CLOSE_TARGET); + yield connection.terminate(true); + } catch (Jingle.Error e) { + session.terminate(Jingle.ReasonElement.FAILED_TRANSPORT, e.message, e.message); + throw new Jingle.Error.GENERAL(@"couldn't create Jingle session: $(e.message)"); + } } public override string get_ns() { return NS_URI; } public override string get_id() { return IDENTITY.id; } } +public class SessionInfoType : Jingle.SessionInfoNs, Object { + + public string ns_uri { get { return NS_URI; } } + + public void handle_content_session_info(XmppStream stream, Jingle.Session session, StanzaNode info, Iq.Stanza iq) throws Jingle.IqError { + switch (info.name) { + case "received": + break; + case "checksum": + // TODO(hrxi): handle hash + break; + default: + throw new Jingle.IqError.UNSUPPORTED_INFO(@"unsupported file transfer info $(info.name)"); + } + } + +} + public class Parameters : Jingle.ContentParameters, Object { Module parent; @@ -127,24 +177,42 @@ public class Parameters : Jingle.ContentParameters, Object { return new Parameters(parent, description, media_type, name, size); } - public void on_session_initiate(XmppStream stream, Jingle.Session session) { - parent.file_incoming(stream, new FileTransfer(session, this)); + public StanzaNode get_description_node() { + return original_description; } + + public async void handle_proposed_content(XmppStream stream, Jingle.Session session, Jingle.Content content) { + parent.file_incoming(stream, new FileTransfer(session, content, this)); + } + + public void modify(XmppStream stream, Jingle.Session session, Jingle.Content content, Jingle.Senders senders) { } + + public void accept(XmppStream stream, Jingle.Session session, Jingle.Content content) { } + + public void handle_accept(XmppStream stream, Jingle.Session session, Jingle.Content content, StanzaNode description_node) { } + + public void terminate(bool we_terminated, string? reason_name, string? reason_text) { } } // Does nothing except wrapping an input stream to signal EOF after reading // `max_size` bytes. private class FileTransferInputStream : InputStream { + + public signal void closed(); + InputStream inner; int64 remaining_size; + public FileTransferInputStream(InputStream inner, int64 max_size) { this.inner = inner; this.remaining_size = max_size; } + private ssize_t update_remaining(ssize_t read) { this.remaining_size -= read; return read; } + public override ssize_t read(uint8[] buffer_, Cancellable? cancellable = null) throws IOError { unowned uint8[] buffer = buffer_; if (remaining_size <= 0) { @@ -155,6 +223,7 @@ private class FileTransferInputStream : InputStream { } return update_remaining(inner.read(buffer, cancellable)); } + public override async ssize_t read_async(uint8[]? buffer_, int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { unowned uint8[] buffer = buffer_; if (remaining_size <= 0) { @@ -165,16 +234,21 @@ private class FileTransferInputStream : InputStream { } return update_remaining(yield inner.read_async(buffer, io_priority, cancellable)); } + public override bool close(Cancellable? cancellable = null) throws IOError { + closed(); return inner.close(cancellable); } + public override async bool close_async(int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { + closed(); return yield inner.close_async(io_priority, cancellable); } } public class FileTransfer : Object { Jingle.Session session; + Jingle.Content content; Parameters parameters; public Jid peer { get { return session.peer_full_jid; } } @@ -184,19 +258,27 @@ public class FileTransfer : Object { public InputStream? stream { get; private set; } - public FileTransfer(Jingle.Session session, Parameters parameters) { + public FileTransfer(Jingle.Session session, Jingle.Content content, Parameters parameters) { this.session = session; + this.content = content; this.parameters = parameters; - this.stream = new FileTransferInputStream(session.conn.input_stream, size); } - public void accept(XmppStream stream) throws IOError { - session.accept(stream, parameters.original_description); - session.conn.output_stream.close(); + public async void accept(XmppStream stream) throws IOError { + content.accept(); + + Jingle.StreamingConnection connection = content.component_connections.values.to_array()[0] as Jingle.StreamingConnection; + IOStream? io_stream = yield connection.stream.wait_async(); + FileTransferInputStream ft_stream = new FileTransferInputStream(io_stream.input_stream, size); + io_stream.output_stream.close(); + ft_stream.closed.connect(() => { + session.terminate(Jingle.ReasonElement.SUCCESS, null, null); + }); + this.stream = ft_stream; } public void reject(XmppStream stream) { - session.reject(stream); + content.reject(); } } diff --git a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala index bb31cadc..1c4e0d38 100644 --- a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala +++ b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala @@ -21,19 +21,13 @@ public class Module : Jingle.Transport, XmppStreamModule { public override string get_ns() { return NS_URI; } public override string get_id() { return IDENTITY.id; } - public async bool is_transport_available(XmppStream stream, Jid full_jid) { - return yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI); + public async bool is_transport_available(XmppStream stream, uint8 components, Jid full_jid) { + return components == 1 && yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI); } - public string transport_ns_uri() { - return NS_URI; - } - public Jingle.TransportType transport_type() { - return Jingle.TransportType.STREAMING; - } - public int transport_priority() { - return 1; - } + public string ns_uri { get { return NS_URI; } } + public Jingle.TransportType type_ { get { return Jingle.TransportType.STREAMING; } } + public int priority { get { return 1; } } private Gee.List get_proxies(XmppStream stream) { Gee.List result = new ArrayList(); int i = 1 << 15; @@ -70,20 +64,21 @@ public class Module : Jingle.Transport, XmppStreamModule { } private void select_candidates(XmppStream stream, Jid local_full_jid, string dstaddr, Parameters result) { result.local_candidates.add_all(get_proxies(stream)); - result.local_candidates.add_all(start_local_listeners(stream, local_full_jid, dstaddr, out result.listener)); + //result.local_candidates.add_all(start_local_listeners(stream, local_full_jid, dstaddr, out result.listener)); result.local_candidates.sort((c1, c2) => { if (c1.priority < c2.priority) { return 1; } if (c1.priority > c2.priority) { return -1; } return 0; }); } - public Jingle.TransportParameters create_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid) { + public Jingle.TransportParameters create_transport_parameters(XmppStream stream, uint8 components, Jid local_full_jid, Jid peer_full_jid) { + assert(components == 1); Parameters result = new Parameters.create(local_full_jid, peer_full_jid, random_uuid()); string dstaddr = calculate_dstaddr(result.sid, local_full_jid, peer_full_jid); select_candidates(stream, local_full_jid, dstaddr, result); return result; } - public Jingle.TransportParameters parse_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws Jingle.IqError { + public Jingle.TransportParameters parse_transport_parameters(XmppStream stream, uint8 components, Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws Jingle.IqError { Parameters result = Parameters.parse(local_full_jid, peer_full_jid, transport); string dstaddr = calculate_dstaddr(result.sid, local_full_jid, peer_full_jid); select_candidates(stream, local_full_jid, dstaddr, result); @@ -335,6 +330,8 @@ class LocalListener { } class Parameters : Jingle.TransportParameters, Object { + public string ns_uri { get { return NS_URI; } } + public uint8 components { get { return 1; } } public Jingle.Role role { get; private set; } public string sid { get; private set; } public string remote_dstaddr { get; private set; } @@ -352,6 +349,7 @@ class Parameters : Jingle.TransportParameters, Object { Candidate? local_selected_candidate = null; SocketConnection? local_selected_candidate_conn = null; weak Jingle.Session? session = null; + weak Jingle.Content? content = null; XmppStream? hack = null; string? waiting_for_activation_cid = null; @@ -367,9 +365,11 @@ class Parameters : Jingle.TransportParameters, Object { this.local_full_jid = local_full_jid; this.peer_full_jid = peer_full_jid; } + public Parameters.create(Jid local_full_jid, Jid peer_full_jid, string sid) { this(Jingle.Role.INITIATOR, sid, local_full_jid, peer_full_jid, null); } + public static Parameters parse(Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws Jingle.IqError { string? dstaddr = transport.get_attribute("dstaddr"); string? mode = transport.get_attribute("mode"); @@ -386,9 +386,11 @@ class Parameters : Jingle.TransportParameters, Object { } return result; } - public string transport_ns_uri() { - return NS_URI; + + public void set_content(Jingle.Content content) { + } + public StanzaNode to_transport_stanza_node() { StanzaNode transport = new StanzaNode.build("transport", NS_URI) .add_self_xmlns() @@ -405,7 +407,8 @@ class Parameters : Jingle.TransportParameters, Object { } return transport; } - public void on_transport_accept(StanzaNode transport) throws Jingle.IqError { + + public void handle_transport_accept(StanzaNode transport) throws Jingle.IqError { Parameters other = Parameters.parse(local_full_jid, peer_full_jid, transport); if (other.sid != sid) { throw new Jingle.IqError.BAD_REQUEST("invalid sid"); @@ -413,7 +416,8 @@ class Parameters : Jingle.TransportParameters, Object { remote_candidates = other.remote_candidates; remote_dstaddr = other.remote_dstaddr; } - public void on_transport_info(StanzaNode transport) throws Jingle.IqError { + + public void handle_transport_info(StanzaNode transport) throws Jingle.IqError { StanzaNode? candidate_error = transport.get_subnode("candidate-error", NS_URI); StanzaNode? candidate_used = transport.get_subnode("candidate-used", NS_URI); StanzaNode? activated = transport.get_subnode("activated", NS_URI); @@ -449,6 +453,7 @@ class Parameters : Jingle.TransportParameters, Object { handle_proxy_error(); } } + private void handle_remote_candidate(string? cid) throws Jingle.IqError { if (remote_sent_selected_candidate) { throw new Jingle.IqError.BAD_REQUEST("remote candidate already specified"); @@ -470,6 +475,7 @@ class Parameters : Jingle.TransportParameters, Object { debug("Remote selected candidate %s", candidate != null ? candidate.cid : "(null)"); try_completing_negotiation(); } + private void handle_activated(string cid) throws Jingle.IqError { if (waiting_for_activation_cid == null || cid != waiting_for_activation_cid) { throw new Jingle.IqError.BAD_REQUEST("unexpected proxy activation message"); @@ -477,6 +483,7 @@ class Parameters : Jingle.TransportParameters, Object { Idle.add((owned)waiting_for_activation_callback); waiting_for_activation_cid = null; } + private void handle_proxy_error() throws Jingle.IqError { if (waiting_for_activation_cid == null) { throw new Jingle.IqError.BAD_REQUEST("unexpected proxy error message"); @@ -486,6 +493,7 @@ class Parameters : Jingle.TransportParameters, Object { waiting_for_activation_error = true; } + private void try_completing_negotiation() { if (!remote_sent_selected_candidate || !local_determined_selected_candidate) { return; @@ -500,7 +508,7 @@ class Parameters : Jingle.TransportParameters, Object { if (num_candidates == 0) { // Notify Jingle of the failed transport. - session.set_transport_connection(hack, null); + content_set_transport_connection(null); return; } @@ -525,7 +533,7 @@ class Parameters : Jingle.TransportParameters, Object { if (strong == null) { return; } - strong.set_transport_connection(hack, local_selected_candidate_conn); + content_set_transport_connection(local_selected_candidate_conn); } else { wait_for_remote_activation.begin(local_selected_candidate, local_selected_candidate_conn); } @@ -538,15 +546,16 @@ class Parameters : Jingle.TransportParameters, Object { SocketConnection? conn = listener.get_connection(remote_selected_candidate.cid); if (conn == null) { // Remote hasn't actually connected to us?! - strong.set_transport_connection(hack, null); + content_set_transport_connection(null); return; } - strong.set_transport_connection(hack, conn); + content_set_transport_connection(conn); } else { connect_to_local_candidate.begin(remote_selected_candidate); } } } + public async void wait_for_remote_activation(Candidate candidate, SocketConnection conn) { debug("Waiting for remote activation of %s", candidate.cid); waiting_for_activation_cid = candidate.cid; @@ -558,11 +567,12 @@ class Parameters : Jingle.TransportParameters, Object { return; } if (!waiting_for_activation_error) { - strong.set_transport_connection(hack, conn); + content_set_transport_connection(conn); } else { - strong.set_transport_connection(hack, null); + content_set_transport_connection(null); } } + public async void connect_to_local_candidate(Candidate candidate) { debug("Connecting to candidate %s", candidate.cid); try { @@ -587,11 +597,11 @@ class Parameters : Jingle.TransportParameters, Object { throw new IOError.PROXY_FAILED("activation iq error"); } - Jingle.Session? strong = session; - if (strong == null) { + Jingle.Content? strong_content = content; + if (strong_content == null) { return; } - strong.send_transport_info(hack, new StanzaNode.build("transport", NS_URI) + strong_content.send_transport_info(new StanzaNode.build("transport", NS_URI) .add_self_xmlns() .put_attribute("sid", sid) .put_node(new StanzaNode.build("activated", NS_URI) @@ -599,20 +609,21 @@ class Parameters : Jingle.TransportParameters, Object { ) ); - strong.set_transport_connection(hack, conn); + content_set_transport_connection(conn); } catch (Error e) { - Jingle.Session? strong = session; - if (strong == null) { + Jingle.Content? strong_content = content; + if (strong_content == null) { return; } - strong.send_transport_info(hack, new StanzaNode.build("transport", NS_URI) + strong_content.send_transport_info(new StanzaNode.build("transport", NS_URI) .add_self_xmlns() .put_attribute("sid", sid) .put_node(new StanzaNode.build("proxy-error", NS_URI)) ); - strong.set_transport_connection(hack, null); + content_set_transport_connection(null); } } + public async SocketConnection connect_to_socks5(Candidate candidate, string dstaddr) throws Error { SocketClient socket_client = new SocketClient() { timeout=NEGOTIATION_TIMEOUT }; @@ -685,6 +696,7 @@ class Parameters : Jingle.TransportParameters, Object { return conn; } + public async void try_connecting_to_candidates(XmppStream stream, Jingle.Session session) throws Error { remote_candidates.sort((c1, c2) => { // sort from priorities from high to low @@ -705,7 +717,7 @@ class Parameters : Jingle.TransportParameters, Object { local_selected_candidate = candidate; local_selected_candidate_conn = conn; debug("Selected candidate %s", candidate.cid); - session.send_transport_info(stream, new StanzaNode.build("transport", NS_URI) + content.send_transport_info(new StanzaNode.build("transport", NS_URI) .add_self_xmlns() .put_attribute("sid", sid) .put_node(new StanzaNode.build("candidate-used", NS_URI) @@ -722,7 +734,7 @@ class Parameters : Jingle.TransportParameters, Object { } local_determined_selected_candidate = true; local_selected_candidate = null; - session.send_transport_info(stream, new StanzaNode.build("transport", NS_URI) + content.send_transport_info(new StanzaNode.build("transport", NS_URI) .add_self_xmlns() .put_attribute("sid", sid) .put_node(new StanzaNode.build("candidate-error", NS_URI)) @@ -730,10 +742,26 @@ class Parameters : Jingle.TransportParameters, Object { // Try remote candidates try_completing_negotiation(); } - public void create_transport_connection(XmppStream stream, Jingle.Session session) { - this.session = session; + + private Jingle.StreamingConnection connection = new Jingle.StreamingConnection(); + + private void content_set_transport_connection(IOStream? ios) { + IOStream? iostream = ios; + Jingle.Content? strong_content = content; + if (strong_content == null) return; + + if (strong_content.security_params != null) { + iostream = strong_content.security_params.wrap_stream(iostream); + } + connection.init.begin(iostream); + } + + public void create_transport_connection(XmppStream stream, Jingle.Content content) { + this.session = content.session; + this.content = content; this.hack = stream; try_connecting_to_candidates.begin(stream, session); + this.content.set_transport_connection(connection, 1); } } diff --git a/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala b/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala index e26d63b7..5bb71831 100644 --- a/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala +++ b/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala @@ -21,41 +21,41 @@ public class Module : Jingle.Transport, XmppStreamModule { public override string get_ns() { return NS_URI; } public override string get_id() { return IDENTITY.id; } - public async bool is_transport_available(XmppStream stream, Jid full_jid) { - return yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI); + public async bool is_transport_available(XmppStream stream, uint8 components, Jid full_jid) { + return components == 1 && yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI); } - public string transport_ns_uri() { - return NS_URI; - } - public Jingle.TransportType transport_type() { - return Jingle.TransportType.STREAMING; - } - public int transport_priority() { - return 0; - } - public Jingle.TransportParameters create_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid) { + public string ns_uri { get { return NS_URI; } } + public Jingle.TransportType type_ { get { return Jingle.TransportType.STREAMING; } } + public int priority { get { return 0; } } + public Jingle.TransportParameters create_transport_parameters(XmppStream stream, uint8 components, Jid local_full_jid, Jid peer_full_jid) { + assert(components == 1); return new Parameters.create(peer_full_jid, random_uuid()); } - public Jingle.TransportParameters parse_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws Jingle.IqError { + public Jingle.TransportParameters parse_transport_parameters(XmppStream stream, uint8 components, Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws Jingle.IqError { return Parameters.parse(peer_full_jid, transport); } } class Parameters : Jingle.TransportParameters, Object { + public string ns_uri { get { return NS_URI; } } + public uint8 components { get { return 1; } } public Jingle.Role role { get; private set; } public Jid peer_full_jid { get; private set; } public string sid { get; private set; } public int block_size { get; private set; } + private Parameters(Jingle.Role role, Jid peer_full_jid, string sid, int block_size) { this.role = role; this.peer_full_jid = peer_full_jid; this.sid = sid; this.block_size = block_size; } + public Parameters.create(Jid peer_full_jid, string sid) { this(Jingle.Role.INITIATOR, peer_full_jid, sid, DEFAULT_BLOCKSIZE); } + public static Parameters parse(Jid peer_full_jid, StanzaNode transport) throws Jingle.IqError { string? sid = transport.get_attribute("sid"); int block_size = transport.get_attribute_int("block-size"); @@ -64,27 +64,43 @@ class Parameters : Jingle.TransportParameters, Object { } return new Parameters(Jingle.Role.RESPONDER, peer_full_jid, sid, block_size); } + public string transport_ns_uri() { return NS_URI; } + + public void set_content(Jingle.Content content) { + + } + public StanzaNode to_transport_stanza_node() { return new StanzaNode.build("transport", NS_URI) .add_self_xmlns() .put_attribute("block-size", block_size.to_string()) .put_attribute("sid", sid); } - public void on_transport_accept(StanzaNode transport) throws Jingle.IqError { + + public void handle_transport_accept(StanzaNode transport) throws Jingle.IqError { Parameters other = Parameters.parse(peer_full_jid, transport); if (other.sid != sid || other.block_size > block_size) { throw new Jingle.IqError.NOT_ACCEPTABLE("invalid IBB sid or block_size"); } block_size = other.block_size; } - public void on_transport_info(StanzaNode transport) throws Jingle.IqError { + + public void handle_transport_info(StanzaNode transport) throws Jingle.IqError { throw new Jingle.IqError.UNSUPPORTED_INFO("transport-info not supported for IBBs"); } - public void create_transport_connection(XmppStream stream, Jingle.Session session) { - session.set_transport_connection(stream, InBandBytestreams.Connection.create(stream, peer_full_jid, sid, block_size, role == Jingle.Role.INITIATOR)); + + public void create_transport_connection(XmppStream stream, Jingle.Content content) { + IOStream iostream = InBandBytestreams.Connection.create(stream, peer_full_jid, sid, block_size, role == Jingle.Role.INITIATOR); + Jingle.StreamingConnection connection = new Jingle.StreamingConnection(); + if (content.security_params != null) { + iostream = content.security_params.wrap_stream(iostream); + } + connection.init.begin(iostream); + debug("set transport conn ibb"); + content.set_transport_connection(connection, 1); } } -- cgit v1.2.3-54-g00ecf From 421f43dd8bd993eb88581e1b5011cc061ceb4fc8 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Sun, 25 Apr 2021 19:49:10 +0200 Subject: Add support for OMEMO call encryption --- libdino/src/service/calls.vala | 44 ++- libdino/src/service/connection_manager.vala | 4 +- main/src/ui/call_window/call_bottom_bar.vala | 17 +- plugins/ice/src/transport_parameters.vala | 15 +- plugins/omemo/CMakeLists.txt | 4 +- .../omemo/src/dtls_srtp_verification_draft.vala | 195 +++++++++++++ plugins/omemo/src/jingle/jet_omemo.vala | 82 ++---- plugins/omemo/src/logic/decrypt.vala | 210 ++++++++++++++ plugins/omemo/src/logic/encrypt.vala | 131 +++++++++ plugins/omemo/src/logic/encrypt_state.vala | 24 -- plugins/omemo/src/logic/manager.vala | 16 +- plugins/omemo/src/logic/trust_manager.vala | 302 +-------------------- plugins/omemo/src/plugin.vala | 22 +- plugins/omemo/src/protocol/stream_module.vala | 6 +- xmpp-vala/CMakeLists.txt | 3 + xmpp-vala/src/module/iq/module.vala | 5 + xmpp-vala/src/module/xep/0166_jingle/content.vala | 3 +- .../module/xep/0166_jingle/content_transport.vala | 2 +- .../src/module/xep/0166_jingle/jingle_module.vala | 4 +- xmpp-vala/src/module/xep/0166_jingle/session.vala | 10 +- .../xep/0167_jingle_rtp/content_parameters.vala | 3 +- .../0176_jingle_ice_udp/jingle_ice_udp_module.vala | 2 +- .../0176_jingle_ice_udp/transport_parameters.vala | 6 +- .../module/xep/0260_jingle_socks5_bytestreams.vala | 2 +- .../xep/0261_jingle_in_band_bytestreams.vala | 2 +- .../module/xep/0353_jingle_message_initiation.vala | 2 + .../src/module/xep/0384_omemo/omemo_decryptor.vala | 62 +++++ .../src/module/xep/0384_omemo/omemo_encryptor.vala | 116 ++++++++ 28 files changed, 859 insertions(+), 435 deletions(-) create mode 100644 plugins/omemo/src/dtls_srtp_verification_draft.vala create mode 100644 plugins/omemo/src/logic/decrypt.vala create mode 100644 plugins/omemo/src/logic/encrypt.vala delete mode 100644 plugins/omemo/src/logic/encrypt_state.vala create mode 100644 xmpp-vala/src/module/xep/0384_omemo/omemo_decryptor.vala create mode 100644 xmpp-vala/src/module/xep/0384_omemo/omemo_encryptor.vala (limited to 'xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala') diff --git a/libdino/src/service/calls.vala b/libdino/src/service/calls.vala index 1d47823d..3615e24f 100644 --- a/libdino/src/service/calls.vala +++ b/libdino/src/service/calls.vala @@ -40,8 +40,8 @@ namespace Dino { private HashMap video_content_parameter = new HashMap(Call.hash_func, Call.equals_func); private HashMap audio_content = new HashMap(Call.hash_func, Call.equals_func); private HashMap video_content = new HashMap(Call.hash_func, Call.equals_func); - private HashMap video_encryption = new HashMap(Call.hash_func, Call.equals_func); - private HashMap audio_encryption = new HashMap(Call.hash_func, Call.equals_func); + private HashMap> video_encryptions = new HashMap>(Call.hash_func, Call.equals_func); + private HashMap> audio_encryptions = new HashMap>(Call.hash_func, Call.equals_func); public static void start(StreamInteractor stream_interactor, Database db) { Calls m = new Calls(stream_interactor, db); @@ -498,24 +498,46 @@ namespace Dino { } if (media == "audio") { - audio_encryption[call] = content.encryption; + audio_encryptions[call] = content.encryptions; } else if (media == "video") { - video_encryption[call] = content.encryption; + video_encryptions[call] = content.encryptions; } - if ((audio_encryption.has_key(call) && audio_encryption[call] == null) || (video_encryption.has_key(call) && video_encryption[call] == null)) { + if ((audio_encryptions.has_key(call) && audio_encryptions[call].is_empty) || (video_encryptions.has_key(call) && video_encryptions[call].is_empty)) { call.encryption = Encryption.NONE; encryption_updated(call, null); return; } - Xep.Jingle.ContentEncryption encryption = audio_encryption[call] ?? video_encryption[call]; - if (encryption.encryption_ns == Xep.JingleIceUdp.DTLS_NS_URI) { + HashMap encryptions = audio_encryptions[call] ?? video_encryptions[call]; + + Xep.Jingle.ContentEncryption? omemo_encryption = null, dtls_encryption = null, srtp_encryption = null; + foreach (string encr_name in encryptions.keys) { + if (video_encryptions.has_key(call) && !video_encryptions[call].has_key(encr_name)) continue; + + var encryption = encryptions[encr_name]; + if (encryption.encryption_ns == "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification") { + omemo_encryption = encryption; + } else if (encryption.encryption_ns == Xep.JingleIceUdp.DTLS_NS_URI) { + dtls_encryption = encryption; + } else if (encryption.encryption_name == "SRTP") { + srtp_encryption = encryption; + } + } + + if (omemo_encryption != null && dtls_encryption != null) { + call.encryption = Encryption.OMEMO; + encryption_updated(call, omemo_encryption); + } else if (dtls_encryption != null) { call.encryption = Encryption.DTLS_SRTP; - } else if (encryption.encryption_name == "SRTP") { + encryption_updated(call, dtls_encryption); + } else if (srtp_encryption != null) { call.encryption = Encryption.SRTP; + encryption_updated(call, srtp_encryption); + } else { + call.encryption = Encryption.NONE; + encryption_updated(call, null); } - encryption_updated(call, encryption); } private void remove_call_from_datastructures(Call call) { @@ -533,8 +555,8 @@ namespace Dino { video_content_parameter.unset(call); audio_content.unset(call); video_content.unset(call); - audio_encryption.unset(call); - video_encryption.unset(call); + audio_encryptions.unset(call); + video_encryptions.unset(call); } private void on_account_added(Account account) { diff --git a/libdino/src/service/connection_manager.vala b/libdino/src/service/connection_manager.vala index 1439c6f3..0eb6a6f5 100644 --- a/libdino/src/service/connection_manager.vala +++ b/libdino/src/service/connection_manager.vala @@ -350,7 +350,9 @@ public class ConnectionManager : Object { foreach (Account account in connections.keys) { try { make_offline(account); - yield connections[account].stream.disconnect(); + if (connections[account].stream != null) { + yield connections[account].stream.disconnect(); + } } catch (Error e) { debug("Error disconnecting stream %p: %s", connections[account].stream, e.message); } diff --git a/main/src/ui/call_window/call_bottom_bar.vala b/main/src/ui/call_window/call_bottom_bar.vala index c6375ea2..a9fee8c3 100644 --- a/main/src/ui/call_window/call_bottom_bar.vala +++ b/main/src/ui/call_window/call_bottom_bar.vala @@ -100,16 +100,25 @@ public class Dino.Ui.CallBottomBar : Gtk.Box { encryption_button.get_style_context().add_class("unencrypted"); popover.add(new Label("This call isn't encrypted.") { margin=10, visible=true } ); + } else if (encryption.encryption_name == "OMEMO") { + encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.BUTTON); + encryption_button.get_style_context().remove_class("unencrypted"); + + popover.add(new Label("This call is encrypted with OMEMO.") { margin=10, visible=true } ); } else { encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.BUTTON); encryption_button.get_style_context().remove_class("unencrypted"); Grid encryption_info_grid = new Grid() { margin=10, row_spacing=3, column_spacing=5, visible=true }; encryption_info_grid.attach(new Label("This call is end-to-end encrypted.") { use_markup=true, xalign=0, visible=true }, 1, 1, 2, 1); - encryption_info_grid.attach(new Label("Peer key") { xalign=0, visible=true }, 1, 2, 1, 1); - encryption_info_grid.attach(new Label("Your key") { xalign=0, visible=true }, 1, 3, 1, 1); - encryption_info_grid.attach(new Label("" + format_fingerprint(encryption.peer_key) + "") { use_markup=true, max_width_chars=25, ellipsize=EllipsizeMode.MIDDLE, xalign=0, hexpand=true, visible=true }, 2, 2, 1, 1); - encryption_info_grid.attach(new Label("" + format_fingerprint(encryption.our_key) + "") { use_markup=true, max_width_chars=25, ellipsize=EllipsizeMode.MIDDLE, xalign=0, hexpand=true, visible=true }, 2, 3, 1, 1); + if (encryption.peer_key.length > 0) { + encryption_info_grid.attach(new Label("Peer key") { xalign=0, visible=true }, 1, 2, 1, 1); + encryption_info_grid.attach(new Label("" + format_fingerprint(encryption.peer_key) + "") { use_markup=true, max_width_chars=25, ellipsize=EllipsizeMode.MIDDLE, xalign=0, hexpand=true, visible=true }, 2, 2, 1, 1); + } + if (encryption.our_key.length > 0) { + encryption_info_grid.attach(new Label("Your key") { xalign=0, visible=true }, 1, 3, 1, 1); + encryption_info_grid.attach(new Label("" + format_fingerprint(encryption.our_key) + "") { use_markup=true, max_width_chars=25, ellipsize=EllipsizeMode.MIDDLE, xalign=0, hexpand=true, visible=true }, 2, 3, 1, 1); + } popover.add(encryption_info_grid); } diff --git a/plugins/ice/src/transport_parameters.vala b/plugins/ice/src/transport_parameters.vala index 52451fcf..38652952 100644 --- a/plugins/ice/src/transport_parameters.vala +++ b/plugins/ice/src/transport_parameters.vala @@ -77,7 +77,10 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport own_setup = "actpass"; dtls_srtp_handler.mode = DtlsSrtp.Mode.SERVER; dtls_srtp_handler.setup_dtls_connection.begin((_, res) => { - this.content.encryption = dtls_srtp_handler.setup_dtls_connection.end(res) ?? this.content.encryption; + var content_encryption = dtls_srtp_handler.setup_dtls_connection.end(res); + if (content_encryption != null) { + this.content.encryptions[content_encryption.encryption_ns] = content_encryption; + } }); } } @@ -157,7 +160,10 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport dtls_srtp_handler.mode = DtlsSrtp.Mode.CLIENT; dtls_srtp_handler.stop_dtls_connection(); dtls_srtp_handler.setup_dtls_connection.begin((_, res) => { - this.content.encryption = dtls_srtp_handler.setup_dtls_connection.end(res) ?? this.content.encryption; + var content_encryption = dtls_srtp_handler.setup_dtls_connection.end(res); + if (content_encryption != null) { + this.content.encryptions[content_encryption.encryption_ns] = content_encryption; + } }); } } else { @@ -225,7 +231,10 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport may_consider_ready(stream_id, component_id); if (incoming && dtls_srtp_handler != null && !dtls_srtp_handler.ready && is_component_ready(agent, stream_id, component_id) && dtls_srtp_handler.mode == DtlsSrtp.Mode.CLIENT) { dtls_srtp_handler.setup_dtls_connection.begin((_, res) => { - this.content.encryption = dtls_srtp_handler.setup_dtls_connection.end(res) ?? this.content.encryption; + Jingle.ContentEncryption? encryption = dtls_srtp_handler.setup_dtls_connection.end(res); + if (encryption != null) { + this.content.encryptions[encryption.encryption_ns] = encryption; + } }); } } diff --git a/plugins/omemo/CMakeLists.txt b/plugins/omemo/CMakeLists.txt index c7a45069..944fc649 100644 --- a/plugins/omemo/CMakeLists.txt +++ b/plugins/omemo/CMakeLists.txt @@ -29,6 +29,7 @@ compile_gresources( vala_precompile(OMEMO_VALA_C SOURCES + src/dtls_srtp_verification_draft.vala src/plugin.vala src/register_plugin.vala src/trust_level.vala @@ -39,7 +40,8 @@ SOURCES src/jingle/jet_omemo.vala src/logic/database.vala - src/logic/encrypt_state.vala + src/logic/decrypt.vala + src/logic/encrypt.vala src/logic/manager.vala src/logic/pre_key_store.vala src/logic/session_store.vala diff --git a/plugins/omemo/src/dtls_srtp_verification_draft.vala b/plugins/omemo/src/dtls_srtp_verification_draft.vala new file mode 100644 index 00000000..e2441670 --- /dev/null +++ b/plugins/omemo/src/dtls_srtp_verification_draft.vala @@ -0,0 +1,195 @@ +using Signal; +using Gee; +using Xmpp; + +namespace Dino.Plugins.Omemo.DtlsSrtpVerificationDraft { + public const string NS_URI = "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"; + + public class StreamModule : XmppStreamModule { + + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "dtls_srtp_omemo_verification_draft"); + + private VerificationSendListener send_listener = new VerificationSendListener(); + private HashMap device_id_by_jingle_sid = new HashMap(); + private HashMap> content_names_by_jingle_sid = new HashMap>(); + + private void on_preprocess_incoming_iq_set_get(XmppStream stream, Xmpp.Iq.Stanza iq) { + if (iq.type_ != Iq.Stanza.TYPE_SET) return; + + Gee.List content_nodes = iq.stanza.get_deep_subnodes(Xep.Jingle.NS_URI + ":jingle", Xep.Jingle.NS_URI + ":content"); + if (content_nodes.size == 0) return; + + string? jingle_sid = iq.stanza.get_deep_attribute(Xep.Jingle.NS_URI + ":jingle", "sid"); + if (jingle_sid == null) return; + + Xep.Omemo.OmemoDecryptor decryptor = stream.get_module(Xep.Omemo.OmemoDecryptor.IDENTITY); + + foreach (StanzaNode content_node in content_nodes) { + string? content_name = content_node.get_attribute("name"); + if (content_name == null) continue; + StanzaNode? transport_node = content_node.get_subnode("transport", Xep.JingleIceUdp.NS_URI); + if (transport_node == null) continue; + StanzaNode? fingerprint_node = transport_node.get_subnode("fingerprint", NS_URI); + if (fingerprint_node == null) continue; + StanzaNode? encrypted_node = fingerprint_node.get_subnode("encrypted", Omemo.NS_URI); + if (encrypted_node == null) continue; + + Xep.Omemo.ParsedData? parsed_data = decryptor.parse_node(encrypted_node); + if (parsed_data == null || parsed_data.ciphertext == null) continue; + + if (device_id_by_jingle_sid.has_key(jingle_sid) && device_id_by_jingle_sid[jingle_sid] != parsed_data.sid) { + warning("Expected DTLS fingerprint to be OMEMO encrypted from %s %d, but it was from %d", iq.from.to_string(), device_id_by_jingle_sid[jingle_sid], parsed_data.sid); + } + + foreach (Bytes encr_key in parsed_data.our_potential_encrypted_keys.keys) { + parsed_data.is_prekey = parsed_data.our_potential_encrypted_keys[encr_key]; + parsed_data.encrypted_key = encr_key.get_data(); + + try { + uint8[] key = decryptor.decrypt_key(parsed_data, iq.from.bare_jid); + string cleartext = decryptor.decrypt(parsed_data.ciphertext, key, parsed_data.iv); + + StanzaNode new_fingerprint_node = new StanzaNode.build("fingerprint", Xep.JingleIceUdp.DTLS_NS_URI).add_self_xmlns() + .put_node(new StanzaNode.text(cleartext)); + string? hash_attr = fingerprint_node.get_attribute("hash", NS_URI); + string? setup_attr = fingerprint_node.get_attribute("setup", NS_URI); + if (hash_attr != null) new_fingerprint_node.put_attribute("hash", hash_attr); + if (setup_attr != null) new_fingerprint_node.put_attribute("setup", setup_attr); + transport_node.put_node(new_fingerprint_node); + + device_id_by_jingle_sid[jingle_sid] = parsed_data.sid; + if (!content_names_by_jingle_sid.has_key(content_name)) { + content_names_by_jingle_sid[content_name] = new ArrayList(); + } + content_names_by_jingle_sid[content_name].add(content_name); + + stream.get_flag(Xep.Jingle.Flag.IDENTITY).get_session.begin(jingle_sid, (_, res) => { + Xep.Jingle.Session? session = stream.get_flag(Xep.Jingle.Flag.IDENTITY).get_session.end(res); + if (session != null) print(@"$(session.contents_map.has_key(content_name))\n"); + if (session == null || !session.contents_map.has_key(content_name)) return; + var encryption = new OmemoContentEncryption() { encryption_ns=NS_URI, encryption_name="OMEMO", our_key=new uint8[0], peer_key=new uint8[0], peer_device_id=device_id_by_jingle_sid[jingle_sid] }; + session.contents_map[content_name].encryptions[NS_URI] = encryption; + + if (iq.stanza.get_deep_attribute(Xep.Jingle.NS_URI + ":jingle", "action") == "session-accept") { + session.additional_content_add_incoming.connect(on_content_add_received); + } + }); + + break; + } catch (Error e) { + debug("Decrypting message from %s/%d failed: %s", iq.from.bare_jid.to_string(), parsed_data.sid, e.message); + } + } + } + } + + private void on_preprocess_outgoing_iq_set_get(XmppStream stream, Xmpp.Iq.Stanza iq) { + if (iq.type_ != Iq.Stanza.TYPE_SET) return; + + StanzaNode? jingle_node = iq.stanza.get_subnode("jingle", Xep.Jingle.NS_URI); + if (jingle_node == null) return; + + string? sid = jingle_node.get_attribute("sid", Xep.Jingle.NS_URI); + if (sid == null || !device_id_by_jingle_sid.has_key(sid)) return; + + Gee.List content_nodes = jingle_node.get_subnodes("content", Xep.Jingle.NS_URI); + if (content_nodes.size == 0) return; + + foreach (StanzaNode content_node in content_nodes) { + StanzaNode? transport_node = content_node.get_subnode("transport", Xep.JingleIceUdp.NS_URI); + if (transport_node == null) continue; + StanzaNode? fingerprint_node = transport_node.get_subnode("fingerprint", Xep.JingleIceUdp.DTLS_NS_URI); + if (fingerprint_node == null) continue; + string fingerprint = fingerprint_node.get_deep_string_content(); + + Xep.Omemo.OmemoEncryptor encryptor = stream.get_module(Xep.Omemo.OmemoEncryptor.IDENTITY); + Xep.Omemo.EncryptionData enc_data = encryptor.encrypt_plaintext(fingerprint); + encryptor.encrypt_key(enc_data, iq.to.bare_jid, device_id_by_jingle_sid[sid]); + + StanzaNode new_fingerprint_node = new StanzaNode.build("fingerprint", NS_URI).add_self_xmlns().put_node(enc_data.get_encrypted_node()); + string? hash_attr = fingerprint_node.get_attribute("hash", Xep.JingleIceUdp.DTLS_NS_URI); + string? setup_attr = fingerprint_node.get_attribute("setup", Xep.JingleIceUdp.DTLS_NS_URI); + if (hash_attr != null) new_fingerprint_node.put_attribute("hash", hash_attr); + if (setup_attr != null) new_fingerprint_node.put_attribute("setup", setup_attr); + transport_node.put_node(new_fingerprint_node); + + transport_node.sub_nodes.remove(fingerprint_node); + } + } + + private void on_message_received(XmppStream stream, Xmpp.MessageStanza message) { + StanzaNode? proceed_node = message.stanza.get_subnode("proceed", Xep.JingleMessageInitiation.NS_URI); + if (proceed_node == null) return; + + string? jingle_sid = proceed_node.get_attribute("id"); + if (jingle_sid == null) return; + + StanzaNode? device_node = proceed_node.get_subnode("device", NS_URI); + if (device_node == null) return; + + int device_id = device_node.get_attribute_int("id", -1); + if (device_id == -1) return; + + device_id_by_jingle_sid[jingle_sid] = device_id; + } + + private void on_session_initiate_received(XmppStream stream, Xep.Jingle.Session session) { + if (device_id_by_jingle_sid.has_key(session.sid)) { + foreach (Xep.Jingle.Content content in session.contents) { + on_content_add_received(stream, content); + } + } + session.additional_content_add_incoming.connect(on_content_add_received); + } + + private void on_content_add_received(XmppStream stream, Xep.Jingle.Content content) { + if (!content_names_by_jingle_sid.has_key(content.session.sid) || content_names_by_jingle_sid[content.session.sid].contains(content.content_name)) { + var encryption = new OmemoContentEncryption() { encryption_ns=NS_URI, encryption_name="OMEMO", our_key=new uint8[0], peer_key=new uint8[0], peer_device_id=device_id_by_jingle_sid[content.session.sid] }; + content.encryptions[encryption.encryption_ns] = encryption; + } + } + + public override void attach(XmppStream stream) { + stream.get_module(Xmpp.MessageModule.IDENTITY).received_message.connect(on_message_received); + stream.get_module(Xmpp.MessageModule.IDENTITY).send_pipeline.connect(send_listener); + stream.get_module(Xmpp.Iq.Module.IDENTITY).preprocess_incoming_iq_set_get.connect(on_preprocess_incoming_iq_set_get); + stream.get_module(Xmpp.Iq.Module.IDENTITY).preprocess_outgoing_iq_set_get.connect(on_preprocess_outgoing_iq_set_get); + stream.get_module(Xep.Jingle.Module.IDENTITY).session_initiate_received.connect(on_session_initiate_received); + } + + public override void detach(XmppStream stream) { + stream.get_module(Xmpp.MessageModule.IDENTITY).received_message.disconnect(on_message_received); + stream.get_module(Xmpp.MessageModule.IDENTITY).send_pipeline.disconnect(send_listener); + stream.get_module(Xmpp.Iq.Module.IDENTITY).preprocess_incoming_iq_set_get.disconnect(on_preprocess_incoming_iq_set_get); + stream.get_module(Xmpp.Iq.Module.IDENTITY).preprocess_outgoing_iq_set_get.disconnect(on_preprocess_outgoing_iq_set_get); + stream.get_module(Xep.Jingle.Module.IDENTITY).session_initiate_received.disconnect(on_session_initiate_received); + } + + public override string get_ns() { return NS_URI; } + + public override string get_id() { return IDENTITY.id; } + } + + public class VerificationSendListener : StanzaListener { + + private const string[] after_actions_const = {}; + + public override string action_group { get { return "REWRITE_NODES"; } } + public override string[] after_actions { get { return after_actions_const; } } + + public override async bool run(XmppStream stream, MessageStanza message) { + StanzaNode? proceed_node = message.stanza.get_subnode("proceed", Xep.JingleMessageInitiation.NS_URI); + if (proceed_node == null) return false; + + StanzaNode device_node = new StanzaNode.build("device", NS_URI).add_self_xmlns() + .put_attribute("id", stream.get_module(Omemo.StreamModule.IDENTITY).store.local_registration_id.to_string()); + proceed_node.put_node(device_node); + return false; + } + } + + public class OmemoContentEncryption : Xep.Jingle.ContentEncryption { + public int peer_device_id { get; set; } + } +} + diff --git a/plugins/omemo/src/jingle/jet_omemo.vala b/plugins/omemo/src/jingle/jet_omemo.vala index 14307be2..afcdfcd6 100644 --- a/plugins/omemo/src/jingle/jet_omemo.vala +++ b/plugins/omemo/src/jingle/jet_omemo.vala @@ -7,18 +7,15 @@ using Xmpp; using Xmpp.Xep; namespace Dino.Plugins.JetOmemo { + private const string NS_URI = "urn:xmpp:jingle:jet-omemo:0"; private const string AES_128_GCM_URI = "urn:xmpp:ciphers:aes-128-gcm-nopadding"; + public class Module : XmppStreamModule, Jet.EnvelopEncoding { public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0396_jet_omemo"); - private Omemo.Plugin plugin; const uint KEY_SIZE = 16; const uint IV_SIZE = 12; - public Module(Omemo.Plugin plugin) { - this.plugin = plugin; - } - public override void attach(XmppStream stream) { if (stream.get_module(Jet.Module.IDENTITY) != null) { stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); @@ -44,71 +41,38 @@ public class Module : XmppStreamModule, Jet.EnvelopEncoding { } public Jet.TransportSecret decode_envolop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws Jingle.IqError { - Store store = stream.get_module(Omemo.StreamModule.IDENTITY).store; StanzaNode? encrypted = security.get_subnode("encrypted", Omemo.NS_URI); if (encrypted == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing encrypted element"); - StanzaNode? header = encrypted.get_subnode("header", Omemo.NS_URI); - if (header == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing header element"); - string? iv_node = header.get_deep_string_content("iv"); - if (header == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing iv element"); - uint8[] iv = Base64.decode((!)iv_node); - foreach (StanzaNode key_node in header.get_subnodes("key")) { - if (key_node.get_attribute_int("rid") == store.local_registration_id) { - string? key_node_content = key_node.get_string_content(); - - uint8[] key; - Address address = new Address(peer_full_jid.bare_jid.to_string(), header.get_attribute_int("sid")); - if (key_node.get_attribute_bool("prekey")) { - PreKeySignalMessage msg = Omemo.Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content)); - SessionCipher cipher = store.create_session_cipher(address); - key = cipher.decrypt_pre_key_signal_message(msg); - } else { - SignalMessage msg = Omemo.Plugin.get_context().deserialize_signal_message(Base64.decode((!)key_node_content)); - SessionCipher cipher = store.create_session_cipher(address); - key = cipher.decrypt_signal_message(msg); - } - address.device_id = 0; // TODO: Hack to have address obj live longer - - uint8[] authtag = null; - if (key.length >= 32) { - int authtaglength = key.length - 16; - authtag = new uint8[authtaglength]; - uint8[] new_key = new uint8[16]; - Memory.copy(authtag, (uint8*)key + 16, 16); - Memory.copy(new_key, key, 16); - key = new_key; - } - // TODO: authtag? - return new Jet.TransportSecret(key, iv); + + Xep.Omemo.OmemoDecryptor decryptor = stream.get_module(Xep.Omemo.OmemoDecryptor.IDENTITY); + + Xmpp.Xep.Omemo.ParsedData? data = decryptor.parse_node(encrypted); + if (data == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: bad encrypted element"); + + foreach (Bytes encr_key in data.our_potential_encrypted_keys.keys) { + data.is_prekey = data.our_potential_encrypted_keys[encr_key]; + data.encrypted_key = encr_key.get_data(); + + try { + uint8[] key = decryptor.decrypt_key(data, peer_full_jid.bare_jid); + return new Jet.TransportSecret(key, data.iv); + } catch (GLib.Error e) { + debug("Decrypting JET key from %s/%d failed: %s", peer_full_jid.bare_jid.to_string(), data.sid, e.message); } } throw new Jingle.IqError.NOT_ACCEPTABLE("Not encrypted for targeted device"); } public void encode_envelop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Jet.SecurityParameters security_params, StanzaNode security) { - ArrayList accounts = plugin.app.stream_interactor.get_accounts(); Store store = stream.get_module(Omemo.StreamModule.IDENTITY).store; - Account? account = null; - foreach (Account compare in accounts) { - if (compare.bare_jid.equals_bare(local_full_jid)) { - account = compare; - break; - } - } - if (account == null) { - // TODO - critical("Sending from offline account %s", local_full_jid.to_string()); - } - StanzaNode header_node; - StanzaNode encrypted_node = new StanzaNode.build("encrypted", Omemo.NS_URI).add_self_xmlns() - .put_node(header_node = new StanzaNode.build("header", Omemo.NS_URI) - .put_attribute("sid", store.local_registration_id.to_string()) - .put_node(new StanzaNode.build("iv", Omemo.NS_URI) - .put_node(new StanzaNode.text(Base64.encode(security_params.secret.initialization_vector))))); + var encryption_data = new Xep.Omemo.EncryptionData(store.local_registration_id); + encryption_data.iv = security_params.secret.initialization_vector; + encryption_data.keytag = security_params.secret.transport_key; + Xep.Omemo.OmemoEncryptor encryptor = stream.get_module(Xep.Omemo.OmemoEncryptor.IDENTITY); + encryptor.encrypt_key_to_recipient(stream, encryption_data, peer_full_jid.bare_jid); - plugin.trust_manager.encrypt_key(header_node, security_params.secret.transport_key, local_full_jid.bare_jid, new ArrayList.wrap(new Jid[] {peer_full_jid.bare_jid}), stream, account); - security.put_node(encrypted_node); + security.put_node(encryption_data.get_encrypted_node()); } public override string get_ns() { return NS_URI; } diff --git a/plugins/omemo/src/logic/decrypt.vala b/plugins/omemo/src/logic/decrypt.vala new file mode 100644 index 00000000..3cdacbf7 --- /dev/null +++ b/plugins/omemo/src/logic/decrypt.vala @@ -0,0 +1,210 @@ +using Dino.Entities; +using Qlite; +using Gee; +using Signal; +using Xmpp; + +namespace Dino.Plugins.Omemo { + + public class OmemoDecryptor : Xep.Omemo.OmemoDecryptor { + + private Account account; + private Store store; + private Database db; + private StreamInteractor stream_interactor; + private TrustManager trust_manager; + + public override uint32 own_device_id { get { return store.local_registration_id; }} + + public OmemoDecryptor(Account account, StreamInteractor stream_interactor, TrustManager trust_manager, Database db, Store store) { + this.account = account; + this.stream_interactor = stream_interactor; + this.trust_manager = trust_manager; + this.db = db; + this.store = store; + } + + public bool decrypt_message(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + StanzaNode? encrypted_node = stanza.stanza.get_subnode("encrypted", NS_URI); + if (encrypted_node == null || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false; + + if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == NS_URI) { + message.body = "[This message is OMEMO encrypted]"; // TODO temporary + } + if (!Plugin.ensure_context()) return false; + int identity_id = db.identity.get_id(conversation.account.id); + + MessageFlag flag = new MessageFlag(); + stanza.add_flag(flag); + + Xep.Omemo.ParsedData? data = parse_node(encrypted_node); + if (data == null || data.ciphertext == null) return false; + + + foreach (Bytes encr_key in data.our_potential_encrypted_keys.keys) { + data.is_prekey = data.our_potential_encrypted_keys[encr_key]; + data.encrypted_key = encr_key.get_data(); + Gee.List possible_jids = get_potential_message_jids(message, data, identity_id); + if (possible_jids.size == 0) { + debug("Received message from unknown entity with device id %d", data.sid); + } + + foreach (Jid possible_jid in possible_jids) { + try { + uint8[] key = decrypt_key(data, possible_jid); + string cleartext = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, data.iv, data.ciphertext)); + + // If we figured out which real jid a message comes from due to decryption working, save it + if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) { + message.real_jid = possible_jid; + } + + trust_manager.message_device_id_map[message] = data.sid; + message.body = cleartext; + message.encryption = Encryption.OMEMO; + return true; + } catch (Error e) { + debug("Decrypting message from %s/%d failed: %s", possible_jid.to_string(), data.sid, e.message); + } + } + } + + if ( + encrypted_node.get_deep_string_content("payload") != null && // Ratchet forwarding doesn't contain payload and might not include us, which is ok + data.our_potential_encrypted_keys.size == 0 && // The message was not encrypted to us + stream_interactor.module_manager.get_module(message.account, StreamModule.IDENTITY).store.local_registration_id != data.sid // Message from this device. Never encrypted to itself. + ) { + db.identity_meta.update_last_message_undecryptable(identity_id, data.sid, message.time); + trust_manager.bad_message_state_updated(conversation.account, message.from, data.sid); + } + + debug("Received OMEMO encryped message that could not be decrypted."); + return false; + } + + public Gee.List get_potential_message_jids(Entities.Message message, Xmpp.Xep.Omemo.ParsedData data, int identity_id) { + Gee.List possible_jids = new ArrayList(); + if (message.type_ == Message.Type.CHAT) { + possible_jids.add(message.from.bare_jid); + } else { + if (message.real_jid != null) { + possible_jids.add(message.real_jid.bare_jid); + } else if (data.is_prekey) { + // pre key messages do store the identity key, so we can use that to find the real jid + PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(data.encrypted_key); + string identity_key = Base64.encode(msg.identity_key.serialize()); + foreach (Row row in db.identity_meta.get_with_device_id(identity_id, data.sid).with(db.identity_meta.identity_key_public_base64, "=", identity_key)) { + try { + possible_jids.add(new Jid(row[db.identity_meta.address_name])); + } catch (InvalidJidError e) { + warning("Ignoring invalid jid from database: %s", e.message); + } + } + } else { + // If we don't know the device name (MUC history w/o MAM), test decryption with all keys with fitting device id + foreach (Row row in db.identity_meta.get_with_device_id(identity_id, data.sid)) { + try { + possible_jids.add(new Jid(row[db.identity_meta.address_name])); + } catch (InvalidJidError e) { + warning("Ignoring invalid jid from database: %s", e.message); + } + } + } + } + return possible_jids; + } + + public override uint8[] decrypt_key(Xmpp.Xep.Omemo.ParsedData data, Jid from_jid) throws GLib.Error { + int sid = data.sid; + uint8[] ciphertext = data.ciphertext; + uint8[] encrypted_key = data.encrypted_key; + + Address address = new Address(from_jid.to_string(), sid); + uint8[] key; + + if (data.is_prekey) { + int identity_id = db.identity.get_id(account.id); + PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(encrypted_key); + string identity_key = Base64.encode(msg.identity_key.serialize()); + + bool ok = update_db_for_prekey(identity_id, identity_key, from_jid, sid); + if (!ok) return null; + + debug("Starting new session for decryption with device from %s/%d", from_jid.to_string(), sid); + SessionCipher cipher = store.create_session_cipher(address); + key = cipher.decrypt_pre_key_signal_message(msg); + // TODO: Finish session + } else { + debug("Continuing session for decryption with device from %s/%d", from_jid.to_string(), sid); + SignalMessage msg = Plugin.get_context().deserialize_signal_message(encrypted_key); + SessionCipher cipher = store.create_session_cipher(address); + key = cipher.decrypt_signal_message(msg); + } + + if (key.length >= 32) { + int authtaglength = key.length - 16; + uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength]; + uint8[] new_key = new uint8[16]; + Memory.copy(new_ciphertext, ciphertext, ciphertext.length); + Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength); + Memory.copy(new_key, key, 16); + data.ciphertext = new_ciphertext; + key = new_key; + } + + return key; + } + + public override string decrypt(uint8[] ciphertext, uint8[] key, uint8[] iv) throws GLib.Error { + return arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, ciphertext)); + } + + private bool update_db_for_prekey(int identity_id, string identity_key, Jid from_jid, int sid) { + Row? device = db.identity_meta.get_device(identity_id, from_jid.to_string(), sid); + if (device != null && device[db.identity_meta.identity_key_public_base64] != null) { + if (device[db.identity_meta.identity_key_public_base64] != identity_key) { + critical("Tried to use a different identity key for a known device id."); + return false; + } + } else { + debug("Learn new device from incoming message from %s/%d", from_jid.to_string(), sid); + bool blind_trust = db.trust.get_blind_trust(identity_id, from_jid.to_string(), true); + if (db.identity_meta.insert_device_session(identity_id, from_jid.to_string(), sid, identity_key, blind_trust ? TrustLevel.TRUSTED : TrustLevel.UNKNOWN) < 0) { + critical("Failed learning a device."); + return false; + } + + XmppStream? stream = stream_interactor.get_stream(account); + if (device == null && stream != null) { + stream.get_module(StreamModule.IDENTITY).request_user_devicelist.begin(stream, from_jid); + } + } + return true; + } + + private string arr_to_str(uint8[] arr) { + // null-terminate the array + uint8[] rarr = new uint8[arr.length+1]; + Memory.copy(rarr, arr, arr.length); + return (string)rarr; + } + } + + public class DecryptMessageListener : MessageListener { + public string[] after_actions_const = new string[]{ }; + public override string action_group { get { return "DECRYPT"; } } + public override string[] after_actions { get { return after_actions_const; } } + + private HashMap decryptors; + + public DecryptMessageListener(HashMap decryptors) { + this.decryptors = decryptors; + } + + public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + decryptors[message.account].decrypt_message(message, stanza, conversation); + return false; + } + } +} + diff --git a/plugins/omemo/src/logic/encrypt.vala b/plugins/omemo/src/logic/encrypt.vala new file mode 100644 index 00000000..cd994c3a --- /dev/null +++ b/plugins/omemo/src/logic/encrypt.vala @@ -0,0 +1,131 @@ +using Gee; +using Signal; +using Dino.Entities; +using Xmpp; +using Xmpp.Xep.Omemo; + +namespace Dino.Plugins.Omemo { + + public class OmemoEncryptor : Xep.Omemo.OmemoEncryptor { + + private Account account; + private Store store; + private TrustManager trust_manager; + + public override uint32 own_device_id { get { return store.local_registration_id; }} + + public OmemoEncryptor(Account account, TrustManager trust_manager, Store store) { + this.account = account; + this.trust_manager = trust_manager; + this.store = store; + } + + public override Xep.Omemo.EncryptionData encrypt_plaintext(string plaintext) throws GLib.Error { + const uint KEY_SIZE = 16; + const uint IV_SIZE = 12; + + //Create a key and use it to encrypt the message + uint8[] key = new uint8[KEY_SIZE]; + Plugin.get_context().randomize(key); + uint8[] iv = new uint8[IV_SIZE]; + Plugin.get_context().randomize(iv); + + uint8[] aes_encrypt_result = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, plaintext.data); + uint8[] ciphertext = aes_encrypt_result[0:aes_encrypt_result.length - 16]; + uint8[] tag = aes_encrypt_result[aes_encrypt_result.length - 16:aes_encrypt_result.length]; + uint8[] keytag = new uint8[key.length + tag.length]; + Memory.copy(keytag, key, key.length); + Memory.copy((uint8*)keytag + key.length, tag, tag.length); + + var ret = new Xep.Omemo.EncryptionData(own_device_id); + ret.ciphertext = ciphertext; + ret.keytag = keytag; + ret.iv = iv; + return ret; + } + + public EncryptState encrypt(MessageStanza message, Jid self_jid, Gee.List recipients, XmppStream stream) { + + EncryptState status = new EncryptState(); + if (!Plugin.ensure_context()) return status; + if (message.to == null) return status; + + try { + EncryptionData enc_data = encrypt_plaintext(message.body); + status = encrypt_key_to_recipients(enc_data, self_jid, recipients, stream); + + message.stanza.put_node(enc_data.get_encrypted_node()); + Xep.ExplicitEncryption.add_encryption_tag_to_message(message, NS_URI, "OMEMO"); + message.body = "[This message is OMEMO encrypted]"; + status.encrypted = true; + } catch (Error e) { + warning(@"Signal error while encrypting message: $(e.message)\n"); + message.body = "[OMEMO encryption failed]"; + status.encrypted = false; + } + return status; + } + + internal EncryptState encrypt_key_to_recipients(EncryptionData enc_data, Jid self_jid, Gee.List recipients, XmppStream stream) throws Error { + EncryptState status = new EncryptState(); + + //Check we have the bundles and device lists needed to send the message + if (!trust_manager.is_known_address(account, self_jid)) return status; + status.own_list = true; + status.own_devices = trust_manager.get_trusted_devices(account, self_jid).size; + status.other_waiting_lists = 0; + status.other_devices = 0; + foreach (Jid recipient in recipients) { + if (!trust_manager.is_known_address(account, recipient)) { + status.other_waiting_lists++; + } + if (status.other_waiting_lists > 0) return status; + status.other_devices += trust_manager.get_trusted_devices(account, recipient).size; + } + if (status.own_devices == 0 || status.other_devices == 0) return status; + + + //Encrypt the key for each recipient's device individually + foreach (Jid recipient in recipients) { + EncryptionResult enc_res = encrypt_key_to_recipient(stream, enc_data, recipient); + status.add_result(enc_res, false); + } + + // Encrypt the key for each own device + EncryptionResult enc_res = encrypt_key_to_recipient(stream, enc_data, self_jid); + status.add_result(enc_res, true); + + return status; + } + + public override EncryptionResult encrypt_key_to_recipient(XmppStream stream, Xep.Omemo.EncryptionData enc_data, Jid recipient) throws GLib.Error { + var result = new EncryptionResult(); + StreamModule module = stream.get_module(StreamModule.IDENTITY); + + foreach(int32 device_id in trust_manager.get_trusted_devices(account, recipient)) { + if (module.is_ignored_device(recipient, device_id)) { + result.lost++; + continue; + } + try { + encrypt_key(enc_data, recipient, device_id); + result.success++; + } catch (Error e) { + if (e.code == ErrorCode.UNKNOWN) result.unknown++; + else result.failure++; + } + } + return result; + } + + public override void encrypt_key(Xep.Omemo.EncryptionData encryption_data, Jid jid, int32 device_id) throws GLib.Error { + Address address = new Address(jid.to_string(), device_id); + SessionCipher cipher = store.create_session_cipher(address); + CiphertextMessage device_key = cipher.encrypt(encryption_data.keytag); + address.device_id = 0; + debug("Created encrypted key for %s/%d", jid.to_string(), device_id); + + encryption_data.add_device_key(device_id, device_key.serialized, device_key.type == CiphertextType.PREKEY); + } + } +} \ No newline at end of file diff --git a/plugins/omemo/src/logic/encrypt_state.vala b/plugins/omemo/src/logic/encrypt_state.vala deleted file mode 100644 index fd72faf4..00000000 --- a/plugins/omemo/src/logic/encrypt_state.vala +++ /dev/null @@ -1,24 +0,0 @@ -namespace Dino.Plugins.Omemo { - -public class EncryptState { - public bool encrypted { get; internal set; } - public int other_devices { get; internal set; } - public int other_success { get; internal set; } - public int other_lost { get; internal set; } - public int other_unknown { get; internal set; } - public int other_failure { get; internal set; } - public int other_waiting_lists { get; internal set; } - - public int own_devices { get; internal set; } - public int own_success { get; internal set; } - public int own_lost { get; internal set; } - public int own_unknown { get; internal set; } - public int own_failure { get; internal set; } - public bool own_list { get; internal set; } - - public string to_string() { - return @"EncryptState (encrypted=$encrypted, other=(devices=$other_devices, success=$other_success, lost=$other_lost, unknown=$other_unknown, failure=$other_failure, waiting_lists=$other_waiting_lists, own=(devices=$own_devices, success=$own_success, lost=$own_lost, unknown=$own_unknown, failure=$own_failure, list=$own_list))"; - } -} - -} diff --git a/plugins/omemo/src/logic/manager.vala b/plugins/omemo/src/logic/manager.vala index 64b117c7..5552e212 100644 --- a/plugins/omemo/src/logic/manager.vala +++ b/plugins/omemo/src/logic/manager.vala @@ -13,11 +13,12 @@ public class Manager : StreamInteractionModule, Object { private StreamInteractor stream_interactor; private Database db; private TrustManager trust_manager; + private HashMap encryptors; private Map message_states = new HashMap(Entities.Message.hash_func, Entities.Message.equals_func); private class MessageState { public Entities.Message msg { get; private set; } - public EncryptState last_try { get; private set; } + public Xep.Omemo.EncryptState last_try { get; private set; } public int waiting_other_sessions { get; set; } public int waiting_own_sessions { get; set; } public bool waiting_own_devicelist { get; set; } @@ -26,11 +27,11 @@ public class Manager : StreamInteractionModule, Object { public bool will_send_now { get; private set; } public bool active_send_attempt { get; set; } - public MessageState(Entities.Message msg, EncryptState last_try) { + public MessageState(Entities.Message msg, Xep.Omemo.EncryptState last_try) { update_from_encrypt_status(msg, last_try); } - public void update_from_encrypt_status(Entities.Message msg, EncryptState new_try) { + public void update_from_encrypt_status(Entities.Message msg, Xep.Omemo.EncryptState new_try) { this.msg = msg; this.last_try = new_try; this.waiting_other_sessions = new_try.other_unknown; @@ -59,10 +60,11 @@ public class Manager : StreamInteractionModule, Object { } } - private Manager(StreamInteractor stream_interactor, Database db, TrustManager trust_manager) { + private Manager(StreamInteractor stream_interactor, Database db, TrustManager trust_manager, HashMap encryptors) { this.stream_interactor = stream_interactor; this.db = db; this.trust_manager = trust_manager; + this.encryptors = encryptors; stream_interactor.stream_negotiated.connect(on_stream_negotiated); stream_interactor.get_module(MessageProcessor.IDENTITY).pre_message_send.connect(on_pre_message_send); @@ -125,7 +127,7 @@ public class Manager : StreamInteractionModule, Object { } //Attempt to encrypt the message - EncryptState enc_state = trust_manager.encrypt(message_stanza, conversation.account.bare_jid, recipients, stream, conversation.account); + Xep.Omemo.EncryptState enc_state = encryptors[conversation.account].encrypt(message_stanza, conversation.account.bare_jid, recipients, stream); MessageState state; lock (message_states) { if (message_states.has_key(message)) { @@ -411,8 +413,8 @@ public class Manager : StreamInteractionModule, Object { return true; // TODO wait for stream? } - public static void start(StreamInteractor stream_interactor, Database db, TrustManager trust_manager) { - Manager m = new Manager(stream_interactor, db, trust_manager); + public static void start(StreamInteractor stream_interactor, Database db, TrustManager trust_manager, HashMap encryptors) { + Manager m = new Manager(stream_interactor, db, trust_manager, encryptors); stream_interactor.add_module(m); } } diff --git a/plugins/omemo/src/logic/trust_manager.vala b/plugins/omemo/src/logic/trust_manager.vala index 1e61b201..20076a43 100644 --- a/plugins/omemo/src/logic/trust_manager.vala +++ b/plugins/omemo/src/logic/trust_manager.vala @@ -12,18 +12,15 @@ public class TrustManager { private StreamInteractor stream_interactor; private Database db; - private DecryptMessageListener decrypt_message_listener; private TagMessageListener tag_message_listener; - private HashMap message_device_id_map = new HashMap(Message.hash_func, Message.equals_func); + public HashMap message_device_id_map = new HashMap(Message.hash_func, Message.equals_func); public TrustManager(StreamInteractor stream_interactor, Database db) { this.stream_interactor = stream_interactor; this.db = db; - decrypt_message_listener = new DecryptMessageListener(stream_interactor, this, db, message_device_id_map); tag_message_listener = new TagMessageListener(stream_interactor, this, db, message_device_id_map); - stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(decrypt_message_listener); stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(tag_message_listener); } @@ -69,127 +66,6 @@ public class TrustManager { } } - private StanzaNode create_encrypted_key_node(uint8[] key, Address address, Store store) throws GLib.Error { - SessionCipher cipher = store.create_session_cipher(address); - CiphertextMessage device_key = cipher.encrypt(key); - debug("Created encrypted key for %s/%d", address.name, address.device_id); - StanzaNode key_node = new StanzaNode.build("key", NS_URI) - .put_attribute("rid", address.device_id.to_string()) - .put_node(new StanzaNode.text(Base64.encode(device_key.serialized))); - if (device_key.type == CiphertextType.PREKEY) key_node.put_attribute("prekey", "true"); - return key_node; - } - - internal EncryptState encrypt_key(StanzaNode header_node, uint8[] keytag, Jid self_jid, Gee.List recipients, XmppStream stream, Account account) throws Error { - EncryptState status = new EncryptState(); - StreamModule module = stream.get_module(StreamModule.IDENTITY); - - //Check we have the bundles and device lists needed to send the message - if (!is_known_address(account, self_jid)) return status; - status.own_list = true; - status.own_devices = get_trusted_devices(account, self_jid).size; - status.other_waiting_lists = 0; - status.other_devices = 0; - foreach (Jid recipient in recipients) { - if (!is_known_address(account, recipient)) { - status.other_waiting_lists++; - } - if (status.other_waiting_lists > 0) return status; - status.other_devices += get_trusted_devices(account, recipient).size; - } - if (status.own_devices == 0 || status.other_devices == 0) return status; - - - //Encrypt the key for each recipient's device individually - Address address = new Address("", 0); - foreach (Jid recipient in recipients) { - foreach(int32 device_id in get_trusted_devices(account, recipient)) { - if (module.is_ignored_device(recipient, device_id)) { - status.other_lost++; - continue; - } - try { - address.name = recipient.bare_jid.to_string(); - address.device_id = (int) device_id; - StanzaNode key_node = create_encrypted_key_node(keytag, address, module.store); - header_node.put_node(key_node); - status.other_success++; - } catch (Error e) { - if (e.code == ErrorCode.UNKNOWN) status.other_unknown++; - else status.other_failure++; - } - } - } - - // Encrypt the key for each own device - address.name = self_jid.bare_jid.to_string(); - foreach(int32 device_id in get_trusted_devices(account, self_jid)) { - if (module.is_ignored_device(self_jid, device_id)) { - status.own_lost++; - continue; - } - if (device_id != module.store.local_registration_id) { - address.device_id = (int) device_id; - try { - StanzaNode key_node = create_encrypted_key_node(keytag, address, module.store); - header_node.put_node(key_node); - status.own_success++; - } catch (Error e) { - if (e.code == ErrorCode.UNKNOWN) status.own_unknown++; - else status.own_failure++; - } - } - } - - return status; - } - - public EncryptState encrypt(MessageStanza message, Jid self_jid, Gee.List recipients, XmppStream stream, Account account) { - const uint KEY_SIZE = 16; - const uint IV_SIZE = 12; - EncryptState status = new EncryptState(); - if (!Plugin.ensure_context()) return status; - if (message.to == null) return status; - - StreamModule module = stream.get_module(StreamModule.IDENTITY); - - try { - //Create a key and use it to encrypt the message - uint8[] key = new uint8[KEY_SIZE]; - Plugin.get_context().randomize(key); - uint8[] iv = new uint8[IV_SIZE]; - Plugin.get_context().randomize(iv); - - uint8[] aes_encrypt_result = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, message.body.data); - uint8[] ciphertext = aes_encrypt_result[0:aes_encrypt_result.length-16]; - uint8[] tag = aes_encrypt_result[aes_encrypt_result.length-16:aes_encrypt_result.length]; - uint8[] keytag = new uint8[key.length + tag.length]; - Memory.copy(keytag, key, key.length); - Memory.copy((uint8*)keytag + key.length, tag, tag.length); - - StanzaNode header_node; - StanzaNode encrypted_node = new StanzaNode.build("encrypted", NS_URI).add_self_xmlns() - .put_node(header_node = new StanzaNode.build("header", NS_URI) - .put_attribute("sid", module.store.local_registration_id.to_string()) - .put_node(new StanzaNode.build("iv", NS_URI) - .put_node(new StanzaNode.text(Base64.encode(iv))))) - .put_node(new StanzaNode.build("payload", NS_URI) - .put_node(new StanzaNode.text(Base64.encode(ciphertext)))); - - status = encrypt_key(header_node, keytag, self_jid, recipients, stream, account); - - message.stanza.put_node(encrypted_node); - Xep.ExplicitEncryption.add_encryption_tag_to_message(message, NS_URI, "OMEMO"); - message.body = "[This message is OMEMO encrypted]"; - status.encrypted = true; - } catch (Error e) { - warning(@"Signal error while encrypting message: $(e.message)\n"); - message.body = "[OMEMO encryption failed]"; - status.encrypted = false; - } - return status; - } - public bool is_known_address(Account account, Jid jid) { int identity_id = db.identity.get_id(account.id); if (identity_id < 0) return false; @@ -260,182 +136,6 @@ public class TrustManager { return false; } } - - private class DecryptMessageListener : MessageListener { - public string[] after_actions_const = new string[]{ }; - public override string action_group { get { return "DECRYPT"; } } - public override string[] after_actions { get { return after_actions_const; } } - - private StreamInteractor stream_interactor; - private TrustManager trust_manager; - private Database db; - private HashMap message_device_id_map; - - public DecryptMessageListener(StreamInteractor stream_interactor, TrustManager trust_manager, Database db, HashMap message_device_id_map) { - this.stream_interactor = stream_interactor; - this.trust_manager = trust_manager; - this.db = db; - this.message_device_id_map = message_device_id_map; - } - - public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { - StreamModule module = stream_interactor.module_manager.get_module(conversation.account, StreamModule.IDENTITY); - Store store = module.store; - - StanzaNode? _encrypted = stanza.stanza.get_subnode("encrypted", NS_URI); - if (_encrypted == null || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false; - StanzaNode encrypted = (!)_encrypted; - if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == NS_URI) { - message.body = "[This message is OMEMO encrypted]"; // TODO temporary - }; - if (!Plugin.ensure_context()) return false; - int identity_id = db.identity.get_id(conversation.account.id); - MessageFlag flag = new MessageFlag(); - stanza.add_flag(flag); - StanzaNode? _header = encrypted.get_subnode("header"); - if (_header == null) return false; - StanzaNode header = (!)_header; - int sid = header.get_attribute_int("sid"); - if (sid <= 0) return false; - - var our_nodes = new ArrayList(); - foreach (StanzaNode key_node in header.get_subnodes("key")) { - debug("Is ours? %d =? %u", key_node.get_attribute_int("rid"), store.local_registration_id); - if (key_node.get_attribute_int("rid") == store.local_registration_id) { - our_nodes.add(key_node); - } - } - - string? payload = encrypted.get_deep_string_content("payload"); - string? iv_node = header.get_deep_string_content("iv"); - - foreach (StanzaNode key_node in our_nodes) { - string? key_node_content = key_node.get_string_content(); - if (payload == null || iv_node == null || key_node_content == null) continue; - uint8[] key; - uint8[] ciphertext = Base64.decode((!)payload); - uint8[] iv = Base64.decode((!)iv_node); - Gee.List possible_jids = new ArrayList(); - if (conversation.type_ == Conversation.Type.CHAT) { - possible_jids.add(stanza.from.bare_jid); - } else { - Jid? real_jid = message.real_jid; - if (real_jid != null) { - possible_jids.add(real_jid.bare_jid); - } else if (key_node.get_attribute_bool("prekey")) { - // pre key messages do store the identity key, so we can use that to find the real jid - PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content)); - string identity_key = Base64.encode(msg.identity_key.serialize()); - foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid).with(db.identity_meta.identity_key_public_base64, "=", identity_key)) { - try { - possible_jids.add(new Jid(row[db.identity_meta.address_name])); - } catch (InvalidJidError e) { - warning("Ignoring invalid jid from database: %s", e.message); - } - } - if (possible_jids.size != 1) { - continue; - } - } else { - // If we don't know the device name (MUC history w/o MAM), test decryption with all keys with fitting device id - foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid)) { - try { - possible_jids.add(new Jid(row[db.identity_meta.address_name])); - } catch (InvalidJidError e) { - warning("Ignoring invalid jid from database: %s", e.message); - } - } - } - } - - if (possible_jids.size == 0) { - debug("Received message from unknown entity with device id %d", sid); - } - - foreach (Jid possible_jid in possible_jids) { - try { - Address address = new Address(possible_jid.to_string(), sid); - if (key_node.get_attribute_bool("prekey")) { - Row? device = db.identity_meta.get_device(identity_id, possible_jid.to_string(), sid); - PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content)); - string identity_key = Base64.encode(msg.identity_key.serialize()); - if (device != null && device[db.identity_meta.identity_key_public_base64] != null) { - if (device[db.identity_meta.identity_key_public_base64] != identity_key) { - critical("Tried to use a different identity key for a known device id."); - continue; - } - } else { - debug("Learn new device from incoming message from %s/%d", possible_jid.to_string(), sid); - bool blind_trust = db.trust.get_blind_trust(identity_id, possible_jid.to_string(), true); - if (db.identity_meta.insert_device_session(identity_id, possible_jid.to_string(), sid, identity_key, blind_trust ? TrustLevel.TRUSTED : TrustLevel.UNKNOWN) < 0) { - critical("Failed learning a device."); - continue; - } - XmppStream? stream = stream_interactor.get_stream(conversation.account); - if (device == null && stream != null) { - module.request_user_devicelist.begin(stream, possible_jid); - } - } - debug("Starting new session for decryption with device from %s/%d", possible_jid.to_string(), sid); - SessionCipher cipher = store.create_session_cipher(address); - key = cipher.decrypt_pre_key_signal_message(msg); - // TODO: Finish session - } else { - debug("Continuing session for decryption with device from %s/%d", possible_jid.to_string(), sid); - SignalMessage msg = Plugin.get_context().deserialize_signal_message(Base64.decode((!)key_node_content)); - SessionCipher cipher = store.create_session_cipher(address); - key = cipher.decrypt_signal_message(msg); - } - //address.device_id = 0; // TODO: Hack to have address obj live longer - - if (key.length >= 32) { - int authtaglength = key.length - 16; - uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength]; - uint8[] new_key = new uint8[16]; - Memory.copy(new_ciphertext, ciphertext, ciphertext.length); - Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength); - Memory.copy(new_key, key, 16); - ciphertext = new_ciphertext; - key = new_key; - } - - message.body = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, ciphertext)); - message_device_id_map[message] = address.device_id; - message.encryption = Encryption.OMEMO; - flag.decrypted = true; - } catch (Error e) { - debug("Decrypting message from %s/%d failed: %s", possible_jid.to_string(), sid, e.message); - continue; - } - - // If we figured out which real jid a message comes from due to decryption working, save it - if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) { - message.real_jid = possible_jid; - } - return false; - } - } - - if ( - payload != null && // Ratchet forwarding doesn't contain payload and might not include us, which is ok - our_nodes.size == 0 && // The message was not encrypted to us - module.store.local_registration_id != sid // Message from this device. Never encrypted to itself. - ) { - db.identity_meta.update_last_message_undecryptable(identity_id, sid, message.time); - trust_manager.bad_message_state_updated(conversation.account, message.from, sid); - } - - debug("Received OMEMO encryped message that could not be decrypted."); - return false; - } - - private string arr_to_str(uint8[] arr) { - // null-terminate the array - uint8[] rarr = new uint8[arr.length+1]; - Memory.copy(rarr, arr, arr.length); - return (string)rarr; - } - } } } diff --git a/plugins/omemo/src/plugin.vala b/plugins/omemo/src/plugin.vala index e739fc4d..7a0304d1 100644 --- a/plugins/omemo/src/plugin.vala +++ b/plugins/omemo/src/plugin.vala @@ -1,3 +1,4 @@ +using Gee; using Dino.Entities; extern const string GETTEXT_PACKAGE; @@ -20,6 +21,7 @@ public class Plugin : RootInterface, Object { } return true; } catch (Error e) { + warning("Error initializing Signal Context %s", e.message); return false; } } @@ -33,6 +35,9 @@ public class Plugin : RootInterface, Object { public DeviceNotificationPopulator device_notification_populator; public OwnNotifications own_notifications; public TrustManager trust_manager; + public DecryptMessageListener decrypt_message_listener; + public HashMap decryptors = new HashMap(Account.hash_func, Account.equals_func); + public HashMap encryptors = new HashMap(Account.hash_func, Account.equals_func); public void registered(Dino.Application app) { ensure_context(); @@ -43,22 +48,33 @@ public class Plugin : RootInterface, Object { this.contact_details_provider = new ContactDetailsProvider(this); this.device_notification_populator = new DeviceNotificationPopulator(this, this.app.stream_interactor); this.trust_manager = new TrustManager(this.app.stream_interactor, this.db); + this.app.plugin_registry.register_encryption_list_entry(list_entry); this.app.plugin_registry.register_account_settings_entry(settings_entry); this.app.plugin_registry.register_contact_details_entry(contact_details_provider); this.app.plugin_registry.register_notification_populator(device_notification_populator); this.app.plugin_registry.register_conversation_addition_populator(new BadMessagesPopulator(this.app.stream_interactor, this)); + this.app.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => { - list.add(new StreamModule()); - list.add(new JetOmemo.Module(this)); + Signal.Store signal_store = Plugin.get_context().create_store(); + list.add(new StreamModule(signal_store)); + decryptors[account] = new OmemoDecryptor(account, app.stream_interactor, trust_manager, db, signal_store); + list.add(decryptors[account]); + encryptors[account] = new OmemoEncryptor(account, trust_manager,signal_store); + list.add(encryptors[account]); + list.add(new JetOmemo.Module()); + list.add(new DtlsSrtpVerificationDraft.StreamModule()); this.own_notifications = new OwnNotifications(this, this.app.stream_interactor, account); }); + decrypt_message_listener = new DecryptMessageListener(decryptors); + app.stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(decrypt_message_listener); + app.stream_interactor.get_module(FileManager.IDENTITY).add_file_decryptor(new OmemoFileDecryptor()); app.stream_interactor.get_module(FileManager.IDENTITY).add_file_encryptor(new OmemoFileEncryptor()); JingleFileHelperRegistry.instance.add_encryption_helper(Encryption.OMEMO, new JetOmemo.EncryptionHelper(app.stream_interactor)); - Manager.start(this.app.stream_interactor, db, trust_manager); + Manager.start(this.app.stream_interactor, db, trust_manager, encryptors); SimpleAction own_keys_action = new SimpleAction("own-keys", VariantType.INT32); own_keys_action.activate.connect((variant) => { diff --git a/plugins/omemo/src/protocol/stream_module.vala b/plugins/omemo/src/protocol/stream_module.vala index e4a2733c..39d9c448 100644 --- a/plugins/omemo/src/protocol/stream_module.vala +++ b/plugins/omemo/src/protocol/stream_module.vala @@ -25,10 +25,8 @@ public class StreamModule : XmppStreamModule { public signal void bundle_fetched(Jid jid, int device_id, Bundle bundle); public signal void bundle_fetch_failed(Jid jid, int device_id); - public StreamModule() { - if (Plugin.ensure_context()) { - this.store = Plugin.get_context().create_store(); - } + public StreamModule(Store store) { + this.store = store; } public override void attach(XmppStream stream) { diff --git a/xmpp-vala/CMakeLists.txt b/xmpp-vala/CMakeLists.txt index 3aa10caf..bf8f0068 100644 --- a/xmpp-vala/CMakeLists.txt +++ b/xmpp-vala/CMakeLists.txt @@ -109,6 +109,9 @@ SOURCES "src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala" "src/module/xep/0176_jingle_ice_udp/transport_parameters.vala" + "src/module/xep/0384_omemo/omemo_encryptor.vala" + "src/module/xep/0384_omemo/omemo_decryptor.vala" + "src/module/xep/0184_message_delivery_receipts.vala" "src/module/xep/0191_blocking_command.vala" "src/module/xep/0198_stream_management.vala" diff --git a/xmpp-vala/src/module/iq/module.vala b/xmpp-vala/src/module/iq/module.vala index 56605d01..17cd3f0d 100644 --- a/xmpp-vala/src/module/iq/module.vala +++ b/xmpp-vala/src/module/iq/module.vala @@ -6,6 +6,9 @@ namespace Xmpp.Iq { public class Module : XmppStreamNegotiationModule { public static ModuleIdentity IDENTITY = new ModuleIdentity(NS_URI, "iq_module"); + public signal void preprocess_incoming_iq_set_get(XmppStream stream, Stanza iq_stanza); + public signal void preprocess_outgoing_iq_set_get(XmppStream stream, Stanza iq_stanza); + private HashMap responseListeners = new HashMap(); private HashMap> namespaceRegistrants = new HashMap>(); @@ -23,6 +26,7 @@ namespace Xmpp.Iq { public delegate void OnResult(XmppStream stream, Iq.Stanza iq); public void send_iq(XmppStream stream, Iq.Stanza iq, owned OnResult? listener = null) { + preprocess_outgoing_iq_set_get(stream, iq); stream.write(iq.stanza); if (listener != null) { responseListeners[iq.id] = new ResponseListener((owned) listener); @@ -70,6 +74,7 @@ namespace Xmpp.Iq { } else { Gee.List children = node.get_all_subnodes(); if (children.size == 1 && namespaceRegistrants.has_key(children[0].ns_uri)) { + preprocess_incoming_iq_set_get(stream, iq); Gee.List handlers = namespaceRegistrants[children[0].ns_uri]; foreach (Handler handler in handlers) { if (iq.type_ == Iq.Stanza.TYPE_GET) { diff --git a/xmpp-vala/src/module/xep/0166_jingle/content.vala b/xmpp-vala/src/module/xep/0166_jingle/content.vala index beb12183..befe02f4 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/content.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/content.vala @@ -34,9 +34,8 @@ public class Xmpp.Xep.Jingle.Content : Object { public weak Session session; public Map component_connections = new HashMap(); // TODO private - public ContentEncryption? encryption { get; set; } + public HashMap encryptions = new HashMap(); - // INITIATE_SENT | INITIATE_RECEIVED | CONNECTING public Set tried_transport_methods = new HashSet(); diff --git a/xmpp-vala/src/module/xep/0166_jingle/content_transport.vala b/xmpp-vala/src/module/xep/0166_jingle/content_transport.vala index cd74c836..2697a01c 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/content_transport.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/content_transport.vala @@ -21,7 +21,7 @@ namespace Xmpp.Xep.Jingle { public abstract uint8 components { get; } public abstract void set_content(Content content); - public abstract StanzaNode to_transport_stanza_node(); + public abstract StanzaNode to_transport_stanza_node(string action_type); public abstract void handle_transport_accept(StanzaNode transport) throws IqError; public abstract void handle_transport_info(StanzaNode transport) throws IqError; public abstract void create_transport_connection(XmppStream stream, Content content); diff --git a/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala b/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala index 7314ca6c..186848f6 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala @@ -3,7 +3,7 @@ using Xmpp; namespace Xmpp.Xep.Jingle { - internal const string NS_URI = "urn:xmpp:jingle:1"; + public const string NS_URI = "urn:xmpp:jingle:1"; private const string ERROR_NS_URI = "urn:xmpp:jingle:errors:1"; // This module can only be attached to one stream at a time. @@ -131,7 +131,7 @@ namespace Xmpp.Xep.Jingle { .put_attribute("name", content.content_name) .put_attribute("senders", content.senders.to_string()) .put_node(content.content_params.get_description_node()) - .put_node(content.transport_params.to_transport_stanza_node()); + .put_node(content.transport_params.to_transport_stanza_node("session-initiate")); if (content.security_params != null) { content_node.put_node(content.security_params.to_security_stanza_node(stream, my_jid, receiver_full_jid)); } diff --git a/xmpp-vala/src/module/xep/0166_jingle/session.vala b/xmpp-vala/src/module/xep/0166_jingle/session.vala index 5fe89415..4d04c8d5 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/session.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/session.vala @@ -221,7 +221,7 @@ public class Xmpp.Xep.Jingle.Session : Object { .put_attribute("name", content.content_name) .put_attribute("senders", content.senders.to_string()) .put_node(content.content_params.get_description_node()) - .put_node(content.transport_params.to_transport_stanza_node())); + .put_node(content.transport_params.to_transport_stanza_node("content-add"))); Iq.Stanza iq = new Iq.Stanza.set(content_add_node) { to=peer_full_jid }; yield stream.get_module(Iq.Module.IDENTITY).send_iq_async(stream, iq); @@ -343,7 +343,7 @@ public class Xmpp.Xep.Jingle.Session : Object { .put_attribute("name", content.content_name) .put_attribute("senders", content.senders.to_string()) .put_node(content.content_params.get_description_node()) - .put_node(content.transport_params.to_transport_stanza_node()); + .put_node(content.transport_params.to_transport_stanza_node("session-accept")); jingle.put_node(content_node); } @@ -379,7 +379,7 @@ public class Xmpp.Xep.Jingle.Session : Object { .put_attribute("name", content.content_name) .put_attribute("senders", content.senders.to_string()) .put_node(content.content_params.get_description_node()) - .put_node(content.transport_params.to_transport_stanza_node())); + .put_node(content.transport_params.to_transport_stanza_node("content-accept"))); Iq.Stanza iq = new Iq.Stanza.set(content_accept_node) { to=peer_full_jid }; stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); @@ -477,7 +477,7 @@ public class Xmpp.Xep.Jingle.Session : Object { .put_node(new StanzaNode.build("content", NS_URI) .put_attribute("creator", "initiator") .put_attribute("name", content.content_name) - .put_node(transport_params.to_transport_stanza_node()) + .put_node(transport_params.to_transport_stanza_node("transport-accept")) ); Iq.Stanza iq_response = new Iq.Stanza.set(jingle_response) { to=peer_full_jid }; stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq_response); @@ -493,7 +493,7 @@ public class Xmpp.Xep.Jingle.Session : Object { .put_node(new StanzaNode.build("content", NS_URI) .put_attribute("creator", "initiator") .put_attribute("name", content.content_name) - .put_node(transport_params.to_transport_stanza_node()) + .put_node(transport_params.to_transport_stanza_node("transport-replace")) ); Iq.Stanza iq = new Iq.Stanza.set(jingle) { to=peer_full_jid }; stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala index d4440169..344fe8b8 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala @@ -133,7 +133,8 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { local_crypto = null; } if (remote_crypto != null && local_crypto != null) { - content.encryption = new Xmpp.Xep.Jingle.ContentEncryption() { encryption_ns = "", encryption_name = "SRTP", our_key=local_crypto.key, peer_key=remote_crypto.key }; + var content_encryption = new Xmpp.Xep.Jingle.ContentEncryption() { encryption_ns = "", encryption_name = "SRTP", our_key=local_crypto.key, peer_key=remote_crypto.key }; + content.encryptions[content_encryption.encryption_name] = content_encryption; } this.stream = parent.create_stream(content); diff --git a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala index 5211e3a9..87c010dd 100644 --- a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala +++ b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala @@ -4,7 +4,7 @@ using Xmpp; namespace Xmpp.Xep.JingleIceUdp { -private const string NS_URI = "urn:xmpp:jingle:transports:ice-udp:1"; +public const string NS_URI = "urn:xmpp:jingle:transports:ice-udp:1"; public const string DTLS_NS_URI = "urn:xmpp:jingle:apps:dtls:0"; public abstract class Module : XmppStreamModule, Jingle.Transport { diff --git a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala index ed0fab50..83da296b 100644 --- a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala +++ b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala @@ -65,13 +65,13 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T this.content = null; } - public StanzaNode to_transport_stanza_node() { + public StanzaNode to_transport_stanza_node(string action_type) { var node = new StanzaNode.build("transport", NS_URI) .add_self_xmlns() .put_attribute("ufrag", local_ufrag) .put_attribute("pwd", local_pwd); - if (own_fingerprint != null) { + if (own_fingerprint != null && action_type != "transport-info") { var fingerprint_node = new StanzaNode.build("fingerprint", DTLS_NS_URI) .add_self_xmlns() .put_attribute("hash", "sha-256") @@ -137,7 +137,7 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T private void check_send_transport_info() { if (this.content != null && unsent_local_candidates.size > 0) { - content.send_transport_info(to_transport_stanza_node()); + content.send_transport_info(to_transport_stanza_node("transport-info")); } } diff --git a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala index 1c4e0d38..6edebbbc 100644 --- a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala +++ b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala @@ -391,7 +391,7 @@ class Parameters : Jingle.TransportParameters, Object { } - public StanzaNode to_transport_stanza_node() { + public StanzaNode to_transport_stanza_node(string action_type) { StanzaNode transport = new StanzaNode.build("transport", NS_URI) .add_self_xmlns() .put_attribute("dstaddr", local_dstaddr); diff --git a/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala b/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala index 5bb71831..f7c77544 100644 --- a/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala +++ b/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala @@ -73,7 +73,7 @@ class Parameters : Jingle.TransportParameters, Object { } - public StanzaNode to_transport_stanza_node() { + public StanzaNode to_transport_stanza_node(string action_type) { return new StanzaNode.build("transport", NS_URI) .add_self_xmlns() .put_attribute("block-size", block_size.to_string()) diff --git a/xmpp-vala/src/module/xep/0353_jingle_message_initiation.vala b/xmpp-vala/src/module/xep/0353_jingle_message_initiation.vala index 08e803a2..e26be515 100644 --- a/xmpp-vala/src/module/xep/0353_jingle_message_initiation.vala +++ b/xmpp-vala/src/module/xep/0353_jingle_message_initiation.vala @@ -102,10 +102,12 @@ namespace Xmpp.Xep.JingleMessageInitiation { } public override void attach(XmppStream stream) { + stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); stream.get_module(MessageModule.IDENTITY).received_message.connect(on_received_message); } public override void detach(XmppStream stream) { + stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI); stream.get_module(MessageModule.IDENTITY).received_message.disconnect(on_received_message); } diff --git a/xmpp-vala/src/module/xep/0384_omemo/omemo_decryptor.vala b/xmpp-vala/src/module/xep/0384_omemo/omemo_decryptor.vala new file mode 100644 index 00000000..8e3213ae --- /dev/null +++ b/xmpp-vala/src/module/xep/0384_omemo/omemo_decryptor.vala @@ -0,0 +1,62 @@ +using Gee; +using Xmpp.Xep; +using Xmpp; + +namespace Xmpp.Xep.Omemo { + + public abstract class OmemoDecryptor : XmppStreamModule { + + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0384_omemo_decryptor"); + + public abstract uint32 own_device_id { get; } + + public abstract string decrypt(uint8[] ciphertext, uint8[] key, uint8[] iv) throws GLib.Error; + + public abstract uint8[] decrypt_key(ParsedData data, Jid from_jid) throws GLib.Error; + + public ParsedData? parse_node(StanzaNode encrypted_node) { + ParsedData ret = new ParsedData(); + + StanzaNode? header_node = encrypted_node.get_subnode("header"); + if (header_node == null) return null; + + ret.sid = header_node.get_attribute_int("sid", -1); + if (ret.sid == -1) return null; + + string? payload_str = encrypted_node.get_deep_string_content("payload"); + if (payload_str != null) ret.ciphertext = Base64.decode(payload_str); + + string? iv_str = header_node.get_deep_string_content("iv"); + if (iv_str == null) return null; + ret.iv = Base64.decode(iv_str); + + foreach (StanzaNode key_node in header_node.get_subnodes("key")) { + debug("Is ours? %d =? %u", key_node.get_attribute_int("rid"), own_device_id); + if (key_node.get_attribute_int("rid") == own_device_id) { + string? key_node_content = key_node.get_string_content(); + if (key_node_content == null) continue; + uchar[] encrypted_key = Base64.decode(key_node_content); + ret.our_potential_encrypted_keys[new Bytes.take(encrypted_key)] = key_node.get_attribute_bool("prekey"); + } + } + + return ret; + } + + public override void attach(XmppStream stream) { } + public override void detach(XmppStream stream) { } + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } + } + + public class ParsedData { + public int sid; + public uint8[] ciphertext; + public uint8[] iv; + public uchar[] encrypted_key; + public bool is_prekey; + + public HashMap our_potential_encrypted_keys = new HashMap(); + } +} + diff --git a/xmpp-vala/src/module/xep/0384_omemo/omemo_encryptor.vala b/xmpp-vala/src/module/xep/0384_omemo/omemo_encryptor.vala new file mode 100644 index 00000000..6509bfe3 --- /dev/null +++ b/xmpp-vala/src/module/xep/0384_omemo/omemo_encryptor.vala @@ -0,0 +1,116 @@ +using Gee; +using Xmpp.Xep; +using Xmpp; + +namespace Xmpp.Xep.Omemo { + + public const string NS_URI = "eu.siacs.conversations.axolotl"; + public const string NODE_DEVICELIST = NS_URI + ".devicelist"; + public const string NODE_BUNDLES = NS_URI + ".bundles"; + public const string NODE_VERIFICATION = NS_URI + ".verification"; + + public abstract class OmemoEncryptor : XmppStreamModule { + + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0384_omemo_encryptor"); + + public abstract uint32 own_device_id { get; } + + public abstract EncryptionData encrypt_plaintext(string plaintext) throws GLib.Error; + + public abstract void encrypt_key(Xep.Omemo.EncryptionData encryption_data, Jid jid, int32 device_id) throws GLib.Error; + + public abstract EncryptionResult encrypt_key_to_recipient(XmppStream stream, Xep.Omemo.EncryptionData enc_data, Jid recipient) throws GLib.Error; + + public override void attach(XmppStream stream) { } + public override void detach(XmppStream stream) { } + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } + } + + public class EncryptionData { + public uint32 own_device_id; + public uint8[] ciphertext; + public uint8[] keytag; + public uint8[] iv; + + public Gee.List key_nodes = new ArrayList(); + + public EncryptionData(uint32 own_device_id) { + this.own_device_id = own_device_id; + } + + public void add_device_key(int device_id, uint8[] device_key, bool prekey) { + StanzaNode key_node = new StanzaNode.build("key", NS_URI) + .put_attribute("rid", device_id.to_string()) + .put_node(new StanzaNode.text(Base64.encode(device_key))); + if (prekey) { + key_node.put_attribute("prekey", "true"); + } + key_nodes.add(key_node); + } + + public StanzaNode get_encrypted_node() { + StanzaNode encrypted_node = new StanzaNode.build("encrypted", NS_URI).add_self_xmlns(); + + StanzaNode header_node = new StanzaNode.build("header", NS_URI) + .put_attribute("sid", own_device_id.to_string()) + .put_node(new StanzaNode.build("iv", NS_URI).put_node(new StanzaNode.text(Base64.encode(iv)))); + encrypted_node.put_node(header_node); + + if (ciphertext != null) { + StanzaNode payload_node = new StanzaNode.build("payload", NS_URI) + .put_node(new StanzaNode.text(Base64.encode(ciphertext))); + encrypted_node.put_node(payload_node); + } + + foreach (StanzaNode key_node in key_nodes) { + header_node.put_node(key_node); + } + + return encrypted_node; + } + } + + public class EncryptionResult { + public int lost { get; internal set; } + public int success { get; internal set; } + public int unknown { get; internal set; } + public int failure { get; internal set; } + } + + public class EncryptState { + public bool encrypted { get; internal set; } + public int other_devices { get; internal set; } + public int other_success { get; internal set; } + public int other_lost { get; internal set; } + public int other_unknown { get; internal set; } + public int other_failure { get; internal set; } + public int other_waiting_lists { get; internal set; } + + public int own_devices { get; internal set; } + public int own_success { get; internal set; } + public int own_lost { get; internal set; } + public int own_unknown { get; internal set; } + public int own_failure { get; internal set; } + public bool own_list { get; internal set; } + + public void add_result(EncryptionResult enc_res, bool own) { + if (own) { + own_lost += enc_res.lost; + own_success += enc_res.success; + own_unknown += enc_res.unknown; + own_failure += enc_res.failure; + } else { + other_lost += enc_res.lost; + other_success += enc_res.success; + other_unknown += enc_res.unknown; + other_failure += enc_res.failure; + } + } + + public string to_string() { + return @"EncryptState (encrypted=$encrypted, other=(devices=$other_devices, success=$other_success, lost=$other_lost, unknown=$other_unknown, failure=$other_failure, waiting_lists=$other_waiting_lists, own=(devices=$own_devices, success=$own_success, lost=$own_lost, unknown=$own_unknown, failure=$own_failure, list=$own_list))"; + } + } +} + -- cgit v1.2.3-54-g00ecf From 7d2e64769067c1b47e0500f6456dd7e6f4eb435a Mon Sep 17 00:00:00 2001 From: fiaxh Date: Thu, 29 Apr 2021 15:56:22 +0200 Subject: Improve call wording, cleanup --- main/src/ui/call_window/call_bottom_bar.vala | 1 - main/src/ui/call_window/call_window.vala | 4 +- .../src/ui/call_window/call_window_controller.vala | 30 +++--- .../ui/conversation_content_view/call_widget.vala | 2 +- main/src/ui/conversation_titlebar/call_entry.vala | 5 +- .../omemo/src/dtls_srtp_verification_draft.vala | 1 - plugins/rtp/src/module.vala | 12 +-- plugins/rtp/src/stream.vala | 4 +- .../src/module/xep/0166_jingle/component.vala | 12 ++- xmpp-vala/src/module/xep/0166_jingle/content.vala | 4 +- .../xep/0167_jingle_rtp/jingle_rtp_module.vala | 18 ++-- .../0176_jingle_ice_udp/transport_parameters.vala | 2 +- .../src/module/xep/0234_jingle_file_transfer.vala | 20 ++-- .../module/xep/0260_jingle_socks5_bytestreams.vala | 118 +++++++++++---------- .../xep/0261_jingle_in_band_bytestreams.vala | 2 +- 15 files changed, 125 insertions(+), 110 deletions(-) (limited to 'xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala') diff --git a/main/src/ui/call_window/call_bottom_bar.vala b/main/src/ui/call_window/call_bottom_bar.vala index a9fee8c3..a3e4b93b 100644 --- a/main/src/ui/call_window/call_bottom_bar.vala +++ b/main/src/ui/call_window/call_bottom_bar.vala @@ -25,7 +25,6 @@ public class Dino.Ui.CallBottomBar : Gtk.Box { private MenuButton video_settings_button = new MenuButton() { halign=Align.END, valign=Align.END }; public VideoSettingsPopover? video_settings_popover; - private EventBox encryption_event_box = new EventBox() { visible=true }; private MenuButton encryption_button = new MenuButton() { relief=ReliefStyle.NONE, height_request=30, width_request=30, margin_start=20, margin_bottom=25, halign=Align.START, valign=Align.END }; private Image encryption_image = new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON) { visible=true }; diff --git a/main/src/ui/call_window/call_window.vala b/main/src/ui/call_window/call_window.vala index 572f73b6..3b3d4dc2 100644 --- a/main/src/ui/call_window/call_window.vala +++ b/main/src/ui/call_window/call_window.vala @@ -158,13 +158,13 @@ namespace Dino.Ui { public void set_status(string state) { switch (state) { case "requested": - header_bar.subtitle = _("Sending a call request…"); + header_bar.subtitle = _("Calling…"); break; case "ringing": header_bar.subtitle = _("Ringing…"); break; case "establishing": - header_bar.subtitle = _("Establishing a (peer-to-peer) connection…"); + header_bar.subtitle = _("Connecting…"); break; default: header_bar.subtitle = null; diff --git a/main/src/ui/call_window/call_window_controller.vala b/main/src/ui/call_window/call_window_controller.vala index 616e341d..0a223d72 100644 --- a/main/src/ui/call_window/call_window_controller.vala +++ b/main/src/ui/call_window/call_window_controller.vala @@ -3,8 +3,6 @@ using Gtk; public class Dino.Ui.CallWindowController : Object { - public signal void terminated(); - private CallWindow call_window; private Call call; private Conversation conversation; @@ -40,8 +38,16 @@ public class Dino.Ui.CallWindowController : Object { call_window.set_status("requested"); } - call_window.bottom_bar.hang_up.connect(end_call); - call_window.destroy.connect(end_call); + call_window.bottom_bar.hang_up.connect(() => { + calls.end_call(conversation, call); + call_window.close(); + call_window.destroy(); + this.dispose(); + }); + call_window.destroy.connect(() => { + calls.end_call(conversation, call); + this.dispose(); + }); call_window.bottom_bar.notify["audio-enabled"].connect(() => { calls.mute_own_audio(call, !call_window.bottom_bar.audio_enabled); @@ -116,16 +122,6 @@ public class Dino.Ui.CallWindowController : Object { this.window_width = this.call_window.get_allocated_width(); } - private void end_call() { - call.notify["state"].disconnect(on_call_state_changed); - calls.call_terminated.disconnect(on_call_terminated); - - calls.end_call(conversation, call); - call_window.close(); - call_window.destroy(); - terminated(); - } - private void on_call_state_changed() { if (call.state == Call.State.IN_PROGRESS) { call_window.set_status(""); @@ -234,4 +230,10 @@ public class Dino.Ui.CallWindowController : Object { call_window.unset_own_video(); } } + + public override void dispose() { + base.dispose(); + call.notify["state"].disconnect(on_call_state_changed); + calls.call_terminated.disconnect(on_call_terminated); + } } \ No newline at end of file diff --git a/main/src/ui/conversation_content_view/call_widget.vala b/main/src/ui/conversation_content_view/call_widget.vala index 66788e28..74525d11 100644 --- a/main/src/ui/conversation_content_view/call_widget.vala +++ b/main/src/ui/conversation_content_view/call_widget.vala @@ -154,7 +154,7 @@ namespace Dino.Ui { case Call.State.FAILED: image.set_from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR); title_label.label = _("Call failed"); - subtitle_label.label = "This call failed to establish"; + subtitle_label.label = "Call failed to establish"; break; } } diff --git a/main/src/ui/conversation_titlebar/call_entry.vala b/main/src/ui/conversation_titlebar/call_entry.vala index e1d10e5c..9353f631 100644 --- a/main/src/ui/conversation_titlebar/call_entry.vala +++ b/main/src/ui/conversation_titlebar/call_entry.vala @@ -92,9 +92,6 @@ namespace Dino.Ui { call_window.present(); update_button_state(); - call_controller.terminated.connect(() => { - update_button_state(); - }); } public new void set_conversation(Conversation conversation) { @@ -119,7 +116,7 @@ namespace Dino.Ui { if (conversation.type_ == Conversation.Type.CHAT) { Conversation conv_bak = conversation; bool audio_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_audio_calls_async(conversation); - bool video_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_audio_calls_async(conversation); + bool video_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_video_calls_async(conversation); if (conv_bak != conversation) return; visible = audio_works; diff --git a/plugins/omemo/src/dtls_srtp_verification_draft.vala b/plugins/omemo/src/dtls_srtp_verification_draft.vala index e2441670..66a31954 100644 --- a/plugins/omemo/src/dtls_srtp_verification_draft.vala +++ b/plugins/omemo/src/dtls_srtp_verification_draft.vala @@ -65,7 +65,6 @@ namespace Dino.Plugins.Omemo.DtlsSrtpVerificationDraft { stream.get_flag(Xep.Jingle.Flag.IDENTITY).get_session.begin(jingle_sid, (_, res) => { Xep.Jingle.Session? session = stream.get_flag(Xep.Jingle.Flag.IDENTITY).get_session.end(res); - if (session != null) print(@"$(session.contents_map.has_key(content_name))\n"); if (session == null || !session.contents_map.has_key(content_name)) return; var encryption = new OmemoContentEncryption() { encryption_ns=NS_URI, encryption_name="OMEMO", our_key=new uint8[0], peer_key=new uint8[0], peer_device_id=device_id_by_jingle_sid[jingle_sid] }; session.contents_map[content_name].encryptions[NS_URI] = encryption; diff --git a/plugins/rtp/src/module.vala b/plugins/rtp/src/module.vala index 13a21cd8..19a7501d 100644 --- a/plugins/rtp/src/module.vala +++ b/plugins/rtp/src/module.vala @@ -216,9 +216,9 @@ public class Dino.Plugins.Rtp.Module : JingleRtp.Module { } public override JingleRtp.Crypto? generate_local_crypto() { - uint8[] keyAndSalt = new uint8[30]; - Crypto.randomize(keyAndSalt); - return JingleRtp.Crypto.create(JingleRtp.Crypto.AES_CM_128_HMAC_SHA1_80, keyAndSalt); + uint8[] key_and_salt = new uint8[30]; + Crypto.randomize(key_and_salt); + return JingleRtp.Crypto.create(JingleRtp.Crypto.AES_CM_128_HMAC_SHA1_80, key_and_salt); } public override JingleRtp.Crypto? pick_remote_crypto(Gee.List cryptos) { @@ -230,8 +230,8 @@ public class Dino.Plugins.Rtp.Module : JingleRtp.Module { public override JingleRtp.Crypto? pick_local_crypto(JingleRtp.Crypto? remote) { if (remote == null || !remote.is_valid) return null; - uint8[] keyAndSalt = new uint8[30]; - Crypto.randomize(keyAndSalt); - return remote.rekey(keyAndSalt); + uint8[] key_and_salt = new uint8[30]; + Crypto.randomize(key_and_salt); + return remote.rekey(key_and_salt); } } \ No newline at end of file diff --git a/plugins/rtp/src/stream.vala b/plugins/rtp/src/stream.vala index 23634aa3..bd8a279f 100644 --- a/plugins/rtp/src/stream.vala +++ b/plugins/rtp/src/stream.vala @@ -256,7 +256,7 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { } private void prepare_local_crypto() { - if (local_crypto != null && !crypto_session.has_encrypt) { + if (local_crypto != null && local_crypto.is_valid && !crypto_session.has_encrypt) { crypto_session.set_encryption_key(local_crypto.crypto_suite, local_crypto.key, local_crypto.salt); debug("Setting up encryption with key params %s", local_crypto.key_params); } @@ -396,7 +396,7 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { } private void prepare_remote_crypto() { - if (remote_crypto != null && !crypto_session.has_decrypt) { + if (remote_crypto != null && remote_crypto.is_valid && !crypto_session.has_decrypt) { crypto_session.set_decryption_key(remote_crypto.crypto_suite, remote_crypto.key, remote_crypto.salt); debug("Setting up decryption with key params %s", remote_crypto.key_params); } diff --git a/xmpp-vala/src/module/xep/0166_jingle/component.vala b/xmpp-vala/src/module/xep/0166_jingle/component.vala index 544bcd69..5d573522 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/component.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/component.vala @@ -31,7 +31,11 @@ namespace Xmpp.Xep.Jingle { protected Gee.Promise promise = new Gee.Promise(); private string? terminated = null; - public async void init(IOStream stream) { + public async void set_stream(IOStream? stream) { + if (stream == null) { + promise.set_exception(new IOError.FAILED("Jingle connection failed")); + return; + } assert(!this.stream.ready); promise.set_value(stream); if (terminated != null) { @@ -39,11 +43,17 @@ namespace Xmpp.Xep.Jingle { } } + public void set_error(GLib.Error? e) { + promise.set_exception(e); + } + public override async void terminate(bool we_terminated, string? reason_name = null, string? reason_text = null) { if (terminated == null) { terminated = (reason_name ?? "") + " - " + (reason_text ?? "") + @"we terminated? $we_terminated"; if (stream.ready) { yield stream.value.close_async(); + } else { + promise.set_exception(new IOError.FAILED("Jingle connection failed")); } } } diff --git a/xmpp-vala/src/module/xep/0166_jingle/content.vala b/xmpp-vala/src/module/xep/0166_jingle/content.vala index befe02f4..41310aeb 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/content.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/content.vala @@ -36,7 +36,7 @@ public class Xmpp.Xep.Jingle.Content : Object { public HashMap encryptions = new HashMap(); - public Set tried_transport_methods = new HashSet(); + private Set tried_transport_methods = new HashSet(); public Content.initiate_sent(string content_name, Senders senders, @@ -109,7 +109,7 @@ public class Xmpp.Xep.Jingle.Content : Object { transport_params.dispose(); foreach (ComponentConnection connection in component_connections.values) { - connection.terminate(we_terminated, reason_name, reason_text); + connection.terminate.begin(we_terminated, reason_name, reason_text); } } diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala index 6eb6289b..6b55cbe6 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala @@ -83,7 +83,7 @@ public abstract class Module : XmppStreamModule { } } - public async Jingle.Content add_outgoing_video_content(XmppStream stream, Jingle.Session session) { + public async Jingle.Content add_outgoing_video_content(XmppStream stream, Jingle.Session session) throws Jingle.Error { Jid my_jid = session.local_full_jid; Jid receiver_full_jid = session.peer_full_jid; @@ -168,7 +168,7 @@ public class Crypto { public string? session_params { get; private set; } public string tag { get; private set; } - public uint8[] key_and_salt { owned get { + public uint8[]? key_and_salt { owned get { if (!key_params.has_prefix("inline:")) return null; int endIndex = key_params.index_of("|"); if (endIndex < 0) endIndex = key_params.length; @@ -221,30 +221,30 @@ public class Crypto { case AES_CM_128_HMAC_SHA1_80: case AES_CM_128_HMAC_SHA1_32: case F8_128_HMAC_SHA1_80: - return key_and_salt.length == 30; + return key_and_salt != null && key_and_salt.length == 30; } return false; }} - public uint8[] key { owned get { - uint8[] key_and_salt = key_and_salt; + public uint8[]? key { owned get { + uint8[]? key_and_salt = key_and_salt; switch(crypto_suite) { case AES_CM_128_HMAC_SHA1_80: case AES_CM_128_HMAC_SHA1_32: case F8_128_HMAC_SHA1_80: - if (key_and_salt.length >= 16) return key_and_salt[0:16]; + if (key_and_salt != null && key_and_salt.length >= 16) return key_and_salt[0:16]; break; } return null; }} - public uint8[] salt { owned get { - uint8[] keyAndSalt = key_and_salt; + public uint8[]? salt { owned get { + uint8[]? key_and_salt = key_and_salt; switch(crypto_suite) { case AES_CM_128_HMAC_SHA1_80: case AES_CM_128_HMAC_SHA1_32: case F8_128_HMAC_SHA1_80: - if (keyAndSalt.length >= 30) return keyAndSalt[16:30]; + if (key_and_salt != null && key_and_salt.length >= 30) return key_and_salt[16:30]; break; } return null; diff --git a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala index 83da296b..07b599ee 100644 --- a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala +++ b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala @@ -152,7 +152,7 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T return sb.str; } - private uint8[] fingerprint_to_bytes(string? fingerprint_) { + private uint8[]? fingerprint_to_bytes(string? fingerprint_) { if (fingerprint_ == null) return null; string fingerprint = fingerprint_.replace(":", "").up(); diff --git a/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala b/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala index 07b158bc..4581019f 100644 --- a/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala +++ b/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala @@ -268,13 +268,19 @@ public class FileTransfer : Object { content.accept(); Jingle.StreamingConnection connection = content.component_connections.values.to_array()[0] as Jingle.StreamingConnection; - IOStream? io_stream = yield connection.stream.wait_async(); - FileTransferInputStream ft_stream = new FileTransferInputStream(io_stream.input_stream, size); - io_stream.output_stream.close(); - ft_stream.closed.connect(() => { - session.terminate(Jingle.ReasonElement.SUCCESS, null, null); - }); - this.stream = ft_stream; + try { + IOStream io_stream = yield connection.stream.wait_async(); + FileTransferInputStream ft_stream = new FileTransferInputStream(io_stream.input_stream, size); + io_stream.output_stream.close(); + ft_stream.closed.connect(() => { + session.terminate(Jingle.ReasonElement.SUCCESS, null, null); + }); + this.stream = ft_stream; + } catch (FutureError.EXCEPTION e) { + warning("Error accepting Jingle file-transfer: %s", connection.stream.exception.message); + } catch (FutureError e) { + warning("FutureError accepting Jingle file-transfer: %s", e.message); + } } public void reject(XmppStream stream) { diff --git a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala index 6edebbbc..47c243e8 100644 --- a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala +++ b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala @@ -28,6 +28,7 @@ public class Module : Jingle.Transport, XmppStreamModule { public string ns_uri { get { return NS_URI; } } public Jingle.TransportType type_ { get { return Jingle.TransportType.STREAMING; } } public int priority { get { return 1; } } + private Gee.List get_proxies(XmppStream stream) { Gee.List result = new ArrayList(); int i = 1 << 15; @@ -37,6 +38,7 @@ public class Module : Jingle.Transport, XmppStreamModule { } return result; } + private Gee.List start_local_listeners(XmppStream stream, Jid local_full_jid, string dstaddr, out LocalListener? local_listener) { Gee.List result = new ArrayList(); SocketListener listener = new SocketListener(); @@ -62,15 +64,17 @@ public class Module : Jingle.Transport, XmppStreamModule { } return result; } + private void select_candidates(XmppStream stream, Jid local_full_jid, string dstaddr, Parameters result) { result.local_candidates.add_all(get_proxies(stream)); - //result.local_candidates.add_all(start_local_listeners(stream, local_full_jid, dstaddr, out result.listener)); + result.local_candidates.add_all(start_local_listeners(stream, local_full_jid, dstaddr, out result.listener)); result.local_candidates.sort((c1, c2) => { if (c1.priority < c2.priority) { return 1; } if (c1.priority > c2.priority) { return -1; } return 0; }); } + public Jingle.TransportParameters create_transport_parameters(XmppStream stream, uint8 components, Jid local_full_jid, Jid peer_full_jid) { assert(components == 1); Parameters result = new Parameters.create(local_full_jid, peer_full_jid, random_uuid()); @@ -78,6 +82,7 @@ public class Module : Jingle.Transport, XmppStreamModule { select_candidates(stream, local_full_jid, dstaddr, result); return result; } + public Jingle.TransportParameters parse_transport_parameters(XmppStream stream, uint8 components, Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws Jingle.IqError { Parameters result = Parameters.parse(local_full_jid, peer_full_jid, transport); string dstaddr = calculate_dstaddr(result.sid, local_full_jid, peer_full_jid); @@ -146,6 +151,7 @@ public class Candidate : Socks5Bytestreams.Proxy { public Candidate.build(string cid, string host, Jid jid, int port, int local_priority, CandidateType type) { this(cid, host, jid, port, type.type_preference() + local_priority, type); } + public Candidate.proxy(string cid, Socks5Bytestreams.Proxy proxy, int local_priority) { this.build(cid, proxy.host, proxy.jid, proxy.port, local_priority, CandidateType.PROXY); } @@ -170,6 +176,7 @@ public class Candidate : Socks5Bytestreams.Proxy { return new Candidate(cid, host, jid, port, priority, type); } + public StanzaNode to_xml() { return new StanzaNode.build("candidate", NS_URI) .put_attribute("cid", cid) @@ -210,6 +217,7 @@ class LocalListener { this.inner = inner; this.dstaddr = dstaddr; } + public LocalListener.empty() { this.inner = null; this.dstaddr = ""; @@ -233,6 +241,7 @@ class LocalListener { handle_conn.begin(((StringWrapper)cid).str, conn); } } + async void handle_conn(string cid, SocketConnection conn) { conn.socket.timeout = NEGOTIATION_TIMEOUT; size_t read; @@ -418,39 +427,39 @@ class Parameters : Jingle.TransportParameters, Object { } public void handle_transport_info(StanzaNode transport) throws Jingle.IqError { - StanzaNode? candidate_error = transport.get_subnode("candidate-error", NS_URI); - StanzaNode? candidate_used = transport.get_subnode("candidate-used", NS_URI); - StanzaNode? activated = transport.get_subnode("activated", NS_URI); - StanzaNode? proxy_error = transport.get_subnode("proxy-error", NS_URI); - int num_children = 0; - if (candidate_error != null) { num_children += 1; } - if (candidate_used != null) { num_children += 1; } - if (activated != null) { num_children += 1; } - if (proxy_error != null) { num_children += 1; } - if (num_children == 0) { - throw new Jingle.IqError.UNSUPPORTED_INFO("unknown transport-info"); - } else if (num_children > 1) { - throw new Jingle.IqError.BAD_REQUEST("transport-info with more than one child"); - } - if (candidate_error != null) { - handle_remote_candidate(null); - } - if (candidate_used != null) { - string? cid = candidate_used.get_attribute("cid"); - if (cid == null) { - throw new Jingle.IqError.BAD_REQUEST("missing cid"); - } - handle_remote_candidate(cid); - } - if (activated != null) { - string? cid = activated.get_attribute("cid"); - if (cid == null) { - throw new Jingle.IqError.BAD_REQUEST("missing cid"); - } - handle_activated(cid); + ArrayList socks5_nodes = new ArrayList(); + foreach (StanzaNode node in transport.sub_nodes) { + if (node.ns_uri == NS_URI) socks5_nodes.add(node); } - if (proxy_error != null) { - handle_proxy_error(); + if (socks5_nodes.is_empty) { warning("No socks5 subnodes in transport node"); return; } + if (socks5_nodes.size > 1) { warning("Too many socks5 subnodes in transport node"); return; } + + StanzaNode node = socks5_nodes[0]; + + switch (node.name) { + case "activated": + string? cid = node.get_attribute("cid"); + if (cid == null) { + throw new Jingle.IqError.BAD_REQUEST("missing cid"); + } + handle_activated(cid); + break; + case "candidate-used": + string? cid = node.get_attribute("cid"); + if (cid == null) { + throw new Jingle.IqError.BAD_REQUEST("missing cid"); + } + handle_remote_candidate(cid); + break; + case "candidate-error": + handle_remote_candidate(null); + break; + case "proxy-error": + handle_proxy_error(); + break; + default: + warning("Unknown transport-info: %s", transport.to_string()); + break; } } @@ -499,32 +508,22 @@ class Parameters : Jingle.TransportParameters, Object { return; } - Candidate? remote = remote_selected_candidate; - Candidate? local = local_selected_candidate; - - int num_candidates = 0; - if (remote != null) { num_candidates += 1; } - if (local != null) { num_candidates += 1; } - - if (num_candidates == 0) { - // Notify Jingle of the failed transport. - content_set_transport_connection(null); + if (remote_selected_candidate == null && local_selected_candidate == null) { + content_set_transport_connection_error(new IOError.FAILED("No candidates")); return; } bool remote_wins; - if (num_candidates == 1) { - remote_wins = remote != null; - } else { - if (local.priority < remote.priority) { - remote_wins = true; - } else if (local.priority > remote.priority) { - remote_wins = false; - } else { + if (remote_selected_candidate != null && local_selected_candidate != null) { + if (local_selected_candidate.priority == remote_selected_candidate.priority) { // equal priority -> XEP-0260 says that the candidate offered // by the initiator wins, so the one that the remote chose remote_wins = role == Jingle.Role.INITIATOR; + } else { + remote_wins = local_selected_candidate.priority < remote_selected_candidate.priority; } + } else { + remote_wins = remote_selected_candidate != null; } if (!remote_wins) { @@ -545,8 +544,7 @@ class Parameters : Jingle.TransportParameters, Object { } SocketConnection? conn = listener.get_connection(remote_selected_candidate.cid); if (conn == null) { - // Remote hasn't actually connected to us?! - content_set_transport_connection(null); + content_set_transport_connection_error(new IOError.FAILED("Remote hasn't actually connected to us?!")); return; } content_set_transport_connection(conn); @@ -569,7 +567,7 @@ class Parameters : Jingle.TransportParameters, Object { if (!waiting_for_activation_error) { content_set_transport_connection(conn); } else { - content_set_transport_connection(null); + content_set_transport_connection_error(new IOError.FAILED("waiting_for_activation_error")); } } @@ -620,7 +618,7 @@ class Parameters : Jingle.TransportParameters, Object { .put_attribute("sid", sid) .put_node(new StanzaNode.build("proxy-error", NS_URI)) ); - content_set_transport_connection(null); + content_set_transport_connection_error(new IOError.FAILED("Connect to local candidate error: %s", e.message)); } } @@ -745,15 +743,19 @@ class Parameters : Jingle.TransportParameters, Object { private Jingle.StreamingConnection connection = new Jingle.StreamingConnection(); - private void content_set_transport_connection(IOStream? ios) { - IOStream? iostream = ios; + private void content_set_transport_connection(IOStream ios) { + IOStream iostream = ios; Jingle.Content? strong_content = content; if (strong_content == null) return; if (strong_content.security_params != null) { iostream = strong_content.security_params.wrap_stream(iostream); } - connection.init.begin(iostream); + connection.set_stream.begin(iostream); + } + + private void content_set_transport_connection_error(Error e) { + connection.set_error(e); } public void create_transport_connection(XmppStream stream, Jingle.Content content) { diff --git a/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala b/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala index f7c77544..09eaf711 100644 --- a/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala +++ b/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala @@ -98,7 +98,7 @@ class Parameters : Jingle.TransportParameters, Object { if (content.security_params != null) { iostream = content.security_params.wrap_stream(iostream); } - connection.init.begin(iostream); + connection.set_stream.begin(iostream); debug("set transport conn ibb"); content.set_transport_connection(connection, 1); } -- cgit v1.2.3-54-g00ecf