nitcorn/file_server: allow default_response if not file match
[nit.git] / lib / nitcorn / file_server.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2013 Jean-Philippe Caissy <jpcaissy@piji.ca>
4 # Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
5 #
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 #
10 # http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
17
18 # Provides the `FileServer` action, which is a standard and minimal file server
19 module file_server
20
21 import reactor
22 import sessions
23 import media_types
24 import http_errors
25
26 redef class String
27 # Returns a `String` copy of `self` without any of the prefixed '/'s
28 #
29 # Examples:
30 #
31 # assert "/home/".strip_start_slashes == "home/"
32 # assert "////home/".strip_start_slashes == "home/"
33 # assert "../home/".strip_start_slashes == "../home/"
34 fun strip_start_slashes: String
35 do
36 for i in chars.length.times do if chars[i] != '/' then return substring_from(i)
37 return ""
38 end
39 end
40
41 # A simple file server
42 class FileServer
43 super Action
44
45 # Root folder of `self` file system
46 var root: String
47
48 init
49 do
50 var root = self.root
51
52 # Simplify the root path as each file requested will also be simplified
53 root = root.simplify_path
54
55 # Make sure the root ends with '/', this makes a difference in the security
56 # check on each file access.
57 root = root + "/"
58
59 self.root = root
60 end
61
62 # Error page template for a given `code`
63 fun error_page(code: Int): Writable do return new ErrorTemplate(code)
64
65 # Header of each directory page
66 var header: nullable Writable = null is writable
67
68 # Custom JavaScript code added within a `<script>` block to each page
69 var javascript_header: nullable Writable = null is writable
70
71 # Caching attributes of served files, used as the `cache-control` field in response headers
72 var cache_control = "public, max-age=360" is writable
73
74 # Show directory listing?
75 var show_directory_listing = true is writable
76
77 # Default file returned when no static file matches the requested URI.
78 #
79 # If no `default_file` is provided, the FileServer responds 404 error to
80 # unmatched queries.
81 var default_file: nullable String = null is writable
82
83 redef fun answer(request, turi)
84 do
85 var response
86
87 var local_file = root.join_path(turi.strip_start_slashes)
88 local_file = local_file.simplify_path
89
90 # Is it reachable?
91 #
92 # This make sure that the requested file is within the root folder.
93 if (local_file + "/").has_prefix(root) then
94 # Does it exists?
95 var file_stat = local_file.file_stat
96 if file_stat != null then
97 if file_stat.is_dir then
98 # If we target a directory without an ending `/`,
99 # redirect to the directory ending with `/`.
100 var uri = request.uri
101 if not uri.is_empty and uri.chars.last != '/' then
102 return answer_redirection(request.uri + "/")
103 end
104
105 # Show index file instead of the directory listing
106 # only if `index.html` or `index.htm` is available
107 var index_file = local_file.join_path("index.html")
108 if index_file.file_exists then
109 local_file = index_file
110 else
111 index_file = local_file.join_path("index.htm")
112 if index_file.file_exists then local_file = index_file
113 end
114 end
115
116 file_stat = local_file.file_stat
117 if show_directory_listing and file_stat != null and file_stat.is_dir then
118 response = answer_directory_listing(request, turi, local_file)
119 else if file_stat != null and not file_stat.is_dir then # It's a single file
120 response = answer_file(local_file)
121 else response = answer_default
122 else response = answer_default
123 else response = new HttpResponse(403)
124
125 if response.status_code != 200 then
126 var tmpl = error_page(response.status_code)
127 if header != null and tmpl isa ErrorTemplate then tmpl.header = header
128 response.body = tmpl.to_s
129 end
130
131 return response
132 end
133
134 # Answer the `default_file` if any.
135 fun answer_default: HttpResponse do
136 var default_file = self.default_file
137 if default_file == null then
138 return new HttpResponse(404)
139 end
140
141 var local_file = (root / default_file).simplify_path
142 return answer_file(local_file)
143 end
144
145 # Answer a 303 redirection to `location`.
146 fun answer_redirection(location: String): HttpResponse do
147 var response = new HttpResponse(303)
148 response.header["Location"] = location
149 return response
150 end
151
152 # Build a reponse containing a single `local_file`.
153 #
154 # Returns a 404 error if local_file does not exists.
155 fun answer_file(local_file: String): HttpResponse do
156 if not local_file.file_exists then return new HttpResponse(404)
157
158 var response = new HttpResponse(200)
159 response.files.add local_file
160
161 # Set Content-Type depending on the file extension
162 var ext = local_file.file_extension
163 if ext != null then
164 var media_type = media_types[ext]
165 if media_type != null then
166 response.header["Content-Type"] = media_type
167 else response.header["Content-Type"] = "application/octet-stream"
168 end
169
170 # Cache control
171 response.header["cache-control"] = cache_control
172 return response
173 end
174
175 # Answer with a directory listing for files within `local_files`.
176 fun answer_directory_listing(request: HttpRequest, turi, local_file: String): HttpResponse do
177 # Show the directory listing
178 var title = turi
179 var files = local_file.files
180
181 alpha_comparator.sort files
182
183 var links = new Array[String]
184 if turi.length > 1 then
185 var path = (request.uri + "/..").simplify_path
186 links.add "<a href=\"{path}/\">..</a>"
187 end
188 for file in files do
189 var local_path = local_file.join_path(file).simplify_path
190 var web_path = file.simplify_path
191 var file_stat = local_path.file_stat
192 if file_stat != null and file_stat.is_dir then web_path = web_path + "/"
193 links.add "<a href=\"{web_path}\">{file}</a>"
194 end
195
196 var header = self.header
197 var header_code
198 if header != null then
199 header_code = header.write_to_string
200 else header_code = ""
201
202 var response = new HttpResponse(200)
203 response.body = """
204 <!DOCTYPE html>
205 <head>
206 <meta charset="utf-8">
207 <meta http-equiv="X-UA-Compatible" content="IE=edge">
208 <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
209 <script>
210 {{{javascript_header or else ""}}}
211 </script>
212 <title>{{{title}}}</title>
213 </head>
214 <body>
215 {{{header_code}}}
216 <div class="container">
217 <h1>{{{title}}}</h1>
218 <ul>
219 <li>{{{links.join("</li>\n\t\t\t<li>")}}}</li>
220 </ul>
221 </div>
222 </body>
223 </html>"""
224
225 response.header["Content-Type"] = media_types["html"].as(not null)
226 return response
227 end
228 end