1using Gee;
2using Xmpp;
3using Xmpp.Xep;
4
5namespace Xmpp.Xep.JingleSocks5Bytestreams {
6
7private const string NS_URI = "urn:xmpp:jingle:transports:s5b:1";
8
9public class Module : Jingle.Transport, XmppStreamModule {
10    public static Xmpp.ModuleIdentity<Module> IDENTITY = new Xmpp.ModuleIdentity<Module>(NS_URI, "0260_jingle_socks5_bytestreams");
11
12    public override void attach(XmppStream stream) {
13        stream.get_module(Jingle.Module.IDENTITY).register_transport(this);
14        stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI);
15    }
16    public override void detach(XmppStream stream) {
17        stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI);
18    }
19
20    public override string get_ns() { return NS_URI; }
21    public override string get_id() { return IDENTITY.id; }
22
23    public async bool is_transport_available(XmppStream stream, Jid full_jid) {
24        return yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI);
25    }
26
27    public string transport_ns_uri() {
28        return NS_URI;
29    }
30    public Jingle.TransportType transport_type() {
31        return Jingle.TransportType.STREAMING;
32    }
33    public int transport_priority() {
34        return 1;
35    }
36    private Gee.List<Candidate> get_local_candidates(XmppStream stream) {
37        Gee.List<Candidate> result = new ArrayList<Candidate>();
38        int i = 1 << 15;
39        foreach (Socks5Bytestreams.Proxy proxy in stream.get_module(Socks5Bytestreams.Module.IDENTITY).get_proxies(stream)) {
40            result.add(new Candidate.proxy(random_uuid(), proxy, i));
41            i -= 1;
42        }
43        return result;
44    }
45    public Jingle.TransportParameters create_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid) {
46        Parameters result = new Parameters.create(local_full_jid, peer_full_jid, random_uuid());
47        result.local_candidates.add_all(get_local_candidates(stream));
48        return result;
49    }
50    public Jingle.TransportParameters parse_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws Jingle.IqError {
51        Parameters result = Parameters.parse(local_full_jid, peer_full_jid, transport);
52        result.local_candidates.add_all(get_local_candidates(stream));
53        return result;
54    }
55}
56
57public enum CandidateType {
58    ASSISTED,
59    DIRECT,
60    PROXY,
61    TUNNEL;
62
63    public static CandidateType parse(string type) throws Jingle.IqError {
64        switch (type) {
65            case "assisted": return CandidateType.ASSISTED;
66            case "direct": return CandidateType.DIRECT;
67            case "proxy": return CandidateType.PROXY;
68            case "tunnel": return CandidateType.TUNNEL;
69        }
70        throw new Jingle.IqError.BAD_REQUEST(@"unknown candidate type $(type)");
71    }
72
73    public string to_string() {
74        switch (this) {
75            case ASSISTED: return "assisted";
76            case DIRECT: return "direct";
77            case PROXY: return "proxy";
78            case TUNNEL: return "tunnel";
79        }
80        assert_not_reached();
81    }
82
83    private int type_preference_impl() {
84        switch (this) {
85            case ASSISTED: return 120;
86            case DIRECT: return 126;
87            case PROXY: return 10;
88            case TUNNEL: return 110;
89        }
90        assert_not_reached();
91    }
92    public int type_preference() {
93        return type_preference_impl() << 16;
94    }
95}
96
97public class Candidate : Socks5Bytestreams.Proxy {
98    public string cid { get; private set; }
99    public int priority { get; private set; }
100    public CandidateType type_ { get; private set; }
101
102    private Candidate(string cid, string host, Jid jid, int port, int priority, CandidateType type) {
103        base(host, jid, port);
104        this.cid = cid;
105        this.priority = priority;
106        this.type_ = type;
107    }
108
109    public Candidate.build(string cid, string host, Jid jid, int port, int local_priority, CandidateType type) {
110        this(cid, host, jid, port, type.type_preference() + local_priority, type);
111    }
112    public Candidate.proxy(string cid, Socks5Bytestreams.Proxy proxy, int local_priority) {
113        this.build(cid, proxy.host, proxy.jid, proxy.port, local_priority, CandidateType.PROXY);
114    }
115
116    public static Candidate parse(StanzaNode candidate) throws Jingle.IqError {
117        string? cid = candidate.get_attribute("cid");
118        string? host = candidate.get_attribute("host");
119        string? jid_str = candidate.get_attribute("jid");
120        Jid? jid = null;
121        try {
122            jid = new Jid(jid_str);
123        } catch (InvalidJidError ignored) {
124        }
125        int port = candidate.get_attribute("port") != null ? candidate.get_attribute_int("port") : 1080;
126        int priority = candidate.get_attribute_int("priority");
127        string? type_str = candidate.get_attribute("type");
128        CandidateType type = type_str != null ? CandidateType.parse(type_str) : CandidateType.DIRECT;
129
130        if (cid == null || host == null || jid == null || port <= 0 || priority <= 0) {
131            throw new Jingle.IqError.BAD_REQUEST("missing or invalid cid, host, jid or port");
132        }
133
134        return new Candidate(cid, host, jid, port, priority, type);
135    }
136    public StanzaNode to_xml() {
137        return new StanzaNode.build("candidate", NS_URI)
138            .put_attribute("cid", cid)
139            .put_attribute("host", host)
140            .put_attribute("jid", jid.to_string())
141            .put_attribute("port", port.to_string())
142            .put_attribute("priority", priority.to_string())
143            .put_attribute("type", type_.to_string());
144    }
145}
146
147bool bytes_equal(uint8[] a, uint8[] b) {
148    if (a.length != b.length) {
149        return false;
150    }
151    for (int i = 0; i < a.length; i++) {
152        if (a[i] != b[i]) {
153            return false;
154        }
155    }
156    return true;
157}
158
159class Parameters : Jingle.TransportParameters, Object {
160    public Jingle.Role role { get; private set; }
161    public string sid { get; private set; }
162    public string remote_dstaddr { get; private set; }
163    public string local_dstaddr { get; private set; }
164    public Gee.List<Candidate> local_candidates = new ArrayList<Candidate>();
165    public Gee.List<Candidate> remote_candidates = new ArrayList<Candidate>();
166
167    Jid local_full_jid;
168    Jid peer_full_jid;
169
170    bool remote_sent_selected_candidate = false;
171    Candidate? remote_selected_candidate = null;
172    bool local_determined_selected_candidate = false;
173    Candidate? local_selected_candidate = null;
174    SocketConnection? local_selected_candidate_conn = null;
175    weak Jingle.Session? session = null;
176    XmppStream? hack = null;
177
178    string? waiting_for_activation_cid = null;
179    SourceFunc waiting_for_activation_callback;
180    bool waiting_for_activation_error = false;
181
182    private static string calculate_dstaddr(string sid, Jid first_jid, Jid second_jid) {
183        string hashed = sid + first_jid.to_string() + second_jid.to_string();
184        return Checksum.compute_for_string(ChecksumType.SHA1, hashed);
185    }
186    private Parameters(Jingle.Role role, string sid, Jid local_full_jid, Jid peer_full_jid, string? remote_dstaddr) {
187        this.role = role;
188        this.sid = sid;
189        this.local_dstaddr = calculate_dstaddr(sid, local_full_jid, peer_full_jid);
190        this.remote_dstaddr = remote_dstaddr ?? calculate_dstaddr(sid, peer_full_jid, local_full_jid);
191
192        this.local_full_jid = local_full_jid;
193        this.peer_full_jid = peer_full_jid;
194    }
195    public Parameters.create(Jid local_full_jid, Jid peer_full_jid, string sid) {
196        this(Jingle.Role.INITIATOR, sid, local_full_jid, peer_full_jid, null);
197    }
198    public static Parameters parse(Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws Jingle.IqError {
199        string? dstaddr = transport.get_attribute("dstaddr");
200        string? mode = transport.get_attribute("mode");
201        string? sid = transport.get_attribute("sid");
202        if (mode != null && mode != "tcp") {
203            throw new Jingle.IqError.BAD_REQUEST(@"unknown transport method $(mode)");
204        }
205        if (dstaddr != null && dstaddr.length > 255) {
206            throw new Jingle.IqError.BAD_REQUEST("too long dstaddr");
207        }
208        Parameters result = new Parameters(Jingle.Role.RESPONDER, sid, local_full_jid, peer_full_jid, dstaddr);
209        foreach (StanzaNode candidate in transport.get_subnodes("candidate", NS_URI)) {
210            result.remote_candidates.add(Candidate.parse(candidate));
211        }
212        return result;
213    }
214    public string transport_ns_uri() {
215        return NS_URI;
216    }
217    public StanzaNode to_transport_stanza_node() {
218        StanzaNode transport = new StanzaNode.build("transport", NS_URI)
219            .add_self_xmlns()
220            .put_attribute("dstaddr", local_dstaddr);
221
222        if (role == Jingle.Role.INITIATOR) {
223            // Must not be included by the responder according to XEP-0260.
224            transport.put_attribute("mode", "tcp");
225        }
226
227        transport.put_attribute("sid", sid);
228        foreach (Candidate candidate in local_candidates) {
229            transport.put_node(candidate.to_xml());
230        }
231        return transport;
232    }
233    public void on_transport_accept(StanzaNode transport) throws Jingle.IqError {
234        Parameters other = Parameters.parse(local_full_jid, peer_full_jid, transport);
235        if (other.sid != sid) {
236            throw new Jingle.IqError.BAD_REQUEST("invalid sid");
237        }
238        remote_candidates = other.remote_candidates;
239        remote_dstaddr = other.remote_dstaddr;
240    }
241    public void on_transport_info(StanzaNode transport) throws Jingle.IqError {
242        StanzaNode? candidate_error = transport.get_subnode("candidate-error", NS_URI);
243        StanzaNode? candidate_used = transport.get_subnode("candidate-used", NS_URI);
244        StanzaNode? activated = transport.get_subnode("activated", NS_URI);
245        StanzaNode? proxy_error = transport.get_subnode("proxy-error", NS_URI);
246        int num_children = 0;
247        if (candidate_error != null) { num_children += 1; }
248        if (candidate_used != null) { num_children += 1; }
249        if (activated != null) { num_children += 1; }
250        if (proxy_error != null) { num_children += 1; }
251        if (num_children == 0) {
252            throw new Jingle.IqError.UNSUPPORTED_INFO("unknown transport-info");
253        } else if (num_children > 1) {
254            throw new Jingle.IqError.BAD_REQUEST("transport-info with more than one child");
255        }
256        if (candidate_error != null) {
257            handle_remote_candidate(null);
258        }
259        if (candidate_used != null) {
260            string? cid = candidate_used.get_attribute("cid");
261            if (cid == null) {
262                throw new Jingle.IqError.BAD_REQUEST("missing cid");
263            }
264            handle_remote_candidate(cid);
265        }
266        if (activated != null) {
267            string? cid = activated.get_attribute("cid");
268            if (cid == null) {
269                throw new Jingle.IqError.BAD_REQUEST("missing cid");
270            }
271            handle_activated(cid);
272        }
273        if (proxy_error != null) {
274            handle_proxy_error();
275        }
276    }
277    private void handle_remote_candidate(string? cid) throws Jingle.IqError {
278        if (remote_sent_selected_candidate) {
279            throw new Jingle.IqError.BAD_REQUEST("remote candidate already specified");
280        }
281        Candidate? candidate = null;
282        if (cid != null) {
283            foreach (Candidate c in local_candidates) {
284                if (c.cid == cid) {
285                    candidate = c;
286                    break;
287                }
288            }
289            if (candidate == null) {
290                throw new Jingle.IqError.BAD_REQUEST("unknown cid");
291            }
292        }
293        remote_sent_selected_candidate = true;
294        remote_selected_candidate = candidate;
295        debug("Remote selected candidate %s", candidate != null ? candidate.cid : "(null)");
296        try_completing_negotiation();
297    }
298    private void handle_activated(string cid) throws Jingle.IqError {
299        if (waiting_for_activation_cid == null || cid != waiting_for_activation_cid) {
300            throw new Jingle.IqError.BAD_REQUEST("unexpected proxy activation message");
301        }
302        Idle.add((owned)waiting_for_activation_callback);
303        waiting_for_activation_cid = null;
304    }
305    private void handle_proxy_error() throws Jingle.IqError {
306        if (waiting_for_activation_cid == null) {
307            throw new Jingle.IqError.BAD_REQUEST("unexpected proxy error message");
308        }
309        Idle.add((owned)waiting_for_activation_callback);
310        waiting_for_activation_cid = null;
311        waiting_for_activation_error = true;
312
313    }
314    private void try_completing_negotiation() {
315        if (!remote_sent_selected_candidate || !local_determined_selected_candidate) {
316            return;
317        }
318
319        Candidate? remote = remote_selected_candidate;
320        Candidate? local = local_selected_candidate;
321
322        int num_candidates = 0;
323        if (remote != null) { num_candidates += 1; }
324        if (local != null) { num_candidates += 1; }
325
326        if (num_candidates == 0) {
327            // Notify Jingle of the failed transport.
328            session.set_transport_connection(hack, null);
329            return;
330        }
331
332        bool remote_wins;
333        if (num_candidates == 1) {
334            remote_wins = remote != null;
335        } else {
336            if (local.priority < remote.priority) {
337                remote_wins = true;
338            } else if (local.priority > remote.priority) {
339                remote_wins = false;
340            } else {
341                // equal priority -> XEP-0260 says that the candidate offered
342                // by the initiator wins, so the one that the remote chose
343                remote_wins = role == Jingle.Role.INITIATOR;
344            }
345        }
346
347        if (!remote_wins) {
348            if (local_selected_candidate.type_ != CandidateType.PROXY) {
349                Jingle.Session? strong = session;
350                if (strong == null) {
351                    return;
352                }
353                strong.set_transport_connection(hack, local_selected_candidate_conn);
354            } else {
355                wait_for_remote_activation.begin(local_selected_candidate, local_selected_candidate_conn);
356            }
357        } else {
358            connect_to_local_candidate.begin(remote_selected_candidate);
359        }
360    }
361    public async void wait_for_remote_activation(Candidate candidate, SocketConnection conn) {
362        debug("Waiting for remote activation of %s", candidate.cid);
363        waiting_for_activation_cid = candidate.cid;
364        waiting_for_activation_callback = wait_for_remote_activation.callback;
365        yield;
366
367        Jingle.Session? strong = session;
368        if (strong == null) {
369            return;
370        }
371        if (!waiting_for_activation_error) {
372            strong.set_transport_connection(hack, conn);
373        } else {
374            strong.set_transport_connection(hack, null);
375        }
376    }
377    public async void connect_to_local_candidate(Candidate candidate) {
378        debug("Connecting to candidate %s", candidate.cid);
379        try {
380            SocketConnection conn = yield connect_to_socks5(candidate, local_dstaddr);
381
382            bool activation_error = false;
383            SourceFunc callback = connect_to_local_candidate.callback;
384            StanzaNode query = new StanzaNode.build("query", Socks5Bytestreams.NS_URI)
385                .add_self_xmlns()
386                .put_attribute("sid", sid)
387                .put_node(new StanzaNode.build("activate", Socks5Bytestreams.NS_URI)
388                    .put_node(new StanzaNode.text(peer_full_jid.to_string()))
389                );
390            Iq.Stanza iq = new Iq.Stanza.set(query) { to=candidate.jid };
391            hack.get_module(Iq.Module.IDENTITY).send_iq(hack, iq, (stream, iq) => {
392                activation_error = iq.is_error();
393                Idle.add((owned)callback);
394            });
395            yield;
396
397            if (activation_error) {
398                throw new IOError.PROXY_FAILED("activation iq error");
399            }
400
401            Jingle.Session? strong = session;
402            if (strong == null) {
403                return;
404            }
405            strong.send_transport_info(hack, new StanzaNode.build("transport", NS_URI)
406                .add_self_xmlns()
407                .put_attribute("sid", sid)
408                .put_node(new StanzaNode.build("activated", NS_URI)
409                    .put_attribute("cid", candidate.cid)
410                )
411            );
412
413            strong.set_transport_connection(hack, conn);
414        } catch (Error e) {
415            Jingle.Session? strong = session;
416            if (strong == null) {
417                return;
418            }
419            strong.send_transport_info(hack, new StanzaNode.build("transport", NS_URI)
420                .add_self_xmlns()
421                .put_attribute("sid", sid)
422                .put_node(new StanzaNode.build("proxy-error", NS_URI))
423            );
424            strong.set_transport_connection(hack, null);
425        }
426    }
427    public async SocketConnection connect_to_socks5(Candidate candidate, string dstaddr) throws Error {
428        SocketClient socket_client = new SocketClient() { timeout=3 };
429
430        string address = @"[$(candidate.host)]:$(candidate.port)";
431        debug("Connecting to SOCKS5 server at %s", address);
432
433        size_t written;
434        size_t read;
435        uint8[] read_buffer = new uint8[1024];
436        ByteArray write_buffer = new ByteArray();
437
438        SocketConnection conn = yield socket_client.connect_to_host_async(address, 0);
439
440        // 05 SOCKS version 5
441        // 01 number of authentication methods: 1
442        // 00 nop authentication
443        yield conn.output_stream.write_all_async({0x05, 0x01, 0x00}, GLib.Priority.DEFAULT, null, out written);
444
445        yield conn.input_stream.read_all_async(read_buffer[0:2], GLib.Priority.DEFAULT, null, out read);
446        // 05 SOCKS version 5
447        // 01 success
448        if (read_buffer[0] != 0x05 || read_buffer[1] != 0x00) {
449            throw new IOError.PROXY_FAILED("wanted 05 00, got %02x %02x".printf(read_buffer[0], read_buffer[1]));
450        }
451
452        // 05 SOCKS version 5
453        // 01 connect
454        // 00 reserved
455        // 03 address type: domain name
456        // ?? length of the domain
457        // .. domain
458        // 00 port 0 (upper half)
459        // 00 port 0 (lower half)
460        write_buffer.append({0x05, 0x01, 0x00, 0x03});
461        write_buffer.append({(uint8)dstaddr.length});
462        write_buffer.append(dstaddr.data);
463        write_buffer.append({0x00, 0x00});
464        yield conn.output_stream.write_all_async(write_buffer.data, GLib.Priority.DEFAULT, null, out written);
465
466        yield conn.input_stream.read_all_async(read_buffer[0:write_buffer.len], GLib.Priority.DEFAULT, null, out read);
467        // 05 SOCKS version 5
468        // 00 success
469        // 00 reserved
470        // 03 address type: domain name
471        // ?? length of the domain
472        // .. domain
473        // 00 port 0 (upper half)
474        // 00 port 0 (lower half)
475        if (read_buffer[0] != 0x05 || read_buffer[1] != 0x00 || read_buffer[3] != 0x03) {
476            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]));
477        }
478        if (read_buffer[4] != (uint8)dstaddr.length) {
479            throw new IOError.PROXY_FAILED("wanted %02x for length, got %02x".printf(dstaddr.length, read_buffer[4]));
480        }
481        if (!bytes_equal(read_buffer[5:5+dstaddr.length], dstaddr.data)) {
482            string repr = ((string)read_buffer[5:5+dstaddr.length]).escape(); // TODO call make_valid() once glib>=2.52 becomes widespread
483            throw new IOError.PROXY_FAILED(@"wanted dstaddr $(dstaddr), got $(repr)");
484        }
485        if (read_buffer[5+dstaddr.length] != 0x00 || read_buffer[5+dstaddr.length+1] != 0x00) {
486            throw new IOError.PROXY_FAILED("wanted port 00 00, got %02x %02x".printf(read_buffer[5+dstaddr.length], read_buffer[5+dstaddr.length+1]));
487        }
488
489        conn.get_socket().set_timeout(0);
490
491        return conn;
492    }
493    public async void try_connecting_to_candidates(XmppStream stream, Jingle.Session session) throws Error {
494        remote_candidates.sort((c1, c2) => {
495            // sort from priorities from high to low
496            if (c1.priority < c2.priority) { return 1; }
497            if (c1.priority > c2.priority) { return -1; }
498            return 0;
499        });
500        foreach (Candidate candidate in remote_candidates) {
501            if (remote_selected_candidate != null && remote_selected_candidate.priority > candidate.priority) {
502                // Don't try candidates with lower priority than the one the
503                // peer already selected.
504                break;
505            }
506            try {
507                SocketConnection conn = yield connect_to_socks5(candidate, remote_dstaddr);
508
509                local_determined_selected_candidate = true;
510                local_selected_candidate = candidate;
511                local_selected_candidate_conn = conn;
512                debug("Selected candidate %s", candidate.cid);
513                session.send_transport_info(stream, new StanzaNode.build("transport", NS_URI)
514                    .add_self_xmlns()
515                    .put_attribute("sid", sid)
516                    .put_node(new StanzaNode.build("candidate-used", NS_URI)
517                        .put_attribute("cid", candidate.cid)
518                    )
519                );
520                try_completing_negotiation();
521                return;
522            } catch (Error e) {
523                // An error in the connection establishment isn't fatal, just
524                // try the next candidate or respond that none of the
525                // candidates work.
526            }
527        }
528        local_determined_selected_candidate = true;
529        local_selected_candidate = null;
530        session.send_transport_info(stream, new StanzaNode.build("transport", NS_URI)
531            .add_self_xmlns()
532            .put_attribute("sid", sid)
533            .put_node(new StanzaNode.build("candidate-error", NS_URI))
534        );
535        // Try remote candidates
536        try_completing_negotiation();
537    }
538    public void create_transport_connection(XmppStream stream, Jingle.Session session) {
539        this.session = session;
540        this.hack = stream;
541        try_connecting_to_candidates.begin(stream, session);
542    }
543}
544
545}
546