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