| 1 | local verse = require "verse"; |
| 2 | |
| 3 | local xmlns_s5b = "urn:xmpp:jingle:transports:s5b:1"; |
| 4 | local xmlns_bytestreams = "http://jabber.org/protocol/bytestreams"; |
| 5 | local sha1 = require "prosody.util.hashes".sha1; |
| 6 | local new_id = require "prosody.util.id".short; |
| 7 | |
| 8 | local function negotiate_socks5(conn, hash) |
| 9 | local function suppress_connected() |
| 10 | conn:unhook("connected", suppress_connected); |
| 11 | return true; |
| 12 | end |
| 13 | local function receive_connection_response(data) |
| 14 | conn:unhook("incoming-raw", receive_connection_response); |
| 15 | |
| 16 | if data:sub(1, 2) ~= "\005\000" then |
| 17 | return conn:event("error", "connection-failure"); |
| 18 | end |
| 19 | conn:event("connected"); |
| 20 | return true; |
| 21 | end |
| 22 | local function receive_auth_response(data) |
| 23 | conn:unhook("incoming-raw", receive_auth_response); |
| 24 | if data ~= "\005\000" then -- SOCKSv5; "NO AUTHENTICATION" |
| 25 | -- Server is not SOCKSv5, or does not allow no auth |
| 26 | local err = "version-mismatch"; |
| 27 | if data:sub(1,1) == "\005" then |
| 28 | err = "authentication-failure"; |
| 29 | end |
| 30 | return conn:event("error", err); |
| 31 | end |
| 32 | -- Request SOCKS5 connection |
| 33 | conn:send(string.char(0x05, 0x01, 0x00, 0x03, #hash)..hash.."\0\0"); --FIXME: Move to "connected"? |
| 34 | conn:hook("incoming-raw", receive_connection_response, 100); |
| 35 | return true; |
| 36 | end |
| 37 | conn:hook("connected", suppress_connected, 200); |
| 38 | conn:hook("incoming-raw", receive_auth_response, 100); |
| 39 | conn:send("\005\001\000"); -- SOCKSv5; 1 mechanism; "NO AUTHENTICATION" |
| 40 | end |
| 41 | |
| 42 | local function connect_to_usable_streamhost(callback, streamhosts, auth_token) |
| 43 | local conn = verse.new(nil, { |
| 44 | streamhosts = streamhosts, |
| 45 | current_host = 0; |
| 46 | }); |
| 47 | --Attempt to connect to the next host |
| 48 | local function attempt_next_streamhost(event) |
| 49 | if event then |
| 50 | return callback(nil, event.reason); |
| 51 | end |
| 52 | -- First connect, or the last connect failed |
| 53 | if conn.current_host < #conn.streamhosts then |
| 54 | conn.current_host = conn.current_host + 1; |
| 55 | conn:debug("Attempting to connect to "..conn.streamhosts[conn.current_host].host..":"..conn.streamhosts[conn.current_host].port.."..."); |
| 56 | local ok, err = conn:connect( |
| 57 | conn.streamhosts[conn.current_host].host, |
| 58 | conn.streamhosts[conn.current_host].port |
| 59 | ); |
| 60 | if not ok then |
| 61 | conn:debug("Error connecting to proxy (%s:%s): %s", |
| 62 | conn.streamhosts[conn.current_host].host, |
| 63 | conn.streamhosts[conn.current_host].port, |
| 64 | err |
| 65 | ); |
| 66 | else |
| 67 | conn:debug("Connecting..."); |
| 68 | end |
| 69 | negotiate_socks5(conn, auth_token); |
| 70 | return true; -- Halt processing of disconnected event |
| 71 | end |
| 72 | -- All streamhosts tried, none successful |
| 73 | conn:unhook("disconnected", attempt_next_streamhost); |
| 74 | return callback(nil); |
| 75 | -- Let disconnected event fall through to user handlers... |
| 76 | end |
| 77 | conn:hook("disconnected", attempt_next_streamhost, 100); |
| 78 | -- When this event fires, we're connected to a streamhost |
| 79 | conn:hook("connected", function () |
| 80 | conn:unhook("disconnected", attempt_next_streamhost); |
| 81 | callback(conn.streamhosts[conn.current_host], conn); |
| 82 | end, 100); |
| 83 | attempt_next_streamhost(); -- Set it in motion |
| 84 | return conn; |
| 85 | end |
| 86 | |
| 87 | function verse.plugins.jingle_s5b(stream) |
| 88 | stream:hook("ready", function () |
| 89 | stream:add_disco_feature(xmlns_s5b); |
| 90 | end, 10); |
| 91 | |
| 92 | local s5b = {}; |
| 93 | |
| 94 | function s5b:generate_initiate() |
| 95 | self.s5b_sid = new_id(); |
| 96 | local transport = verse.stanza("transport", { xmlns = xmlns_s5b, |
| 97 | mode = "tcp", sid = self.s5b_sid }); |
| 98 | local p = 0; |
| 99 | for jid, streamhost in pairs(stream.proxy65.available_streamhosts) do |
| 100 | p = p + 1; |
| 101 | transport:tag("candidate", { jid = jid, host = streamhost.host, |
| 102 | port = streamhost.port, cid=jid, priority = p, type = "proxy" }):up(); |
| 103 | end |
| 104 | stream:debug("Have %d proxies", p) |
| 105 | return transport; |
| 106 | end |
| 107 | |
| 108 | function s5b:generate_accept(initiate_transport) |
| 109 | local candidates = {}; |
| 110 | self.s5b_peer_candidates = candidates; |
| 111 | self.s5b_mode = initiate_transport.attr.mode or "tcp"; |
| 112 | self.s5b_sid = initiate_transport.attr.sid or self.jingle.sid; |
| 113 | |
| 114 | -- Import the list of candidates the initiator offered us |
| 115 | for candidate in initiate_transport:childtags() do |
| 116 | --if candidate.attr.jid == "asterix4@jabber.lagaule.org/Gajim" |
| 117 | --and candidate.attr.host == "82.246.25.239" then |
| 118 | candidates[candidate.attr.cid] = { |
| 119 | type = candidate.attr.type; |
| 120 | jid = candidate.attr.jid; |
| 121 | host = candidate.attr.host; |
| 122 | port = tonumber(candidate.attr.port) or 0; |
| 123 | priority = tonumber(candidate.attr.priority) or 0; |
| 124 | cid = candidate.attr.cid; |
| 125 | }; |
| 126 | --end |
| 127 | end |
| 128 | |
| 129 | -- Import our own candidates |
| 130 | -- TODO ^ |
| 131 | local transport = verse.stanza("transport", { xmlns = xmlns_s5b }); |
| 132 | return transport; |
| 133 | end |
| 134 | |
| 135 | function s5b:connect(callback) |
| 136 | stream:warn("Connecting!"); |
| 137 | |
| 138 | local streamhost_array = {}; |
| 139 | for cid, streamhost in pairs(self.s5b_peer_candidates or {}) do |
| 140 | streamhost_array[#streamhost_array+1] = streamhost; |
| 141 | end |
| 142 | |
| 143 | if #streamhost_array > 0 then |
| 144 | self.connecting_peer_candidates = true; |
| 145 | local function onconnect(streamhost, conn) |
| 146 | self.jingle:send_command("transport-info", verse.stanza("content", { creator = self.creator, name = self.name }) |
| 147 | :tag("transport", { xmlns = xmlns_s5b, sid = self.s5b_sid }) |
| 148 | :tag("candidate-used", { cid = streamhost.cid })); |
| 149 | self.onconnect_callback = callback; |
| 150 | self.conn = conn; |
| 151 | end |
| 152 | local auth_token = sha1(self.s5b_sid..self.peer..stream.jid, true); |
| 153 | connect_to_usable_streamhost(onconnect, streamhost_array, auth_token); |
| 154 | else |
| 155 | stream:warn("Actually, I'm going to wait for my peer to tell me its streamhost..."); |
| 156 | self.onconnect_callback = callback; |
| 157 | end |
| 158 | end |
| 159 | |
| 160 | function s5b:info_received(jingle_tag) |
| 161 | stream:warn("Info received"); |
| 162 | local content_tag = jingle_tag:child_with_name("content"); |
| 163 | local transport_tag = content_tag:child_with_name("transport"); |
| 164 | if transport_tag:get_child("candidate-used") and not self.connecting_peer_candidates then |
| 165 | local candidate_used = transport_tag:child_with_name("candidate-used"); |
| 166 | if candidate_used then |
| 167 | -- Connect straight away to candidate used, we weren't trying any anyway |
| 168 | local function onconnect(streamhost, conn) |
| 169 | if self.jingle.role == "initiator" then -- More correct would be - "is this a candidate we offered?" |
| 170 | -- Activate the stream |
| 171 | self.jingle.stream:send_iq(verse.iq({ to = streamhost.jid, type = "set" }) |
| 172 | :tag("query", { xmlns = xmlns_bytestreams, sid = self.s5b_sid }) |
| 173 | :tag("activate"):text(self.jingle.peer), function (result) |
| 174 | |
| 175 | if result.attr.type == "result" then |
| 176 | self.jingle:send_command("transport-info", verse.stanza("content", content_tag.attr) |
| 177 | :tag("transport", { xmlns = xmlns_s5b, sid = self.s5b_sid }) |
| 178 | :tag("activated", { cid = candidate_used.attr.cid })); |
| 179 | self.conn = conn; |
| 180 | self.onconnect_callback(conn); |
| 181 | else |
| 182 | self.jingle.stream:error("Failed to activate bytestream"); |
| 183 | end |
| 184 | end); |
| 185 | end |
| 186 | end |
| 187 | |
| 188 | -- FIXME: Another assumption that cid==jid, and that it was our candidate |
| 189 | self.jingle.stream:debug("CID: %s", self.jingle.stream.proxy65.available_streamhosts[candidate_used.attr.cid]); |
| 190 | local streamhost_array = { |
| 191 | self.jingle.stream.proxy65.available_streamhosts[candidate_used.attr.cid]; |
| 192 | }; |
| 193 | |
| 194 | local auth_token = sha1(self.s5b_sid..stream.jid..self.peer, true); |
| 195 | connect_to_usable_streamhost(onconnect, streamhost_array, auth_token); |
| 196 | end |
| 197 | elseif transport_tag:get_child("activated") then |
| 198 | self.onconnect_callback(self.conn); |
| 199 | end |
| 200 | end |
| 201 | |
| 202 | function s5b:disconnect() |
| 203 | if self.conn then |
| 204 | self.conn:close(); |
| 205 | end |
| 206 | end |
| 207 | |
| 208 | function s5b:handle_accepted(jingle_tag) |
| 209 | end |
| 210 | |
| 211 | local s5b_mt = { __index = s5b }; |
| 212 | stream:hook("jingle/transport/"..xmlns_s5b, function (jingle) |
| 213 | return setmetatable({ |
| 214 | role = jingle.role, |
| 215 | peer = jingle.peer, |
| 216 | stream = jingle.stream, |
| 217 | jingle = jingle, |
| 218 | }, s5b_mt); |
| 219 | end); |
| 220 | end |