/
static_file_handler.cr
104 lines (88 loc) · 3.22 KB
/
static_file_handler.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
require "ecr/macros"
require "html"
require "uri"
# A simple handler that lists directories and serves files under a given public directory.
class HTTP::StaticFileHandler < HTTP::Handler
@public_dir : String
# Creates a handler that will serve files in the given *public_dir*, after
# expanding it (using `File#expand_path`).
#
# If *fallthrough* is `false`, this handler does not call next handler when
# request method is neither GET or HEAD, then serves `405 Method Not Allowed`.
# Otherwise, it calls next handler.
def initialize(public_dir : String, fallthrough = true)
@public_dir = File.expand_path public_dir
@fallthrough = !!fallthrough
end
def call(context)
unless context.request.method == "GET" || context.request.method == "HEAD"
if @fallthrough
call_next(context)
else
context.response.status_code = 405
context.response.headers.add("Allow", "GET, HEAD")
end
return
end
original_path = context.request.path.not_nil!
is_dir_path = original_path.ends_with? "/"
request_path = URI.unescape(original_path)
# File path cannot contains '\0' (NUL) because all filesystem I know
# don't accept '\0' character as file name.
if request_path.includes? '\0'
context.response.status_code = 400
return
end
expanded_path = File.expand_path(request_path, "/")
if is_dir_path && !expanded_path.ends_with? "/"
expanded_path = "#{expanded_path}/"
end
is_dir_path = expanded_path.ends_with? "/"
file_path = File.join(@public_dir, expanded_path)
is_dir = Dir.exists? file_path
if request_path != expanded_path || is_dir && !is_dir_path
redirect_to context, "#{expanded_path}#{is_dir && !is_dir_path ? "/" : ""}"
return
end
if Dir.exists?(file_path)
context.response.content_type = "text/html"
directory_listing(context.response, request_path, file_path)
elsif File.exists?(file_path)
context.response.content_type = mime_type(file_path)
context.response.content_length = File.size(file_path)
File.open(file_path) do |file|
IO.copy(file, context.response)
end
else
call_next(context)
end
end
private def redirect_to(context, url)
context.response.status_code = 302
url = URI.escape(url) { |b| URI.unreserved?(b) || b != '/' }
context.response.headers.add "Location", url
end
private def mime_type(path)
case File.extname(path)
when ".txt" then "text/plain"
when ".htm", ".html" then "text/html"
when ".css" then "text/css"
when ".js" then "application/javascript"
else "application/octet-stream"
end
end
record DirectoryListing, request_path : String, path : String do
@escaped_request_path : String?
def escaped_request_path
@escaped_request_path ||= begin
esc_path = URI.escape(request_path) { |b| URI.unreserved?(b) || b != '/' }
esc_path = esc_path[0..-2] if !esc_path.empty? && esc_path[-1] == '/'
esc_path
end
end
ECR.def_to_s "#{__DIR__}/static_file_handler.html"
end
private def directory_listing(io, request_path, path)
DirectoryListing.new(request_path, path).to_s(io)
end
end