blob: 12e8f75aa7bd77d2e81d5d3a7a4d09dd915854aa [file] [log] [blame]
#include <unistd.h>
#include <sys/stat.h>
#include <algorithm>
#include <map>
#include <string>
#include <vector>
#include <boost/shared_array.hpp>
#include <process/deferred.hpp> // TODO(benh): This is required by Clang.
#include <process/dispatch.hpp>
#include <process/future.hpp>
#include <process/http.hpp>
#include <process/io.hpp>
#include <process/mime.hpp>
#include <process/process.hpp>
#include <stout/error.hpp>
#include <stout/hashmap.hpp>
#include <stout/json.hpp>
#include <stout/lambda.hpp>
#include <stout/none.hpp>
#include <stout/numify.hpp>
#include <stout/option.hpp>
#include <stout/os.hpp>
#include <stout/path.hpp>
#include <stout/result.hpp>
#include <stout/stringify.hpp>
#include <stout/strings.hpp>
#include <stout/try.hpp>
#include "files/files.hpp"
#include "logging/logging.hpp"
using namespace process;
using process::wait; // Necessary on some OS's to disambiguate.
using process::http::BadRequest;
using process::http::InternalServerError;
using process::http::NotFound;
using process::http::OK;
using process::http::Response;
using process::http::Request;
using std::list;
using std::map;
using std::string;
using std::vector;
namespace mesos {
namespace internal {
class FilesProcess : public Process<FilesProcess>
{
public:
FilesProcess();
// Files implementation.
Future<Nothing> attach(const string& path, const string& name);
void detach(const string& name);
protected:
virtual void initialize();
private:
// Resolves the virtual path to an actual path.
// Returns the actual path if found.
// Returns None if the file is not found.
// Returns Error if we find the file but it cannot be resolved or it breaks
// out of the chroot.
Result<string> resolve(const string& path);
// HTTP endpoints.
// Returns a file listing for a directory.
// Requests have the following parameters:
// path: The directory to browse. Required.
// The response will contain a list of JSON files and directories contained
// in the path (see files::jsonFileInfo for the format).
Future<Response> browse(const Request& request);
// Reads data from a file at a given offset and for a given length.
// See the jquery pailer for the expected behavior.
Future<Response> read(const Request& request);
// Returns the raw file contents for a given path.
// Requests have the following parameters:
// path: The directory to browse. Required.
Future<Response> download(const Request& request);
// Returns the internal virtual path mapping.
Future<Response> debug(const Request& request);
hashmap<string, string> paths;
};
FilesProcess::FilesProcess()
: ProcessBase("files")
{}
void FilesProcess::initialize()
{
route("/browse.json", None(), &FilesProcess::browse);
route("/read.json", None(), &FilesProcess::read);
route("/download.json", None(), &FilesProcess::download);
route("/debug.json", None(), &FilesProcess::debug);
}
Future<Nothing> FilesProcess::attach(const string& path, const string& name)
{
Result<string> result = os::realpath(path);
if (!result.isSome()) {
return Failure(
"Failed to get realpath of '" + path + "': " +
(result.isError()
? result.error()
: "No such file or directory"));
}
// Make sure we have permissions to read the file/dir.
Try<bool> access = os::access(result.get(), R_OK);
if (access.isError() || !access.get()) {
return Failure("Failed to access '" + path + "': " +
(access.isError() ? access.error() : "Access denied"));
}
// To simplify the read/browse logic, strip any trailing / from the name.
string cleanedName = strings::remove(name, "/", strings::SUFFIX);
// TODO(bmahler): Do we want to always wipe out the previous path?
paths[cleanedName] = result.get();
return Nothing();
}
void FilesProcess::detach(const string& name)
{
paths.erase(name);
}
Future<Response> FilesProcess::browse(const Request& request)
{
Option<string> path = request.query.get("path");
if (!path.isSome() || path.get().empty()) {
return BadRequest("Expecting 'path=value' in query.\n");
}
Result<string> resolvedPath = resolve(path.get());
if (resolvedPath.isError()) {
return InternalServerError(resolvedPath.error() + ".\n");
} else if (resolvedPath.isNone()) {
return NotFound();
}
// The result will be a sorted (on path) array of files and dirs:
// [{"name": "README", "path": "dir/README" "dir":False, "size":42}, ...]
map<string, JSON::Object> files;
Try<list<string> > entries = os::ls(resolvedPath.get());
if (entries.isSome()) {
foreach (const string& entry, entries.get()) {
struct stat s;
string fullPath = path::join(resolvedPath.get(), entry);
if (stat(fullPath.c_str(), &s) < 0) {
PLOG(WARNING) << "Found " << fullPath << " in ls but stat failed";
continue;
}
files[fullPath] = jsonFileInfo(path::join(path.get(), entry), s);
}
}
JSON::Array listing;
foreachvalue(const JSON::Object& file, files) {
listing.values.push_back(file);
}
return OK(listing, request.query.get("jsonp"));
}
// TODO(benh): Remove 'const &' from size after fixing libprocess.
Future<Response> _read(int fd,
const size_t& size,
off_t offset,
const boost::shared_array<char>& data,
const Option<string>& jsonp) {
JSON::Object object;
object.values["offset"] = offset;
object.values["data"] = string(data.get(), size);
os::close(fd);
return OK(object, jsonp);
}
Future<Response> FilesProcess::read(const Request& request)
{
Option<string> path = request.query.get("path");
if (!path.isSome() || path.get().empty()) {
return BadRequest("Expecting 'path=value' in query.\n");
}
off_t offset = -1;
if (request.query.get("offset").isSome()) {
Try<off_t> result = numify<off_t>(request.query.get("offset").get());
if (result.isError()) {
return BadRequest("Failed to parse offset: " + result.error() + ".\n");
}
offset = result.get();
}
ssize_t length = -1;
if (request.query.get("length").isSome()) {
Try<ssize_t> result = numify<ssize_t>(request.query.get("length").get());
if (result.isError()) {
return BadRequest("Failed to parse length: " + result.error() + ".\n");
}
length = result.get();
}
Result<string> resolvedPath = resolve(path.get());
if (resolvedPath.isError()) {
return BadRequest(resolvedPath.error() + ".\n");
} else if (!resolvedPath.isSome()) {
return NotFound();
}
// Don't read directories.
if (os::isdir(resolvedPath.get())) {
return BadRequest("Cannot read a directory.\n");
}
// TODO(benh): Cache file descriptors so we aren't constantly
// opening them and paging the data in from disk.
Try<int> fd = os::open(resolvedPath.get(), O_RDONLY);
if (fd.isError()) {
string error = strings::format("Failed to open file at '%s': %s",
resolvedPath.get(), fd.error()).get();
LOG(WARNING) << error;
return InternalServerError(error + ".\n");
}
off_t size = lseek(fd.get(), 0, SEEK_END);
if (size == -1) {
string error = strings::format("Failed to open file at '%s': %s",
resolvedPath.get(), strerror(errno)).get();
LOG(WARNING) << error;
os::close(fd.get());
return InternalServerError(error + ".\n");
}
if (offset == -1) {
offset = size;
}
if (length == -1) {
length = size - offset;
}
// Cap the read length at 16 pages.
length = std::min<ssize_t>(length, sysconf(_SC_PAGE_SIZE) * 16);
if (offset >= size) {
os::close(fd.get());
JSON::Object object;
object.values["offset"] = size;
object.values["data"] = "";
return OK(object, request.query.get("jsonp"));
}
// Seek to the offset we want to read from.
if (lseek(fd.get(), offset, SEEK_SET) == -1) {
string error = strings::format("Failed to seek file at '%s': %s",
resolvedPath.get(), strerror(errno)).get();
LOG(WARNING) << error;
os::close(fd.get());
return InternalServerError(error);
}
Try<Nothing> nonblock = os::nonblock(fd.get());
if (nonblock.isError()) {
string error =
"Failed to set file descriptor nonblocking: " + nonblock.error();
LOG(WARNING) << error;
return InternalServerError(error);
}
// Read 'length' bytes (or to EOF).
boost::shared_array<char> data(new char[length]);
return io::read(fd.get(), data.get(), static_cast<size_t>(length))
.then(lambda::bind(
_read,
fd.get(),
lambda::_1,
offset,
data,
request.query.get("jsonp")));
}
Future<Response> FilesProcess::download(const Request& request)
{
Option<string> path = request.query.get("path");
if (!path.isSome() || path.get().empty()) {
return BadRequest("Expecting 'path=value' in query.\n");
}
Result<string> resolvedPath = resolve(path.get());
if (resolvedPath.isError()) {
return BadRequest(resolvedPath.error() + ".\n");
} else if (!resolvedPath.isSome()) {
return NotFound();
}
// Don't download directories.
if (os::isdir(resolvedPath.get())) {
return BadRequest("Cannot download a directory.\n");
}
Try<string> basename = os::basename(resolvedPath.get());
if (basename.isError()) {
LOG(ERROR) << basename.error();
return InternalServerError(basename.error() + ".\n");
}
OK response;
response.type = response.PATH;
response.path = resolvedPath.get();
response.headers["Content-Type"] = "application/octet-stream";
response.headers["Content-Disposition"] =
strings::format("attachment; filename=%s", basename.get()).get();
// Attempt to detect the mime type.
size_t index = basename.get().find_last_of('.');
if (index != string::npos) {
string extension = basename.get().substr(index);
if (mime::types.count(extension) > 0) {
response.headers["Content-Type"] = mime::types[extension];
}
}
return response;
}
Future<Response> FilesProcess::debug(const Request& request)
{
JSON::Object object;
foreachpair(const string& name, const string& path, paths) {
object.values[name] = path;
}
return OK(object, request.query.get("jsonp"));
}
Result<string> FilesProcess::resolve(const string& path)
{
// Suppose we have: /1/2/hello_world.txt
// And we attach: /1/2 as /sandbox
// Then this function would resolve the following virtual path
// into the actual path:
// input: /sandbox/hello_world.txt
// output: /1/2/hello_world.txt
//
// Try and see if this path has been attached. We check for the
// longest possible prefix match and if found append any suffix to
// the attached path (provided the path is to a directory).
vector<string> tokens = strings::split(
strings::remove(path, "/", strings::SUFFIX), "/");
string suffix;
while (!tokens.empty()) {
string prefix = path::join(tokens);
if (!paths.contains(prefix)) {
if (suffix.empty()) {
suffix = tokens.back();
} else {
suffix = path::join(tokens.back(), suffix);
}
tokens.pop_back();
continue;
}
// Determine the final path: if it's a directory, append the
// suffix, if it's not a directory and there is a suffix, return
// 'Not Found'.
string path = paths[prefix];
if (os::isdir(path)) {
path = path::join(path, suffix);
// Canonicalize the absolute path.
Result<string> realpath = os::realpath(path);
if (realpath.isError()) {
return Error(
"Failed to determine canonical path of '" + path +
"': " + realpath.error());
} else if (realpath.isNone()) {
return None();
}
// Make sure the canonicalized absolute path is accessible
// (i.e., not outside the "chroot").
if (!strings::startsWith(realpath.get(), paths[prefix])) {
return Error("'" + path + "' is inaccessible");
}
path = realpath.get();
} else if (suffix != "") {
// Request is assuming attached path is a directory, but it is
// not! Rather than 'Bad Request', treat this as 'Not Found'.
return None();
}
return path;
}
return None();
}
Files::Files()
{
process = new FilesProcess();
spawn(process);
}
Files::~Files()
{
terminate(process);
wait(process);
delete process;
}
Future<Nothing> Files::attach(const string& path, const string& name)
{
return dispatch(process, &FilesProcess::attach, path, name);
}
void Files::detach(const string& name)
{
dispatch(process, &FilesProcess::detach, name);
}
} // namespace internal {
} // namespace mesos {