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 });