Mercurial > prosody-modules
comparison mod_http_connect/mod_http_connect.lua @ 6269:706867e05809
mod_http_connect: Add authentication, target filtering and advertisement via XEP-0215
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Thu, 26 Jun 2025 13:10:10 +0100 |
| parents | 5bd4cbe2bfee |
| children |
comparison
equal
deleted
inserted
replaced
| 6268:5bd4cbe2bfee | 6269:706867e05809 |
|---|---|
| 1 module:set_global(); | 1 -- This feature was added after Prosody 13.0 |
| 2 --% requires: net-connect-filter | |
| 2 | 3 |
| 4 local hashes = require "prosody.util.hashes"; | |
| 3 local server = require "prosody.net.server"; | 5 local server = require "prosody.net.server"; |
| 4 local async = require "prosody.util.async"; | |
| 5 local connect = require"prosody.net.connect".connect; | 6 local connect = require"prosody.net.connect".connect; |
| 6 local basic = require "prosody.net.resolvers.basic"; | 7 local basic = require "prosody.net.resolvers.basic"; |
| 8 local new_ip = require "prosody.util.ip".new_ip; | |
| 9 | |
| 10 local b64_decode = require "prosody.util.encodings".base64.decode; | |
| 11 | |
| 12 local proxy_secret = module:get_option_string("http_proxy_secret", require "prosody.util.id".long()); | |
| 13 | |
| 14 local allow_private_ips = module:get_option_boolean("http_proxy_to_private_ips", false); | |
| 15 local allow_all_ports = module:get_option_boolean("http_proxy_to_all_ports", false); | |
| 16 | |
| 17 local allowed_target_ports = module:get_option_set("http_proxy_to_ports", { "443", "5281", "5443", "7443" }) / tonumber; | |
| 7 | 18 |
| 8 local sessions = {}; | 19 local sessions = {}; |
| 9 | 20 |
| 10 local listeners = {}; | 21 local listeners = {}; |
| 11 | 22 |
| 31 response.status_code = 500; | 42 response.status_code = 500; |
| 32 response:send(err); | 43 response:send(err); |
| 33 end | 44 end |
| 34 end | 45 end |
| 35 | 46 |
| 36 function listeners.ondisconnect(conn, err) | 47 function listeners.ondisconnect(conn, err) --luacheck: ignore 212/conn 212/err |
| 48 end | |
| 49 | |
| 50 local function is_permitted_target(conn_type, ip, port) | |
| 51 if not (allow_all_ports or allowed_target_ports:contains(tonumber(port))) then | |
| 52 module:log("warn", "Forbidding tunnel to %s:%d (forbidden port)", ip, port); | |
| 53 return false; | |
| 54 end | |
| 55 if not allow_private_ips then | |
| 56 local family = (conn_type:byte(-1, -1) == 54) and "IPv6" or "IPv4"; | |
| 57 local parsed_ip = new_ip(ip, family); | |
| 58 if parsed_ip.private then | |
| 59 module:log("warn", "Forbidding tunnel to %s:%d (forbidden ip)", ip, port); | |
| 60 return false; | |
| 61 end | |
| 62 end | |
| 63 return true; | |
| 64 end | |
| 65 | |
| 66 local function verify_auth(user, password) | |
| 67 local expiry = tonumber(user, 10); | |
| 68 if os.time() > expiry then | |
| 69 module:log("warn", "Attempt to use expired credentials"); | |
| 70 return nil; | |
| 71 end | |
| 72 local expected_password = hashes.hmac_sha1(proxy_secret, user); | |
| 73 if hashes.equals(b64_decode(password), expected_password) then | |
| 74 return true; | |
| 75 end | |
| 76 module:log("warn", "Credential mismatch for %s: expected '%q' got '%q'", user, expected_password, password); | |
| 37 end | 77 end |
| 38 | 78 |
| 39 module:depends("http"); | 79 module:depends("http"); |
| 40 module:provides("http", { | 80 module:provides("http", { |
| 41 default_path = "/"; | 81 default_path = "/"; |
| 42 route = { | 82 route = { |
| 43 ["CONNECT /*"] = function(event) | 83 ["CONNECT /*"] = function(event) |
| 44 local request = event.request; | 84 local request, response = event.request, event.response; |
| 45 local host, port = request.url.scheme, request.url.path; | 85 local host, port = request.url.scheme, request.url.path; |
| 46 if port == "" then return 400 end | 86 if port == "" then return 400 end |
| 47 -- TODO proxy-auth here, presumably same as stun/turn? | 87 |
| 48 local resolve = basic.new(host, port); | 88 -- Auth check |
| 89 local realm = host; | |
| 90 local headers = request.headers; | |
| 91 if not headers.proxy_authorization then | |
| 92 response.headers.proxy_authenticate = ("Basic realm=%q"):format(realm); | |
| 93 return 407 | |
| 94 end | |
| 95 local user, password = b64_decode(headers.proxy_authorization:match"[^ ]*$"):match"([^:]*):(.*)"; | |
| 96 if not verify_auth(user, password) then | |
| 97 response.headers.proxy_authenticate = ("Basic realm=%q"):format(realm); | |
| 98 return 407 | |
| 99 end | |
| 100 | |
| 101 local resolve = basic.new(host, port, "tcp", { | |
| 102 filter = is_permitted_target; | |
| 103 }); | |
| 49 connect(resolve, listeners, nil, event) | 104 connect(resolve, listeners, nil, event) |
| 50 return true; | 105 return true; |
| 51 end; | 106 end; |
| 52 } | 107 } |
| 53 }); | 108 }); |
| 109 | |
| 110 local http_url = module:http_url(); | |
| 111 local parsed_url = require "socket.url".parse(http_url); | |
| 112 | |
| 113 local proxy_host = parsed_url.host; | |
| 114 local proxy_port = tonumber(parsed_url.port); | |
| 115 | |
| 116 if not proxy_port then | |
| 117 if parsed_url.scheme == "https" then | |
| 118 proxy_port = 443; | |
| 119 elseif parsed_url.scheme == "http" then | |
| 120 proxy_port = 80; | |
| 121 end | |
| 122 end | |
| 123 | |
| 124 module:depends "external_services"; | |
| 125 | |
| 126 module:add_item("external_service", { | |
| 127 type = "http"; | |
| 128 transport = "tcp"; | |
| 129 host = proxy_host; | |
| 130 port = proxy_port; | |
| 131 | |
| 132 secret = proxy_secret; | |
| 133 algorithm = "turn"; | |
| 134 ttl = 3600; | |
| 135 }); |
