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