1 /*
2 * Copyright (C) 2008 Emweb bv, Herent, Belgium.
3 *
4 * All rights reserved.
5 */
6
7 #include <boost/spirit/include/classic_core.hpp>
8
9 #include "Configuration.h"
10 #include "StaticReply.h"
11 #include "Request.h"
12 #include "StockReply.h"
13 #include "MimeTypes.h"
14
15 #include "FileUtils.h"
16
17 #include "Wt/WLogger.h"
18
19 using namespace BOOST_SPIRIT_CLASSIC_NS;
20
21 namespace Wt {
22 LOGGER("wthttp");
23 }
24
25 namespace {
26
openStream(std::ifstream & stream,std::string & path,bool acceptGzip)27 static bool openStream(std::ifstream &stream, std::string &path, bool acceptGzip) {
28 bool gzipReply = false;
29 if (acceptGzip) {
30 std::string gzipPath = path + ".gz";
31 stream.open(gzipPath.c_str(), std::ios::in | std::ios::binary);
32
33 if (stream) {
34 path = gzipPath;
35 gzipReply = true;
36 } else {
37 stream.clear();
38 stream.open(path.c_str(), std::ios::in | std::ios::binary);
39 }
40 } else {
41 stream.open(path.c_str(), std::ios::in | std::ios::binary);
42 }
43 return gzipReply;
44 }
45
46 }
47
48 namespace http {
49 namespace server {
50
StaticReply(Request & request,const Configuration & config)51 StaticReply::StaticReply(Request& request, const Configuration& config)
52 : Reply(request, config)
53 {
54 reset(0);
55 }
56
reset(const Wt::EntryPoint * ep)57 void StaticReply::reset(const Wt::EntryPoint *ep)
58 {
59 Reply::reset(ep);
60
61 stream_.close();
62 stream_.clear();
63
64 hasRange_ = false;
65
66 std::string request_path = request_.request_path;
67
68 // Request path for a static file must be absolute and not contain "..".
69 if (request_path.empty() || request_path[0] != '/'
70 || request_path.find("..") != std::string::npos) {
71 setRelay(ReplyPtr(new StockReply(request_, StockReply::not_found,
72 "", configuration())));
73 return;
74 }
75
76 // If path ends in slash (i.e. is a directory) then add "index.html".
77 if (request_path[request_path.size() - 1] == '/')
78 request_path += "index.html";
79
80 // Determine the file extension.
81 std::size_t last_slash_pos = request_path.find_last_of("/");
82 std::size_t last_dot_pos = request_path.find_last_of(".");
83
84 if (last_dot_pos != std::string::npos && last_dot_pos > last_slash_pos)
85 extension_ = request_path.substr(last_dot_pos + 1);
86 else
87 extension_.clear();
88
89 path_ = configuration().docRoot() + request_path;
90
91 bool gzipReply = false;
92 std::string modifiedDate, etag;
93
94 parseRangeHeader();
95
96 // Do not consider .gz files if we will respond with a range, as we cannot
97 // stream partial data from a .gz file
98 bool acceptGzip = request_.acceptGzipEncoding() && !hasRange_;
99 gzipReply = openStream(stream_, path_, acceptGzip);
100
101 // Try fallback resources folder if not found
102 if (!stream_ && !configuration().resourcesDir().empty() &&
103 boost::starts_with(request_path, "/resources/")) {
104 path_ = configuration().resourcesDir() + request_path.substr(sizeof("/resources") - 1);
105 gzipReply = openStream(stream_, path_, acceptGzip);
106 }
107
108 if (!stream_) {
109 setRelay(ReplyPtr(new StockReply(request_, StockReply::not_found,
110 "", configuration())));
111 return;
112 } else {
113 try {
114 fileSize_ = Wt::FileUtils::size(path_);
115 modifiedDate = computeModifiedDate();
116 etag = computeETag();
117 } catch (...) {
118 fileSize_ = -1;
119 }
120 }
121
122 // Can't specify zero-length Content-Range headers. But for zero-length
123 // files, we just ignore the Range header and send the full file instead of
124 // a 416 Requested Range Not Satisfiable error
125 if (fileSize_ == 0)
126 hasRange_ = false;
127
128 if (hasRange_) {
129 stream_.seekg((std::streamoff)rangeBegin_, std::ios_base::cur);
130 std::streamoff curpos = stream_.tellg();
131 if (curpos != rangeBegin_) {
132 // Won't be able to send even a single byte -> error 416
133 ReplyPtr sr(new StockReply
134 (request_, StockReply::requested_range_not_satisfiable,
135 "", configuration()));
136 if (fileSize_ != -1) {
137 // 416 SHOULD include a Content-Range with byte-range-resp-spec * and
138 // instance-length set to current lenght
139 sr->addHeader("Content-Range", "bytes */" + std::to_string(fileSize_));
140 }
141 setRelay(sr);
142 stream_.close();
143 return;
144 } else {
145 ::int64_t last = rangeEnd_;
146 if (fileSize_ != -1 && last >= fileSize_) {
147 last = fileSize_ - 1;
148 }
149 std::stringstream contentRange;
150 // Note: if fileSize is unknown, we're not sure we'll be able to
151 // transmit the requested range (i.e. when the file is not large enough
152 // to satisfy the request). Wt wil report that it understood the request,
153 // and close the link prematurely if it can't provide the requested bytes
154 contentRange << "bytes " << rangeBegin_ << "-" << last << "/";
155 if (fileSize_ == -1) {
156 contentRange << "*";
157 } else {
158 contentRange << fileSize_;
159 }
160
161 LOG_INFO("sending: " << contentRange.str());
162
163 addHeader("Content-Range", contentRange.str());
164 }
165 }
166
167 /*
168 * Check if can send a 304 not modified reply
169 */
170 const Request::Header *ims = request_.getHeader("If-Modified-Since");
171 const Request::Header *inm = request_.getHeader("If-None-Match");
172
173 if ((ims && ims->value == modifiedDate) || (inm && inm->value == etag)) {
174 setRelay(ReplyPtr(new StockReply(request_, StockReply::not_modified,
175 configuration())));
176 stream_.close();
177 return;
178 }
179
180 /*
181 * Add headers for caching, but not for IE since it in fact makes it
182 * cache less (images)
183 */
184 const Request::Header *ua = request_.getHeader("User-Agent");
185
186 if (!ua || !ua->value.contains("MSIE")) {
187 addHeader("Cache-Control", "max-age=3600");
188 if (!etag.empty())
189 addHeader("ETag", etag);
190
191 addHeader("Expires", computeExpires());
192 } else {
193 // We experienced problems with some swf files if they are cached in IE.
194 // Therefore, don't cache swf files on IE.
195 if (boost::iequals(extension_, "swf"))
196 addHeader("Cache-Control", "no-cache");
197 }
198
199 if (!modifiedDate.empty())
200 addHeader("Last-Modified", modifiedDate);
201
202 if (gzipReply)
203 addHeader("Content-Encoding", "gzip");
204
205 if (hasRange_)
206 setStatus(partial_content);
207 else
208 setStatus(ok);
209 }
210
computeModifiedDate()211 std::string StaticReply::computeModifiedDate() const
212 {
213 return httpDate(Wt::FileUtils::lastWriteTime(path_));
214 }
215
computeETag()216 std::string StaticReply::computeETag() const
217 {
218 return std::to_string(fileSize_) + "-" + computeModifiedDate();
219 }
220
computeExpires()221 std::string StaticReply::computeExpires()
222 {
223 time_t t = time(0);
224 t += 3600*24*31;
225 return httpDate(t);
226 }
227
consumeData(const char * begin,const char * end,Request::State state)228 bool StaticReply::consumeData(const char *begin,
229 const char *end,
230 Request::State state)
231 {
232 if (state != Request::Partial)
233 send();
234 return true;
235 }
236
contentType()237 std::string StaticReply::contentType()
238 {
239 return mime_types::extensionToType(extension_);
240 }
241
contentLength()242 ::int64_t StaticReply::contentLength()
243 {
244 if (hasRange_) {
245 if (fileSize_ == -1) {
246 return -1;
247 }
248 if (rangeBegin_ >= fileSize_) {
249 return 0;
250 }
251 if (rangeEnd_ < fileSize_) {
252 return rangeEnd_ - rangeBegin_ + 1;
253 } else {
254 return fileSize_ - rangeBegin_;
255 }
256 } else {
257 return fileSize_;
258 }
259 }
260
writeDone(bool success)261 void StaticReply::writeDone(bool success)
262 {
263 if (relay()) {
264 relay()->writeDone(success);
265 return;
266 }
267
268 if (success && stream_.is_open())
269 send();
270 }
271
nextContentBuffers(std::vector<asio::const_buffer> & result)272 bool StaticReply::nextContentBuffers(std::vector<asio::const_buffer>& result)
273 {
274 if (request_.method != "HEAD") {
275 boost::uintmax_t rangeRemainder = (std::numeric_limits< ::int64_t>::max)();
276
277 if (hasRange_)
278 rangeRemainder = rangeEnd_ - stream_.tellg() + 1;
279
280 stream_.read(buf_, (std::streamsize)
281 (std::min<boost::uintmax_t>)(rangeRemainder, sizeof(buf_)));
282
283 if (stream_.gcount() > 0) {
284 result.push_back(asio::buffer(buf_, stream_.gcount()));
285 return false;
286 } else {
287 stream_.close();
288 return true;
289 }
290 } else {
291 stream_.close();
292 return true;
293 }
294 }
295
parseRangeHeader()296 void StaticReply::parseRangeHeader()
297 {
298 // Wt only support these types of ranges for now:
299 // Range: bytes=0-
300 // Range: bytes=10-
301 // Range: bytes=250-499
302 // NOT SUPPORTED: multiple ranges, and the suffix-byte-range-spec:
303 // Range: bytes=10-20,30-40
304 // Range: bytes=-500 // 'last 500 bytes'
305 const Request::Header *range = request_.getHeader("Range");
306
307 hasRange_ = false;
308 rangeBegin_ = (std::numeric_limits< ::int64_t>::max)();
309 rangeEnd_ = (std::numeric_limits< ::int64_t>::max)();
310 if (range) {
311 std::string rangeHeader = range->value.str();
312
313 uint_parser< ::int64_t> const uint_max_p = uint_parser< ::int64_t>();
314 hasRange_ = parse(rangeHeader.c_str(),
315 str_p("bytes") >> ch_p('=') >>
316 (uint_max_p[assign_a(rangeBegin_)] >>
317 ch_p('-') >>
318 !uint_max_p[assign_a(rangeEnd_)]),
319 space_p).full;
320 if (hasRange_) {
321 // Validation of the Range header
322 if (rangeBegin_ > rangeEnd_)
323 hasRange_ = false;
324 }
325 }
326 }
327
328 }
329 }
330