Software /
code /
prosody
Comparison
plugins/mod_http.lua @ 11200:bf8f2da84007
Merge 0.11->trunk
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Thu, 05 Nov 2020 22:31:25 +0100 |
parent | 11066:dc41c8dfd2b1 |
child | 11382:a0477656258c |
comparison
equal
deleted
inserted
replaced
11199:6c7c50a4de32 | 11200:bf8f2da84007 |
---|---|
5 -- This project is MIT/X11 licensed. Please see the | 5 -- This project is MIT/X11 licensed. Please see the |
6 -- COPYING file in the source package for more information. | 6 -- COPYING file in the source package for more information. |
7 -- | 7 -- |
8 | 8 |
9 module:set_global(); | 9 module:set_global(); |
10 module:depends("http_errors"); | 10 pcall(function () |
11 module:depends("http_errors"); | |
12 end); | |
11 | 13 |
12 local portmanager = require "core.portmanager"; | 14 local portmanager = require "core.portmanager"; |
13 local moduleapi = require "core.moduleapi"; | 15 local moduleapi = require "core.moduleapi"; |
14 local url_parse = require "socket.url".parse; | 16 local url_parse = require "socket.url".parse; |
15 local url_build = require "socket.url".build; | 17 local url_build = require "socket.url".build; |
16 local normalize_path = require "util.http".normalize_path; | 18 local normalize_path = require "util.http".normalize_path; |
19 local set = require "util.set"; | |
20 | |
21 local ip_util = require "util.ip"; | |
22 local new_ip = ip_util.new_ip; | |
23 local match_ip = ip_util.match; | |
24 local parse_cidr = ip_util.parse_cidr; | |
17 | 25 |
18 local server = require "net.http.server"; | 26 local server = require "net.http.server"; |
19 | 27 |
20 server.set_default_host(module:get_option_string("http_default_host")); | 28 server.set_default_host(module:get_option_string("http_default_host")); |
21 | 29 |
22 server.set_option("body_size_limit", module:get_option_number("http_max_content_size")); | 30 server.set_option("body_size_limit", module:get_option_number("http_max_content_size")); |
23 server.set_option("buffer_size_limit", module:get_option_number("http_max_buffer_size")); | 31 server.set_option("buffer_size_limit", module:get_option_number("http_max_buffer_size")); |
32 | |
33 -- CORS settigs | |
34 local opt_methods = module:get_option_set("access_control_allow_methods", { "GET", "OPTIONS" }); | |
35 local opt_headers = module:get_option_set("access_control_allow_headers", { "Content-Type" }); | |
36 local opt_credentials = module:get_option_boolean("access_control_allow_credentials", false); | |
37 local opt_max_age = module:get_option_number("access_control_max_age", 2 * 60 * 60); | |
24 | 38 |
25 local function get_http_event(host, app_path, key) | 39 local function get_http_event(host, app_path, key) |
26 local method, path = key:match("^(%S+)%s+(.+)$"); | 40 local method, path = key:match("^(%S+)%s+(.+)$"); |
27 if not method then -- No path specified, default to "" (base path) | 41 if not method then -- No path specified, default to "" (base path) |
28 method, path = key, ""; | 42 method, path = key, ""; |
77 } | 91 } |
78 if ports_by_scheme[url.scheme] == url.port then url.port = nil end | 92 if ports_by_scheme[url.scheme] == url.port then url.port = nil end |
79 return url_build(url); | 93 return url_build(url); |
80 end | 94 end |
81 end | 95 end |
82 module:log("warn", "No http ports enabled, can't generate an external URL"); | 96 if prosody.process_type == "prosody" then |
97 module:log("warn", "No http ports enabled, can't generate an external URL"); | |
98 end | |
83 return "http://disabled.invalid/"; | 99 return "http://disabled.invalid/"; |
100 end | |
101 | |
102 local function apply_cors_headers(response, methods, headers, max_age, allow_credentials, origin) | |
103 response.headers.access_control_allow_methods = tostring(methods); | |
104 response.headers.access_control_allow_headers = tostring(headers); | |
105 response.headers.access_control_max_age = tostring(max_age) | |
106 response.headers.access_control_allow_origin = origin or "*"; | |
107 if allow_credentials then | |
108 response.headers.access_control_allow_credentials = "true"; | |
109 end | |
84 end | 110 end |
85 | 111 |
86 function module.add_host(module) | 112 function module.add_host(module) |
87 local host = module.host; | 113 local host = module.host; |
88 if host ~= "*" then | 114 if host ~= "*" then |
99 module:log("error", "HTTP app has no 'name', add one or use module:provides('http', app)"); | 125 module:log("error", "HTTP app has no 'name', add one or use module:provides('http', app)"); |
100 return; | 126 return; |
101 end | 127 end |
102 apps[app_name] = apps[app_name] or {}; | 128 apps[app_name] = apps[app_name] or {}; |
103 local app_handlers = apps[app_name]; | 129 local app_handlers = apps[app_name]; |
130 | |
131 local app_methods = opt_methods; | |
132 | |
133 local function cors_handler(event_data) | |
134 local request, response = event_data.request, event_data.response; | |
135 apply_cors_headers(response, app_methods, opt_headers, opt_max_age, opt_credentials, request.headers.origin); | |
136 end | |
137 | |
138 local function options_handler(event_data) | |
139 cors_handler(event_data); | |
140 return ""; | |
141 end | |
142 | |
143 local streaming = event.item.streaming_uploads; | |
144 | |
104 for key, handler in pairs(event.item.route or {}) do | 145 for key, handler in pairs(event.item.route or {}) do |
105 local event_name = get_http_event(host, app_path, key); | 146 local event_name = get_http_event(host, app_path, key); |
106 if event_name then | 147 if event_name then |
148 local method = event_name:match("^%S+"); | |
149 if not app_methods:contains(method) then | |
150 app_methods = app_methods + set.new{ method }; | |
151 end | |
152 local options_event_name = event_name:gsub("^%S+", "OPTIONS"); | |
107 if type(handler) ~= "function" then | 153 if type(handler) ~= "function" then |
108 local data = handler; | 154 local data = handler; |
109 handler = function () return data; end | 155 handler = function () return data; end |
110 elseif event_name:sub(-2, -1) == "/*" then | 156 elseif event_name:sub(-2, -1) == "/*" then |
111 local base_path_len = #event_name:match("/.+$"); | 157 local base_path_len = #event_name:match("/.+$"); |
116 end; | 162 end; |
117 module:hook_object_event(server, event_name:sub(1, -3), redir_handler, -1); | 163 module:hook_object_event(server, event_name:sub(1, -3), redir_handler, -1); |
118 elseif event_name:sub(-1, -1) == "/" then | 164 elseif event_name:sub(-1, -1) == "/" then |
119 module:hook_object_event(server, event_name:sub(1, -2), redir_handler, -1); | 165 module:hook_object_event(server, event_name:sub(1, -2), redir_handler, -1); |
120 end | 166 end |
167 if not streaming then | |
168 -- COMPAT Modules not compatible with streaming uploads behave as before. | |
169 local _handler = handler; | |
170 function handler(event) -- luacheck: ignore 432/event | |
171 if event.request.body ~= false then | |
172 return _handler(event); | |
173 end | |
174 end | |
175 end | |
121 if not app_handlers[event_name] then | 176 if not app_handlers[event_name] then |
122 app_handlers[event_name] = handler; | 177 app_handlers[event_name] = { |
178 main = handler; | |
179 cors = cors_handler; | |
180 options = options_handler; | |
181 }; | |
123 module:hook_object_event(server, event_name, handler); | 182 module:hook_object_event(server, event_name, handler); |
183 module:hook_object_event(server, event_name, cors_handler, 1); | |
184 module:hook_object_event(server, options_event_name, options_handler, -1); | |
124 else | 185 else |
125 module:log("warn", "App %s added handler twice for '%s', ignoring", app_name, event_name); | 186 module:log("warn", "App %s added handler twice for '%s', ignoring", app_name, event_name); |
126 end | 187 end |
127 else | 188 else |
128 module:log("error", "Invalid route in %s, %q. See https://prosody.im/doc/developers/http#routes", app_name, key); | 189 module:log("error", "Invalid route in %s, %q. See https://prosody.im/doc/developers/http#routes", app_name, key); |
129 end | 190 end |
130 end | 191 end |
131 local services = portmanager.get_active_services(); | 192 local services = portmanager.get_active_services(); |
132 if services:get("https") or services:get("http") then | 193 if services:get("https") or services:get("http") then |
133 module:log("debug", "Serving '%s' at %s", app_name, module:http_url(app_name, app_path)); | 194 module:log("info", "Serving '%s' at %s", app_name, module:http_url(app_name, app_path)); |
134 else | 195 elseif prosody.process_type == "prosody" then |
135 module:log("warn", "Not listening on any ports, '%s' will be unreachable", app_name); | 196 module:log("warn", "Not listening on any ports, '%s' will be unreachable", app_name); |
136 end | 197 end |
137 end | 198 end |
138 | 199 |
139 local function http_app_removed(event) | 200 local function http_app_removed(event) |
140 local app_handlers = apps[event.item.name]; | 201 local app_handlers = apps[event.item.name]; |
141 apps[event.item.name] = nil; | 202 apps[event.item.name] = nil; |
142 for event_name, handler in pairs(app_handlers) do | 203 for event_name, handlers in pairs(app_handlers) do |
143 module:unhook_object_event(server, event_name, handler); | 204 module:unhook_object_event(server, event_name, handlers.main); |
205 module:unhook_object_event(server, event_name, handlers.cors); | |
206 local options_event_name = event_name:gsub("^%S+", "OPTIONS"); | |
207 module:unhook_object_event(server, options_event_name, handlers.options); | |
144 end | 208 end |
145 end | 209 end |
146 | 210 |
147 module:handle_items("http-provider", http_app_added, http_app_removed); | 211 module:handle_items("http-provider", http_app_added, http_app_removed); |
148 | 212 |
156 | 220 |
157 module.add_host(module); -- set up handling on global context too | 221 module.add_host(module); -- set up handling on global context too |
158 | 222 |
159 local trusted_proxies = module:get_option_set("trusted_proxies", { "127.0.0.1", "::1" })._items; | 223 local trusted_proxies = module:get_option_set("trusted_proxies", { "127.0.0.1", "::1" })._items; |
160 | 224 |
225 local function is_trusted_proxy(ip) | |
226 local parsed_ip = new_ip(ip) | |
227 for trusted_proxy in trusted_proxies do | |
228 if match_ip(parsed_ip, parse_cidr(trusted_proxy)) then | |
229 return true; | |
230 end | |
231 end | |
232 return false | |
233 end | |
234 | |
161 local function get_ip_from_request(request) | 235 local function get_ip_from_request(request) |
162 local ip = request.conn:ip(); | 236 local ip = request.conn:ip(); |
163 local forwarded_for = request.headers.x_forwarded_for; | 237 local forwarded_for = request.headers.x_forwarded_for; |
164 if forwarded_for then | 238 if forwarded_for then |
239 -- luacheck: ignore 631 | |
240 -- This logic looks weird at first, but it makes sense. | |
241 -- The for loop will take the last non-trusted-proxy IP from `forwarded_for`. | |
242 -- We append the original request IP to the header. Then, since the last IP wins, there are two cases: | |
243 -- Case a) The original request IP is *not* in trusted proxies, in which case the X-Forwarded-For header will, effectively, be ineffective; the original request IP will win because it overrides any other IP in the header. | |
244 -- Case b) The original request IP is in trusted proxies. In that case, the if branch in the for loop will skip the last IP, causing it to be ignored. The second-to-last IP will be taken instead. | |
245 -- Case c) If the second-to-last IP is also a trusted proxy, it will also be ignored, iteratively, up to the last IP which isn’t in trusted proxies. | |
246 -- Case d) If all IPs are in trusted proxies, something went obviously wrong and the logic never overwrites `ip`, leaving it at the original request IP. | |
165 forwarded_for = forwarded_for..", "..ip; | 247 forwarded_for = forwarded_for..", "..ip; |
166 for forwarded_ip in forwarded_for:gmatch("[^%s,]+") do | 248 for forwarded_ip in forwarded_for:gmatch("[^%s,]+") do |
167 if not trusted_proxies[forwarded_ip] then | 249 if not is_trusted_proxy(forwarded_ip) then |
168 ip = forwarded_ip; | 250 ip = forwarded_ip; |
169 end | 251 end |
170 end | 252 end |
171 end | 253 end |
172 return ip; | 254 return ip; |
193 module:provides("net", { | 275 module:provides("net", { |
194 name = "https"; | 276 name = "https"; |
195 listener = server.listener; | 277 listener = server.listener; |
196 default_port = 5281; | 278 default_port = 5281; |
197 encryption = "ssl"; | 279 encryption = "ssl"; |
198 ssl_config = { | |
199 verify = "none"; | |
200 }; | |
201 multiplex = { | 280 multiplex = { |
281 protocol = "http/1.1"; | |
202 pattern = "^[A-Z]"; | 282 pattern = "^[A-Z]"; |
203 }; | 283 }; |
204 }); | 284 }); |