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