Software / code / prosody-modules
Comparison
mod_http_connect/mod_http_connect.lua @ 6344:eb834f754f57 draft default tip
Merge update
| author | Trần H. Trung <xmpp:trần.h.trung@trung.fun> |
|---|---|
| date | Fri, 18 Jul 2025 20:45:38 +0700 |
| parent | 6314:706867e05809 |
comparison
equal
deleted
inserted
replaced
| 6309:342f88e8d522 | 6344:eb834f754f57 |
|---|---|
| 1 -- This feature was added after Prosody 13.0 | |
| 2 --% requires: net-connect-filter | |
| 3 | |
| 4 local hashes = require "prosody.util.hashes"; | |
| 5 local server = require "prosody.net.server"; | |
| 6 local connect = require"prosody.net.connect".connect; | |
| 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; | |
| 18 | |
| 19 local sessions = {}; | |
| 20 | |
| 21 local listeners = {}; | |
| 22 | |
| 23 function listeners.onconnect(conn) | |
| 24 local event = sessions[conn]; | |
| 25 local response = event.response; | |
| 26 response.status_code = 200; | |
| 27 response:send(""); | |
| 28 response.conn:onwritable(); | |
| 29 response.conn:setlistener(listeners, event); | |
| 30 server.link(conn, response.conn); | |
| 31 server.link(response.conn, conn); | |
| 32 response.conn = nil; | |
| 33 end | |
| 34 | |
| 35 function listeners.onattach(conn, event) | |
| 36 sessions[conn] = event; | |
| 37 end | |
| 38 | |
| 39 function listeners.onfail(event, err) | |
| 40 local response = event.response; | |
| 41 if assert(response) then | |
| 42 response.status_code = 500; | |
| 43 response:send(err); | |
| 44 end | |
| 45 end | |
| 46 | |
| 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); | |
| 77 end | |
| 78 | |
| 79 module:depends("http"); | |
| 80 module:provides("http", { | |
| 81 default_path = "/"; | |
| 82 route = { | |
| 83 ["CONNECT /*"] = function(event) | |
| 84 local request, response = event.request, event.response; | |
| 85 local host, port = request.url.scheme, request.url.path; | |
| 86 if port == "" then return 400 end | |
| 87 | |
| 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 }); | |
| 104 connect(resolve, listeners, nil, event) | |
| 105 return true; | |
| 106 end; | |
| 107 } | |
| 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 }); |