2 -- Copyright (C) 2008-2012 Matthew Wild
3 -- Copyright (C) 2008-2012 Waqas Hussain
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
10 module
:depends("http_errors");
12 local portmanager
= require
"core.portmanager";
13 local moduleapi
= require
"core.moduleapi";
14 local url_parse
= require
"socket.url".parse
;
15 local url_build
= require
"socket.url".build
;
16 local normalize_path
= require
"util.http".normalize_path
;
17 local set
= require
"util.set";
19 local server
= require
"net.http.server";
21 server
.set_default_host(module
:get_option_string("http_default_host"));
23 server
.set_option("body_size_limit", module
:get_option_number("http_max_content_size"));
24 server
.set_option("buffer_size_limit", module
:get_option_number("http_max_buffer_size"));
27 local opt_methods
= module
:get_option_set("access_control_allow_methods", { "GET", "OPTIONS" });
28 local opt_headers
= module
:get_option_set("access_control_allow_headers", { "Content-Type" });
29 local opt_credentials
= module
:get_option_boolean("access_control_allow_credentials", false);
30 local opt_max_age
= module
:get_option_number("access_control_max_age", 2 * 60 * 60);
32 local function get_http_event(host
, app_path
, key
)
33 local method
, path
= key
:match("^(%S+)%s+(.+)$");
34 if not method
then -- No path specified, default to "" (base path)
35 method
, path
= key
, "";
37 if method
:sub(1,1) == "/" then
40 if app_path
== "/" and path
:sub(1,1) == "/" then
44 return method
:upper().." "..app_path
..path
;
46 return method
:upper().." "..host
..app_path
..path
;
50 local function get_base_path(host_module
, app_name
, default_app_path
)
51 return (normalize_path(host_module
:get_option("http_paths", {})[app_name
] -- Host
52 or module
:get_option("http_paths", {})[app_name
] -- Global
53 or default_app_path
)) -- Default
54 :gsub("%$(%w+)", { host
= host_module
.host
});
57 local function redir_handler(event
)
58 event
.response
.headers
.location
= event
.request
.path
.."/";
59 if event
.request
.url
.query
then
60 event
.response
.headers
.location
= event
.response
.headers
.location
.. "?" .. event
.request
.url
.query
65 local ports_by_scheme
= { http
= 80, https
= 443, };
67 -- Helper to deduce a module's external URL
68 function moduleapi
.http_url(module
, app_name
, default_path
)
69 app_name
= app_name
or (module
.name
:gsub("^http_", ""));
70 local external_url
= url_parse(module
:get_option_string("http_external_url")) or {};
71 if external_url
.scheme
and external_url
.port
== nil then
72 external_url
.port
= ports_by_scheme
[external_url
.scheme
];
74 local services
= portmanager
.get_active_services();
75 local http_services
= services
:get("https") or services
:get("http") or {};
76 for interface
, ports
in pairs(http_services
) do -- luacheck: ignore 213/interface
77 for port
, service
in pairs(ports
) do -- luacheck: ignore 512
79 scheme
= (external_url
.scheme
or service
[1].service
.name
);
80 host
= (external_url
.host
or module
:get_option_string("http_host", module
.host
));
81 port
= tonumber(external_url
.port
) or port
or 80;
82 path
= normalize_path(external_url
.path
or "/", true)..
83 (get_base_path(module
, app_name
, default_path
or "/"..app_name
):sub(2));
85 if ports_by_scheme
[url
.scheme
] == url
.port
then url
.port
= nil end
86 return url_build(url
);
89 module
:log("warn", "No http ports enabled, can't generate an external URL");
90 return "http://disabled.invalid/";
93 local function apply_cors_headers(response
, methods
, headers
, max_age
, allow_credentials
, origin
)
94 response
.headers
.access_control_allow_methods
= tostring(methods
);
95 response
.headers
.access_control_allow_headers
= tostring(headers
);
96 response
.headers
.access_control_max_age
= tostring(max_age
)
97 response
.headers
.access_control_allow_origin
= origin
or "*";
98 if allow_credentials
then
99 response
.headers
.access_control_allow_credentials
= "true";
103 function module
.add_host(module
)
104 local host
= module
.host
;
106 host
= module
:get_option_string("http_host", host
);
109 module
.environment
.apps
= apps
;
110 local function http_app_added(event
)
111 local app_name
= event
.item
.name
;
112 local default_app_path
= event
.item
.default_path
or "/"..app_name
;
113 local app_path
= get_base_path(module
, app_name
, default_app_path
);
115 -- TODO: Link to docs
116 module
:log("error", "HTTP app has no 'name', add one or use module:provides('http', app)");
119 apps
[app_name
] = apps
[app_name
] or {};
120 local app_handlers
= apps
[app_name
];
122 local app_methods
= opt_methods
;
124 local function cors_handler(event_data
)
125 local request
, response
= event_data
.request
, event_data
.response
;
126 apply_cors_headers(response
, app_methods
, opt_headers
, opt_max_age
, opt_credentials
, request
.headers
.origin
);
129 local function options_handler(event_data
)
130 cors_handler(event_data
);
134 for key
, handler
in pairs(event
.item
.route
or {}) do
135 local event_name
= get_http_event(host
, app_path
, key
);
137 local method
= event_name
:match("^%S+");
138 if not app_methods
:contains(method
) then
139 app_methods
= app_methods
+ set
.new
{ method
};
141 local options_event_name
= event_name
:gsub("^%S+", "OPTIONS");
142 if type(handler
) ~= "function" then
143 local data
= handler
;
144 handler
= function () return data
; end
145 elseif event_name
:sub(-2, -1) == "/*" then
146 local base_path_len
= #event_name
:match("/.+$");
147 local _handler
= handler
;
148 handler
= function (_event
)
149 local path
= _event
.request
.path
:sub(base_path_len
);
150 return _handler(_event
, path
);
152 module
:hook_object_event(server
, event_name
:sub(1, -3), redir_handler
, -1);
153 elseif event_name
:sub(-1, -1) == "/" then
154 module
:hook_object_event(server
, event_name
:sub(1, -2), redir_handler
, -1);
156 if not app_handlers
[event_name
] then
157 app_handlers
[event_name
] = handler
;
158 module
:hook_object_event(server
, event_name
, handler
);
159 module
:hook_object_event(server
, event_name
, cors_handler
, 1);
160 module
:hook_object_event(server
, options_event_name
, options_handler
, -1);
162 module
:log("warn", "App %s added handler twice for '%s', ignoring", app_name
, event_name
);
165 module
:log("error", "Invalid route in %s, %q. See https://prosody.im/doc/developers/http#routes", app_name
, key
);
168 local services
= portmanager
.get_active_services();
169 if services
:get("https") or services
:get("http") then
170 module
:log("debug", "Serving '%s' at %s", app_name
, module
:http_url(app_name
, app_path
));
172 module
:log("warn", "Not listening on any ports, '%s' will be unreachable", app_name
);
176 local function http_app_removed(event
)
177 local app_handlers
= apps
[event
.item
.name
];
178 apps
[event
.item
.name
] = nil;
179 for event_name
, handler
in pairs(app_handlers
) do
180 module
:unhook_object_event(server
, event_name
, handler
);
184 module
:handle_items("http-provider", http_app_added
, http_app_removed
);
187 server
.add_host(host
);
188 function module
.unload()
189 server
.remove_host(host
);
194 module
.add_host(module
); -- set up handling on global context too
196 local trusted_proxies
= module
:get_option_set("trusted_proxies", { "127.0.0.1", "::1" })._items
;
198 local function get_ip_from_request(request
)
199 local ip
= request
.conn
:ip();
200 local forwarded_for
= request
.headers
.x_forwarded_for
;
201 if forwarded_for
then
202 forwarded_for
= forwarded_for
..", "..ip
;
203 for forwarded_ip
in forwarded_for
:gmatch("[^%s,]+") do
204 if not trusted_proxies
[forwarded_ip
] then
212 module
:wrap_object_event(server
._events
, false, function (handlers
, event_name
, event_data
)
213 local request
= event_data
.request
;
215 -- Not included in eg http-error events
216 request
.ip
= get_ip_from_request(request
);
218 return handlers(event_name
, event_data
);
221 module
:provides("net", {
223 listener
= server
.listener
;
230 module
:provides("net", {
232 listener
= server
.listener
;