net/http/files.lua
changeset 10438 8f709577fe8e
parent 9466 f7530c846f98
parent 10415 db2a06b9ff98
child 10567 e8db377a2983
equal deleted inserted replaced
10437:7777f25d5266 10438:8f709577fe8e
       
     1 -- Prosody IM
       
     2 -- Copyright (C) 2008-2010 Matthew Wild
       
     3 -- Copyright (C) 2008-2010 Waqas Hussain
       
     4 --
       
     5 -- This project is MIT/X11 licensed. Please see the
       
     6 -- COPYING file in the source package for more information.
       
     7 --
       
     8 
       
     9 local server = require"net.http.server";
       
    10 local lfs = require "lfs";
       
    11 local new_cache = require "util.cache".new;
       
    12 local log = require "util.logger".init("net.http.files");
       
    13 
       
    14 local os_date = os.date;
       
    15 local open = io.open;
       
    16 local stat = lfs.attributes;
       
    17 local build_path = require"socket.url".build_path;
       
    18 local path_sep = package.config:sub(1,1);
       
    19 
       
    20 
       
    21 local forbidden_chars_pattern = "[/%z]";
       
    22 if package.config:sub(1,1) == "\\" then
       
    23 	forbidden_chars_pattern = "[/%z\001-\031\127\"*:<>?|]"
       
    24 end
       
    25 
       
    26 local urldecode = require "util.http".urldecode;
       
    27 local function sanitize_path(path) --> util.paths or util.http?
       
    28 	if not path then return end
       
    29 	local out = {};
       
    30 
       
    31 	local c = 0;
       
    32 	for component in path:gmatch("([^/]+)") do
       
    33 		component = urldecode(component);
       
    34 		if component:find(forbidden_chars_pattern) then
       
    35 			return nil;
       
    36 		elseif component == ".." then
       
    37 			if c <= 0 then
       
    38 				return nil;
       
    39 			end
       
    40 			out[c] = nil;
       
    41 			c = c - 1;
       
    42 		elseif component ~= "." then
       
    43 			c = c + 1;
       
    44 			out[c] = component;
       
    45 		end
       
    46 	end
       
    47 	if path:sub(-1,-1) == "/" then
       
    48 		out[c+1] = "";
       
    49 	end
       
    50 	return "/"..table.concat(out, "/");
       
    51 end
       
    52 
       
    53 local function serve(opts)
       
    54 	if type(opts) ~= "table" then -- assume path string
       
    55 		opts = { path = opts };
       
    56 	end
       
    57 	local mime_map = opts.mime_map or { html = "text/html" };
       
    58 	local cache = new_cache(opts.cache_size or 256);
       
    59 	local cache_max_file_size = tonumber(opts.cache_max_file_size) or 1024
       
    60 	-- luacheck: ignore 431
       
    61 	local base_path = opts.path;
       
    62 	local dir_indices = opts.index_files or { "index.html", "index.htm" };
       
    63 	local directory_index = opts.directory_index;
       
    64 	local function serve_file(event, path)
       
    65 		local request, response = event.request, event.response;
       
    66 		local sanitized_path = sanitize_path(path);
       
    67 		if path and not sanitized_path then
       
    68 			return 400;
       
    69 		end
       
    70 		path = sanitized_path;
       
    71 		local orig_path = sanitize_path(request.path);
       
    72 		local full_path = base_path .. (path or ""):gsub("/", path_sep);
       
    73 		local attr = stat(full_path:match("^.*[^\\/]")); -- Strip trailing path separator because Windows
       
    74 		if not attr then
       
    75 			return 404;
       
    76 		end
       
    77 
       
    78 		local request_headers, response_headers = request.headers, response.headers;
       
    79 
       
    80 		local last_modified = os_date('!%a, %d %b %Y %H:%M:%S GMT', attr.modification);
       
    81 		response_headers.last_modified = last_modified;
       
    82 
       
    83 		local etag = ('"%02x-%x-%x-%x"'):format(attr.dev or 0, attr.ino or 0, attr.size or 0, attr.modification or 0);
       
    84 		response_headers.etag = etag;
       
    85 
       
    86 		local if_none_match = request_headers.if_none_match
       
    87 		local if_modified_since = request_headers.if_modified_since;
       
    88 		if etag == if_none_match
       
    89 		or (not if_none_match and last_modified == if_modified_since) then
       
    90 			return 304;
       
    91 		end
       
    92 
       
    93 		local data = cache:get(orig_path);
       
    94 		if data and data.etag == etag then
       
    95 			response_headers.content_type = data.content_type;
       
    96 			data = data.data;
       
    97 			cache:set(orig_path, data);
       
    98 		elseif attr.mode == "directory" and path then
       
    99 			if full_path:sub(-1) ~= "/" then
       
   100 				local dir_path = { is_absolute = true, is_directory = true };
       
   101 				for dir in orig_path:gmatch("[^/]+") do dir_path[#dir_path+1]=dir; end
       
   102 				response_headers.location = build_path(dir_path);
       
   103 				return 301;
       
   104 			end
       
   105 			for i=1,#dir_indices do
       
   106 				if stat(full_path..dir_indices[i], "mode") == "file" then
       
   107 					return serve_file(event, path..dir_indices[i]);
       
   108 				end
       
   109 			end
       
   110 
       
   111 			if directory_index then
       
   112 				data = server._events.fire_event("directory-index", { path = request.path, full_path = full_path });
       
   113 			end
       
   114 			if not data then
       
   115 				return 403;
       
   116 			end
       
   117 			cache:set(orig_path, { data = data, content_type = mime_map.html; etag = etag; });
       
   118 			response_headers.content_type = mime_map.html;
       
   119 
       
   120 		else
       
   121 			local f, err = open(full_path, "rb");
       
   122 			if not f then
       
   123 				log("debug", "Could not open %s. Error was %s", full_path, err);
       
   124 				return 403;
       
   125 			end
       
   126 			local ext = full_path:match("%.([^./]+)$");
       
   127 			local content_type = ext and mime_map[ext];
       
   128 			response_headers.content_type = content_type;
       
   129 			if attr.size > cache_max_file_size then
       
   130 				response_headers.content_length = ("%d"):format(attr.size);
       
   131 				log("debug", "%d > cache_max_file_size", attr.size);
       
   132 				return response:send_file(f);
       
   133 			else
       
   134 				data = f:read("*a");
       
   135 				f:close();
       
   136 			end
       
   137 			cache:set(orig_path, { data = data; content_type = content_type; etag = etag });
       
   138 		end
       
   139 
       
   140 		return response:send(data);
       
   141 	end
       
   142 
       
   143 	return serve_file;
       
   144 end
       
   145 
       
   146 return {
       
   147 	serve = serve;
       
   148 }
       
   149