net.http.server: Add response method for reading response body from a file handle
[prosody.git] / net / http / server.lua
index 5961169fd3a42e58753fc371062acc950b9945bc..bc39767fe49562e1d95c9c873902a27efb67f03a 100644 (file)
@@ -11,11 +11,14 @@ local setmetatable = setmetatable;
 local xpcall = xpcall;
 local traceback = debug.traceback;
 local tostring = tostring;
+local cache = require "util.cache";
 local codes = require "net.http.codes";
+local blocksize = require "socket".BLOCKSIZE or 2048;
 
 local _M = {};
 
 local sessions = {};
+local incomplete = {};
 local listener = {};
 local hosts = {};
 local default_host;
@@ -27,7 +30,10 @@ local function is_wildcard_match(wildcard_event, event)
        return wildcard_event:sub(1, -2) == event:sub(1, #wildcard_event-1);
 end
 
-local recent_wildcard_events, max_cached_wildcard_events = {}, 10000;
+local _handlers = events._handlers;
+local recent_wildcard_events = cache.new(10000, function (key, value)
+       rawset(_handlers, key, nil);
+end);
 
 local event_map = events._event_map;
 setmetatable(events._handlers, {
@@ -62,10 +68,7 @@ setmetatable(events._handlers, {
                end
                rawset(handlers, curr_event, handlers_array);
                if not event_map[curr_event] then -- Only wildcard handlers match, if any
-                       table.insert(recent_wildcard_events, curr_event);
-                       if #recent_wildcard_events > max_cached_wildcard_events then
-                               rawset(handlers, table.remove(recent_wildcard_events, 1), nil);
-                       end
+                       recent_wildcard_events:set(curr_event, true);
                end
                return handlers_array;
        end;
@@ -98,7 +101,7 @@ function listener.onconnect(conn)
        local pending = {};
        local waiting = false;
        local function process_next()
-               if waiting then log("debug", "can't process_next, waiting"); return; end
+               if waiting then return; end -- log("debug", "can't process_next, waiting");
                waiting = true;
                while sessions[conn] and #pending > 0 do
                        local request = t_remove(pending);
@@ -139,13 +142,26 @@ function listener.ondisconnect(conn)
                open_response.finished = true;
                open_response:on_destroy();
        end
+       incomplete[conn] = nil;
+       sessions[conn] = nil;
+end
+
+function listener.ondetach(conn)
        sessions[conn] = nil;
+       incomplete[conn] = nil;
 end
 
 function listener.onincoming(conn, data)
        sessions[conn]:feed(data);
 end
 
+function listener.ondrain(conn)
+       local response = incomplete[conn];
+       if response and response._send_more then
+               response._send_more();
+       end
+end
+
 local headerfix = setmetatable({}, {
        __index = function(t, k)
                local v = "\r\n"..k:gsub("_", "-"):gsub("%f[%w].", s_upper)..": ";
@@ -185,6 +201,8 @@ function handle_request(conn, request, finish_cb)
                persistent = persistent;
                conn = conn;
                send = _M.send_response;
+               send_file = _M.send_file;
+               done = _M.finish_response;
                finish_cb = finish_cb;
        };
        conn._http_open_response = response;
@@ -213,7 +231,7 @@ function handle_request(conn, request, finish_cb)
 
        local event = request.method.." "..host..request.path:match("[^?]*");
        local payload = { request = request, response = response };
-       --log("debug", "Firing event: %s", event);
+       log("debug", "Firing event: %s", event);
        local result = events.fire_event(event, payload);
        if result ~= nil then
                if result ~= true then
@@ -246,24 +264,60 @@ function handle_request(conn, request, finish_cb)
        response.status_code = 404;
        response:send(events.fire_event("http-error", { code = 404 }));
 end
-function _M.send_response(response, body)
-       if response.finished then return; end
-       response.finished = true;
-       response.conn._http_open_response = nil;
-
+local function prepare_header(response)
        local status_line = "HTTP/"..response.request.httpversion.." "..(response.status or codes[response.status_code]);
        local headers = response.headers;
-       body = body or response.body or "";
-       headers.content_length = #body;
-
        local output = { status_line };
        for k,v in pairs(headers) do
                t_insert(output, headerfix[k]..v);
        end
        t_insert(output, "\r\n\r\n");
+       return output;
+end
+_M.prepare_header = prepare_header;
+function _M.send_response(response, body)
+       if response.finished then return; end
+       body = body or response.body or "";
+       response.headers.content_length = #body;
+       local output = prepare_header(response);
        t_insert(output, body);
-
        response.conn:write(t_concat(output));
+       response:done();
+end
+function _M.send_file(response, f)
+       if response.finished then return; end
+       local chunked = not response.headers.content_length;
+       if chunked then response.headers.transfer_encoding = "chunked"; end
+       incomplete[response.conn] = response;
+       response._send_more = function ()
+               if response.finished then
+                       incomplete[response.conn] = nil;
+                       return;
+               end
+               local chunk = f:read(blocksize);
+               if chunk then
+                       if chunked then
+                               chunk = ("%x\r\n%s\r\n"):format(#chunk, chunk);
+                       end
+                       -- io.write("."); io.flush();
+                       response.conn:write(chunk);
+               else
+                       if chunked then
+                               response.conn:write("0\r\n\r\n");
+                       end
+                       -- io.write("\n");
+                       if f.close then f:close(); end
+                       incomplete[response.conn] = nil;
+                       return response:done();
+               end
+       end
+       response.conn:write(t_concat(prepare_header(response)));
+       return true;
+end
+function _M.finish_response(response)
+       if response.finished then return; end
+       response.finished = true;
+       response.conn._http_open_response = nil;
        if response.on_destroy then
                response:on_destroy();
                response.on_destroy = nil;
@@ -282,7 +336,7 @@ function _M.remove_handler(event, handler)
 end
 
 function _M.listen_on(port, interface, ssl)
-       addserver(interface or "*", port, listener, "*a", ssl);
+       return addserver(interface or "*", port, listener, "*a", ssl);
 end
 function _M.add_host(host)
        hosts[host] = true;