pion-net  4.0.9
FileService.cpp
1 // ------------------------------------------------------------------
2 // pion-net: a C++ framework for building lightweight HTTP interfaces
3 // ------------------------------------------------------------------
4 // Copyright (C) 2007-2008 Atomic Labs, Inc. (http://www.atomiclabs.com)
5 //
6 // Distributed under the Boost Software License, Version 1.0.
7 // See http://www.boost.org/LICENSE_1_0.txt
8 //
9 
10 #include <boost/asio.hpp>
11 #include <boost/bind.hpp>
12 #include <boost/lexical_cast.hpp>
13 #include <boost/filesystem/operations.hpp>
14 #include <boost/filesystem/fstream.hpp>
15 #include <boost/algorithm/string/case_conv.hpp>
16 
17 #include "FileService.hpp"
18 #include <pion/PionPlugin.hpp>
19 #include <pion/net/HTTPResponseWriter.hpp>
20 
21 using namespace pion;
22 using namespace pion::net;
23 
24 namespace pion { // begin namespace pion
25 namespace plugins { // begin namespace plugins
26 
27 
28 // static members of FileService
29 
30 const std::string FileService::DEFAULT_MIME_TYPE("application/octet-stream");
31 const unsigned int FileService::DEFAULT_CACHE_SETTING = 1;
32 const unsigned int FileService::DEFAULT_SCAN_SETTING = 0;
33 const unsigned long FileService::DEFAULT_MAX_CACHE_SIZE = 0; /* 0=disabled */
34 const unsigned long FileService::DEFAULT_MAX_CHUNK_SIZE = 0; /* 0=disabled */
35 boost::once_flag FileService::m_mime_types_init_flag = BOOST_ONCE_INIT;
36 FileService::MIMETypeMap *FileService::m_mime_types_ptr = NULL;
37 
38 
39 // FileService member functions
40 
41 FileService::FileService(void)
42  : m_logger(PION_GET_LOGGER("pion.FileService")),
43  m_cache_setting(DEFAULT_CACHE_SETTING),
44  m_scan_setting(DEFAULT_SCAN_SETTING),
45  m_max_cache_size(DEFAULT_MAX_CACHE_SIZE),
46  m_max_chunk_size(DEFAULT_MAX_CHUNK_SIZE),
47  m_writable(false)
48 {}
49 
50 void FileService::setOption(const std::string& name, const std::string& value)
51 {
52  if (name == "directory") {
53  m_directory = value;
54  PionPlugin::checkCygwinPath(m_directory, value);
55  // make sure that the directory exists
56  if (! boost::filesystem::exists(m_directory) )
57  throw DirectoryNotFoundException(value);
58  if (! boost::filesystem::is_directory(m_directory) )
59  throw NotADirectoryException(value);
60  } else if (name == "file") {
61  m_file = value;
62  PionPlugin::checkCygwinPath(m_file, value);
63  // make sure that the directory exists
64  if (! boost::filesystem::exists(m_file) )
65  throw FileNotFoundException(value);
66  if (boost::filesystem::is_directory(m_file) )
67  throw NotAFileException(value);
68  } else if (name == "cache") {
69  if (value == "0") {
70  m_cache_setting = 0;
71  } else if (value == "1") {
72  m_cache_setting = 1;
73  } else if (value == "2") {
74  m_cache_setting = 2;
75  } else {
76  throw InvalidCacheException(value);
77  }
78  } else if (name == "scan") {
79  if (value == "0") {
80  m_scan_setting = 0;
81  } else if (value == "1") {
82  m_scan_setting = 1;
83  } else if (value == "2") {
84  m_scan_setting = 2;
85  } else if (value == "3") {
86  m_scan_setting = 3;
87  } else {
88  throw InvalidScanException(value);
89  }
90  } else if (name == "max_chunk_size") {
91  m_max_chunk_size = boost::lexical_cast<unsigned long>(value);
92  } else if (name == "writable") {
93  if (value == "true") {
94  m_writable = true;
95  } else if (value == "false") {
96  m_writable = false;
97  } else {
98  throw InvalidOptionValueException("writable", value);
99  }
100  } else {
101  throw UnknownOptionException(name);
102  }
103 }
104 
105 void FileService::operator()(HTTPRequestPtr& request, TCPConnectionPtr& tcp_conn)
106 {
107  // get the relative resource path for the request
108  const std::string relative_path(getRelativeResource(request->getResource()));
109 
110  // determine the path of the file being requested
111  boost::filesystem::path file_path;
112  if (relative_path.empty()) {
113  // request matches resource exactly
114 
115  if (m_file.empty()) {
116  // no file is specified, either in the request or in the options
117  PION_LOG_WARN(m_logger, "No file option defined ("
118  << getResource() << ")");
119  sendNotFoundResponse(request, tcp_conn);
120  return;
121  } else {
122  file_path = m_file;
123  }
124  } else {
125  // request does not match resource
126 
127  if (m_directory.empty()) {
128  // no directory is specified for the relative file
129  PION_LOG_WARN(m_logger, "No directory option defined ("
130  << getResource() << "): " << relative_path);
131  sendNotFoundResponse(request, tcp_conn);
132  return;
133  } else {
134  file_path = m_directory / relative_path;
135  }
136  }
137 
138  // make sure that the requested file is within the configured directory
139  file_path.normalize();
140  std::string file_string = file_path.string();
141  if (file_string.find(m_directory.string()) != 0) {
142  PION_LOG_WARN(m_logger, "Request for file outside of directory ("
143  << getResource() << "): " << relative_path);
144  static const std::string FORBIDDEN_HTML_START =
145  "<html><head>\n"
146  "<title>403 Forbidden</title>\n"
147  "</head><body>\n"
148  "<h1>Forbidden</h1>\n"
149  "<p>The requested URL ";
150  static const std::string FORBIDDEN_HTML_FINISH =
151  " is not in the configured directory.</p>\n"
152  "</body></html>\n";
153  HTTPResponseWriterPtr writer(HTTPResponseWriter::create(tcp_conn, *request,
154  boost::bind(&TCPConnection::finish, tcp_conn)));
155  writer->getResponse().setStatusCode(HTTPTypes::RESPONSE_CODE_FORBIDDEN);
156  writer->getResponse().setStatusMessage(HTTPTypes::RESPONSE_MESSAGE_FORBIDDEN);
157  if (request->getMethod() != HTTPTypes::REQUEST_METHOD_HEAD) {
158  writer->writeNoCopy(FORBIDDEN_HTML_START);
159  writer << request->getResource();
160  writer->writeNoCopy(FORBIDDEN_HTML_FINISH);
161  }
162  writer->send();
163  return;
164  }
165 
166  // requests specifying directories are not allowed
167  if (boost::filesystem::is_directory(file_path)) {
168  PION_LOG_WARN(m_logger, "Request for directory ("
169  << getResource() << "): " << relative_path);
170  static const std::string FORBIDDEN_HTML_START =
171  "<html><head>\n"
172  "<title>403 Forbidden</title>\n"
173  "</head><body>\n"
174  "<h1>Forbidden</h1>\n"
175  "<p>The requested URL ";
176  static const std::string FORBIDDEN_HTML_FINISH =
177  " is a directory.</p>\n"
178  "</body></html>\n";
179  HTTPResponseWriterPtr writer(HTTPResponseWriter::create(tcp_conn, *request,
180  boost::bind(&TCPConnection::finish, tcp_conn)));
181  writer->getResponse().setStatusCode(HTTPTypes::RESPONSE_CODE_FORBIDDEN);
182  writer->getResponse().setStatusMessage(HTTPTypes::RESPONSE_MESSAGE_FORBIDDEN);
183  if (request->getMethod() != HTTPTypes::REQUEST_METHOD_HEAD) {
184  writer->writeNoCopy(FORBIDDEN_HTML_START);
185  writer << request->getResource();
186  writer->writeNoCopy(FORBIDDEN_HTML_FINISH);
187  }
188  writer->send();
189  return;
190  }
191 
192  if (request->getMethod() == HTTPTypes::REQUEST_METHOD_GET
193  || request->getMethod() == HTTPTypes::REQUEST_METHOD_HEAD)
194  {
195  // the type of response we will send
196  enum ResponseType {
197  RESPONSE_UNDEFINED, // initial state until we know how to respond
198  RESPONSE_OK, // normal response that includes the file's content
199  RESPONSE_HEAD_OK, // response to HEAD request (would send file's content)
200  RESPONSE_NOT_FOUND, // Not Found (404)
201  RESPONSE_NOT_MODIFIED // Not Modified (304) response to If-Modified-Since
202  } response_type = RESPONSE_UNDEFINED;
203 
204  // used to hold our response information
205  DiskFile response_file;
206 
207  // get the If-Modified-Since request header
208  const std::string if_modified_since(request->getHeader(HTTPTypes::HEADER_IF_MODIFIED_SINCE));
209 
210  // check the cache for a corresponding entry (if enabled)
211  // note that m_cache_setting may equal 0 if m_scan_setting == 1
212  if (m_cache_setting > 0 || m_scan_setting > 0) {
213 
214  // search for a matching cache entry
215  boost::mutex::scoped_lock cache_lock(m_cache_mutex);
216  CacheMap::iterator cache_itr = m_cache_map.find(relative_path);
217 
218  if (cache_itr == m_cache_map.end()) {
219  // no existing cache entries found
220 
221  if (m_scan_setting == 1 || m_scan_setting == 3) {
222  // do not allow files to be added;
223  // all requests must correspond with existing cache entries
224  // since no match was found, just return file not found
225  PION_LOG_WARN(m_logger, "Request for unknown file ("
226  << getResource() << "): " << relative_path);
227  response_type = RESPONSE_NOT_FOUND;
228  } else {
229  PION_LOG_DEBUG(m_logger, "No cache entry for request ("
230  << getResource() << "): " << relative_path);
231  }
232 
233  } else {
234  // found an existing cache entry
235 
236  PION_LOG_DEBUG(m_logger, "Found cache entry for request ("
237  << getResource() << "): " << relative_path);
238 
239  if (m_cache_setting == 0) {
240  // cache is disabled
241 
242  // copy & re-use file_path and mime_type
243  response_file.setFilePath(cache_itr->second.getFilePath());
244  response_file.setMimeType(cache_itr->second.getMimeType());
245 
246  // get the file_size and last_modified timestamp
247  response_file.update();
248 
249  // just compare strings for simplicity (parsing this date format sucks!)
250  if (response_file.getLastModifiedString() == if_modified_since) {
251  // no need to read the file; the modified times match!
252  response_type = RESPONSE_NOT_MODIFIED;
253  } else {
254  if (request->getMethod() == HTTPTypes::REQUEST_METHOD_HEAD) {
255  response_type = RESPONSE_HEAD_OK;
256  } else {
257  response_type = RESPONSE_OK;
258  PION_LOG_DEBUG(m_logger, "Cache disabled, reading file ("
259  << getResource() << "): " << relative_path);
260  }
261  }
262 
263  } else {
264  // cache is enabled
265 
266  // true if the entry was updated (used for log message)
267  bool cache_was_updated = false;
268 
269  if (cache_itr->second.getLastModified() == 0) {
270 
271  // cache file for the first time
272  cache_was_updated = true;
273  cache_itr->second.update();
274  if (m_max_cache_size==0 || cache_itr->second.getFileSize() <= m_max_cache_size) {
275  // read the file (may throw exception)
276  cache_itr->second.read();
277  } else {
278  cache_itr->second.resetFileContent();
279  }
280 
281  } else if (m_cache_setting == 1) {
282 
283  // check if file has been updated (may throw exception)
284  cache_was_updated = cache_itr->second.checkUpdated();
285 
286  } // else cache_setting == 2 (use existing values)
287 
288  // get the response type
289  if (cache_itr->second.getLastModifiedString() == if_modified_since) {
290  response_type = RESPONSE_NOT_MODIFIED;
291  } else if (request->getMethod() == HTTPTypes::REQUEST_METHOD_HEAD) {
292  response_type = RESPONSE_HEAD_OK;
293  } else {
294  response_type = RESPONSE_OK;
295  }
296 
297  // copy cache contents so that we can release the mutex
298  response_file = cache_itr->second;
299 
300  PION_LOG_DEBUG(m_logger, (cache_was_updated ? "Updated" : "Using")
301  << " cache entry for request ("
302  << getResource() << "): " << relative_path);
303  }
304  }
305  }
306 
307  if (response_type == RESPONSE_UNDEFINED) {
308  // make sure that the file exists
309  if (! boost::filesystem::exists(file_path)) {
310  PION_LOG_WARN(m_logger, "File not found ("
311  << getResource() << "): " << relative_path);
312  sendNotFoundResponse(request, tcp_conn);
313  return;
314  }
315 
316  response_file.setFilePath(file_path);
317 
318  PION_LOG_DEBUG(m_logger, "Found file for request ("
319  << getResource() << "): " << relative_path);
320 
321  // determine the MIME type
322  response_file.setMimeType(findMIMEType( response_file.getFilePath().filename().native() ));
323 
324  // get the file_size and last_modified timestamp
325  response_file.update();
326 
327  // just compare strings for simplicity (parsing this date format sucks!)
328  if (response_file.getLastModifiedString() == if_modified_since) {
329  // no need to read the file; the modified times match!
330  response_type = RESPONSE_NOT_MODIFIED;
331  } else if (request->getMethod() == HTTPTypes::REQUEST_METHOD_HEAD) {
332  response_type = RESPONSE_HEAD_OK;
333  } else {
334  response_type = RESPONSE_OK;
335  if (m_cache_setting != 0) {
336  if (m_max_cache_size==0 || response_file.getFileSize() <= m_max_cache_size) {
337  // read the file (may throw exception)
338  response_file.read();
339  }
340  // add new entry to the cache
341  PION_LOG_DEBUG(m_logger, "Adding cache entry for request ("
342  << getResource() << "): " << relative_path);
343  boost::mutex::scoped_lock cache_lock(m_cache_mutex);
344  m_cache_map.insert( std::make_pair(relative_path, response_file) );
345  }
346  }
347  }
348 
349  if (response_type == RESPONSE_OK) {
350  // use DiskFileSender to send a file
351  DiskFileSenderPtr sender_ptr(DiskFileSender::create(response_file,
352  request, tcp_conn,
353  m_max_chunk_size));
354  sender_ptr->send();
355  } else if (response_type == RESPONSE_NOT_FOUND) {
356  sendNotFoundResponse(request, tcp_conn);
357  } else {
358  // sending headers only -> use our own response object
359 
360  // prepare a response and set the Content-Type
361  HTTPResponseWriterPtr writer(HTTPResponseWriter::create(tcp_conn, *request,
362  boost::bind(&TCPConnection::finish, tcp_conn)));
363  writer->getResponse().setContentType(response_file.getMimeType());
364 
365  // set Last-Modified header to enable client-side caching
366  writer->getResponse().addHeader(HTTPTypes::HEADER_LAST_MODIFIED,
367  response_file.getLastModifiedString());
368 
369  switch(response_type) {
370  case RESPONSE_UNDEFINED:
371  case RESPONSE_NOT_FOUND:
372  case RESPONSE_OK:
373  // this should never happen
374  throw UndefinedResponseException(request->getResource());
375  break;
376  case RESPONSE_NOT_MODIFIED:
377  // set "Not Modified" response
378  writer->getResponse().setStatusCode(HTTPTypes::RESPONSE_CODE_NOT_MODIFIED);
379  writer->getResponse().setStatusMessage(HTTPTypes::RESPONSE_MESSAGE_NOT_MODIFIED);
380  break;
381  case RESPONSE_HEAD_OK:
382  // set "OK" response (not really necessary since this is the default)
383  writer->getResponse().setStatusCode(HTTPTypes::RESPONSE_CODE_OK);
384  writer->getResponse().setStatusMessage(HTTPTypes::RESPONSE_MESSAGE_OK);
385  break;
386  }
387 
388  // send the response
389  writer->send();
390  }
391  } else if (request->getMethod() == HTTPTypes::REQUEST_METHOD_POST
392  || request->getMethod() == HTTPTypes::REQUEST_METHOD_PUT
393  || request->getMethod() == HTTPTypes::REQUEST_METHOD_DELETE)
394  {
395  // If not writable, then send 405 (Method Not Allowed) response for POST, PUT or DELETE requests.
396  if (!m_writable) {
397  static const std::string NOT_ALLOWED_HTML_START =
398  "<html><head>\n"
399  "<title>405 Method Not Allowed</title>\n"
400  "</head><body>\n"
401  "<h1>Not Allowed</h1>\n"
402  "<p>The requested method ";
403  static const std::string NOT_ALLOWED_HTML_FINISH =
404  " is not allowed on this server.</p>\n"
405  "</body></html>\n";
406  HTTPResponseWriterPtr writer(HTTPResponseWriter::create(tcp_conn, *request,
407  boost::bind(&TCPConnection::finish, tcp_conn)));
408  writer->getResponse().setStatusCode(HTTPTypes::RESPONSE_CODE_METHOD_NOT_ALLOWED);
409  writer->getResponse().setStatusMessage(HTTPTypes::RESPONSE_MESSAGE_METHOD_NOT_ALLOWED);
410  writer->writeNoCopy(NOT_ALLOWED_HTML_START);
411  writer << request->getMethod();
412  writer->writeNoCopy(NOT_ALLOWED_HTML_FINISH);
413  writer->getResponse().addHeader("Allow", "GET, HEAD");
414  writer->send();
415  } else {
416  HTTPResponseWriterPtr writer(HTTPResponseWriter::create(tcp_conn, *request,
417  boost::bind(&TCPConnection::finish, tcp_conn)));
418  if (request->getMethod() == HTTPTypes::REQUEST_METHOD_POST
419  || request->getMethod() == HTTPTypes::REQUEST_METHOD_PUT)
420  {
421  if (boost::filesystem::exists(file_path)) {
422  writer->getResponse().setStatusCode(HTTPTypes::RESPONSE_CODE_NO_CONTENT);
423  writer->getResponse().setStatusMessage(HTTPTypes::RESPONSE_MESSAGE_NO_CONTENT);
424  } else {
425  // The file doesn't exist yet, so it will be created below, unless the
426  // directory of the requested file also doesn't exist.
427  if (!boost::filesystem::exists(file_path.branch_path())) {
428  static const std::string NOT_FOUND_HTML_START =
429  "<html><head>\n"
430  "<title>404 Not Found</title>\n"
431  "</head><body>\n"
432  "<h1>Not Found</h1>\n"
433  "<p>The directory of the requested URL ";
434  static const std::string NOT_FOUND_HTML_FINISH =
435  " was not found on this server.</p>\n"
436  "</body></html>\n";
437  writer->getResponse().setStatusCode(HTTPTypes::RESPONSE_CODE_NOT_FOUND);
438  writer->getResponse().setStatusMessage(HTTPTypes::RESPONSE_MESSAGE_NOT_FOUND);
439  writer->writeNoCopy(NOT_FOUND_HTML_START);
440  writer << request->getResource();
441  writer->writeNoCopy(NOT_FOUND_HTML_FINISH);
442  writer->send();
443  return;
444  }
445  static const std::string CREATED_HTML_START =
446  "<html><head>\n"
447  "<title>201 Created</title>\n"
448  "</head><body>\n"
449  "<h1>Created</h1>\n"
450  "<p>";
451  static const std::string CREATED_HTML_FINISH =
452  "</p>\n"
453  "</body></html>\n";
454  writer->getResponse().setStatusCode(HTTPTypes::RESPONSE_CODE_CREATED);
455  writer->getResponse().setStatusMessage(HTTPTypes::RESPONSE_MESSAGE_CREATED);
456  writer->getResponse().addHeader(HTTPTypes::HEADER_LOCATION, request->getResource());
457  writer->writeNoCopy(CREATED_HTML_START);
458  writer << request->getResource();
459  writer->writeNoCopy(CREATED_HTML_FINISH);
460  }
461  std::ios_base::openmode mode = request->getMethod() == HTTPTypes::REQUEST_METHOD_POST?
462  std::ios::app : std::ios::out;
463  boost::filesystem::ofstream file_stream(file_path, mode);
464  file_stream.write(request->getContent(), request->getContentLength());
465  file_stream.close();
466  if (!boost::filesystem::exists(file_path)) {
467  static const std::string PUT_FAILED_HTML_START =
468  "<html><head>\n"
469  "<title>500 Server Error</title>\n"
470  "</head><body>\n"
471  "<h1>Server Error</h1>\n"
472  "<p>Error writing to ";
473  static const std::string PUT_FAILED_HTML_FINISH =
474  ".</p>\n"
475  "</body></html>\n";
476  writer->getResponse().setStatusCode(HTTPTypes::RESPONSE_CODE_SERVER_ERROR);
477  writer->getResponse().setStatusMessage(HTTPTypes::RESPONSE_MESSAGE_SERVER_ERROR);
478  writer->writeNoCopy(PUT_FAILED_HTML_START);
479  writer << request->getResource();
480  writer->writeNoCopy(PUT_FAILED_HTML_FINISH);
481  }
482  writer->send();
483  } else if (request->getMethod() == HTTPTypes::REQUEST_METHOD_DELETE) {
484  if (!boost::filesystem::exists(file_path)) {
485  sendNotFoundResponse(request, tcp_conn);
486  } else {
487  try {
488  boost::filesystem::remove(file_path);
489  writer->getResponse().setStatusCode(HTTPTypes::RESPONSE_CODE_NO_CONTENT);
490  writer->getResponse().setStatusMessage(HTTPTypes::RESPONSE_MESSAGE_NO_CONTENT);
491  writer->send();
492  } catch (...) {
493  static const std::string DELETE_FAILED_HTML_START =
494  "<html><head>\n"
495  "<title>500 Server Error</title>\n"
496  "</head><body>\n"
497  "<h1>Server Error</h1>\n"
498  "<p>Could not delete ";
499  static const std::string DELETE_FAILED_HTML_FINISH =
500  ".</p>\n"
501  "</body></html>\n";
502  writer->getResponse().setStatusCode(HTTPTypes::RESPONSE_CODE_SERVER_ERROR);
503  writer->getResponse().setStatusMessage(HTTPTypes::RESPONSE_MESSAGE_SERVER_ERROR);
504  writer->writeNoCopy(DELETE_FAILED_HTML_START);
505  writer << request->getResource();
506  writer->writeNoCopy(DELETE_FAILED_HTML_FINISH);
507  writer->send();
508  }
509  }
510  } else {
511  // This should never be reached.
512  writer->getResponse().setStatusCode(HTTPTypes::RESPONSE_CODE_SERVER_ERROR);
513  writer->getResponse().setStatusMessage(HTTPTypes::RESPONSE_MESSAGE_SERVER_ERROR);
514  writer->send();
515  }
516  }
517  }
518  // Any method not handled above is unimplemented.
519  else {
520  static const std::string NOT_IMPLEMENTED_HTML_START =
521  "<html><head>\n"
522  "<title>501 Not Implemented</title>\n"
523  "</head><body>\n"
524  "<h1>Not Implemented</h1>\n"
525  "<p>The requested method ";
526  static const std::string NOT_IMPLEMENTED_HTML_FINISH =
527  " is not implemented on this server.</p>\n"
528  "</body></html>\n";
529  HTTPResponseWriterPtr writer(HTTPResponseWriter::create(tcp_conn, *request,
530  boost::bind(&TCPConnection::finish, tcp_conn)));
531  writer->getResponse().setStatusCode(HTTPTypes::RESPONSE_CODE_NOT_IMPLEMENTED);
532  writer->getResponse().setStatusMessage(HTTPTypes::RESPONSE_MESSAGE_NOT_IMPLEMENTED);
533  writer->writeNoCopy(NOT_IMPLEMENTED_HTML_START);
534  writer << request->getMethod();
535  writer->writeNoCopy(NOT_IMPLEMENTED_HTML_FINISH);
536  writer->send();
537  }
538 }
539 
540 void FileService::sendNotFoundResponse(HTTPRequestPtr& http_request,
541  TCPConnectionPtr& tcp_conn)
542 {
543  static const std::string NOT_FOUND_HTML_START =
544  "<html><head>\n"
545  "<title>404 Not Found</title>\n"
546  "</head><body>\n"
547  "<h1>Not Found</h1>\n"
548  "<p>The requested URL ";
549  static const std::string NOT_FOUND_HTML_FINISH =
550  " was not found on this server.</p>\n"
551  "</body></html>\n";
552  HTTPResponseWriterPtr writer(HTTPResponseWriter::create(tcp_conn, *http_request,
553  boost::bind(&TCPConnection::finish, tcp_conn)));
554  writer->getResponse().setStatusCode(HTTPTypes::RESPONSE_CODE_NOT_FOUND);
555  writer->getResponse().setStatusMessage(HTTPTypes::RESPONSE_MESSAGE_NOT_FOUND);
556  if (http_request->getMethod() != HTTPTypes::REQUEST_METHOD_HEAD) {
557  writer->writeNoCopy(NOT_FOUND_HTML_START);
558  writer << http_request->getResource();
559  writer->writeNoCopy(NOT_FOUND_HTML_FINISH);
560  }
561  writer->send();
562 }
563 
565 {
566  PION_LOG_DEBUG(m_logger, "Starting up resource (" << getResource() << ')');
567 
568  // scan directory/file if scan setting != 0
569  if (m_scan_setting != 0) {
570  // force caching if scan == (2 | 3)
571  if (m_cache_setting == 0 && m_scan_setting > 1)
572  m_cache_setting = 1;
573 
574  boost::mutex::scoped_lock cache_lock(m_cache_mutex);
575 
576  // add entry for file if one is defined
577  if (! m_file.empty()) {
578  // use empty relative_path for file option
579  // use placeholder entry (do not pre-populate) if scan == 1
580  addCacheEntry("", m_file, m_scan_setting == 1);
581  }
582 
583  // scan directory if one is defined
584  if (! m_directory.empty())
585  scanDirectory(m_directory);
586  }
587 }
588 
590 {
591  PION_LOG_DEBUG(m_logger, "Shutting down resource (" << getResource() << ')');
592  // clear cached files (if started again, it will re-scan)
593  boost::mutex::scoped_lock cache_lock(m_cache_mutex);
594  m_cache_map.clear();
595 }
596 
597 void FileService::scanDirectory(const boost::filesystem::path& dir_path)
598 {
599  PION_LOG_DEBUG(m_logger, "Scanning directory (" << getResource() << "): "
600  << dir_path.string());
601 
602  // iterate through items in the directory
603  boost::filesystem::directory_iterator end_itr;
604  for ( boost::filesystem::directory_iterator itr( dir_path );
605  itr != end_itr; ++itr )
606  {
607  if ( boost::filesystem::is_directory(*itr) ) {
608  // item is a sub-directory
609 
610  // recursively call scanDirectory()
611  scanDirectory(*itr);
612 
613  } else {
614  // item is a regular file
615 
616  // figure out relative path to the file
617  std::string file_path_string( itr->path().string() );
618  std::string relative_path( file_path_string.substr(m_directory.string().size() + 1) );
619 
620  // add item to cache (use placeholder if scan == 1)
621  addCacheEntry(relative_path, *itr, m_scan_setting == 1);
622  }
623  }
624 }
625 
626 std::pair<FileService::CacheMap::iterator, bool>
627 FileService::addCacheEntry(const std::string& relative_path,
628  const boost::filesystem::path& file_path,
629  const bool placeholder)
630 {
631  DiskFile cache_entry(file_path, NULL, 0, 0, findMIMEType(file_path.filename().native()));
632  if (! placeholder) {
633  cache_entry.update();
634  // only read the file if its size is <= max_cache_size
635  if (m_max_cache_size==0 || cache_entry.getFileSize() <= m_max_cache_size) {
636  try { cache_entry.read(); }
637  catch (std::exception&) {
638  PION_LOG_ERROR(m_logger, "Unable to add file to cache: "
639  << file_path.string());
640  return std::make_pair(m_cache_map.end(), false);
641  }
642  }
643  }
644 
645  std::pair<CacheMap::iterator, bool> add_entry_result
646  = m_cache_map.insert( std::make_pair(relative_path, cache_entry) );
647 
648  if (add_entry_result.second) {
649  PION_LOG_DEBUG(m_logger, "Added file to cache: "
650  << file_path.string());
651  } else {
652  PION_LOG_ERROR(m_logger, "Unable to insert cache entry for file: "
653  << file_path.string());
654  }
655 
656  return add_entry_result;
657 }
658 
659 std::string FileService::findMIMEType(const std::string& file_name) {
660  // initialize m_mime_types if it hasn't been done already
661  boost::call_once(FileService::createMIMETypes, m_mime_types_init_flag);
662 
663  // determine the file's extension
664  std::string extension(file_name.substr(file_name.find_last_of('.') + 1));
665  boost::algorithm::to_lower(extension);
666 
667  // search for the matching mime type and return the result
668  MIMETypeMap::iterator i = m_mime_types_ptr->find(extension);
669  return (i == m_mime_types_ptr->end() ? DEFAULT_MIME_TYPE : i->second);
670 }
671 
672 void FileService::createMIMETypes(void) {
673  // create the map
674  static MIMETypeMap mime_types;
675 
676  // populate mime types
677  mime_types["js"] = "text/javascript";
678  mime_types["txt"] = "text/plain";
679  mime_types["xml"] = "text/xml";
680  mime_types["css"] = "text/css";
681  mime_types["htm"] = "text/html";
682  mime_types["html"] = "text/html";
683  mime_types["xhtml"] = "text/html";
684  mime_types["gif"] = "image/gif";
685  mime_types["png"] = "image/png";
686  mime_types["jpg"] = "image/jpeg";
687  mime_types["jpeg"] = "image/jpeg";
688  // ...
689 
690  // set the static pointer
691  m_mime_types_ptr = &mime_types;
692 }
693 
694 
695 // DiskFile member functions
696 
698 {
699  // set file_size and last_modified
700  m_file_size = boost::numeric_cast<std::streamsize>(boost::filesystem::file_size( m_file_path ));
701  m_last_modified = boost::filesystem::last_write_time( m_file_path );
702  m_last_modified_string = HTTPTypes::get_date_string( m_last_modified );
703 }
704 
705 void DiskFile::read(void)
706 {
707  // re-allocate storage buffer for the file's content
708  m_file_content.reset(new char[m_file_size]);
709 
710  // open the file for reading
711  boost::filesystem::ifstream file_stream;
712  file_stream.open(m_file_path, std::ios::in | std::ios::binary);
713 
714  // read the file into memory
715  if (!file_stream.is_open() || !file_stream.read(m_file_content.get(), m_file_size))
717 }
718 
720 {
721  // get current values
722  std::streamsize cur_size = boost::numeric_cast<std::streamsize>(boost::filesystem::file_size( m_file_path ));
723  time_t cur_modified = boost::filesystem::last_write_time( m_file_path );
724 
725  // check if file has not been updated
726  if (cur_modified == m_last_modified && cur_size == m_file_size)
727  return false;
728 
729  // file has been updated
730 
731  // update file_size and last_modified timestamp
732  m_file_size = cur_size;
733  m_last_modified = cur_modified;
734  m_last_modified_string = HTTPTypes::get_date_string( m_last_modified );
735 
736  // read new contents
737  read();
738 
739  return true;
740 }
741 
742 
743 // DiskFileSender member functions
744 
745 DiskFileSender::DiskFileSender(DiskFile& file, pion::net::HTTPRequestPtr& request,
746  pion::net::TCPConnectionPtr& tcp_conn,
747  unsigned long max_chunk_size)
748  : m_logger(PION_GET_LOGGER("pion.FileService.DiskFileSender")), m_disk_file(file),
749  m_writer(pion::net::HTTPResponseWriter::create(tcp_conn, *request, boost::bind(&TCPConnection::finish, tcp_conn))),
750  m_max_chunk_size(max_chunk_size), m_file_bytes_to_send(0), m_bytes_sent(0)
751 {
752  PION_LOG_DEBUG(m_logger, "Preparing to send file"
753  << (m_disk_file.hasFileContent() ? " (cached): " : ": ")
754  << m_disk_file.getFilePath().string());
755 
756  // set the Content-Type HTTP header using the file's MIME type
757  m_writer->getResponse().setContentType(m_disk_file.getMimeType());
758 
759  // set Last-Modified header to enable client-side caching
760  m_writer->getResponse().addHeader(HTTPTypes::HEADER_LAST_MODIFIED,
761  m_disk_file.getLastModifiedString());
762 
763  // use "200 OK" HTTP response
764  m_writer->getResponse().setStatusCode(HTTPTypes::RESPONSE_CODE_OK);
765  m_writer->getResponse().setStatusMessage(HTTPTypes::RESPONSE_MESSAGE_OK);
766 }
767 
769 {
770  // check if we have nothing to send (send 0 byte response content)
771  if (m_disk_file.getFileSize() <= m_bytes_sent) {
772  m_writer->send();
773  return;
774  }
775 
776  // calculate the number of bytes to send (m_file_bytes_to_send)
777  m_file_bytes_to_send = m_disk_file.getFileSize() - m_bytes_sent;
778  if (m_max_chunk_size > 0 && m_file_bytes_to_send > m_max_chunk_size)
779  m_file_bytes_to_send = m_max_chunk_size;
780 
781  // get the content to send (file_content_ptr)
782  char *file_content_ptr;
783 
784  if (m_disk_file.hasFileContent()) {
785 
786  // the entire file IS cached in memory (m_disk_file.file_content)
787  file_content_ptr = m_disk_file.getFileContent() + m_bytes_sent;
788 
789  } else {
790  // the file is not cached in memory
791 
792  // check if the file has been opened yet
793  if (! m_file_stream.is_open()) {
794  // open the file for reading
795  m_file_stream.open(m_disk_file.getFilePath(), std::ios::in | std::ios::binary);
796  if (! m_file_stream.is_open()) {
797  PION_LOG_ERROR(m_logger, "Unable to open file: "
798  << m_disk_file.getFilePath().string());
799  return;
800  }
801  }
802 
803  // check if the content buffer was initialized yet
804  if (! m_content_buf) {
805  // allocate memory for the new content buffer
806  m_content_buf.reset(new char[m_file_bytes_to_send]);
807  }
808  file_content_ptr = m_content_buf.get();
809 
810  // read a block of data from the file into the content buffer
811  if (! m_file_stream.read(m_content_buf.get(), m_file_bytes_to_send)) {
812  if (m_file_stream.gcount() > 0) {
813  PION_LOG_ERROR(m_logger, "File size inconsistency: "
814  << m_disk_file.getFilePath().string());
815  } else {
816  PION_LOG_ERROR(m_logger, "Unable to read file: "
817  << m_disk_file.getFilePath().string());
818  }
819  return;
820  }
821  }
822 
823  // send the content
824  m_writer->writeNoCopy(file_content_ptr, m_file_bytes_to_send);
825 
826  if (m_bytes_sent + m_file_bytes_to_send >= m_disk_file.getFileSize()) {
827  // this is the last piece of data to send
828  if (m_bytes_sent > 0) {
829  // send last chunk in a series
830  m_writer->sendFinalChunk(boost::bind(&DiskFileSender::handleWrite,
831  shared_from_this(),
832  boost::asio::placeholders::error,
833  boost::asio::placeholders::bytes_transferred));
834  } else {
835  // sending entire file at once
836  m_writer->send(boost::bind(&DiskFileSender::handleWrite,
837  shared_from_this(),
838  boost::asio::placeholders::error,
839  boost::asio::placeholders::bytes_transferred));
840  }
841  } else {
842  // there will be more data -> send a chunk
843  m_writer->sendChunk(boost::bind(&DiskFileSender::handleWrite,
844  shared_from_this(),
845  boost::asio::placeholders::error,
846  boost::asio::placeholders::bytes_transferred));
847  }
848 }
849 
850 void DiskFileSender::handleWrite(const boost::system::error_code& write_error,
851  std::size_t bytes_written)
852 {
853  bool finished_sending = true;
854 
855  if (write_error) {
856  // encountered error sending response data
857  m_writer->getTCPConnection()->setLifecycle(TCPConnection::LIFECYCLE_CLOSE); // make sure it will get closed
858  PION_LOG_WARN(m_logger, "Error sending file (" << write_error.message() << ')');
859  } else {
860  // response data sent OK
861 
862  // use m_file_bytes_to_send instead of bytes_written; bytes_written
863  // includes bytes for HTTP headers and chunking headers
864  m_bytes_sent += m_file_bytes_to_send;
865 
866  if (m_bytes_sent >= m_disk_file.getFileSize()) {
867  // finished sending
868  PION_LOG_DEBUG(m_logger, "Sent "
869  << (m_file_bytes_to_send < m_disk_file.getFileSize() ? "file chunk" : "complete file")
870  << " of " << m_file_bytes_to_send << " bytes (finished"
871  << (m_writer->getTCPConnection()->getKeepAlive() ? ", keeping alive)" : ", closing)") );
872  } else {
873  // NOT finished sending
874  PION_LOG_DEBUG(m_logger, "Sent file chunk of " << m_file_bytes_to_send << " bytes");
875  finished_sending = false;
876  m_writer->clear();
877  }
878  }
879 
880  if (finished_sending) {
881  // TCPConnection::finish() calls TCPServer::finishConnection, which will either:
882  // a) call HTTPServer::handleConnection again if keep-alive is true; or,
883  // b) close the socket and remove it from the server's connection pool
884  m_writer->getTCPConnection()->finish();
885  } else {
886  send();
887  }
888 }
889 
890 
891 } // end namespace plugins
892 } // end namespace pion
893 
894 
896 extern "C" PION_SERVICE_API pion::plugins::FileService *pion_create_FileService(void)
897 {
898  return new pion::plugins::FileService();
899 }
900 
902 extern "C" PION_SERVICE_API void pion_destroy_FileService(pion::plugins::FileService *service_ptr)
903 {
904  delete service_ptr;
905 }