| 1 | local verse = require "verse"; |
| 2 | local new_id = require "prosody.util.id".short; |
| 3 | local sha1 = require "prosody.util.hashes".sha1; |
| 4 | |
| 5 | local proxy65_mt = {}; |
| 6 | proxy65_mt.__index = proxy65_mt; |
| 7 | |
| 8 | local xmlns_bytestreams = "http://jabber.org/protocol/bytestreams"; |
| 9 | |
| 10 | local negotiate_socks5; |
| 11 | |
| 12 | function verse.plugins.proxy65(stream) |
| 13 | stream.proxy65 = setmetatable({ stream = stream }, proxy65_mt); |
| 14 | stream.proxy65.available_streamhosts = {}; |
| 15 | local outstanding_proxies = 0; |
| 16 | stream:hook("disco/service-discovered/proxy", function (service) |
| 17 | -- Fill list with available proxies |
| 18 | if service.type == "bytestreams" then |
| 19 | outstanding_proxies = outstanding_proxies + 1; |
| 20 | stream:send_iq(verse.iq({ to = service.jid, type = "get" }) |
| 21 | :tag("query", { xmlns = xmlns_bytestreams }), function (result) |
| 22 | |
| 23 | outstanding_proxies = outstanding_proxies - 1; |
| 24 | if result.attr.type == "result" then |
| 25 | local streamhost = result:get_child("query", xmlns_bytestreams) |
| 26 | :get_child("streamhost").attr; |
| 27 | |
| 28 | stream.proxy65.available_streamhosts[streamhost.jid] = { |
| 29 | jid = streamhost.jid; |
| 30 | host = streamhost.host; |
| 31 | port = tonumber(streamhost.port); |
| 32 | }; |
| 33 | end |
| 34 | if outstanding_proxies == 0 then |
| 35 | stream:event("proxy65/discovered-proxies", stream.proxy65.available_streamhosts); |
| 36 | end |
| 37 | end); |
| 38 | end |
| 39 | end); |
| 40 | stream:hook("iq/"..xmlns_bytestreams, function (request) |
| 41 | local conn = verse.new(nil, { |
| 42 | initiator_jid = request.attr.from, |
| 43 | streamhosts = {}, |
| 44 | current_host = 0; |
| 45 | }); |
| 46 | |
| 47 | -- Parse hosts from request |
| 48 | for tag in request.tags[1]:childtags() do |
| 49 | if tag.name == "streamhost" then |
| 50 | table.insert(conn.streamhosts, tag.attr); |
| 51 | end |
| 52 | end |
| 53 | |
| 54 | --Attempt to connect to the next host |
| 55 | local function attempt_next_streamhost() |
| 56 | -- First connect, or the last connect failed |
| 57 | if conn.current_host < #conn.streamhosts then |
| 58 | conn.current_host = conn.current_host + 1; |
| 59 | conn:connect( |
| 60 | conn.streamhosts[conn.current_host].host, |
| 61 | conn.streamhosts[conn.current_host].port |
| 62 | ); |
| 63 | negotiate_socks5(stream, conn, request.tags[1].attr.sid, request.attr.from, stream.jid); |
| 64 | return true; -- Halt processing of disconnected event |
| 65 | end |
| 66 | -- All streamhosts tried, none successful |
| 67 | conn:unhook("disconnected", attempt_next_streamhost); |
| 68 | stream:send(verse.error_reply(request, "cancel", "item-not-found")); |
| 69 | -- Let disconnected event fall through to user handlers... |
| 70 | end |
| 71 | |
| 72 | function conn:accept() |
| 73 | conn:hook("disconnected", attempt_next_streamhost, 100); |
| 74 | -- When this event fires, we're connected to a streamhost |
| 75 | conn:hook("connected", function () |
| 76 | conn:unhook("disconnected", attempt_next_streamhost); |
| 77 | -- Send XMPP success notification |
| 78 | local reply = verse.reply(request) |
| 79 | :tag("query", request.tags[1].attr) |
| 80 | :tag("streamhost-used", { jid = conn.streamhosts[conn.current_host].jid }); |
| 81 | stream:send(reply); |
| 82 | end, 100); |
| 83 | attempt_next_streamhost(); |
| 84 | end |
| 85 | function conn:refuse() |
| 86 | -- FIXME: XMPP refused reply |
| 87 | end |
| 88 | stream:event("proxy65/request", conn); |
| 89 | end); |
| 90 | end |
| 91 | |
| 92 | function proxy65_mt:new(target_jid, proxies) |
| 93 | local conn = verse.new(nil, { |
| 94 | target_jid = target_jid; |
| 95 | bytestream_sid = new_id(); |
| 96 | }); |
| 97 | |
| 98 | local request = verse.iq{type="set", to = target_jid} |
| 99 | :tag("query", { xmlns = xmlns_bytestreams, mode = "tcp", sid = conn.bytestream_sid }); |
| 100 | for _, proxy in ipairs(proxies or self.proxies) do |
| 101 | request:tag("streamhost", proxy):up(); |
| 102 | end |
| 103 | |
| 104 | |
| 105 | self.stream:send_iq(request, function (reply) |
| 106 | if reply.attr.type == "error" then |
| 107 | local type, condition, text = reply:get_error(); |
| 108 | conn:event("connection-failed", { conn = conn, type = type, condition = condition, text = text }); |
| 109 | else |
| 110 | -- Target connected to streamhost, connect ourselves |
| 111 | local streamhost_used = reply.tags[1]:get_child("streamhost-used"); |
| 112 | -- if not streamhost_used then |
| 113 | --FIXME: Emit error |
| 114 | -- end |
| 115 | conn.streamhost_jid = streamhost_used.attr.jid; |
| 116 | local host, port; |
| 117 | for _, proxy in ipairs(proxies or self.proxies) do |
| 118 | if proxy.jid == conn.streamhost_jid then |
| 119 | host, port = proxy.host, proxy.port; |
| 120 | break; |
| 121 | end |
| 122 | end |
| 123 | -- if not (host and port) then |
| 124 | --FIXME: Emit error |
| 125 | -- end |
| 126 | |
| 127 | conn:connect(host, port); |
| 128 | |
| 129 | local function handle_proxy_connected() |
| 130 | conn:unhook("connected", handle_proxy_connected); |
| 131 | -- Both of us connected, tell proxy to activate connection |
| 132 | local activate_request = verse.iq{to = conn.streamhost_jid, type="set"} |
| 133 | :tag("query", { xmlns = xmlns_bytestreams, sid = conn.bytestream_sid }) |
| 134 | :tag("activate"):text(target_jid); |
| 135 | self.stream:send_iq(activate_request, function (activated) |
| 136 | if activated.attr.type == "result" then |
| 137 | -- Connection activated, ready to use |
| 138 | conn:event("connected", conn); |
| 139 | -- else --FIXME: Emit error |
| 140 | end |
| 141 | end); |
| 142 | return true; |
| 143 | end |
| 144 | conn:hook("connected", handle_proxy_connected, 100); |
| 145 | |
| 146 | negotiate_socks5(self.stream, conn, conn.bytestream_sid, self.stream.jid, target_jid); |
| 147 | end |
| 148 | end); |
| 149 | return conn; |
| 150 | end |
| 151 | |
| 152 | function negotiate_socks5(stream, conn, sid, requester_jid, target_jid) |
| 153 | local hash = sha1(sid..requester_jid..target_jid); |
| 154 | local function suppress_connected() |
| 155 | conn:unhook("connected", suppress_connected); |
| 156 | return true; |
| 157 | end |
| 158 | local function receive_connection_response(data) |
| 159 | conn:unhook("incoming-raw", receive_connection_response); |
| 160 | |
| 161 | if data:sub(1, 2) ~= "\005\000" then |
| 162 | return conn:event("error", "connection-failure"); |
| 163 | end |
| 164 | conn:event("connected"); |
| 165 | return true; |
| 166 | end |
| 167 | local function receive_auth_response(data) |
| 168 | conn:unhook("incoming-raw", receive_auth_response); |
| 169 | if data ~= "\005\000" then -- SOCKSv5; "NO AUTHENTICATION" |
| 170 | -- Server is not SOCKSv5, or does not allow no auth |
| 171 | local err = "version-mismatch"; |
| 172 | if data:sub(1,1) == "\005" then |
| 173 | err = "authentication-failure"; |
| 174 | end |
| 175 | return conn:event("error", err); |
| 176 | end |
| 177 | -- Request SOCKS5 connection |
| 178 | conn:send(string.char(0x05, 0x01, 0x00, 0x03, #hash)..hash.."\0\0"); --FIXME: Move to "connected"? |
| 179 | conn:hook("incoming-raw", receive_connection_response, 100); |
| 180 | return true; |
| 181 | end |
| 182 | conn:hook("connected", suppress_connected, 200); |
| 183 | conn:hook("incoming-raw", receive_auth_response, 100); |
| 184 | conn:send("\005\001\000"); -- SOCKSv5; 1 mechanism; "NO AUTHENTICATION" |
| 185 | end |