mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-01-30 16:06:00 -05:00
055883d248
This wraps libevent's evhttp_parse_query_str and friends. It's easier to use than the raw libevent stuff because it handles initialization (formerly not done properly in profiler.cc) and cleans up with RAII.
325 lines
11 KiB
C++
325 lines
11 KiB
C++
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
|
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// In addition, as a special exception, the copyright holders give
|
|
// permission to link the code of portions of this program with the
|
|
// OpenSSL library under certain conditions as described in each
|
|
// individual source file, and distribute linked combinations including
|
|
// the two.
|
|
//
|
|
// You must obey the GNU General Public License in all respects for all
|
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
|
// exception, you may extend this exception to your version of the
|
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
|
// so, delete this exception statement from your version. If you delete
|
|
// this exception statement from all source files in the program, then
|
|
// also delete it here.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
//
|
|
// http.h: classes for HTTP serving. In particular, there are helpers for
|
|
// serving HTTP byte range requests with libevent.
|
|
|
|
#ifndef MOONFIRE_NVR_HTTP_H
|
|
#define MOONFIRE_NVR_HTTP_H
|
|
|
|
#include <dirent.h>
|
|
#include <stdarg.h>
|
|
#include <sys/queue.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <string>
|
|
|
|
#include <event2/buffer.h>
|
|
#include <event2/keyvalq_struct.h>
|
|
#include <event2/http.h>
|
|
#include <glog/logging.h>
|
|
#include <re2/stringpiece.h>
|
|
|
|
#include "string.h"
|
|
|
|
namespace moonfire_nvr {
|
|
|
|
// Single-use object to represent a set of HTTP query parameters.
|
|
class QueryParameters {
|
|
public:
|
|
// Parse parameters from the given URI.
|
|
// Caller should check ok() afterward.
|
|
QueryParameters(const char *uri) {
|
|
TAILQ_INIT(&me_);
|
|
ok_ = evhttp_parse_query_str(uri, &me_) == 0;
|
|
}
|
|
QueryParameters(const QueryParameters &) = delete;
|
|
void operator=(const QueryParameters &) = delete;
|
|
|
|
~QueryParameters() { evhttp_clear_headers(&me_); }
|
|
|
|
bool ok() const { return ok_; }
|
|
const char *Get(const char *param) const {
|
|
return evhttp_find_header(&me_, param);
|
|
}
|
|
|
|
private:
|
|
struct evkeyvalq me_;
|
|
bool ok_ = false;
|
|
};
|
|
|
|
// Wrapped version of libevent's "struct evbuffer" which uses RAII and simply
|
|
// aborts the process if allocations fail. (Moonfire NVR is intended to run on
|
|
// Linux systems with the default vm.overcommit_memory=0, so there's probably
|
|
// no point in trying to gracefully recover from a condition that's unlikely
|
|
// to ever happen.)
|
|
class EvBuffer {
|
|
public:
|
|
EvBuffer() { buf_ = CHECK_NOTNULL(evbuffer_new()); }
|
|
EvBuffer(const EvBuffer &) = delete;
|
|
EvBuffer &operator=(const EvBuffer &) = delete;
|
|
~EvBuffer() { evbuffer_free(buf_); }
|
|
|
|
struct evbuffer *get() {
|
|
return buf_;
|
|
}
|
|
|
|
void Add(const re2::StringPiece &s) {
|
|
CHECK_EQ(0, evbuffer_add(buf_, s.data(), s.size()));
|
|
}
|
|
|
|
void AddPrintf(const char *fmt, ...) __attribute__((format(printf, 2, 3))) {
|
|
va_list argp;
|
|
va_start(argp, fmt);
|
|
CHECK_LE(0, evbuffer_add_vprintf(buf_, fmt, argp));
|
|
va_end(argp);
|
|
}
|
|
|
|
// Delegates to evbuffer_add_file.
|
|
// On success, |fd| will be closed by libevent. On failure, it remains open.
|
|
bool AddFile(int fd, ev_off_t offset, ev_off_t length,
|
|
std::string *error_message);
|
|
|
|
void AddReference(const void *data, size_t datlen,
|
|
evbuffer_ref_cleanup_cb cleanupfn, void *cleanupfn_arg) {
|
|
CHECK_EQ(
|
|
0, evbuffer_add_reference(buf_, data, datlen, cleanupfn, cleanupfn_arg))
|
|
<< strerror(errno);
|
|
}
|
|
|
|
private:
|
|
struct evbuffer *buf_;
|
|
};
|
|
|
|
struct ByteRange {
|
|
ByteRange() {}
|
|
ByteRange(int64_t begin, int64_t end) : begin(begin), end(end) {}
|
|
int64_t begin = 0;
|
|
int64_t end = 0; // exclusive.
|
|
int64_t size() const { return end - begin; }
|
|
bool operator==(const ByteRange &o) const {
|
|
return begin == o.begin && end == o.end;
|
|
}
|
|
std::string DebugString() const { return StrCat("[", begin, ", ", end, ")"); }
|
|
};
|
|
|
|
inline std::ostream &operator<<(std::ostream &out, const ByteRange &range) {
|
|
return out << range.DebugString();
|
|
}
|
|
|
|
// Helper for sending HTTP errors based on POSIX error returns.
|
|
void HttpSendError(evhttp_request *req, int http_err, const std::string &prefix,
|
|
int posix_errno);
|
|
|
|
class FileSlice {
|
|
public:
|
|
virtual ~FileSlice() {}
|
|
|
|
virtual int64_t size() const = 0;
|
|
|
|
// Add some to all of the given non-empty |range| to |buf|.
|
|
// Returns the number of bytes added, or < 0 on error.
|
|
// On error, |error_message| should be populated. (|error_message| may also be
|
|
// populated if 0 <= return value < range.size(), such as if one of a
|
|
// FileSlices object's failed. However, it's safe to simply retry such
|
|
// partial failures later.)
|
|
virtual int64_t AddRange(ByteRange range, EvBuffer *buf,
|
|
std::string *error_message) const = 0;
|
|
};
|
|
|
|
class VirtualFile : public FileSlice {
|
|
public:
|
|
virtual ~VirtualFile() {}
|
|
|
|
// Return the given property of the file.
|
|
virtual time_t last_modified() const = 0;
|
|
virtual std::string etag() const = 0;
|
|
virtual std::string mime_type() const = 0;
|
|
};
|
|
|
|
class RealFileSlice : public FileSlice {
|
|
public:
|
|
void Init(re2::StringPiece filename, ByteRange range);
|
|
|
|
int64_t size() const final { return range_.size(); }
|
|
|
|
int64_t AddRange(ByteRange range, EvBuffer *buf,
|
|
std::string *error_message) const final;
|
|
|
|
private:
|
|
std::string filename_;
|
|
ByteRange range_;
|
|
};
|
|
|
|
// A FileSlice of a pre-defined length which calls a function which fills the
|
|
// slice on demand. The FillerFileSlice is responsible for subsetting.
|
|
class FillerFileSlice : public FileSlice {
|
|
public:
|
|
using FillFunction =
|
|
std::function<bool(std::string *slice, std::string *error_message)>;
|
|
|
|
void Init(size_t size, FillFunction fn) {
|
|
fn_ = fn;
|
|
size_ = size;
|
|
}
|
|
|
|
int64_t size() const final { return size_; }
|
|
|
|
int64_t AddRange(ByteRange range, EvBuffer *buf,
|
|
std::string *error_message) const final;
|
|
|
|
private:
|
|
FillFunction fn_;
|
|
size_t size_;
|
|
};
|
|
|
|
// A FileSlice backed by in-memory data which lives forever (static data).
|
|
class StaticStringPieceSlice : public FileSlice {
|
|
public:
|
|
explicit StaticStringPieceSlice(re2::StringPiece piece) : piece_(piece) {}
|
|
|
|
int64_t size() const final { return piece_.size(); }
|
|
int64_t AddRange(ByteRange range, EvBuffer *buf,
|
|
std::string *error_message) const final;
|
|
|
|
private:
|
|
re2::StringPiece piece_;
|
|
};
|
|
|
|
// A FileSlice backed by in-memory data which should be copied.
|
|
class CopyingStringPieceSlice : public FileSlice {
|
|
public:
|
|
explicit CopyingStringPieceSlice(re2::StringPiece piece) : piece_(piece) {}
|
|
|
|
int64_t size() const final { return piece_.size(); }
|
|
int64_t AddRange(ByteRange range, EvBuffer *buf,
|
|
std::string *error_message) const final;
|
|
|
|
private:
|
|
re2::StringPiece piece_;
|
|
};
|
|
|
|
// A slice composed of other slices.
|
|
class FileSlices : public FileSlice {
|
|
public:
|
|
FileSlices() {}
|
|
FileSlices(const FileSlices &) = delete;
|
|
FileSlices &operator=(const FileSlices &) = delete;
|
|
|
|
// |slice| must outlive the FileSlices.
|
|
// |slice->size()| should not change after this call.
|
|
// |flags| should be a bitmask of Flags values below.
|
|
void Append(const FileSlice *slice, int flags = 0) {
|
|
int64_t new_size = size_ + slice->size();
|
|
slices_.emplace_back(ByteRange(size_, new_size), slice, flags);
|
|
size_ = new_size;
|
|
}
|
|
|
|
int64_t size() const final { return size_; }
|
|
int64_t AddRange(ByteRange range, EvBuffer *buf,
|
|
std::string *error_message) const final;
|
|
|
|
enum Flags {
|
|
// kLazy, as an argument to Append, instructs the FileSlices to append
|
|
// this slice in AddRange only if it is the first slice in the requested
|
|
// range. Otherwise it returns early, expecting HttpServe to call AddRange
|
|
// again after the earlier ranges have been sent. This is useful if it is
|
|
// expensive to have the given slice pending. In particular, it is useful
|
|
// when serving many file slices on 32-bit machines to avoid exhausting
|
|
// the address space with too many memory mappings.
|
|
kLazy = 1
|
|
};
|
|
|
|
private:
|
|
struct SliceInfo {
|
|
SliceInfo(ByteRange range, const FileSlice *slice, int flags)
|
|
: range(range), slice(slice), flags(flags) {}
|
|
ByteRange range;
|
|
const FileSlice *slice = nullptr;
|
|
int flags;
|
|
};
|
|
int64_t size_ = 0;
|
|
|
|
std::vector<SliceInfo> slices_;
|
|
};
|
|
|
|
// Serve an HTTP request |req| from |file|, handling byte range and
|
|
// conditional serving. (Similar to golang's http.ServeContent.)
|
|
//
|
|
// |file| only needs to live through the call to HttpServe itself.
|
|
// This contract may change in the future; currently all the ranges are added
|
|
// at the beginning of the request, so if large memory-backed buffers (as
|
|
// opposed to file-backed buffers) are used, the program's memory usage will
|
|
// spike, even if the HTTP client aborts early in the request. If this becomes
|
|
// problematic, this interface may change to take advantage of
|
|
// evbuffer_add_cb, adding buffers incrementally, and some mechanism will be
|
|
// added to guarantee VirtualFile objects outlive the HTTP requests they serve.
|
|
void HttpServe(const std::shared_ptr<VirtualFile> &file, evhttp_request *req);
|
|
|
|
// Serve a file over HTTP. Expects the caller to supply a sanitized |filename|
|
|
// (rather than taking it straight from the path specified in |req|).
|
|
void HttpServeFile(evhttp_request *req, const std::string &mime_type,
|
|
const std::string &filename, const struct stat &statbuf);
|
|
|
|
namespace internal {
|
|
|
|
// Value to represent result of parsing HTTP 1.1 "Range:" header.
|
|
enum class RangeHeaderType {
|
|
// Ignore the header, serving all bytes in the file.
|
|
kAbsentOrInvalid,
|
|
|
|
// The server SHOULD return a response with status 416 (Requested range not
|
|
// satisfiable).
|
|
kNotSatisfiable,
|
|
|
|
// The server SHOULD return a response with status 406 (Partial Content).
|
|
kSatisfiable
|
|
};
|
|
|
|
// Parse an HTTP 1.1 "Range:" header value, following RFC 2616 section 14.35.
|
|
// This function is for use by HttpServe; it is exposed for testing only.
|
|
//
|
|
// |value| on entry should be the header value (after the ": "), or nullptr.
|
|
// |size| on entry should be the number of bytes available to serve.
|
|
// On kSatisfiable return, |ranges| will be filled with the satisfiable ranges.
|
|
// Otherwise, its contents are undefined.
|
|
RangeHeaderType ParseRangeHeader(const char *value, int64_t size,
|
|
std::vector<ByteRange> *ranges);
|
|
|
|
} // namespace internal
|
|
|
|
} // namespace moonfire_nvr
|
|
|
|
#endif // MOONFIRE_NVR_HTTP_H
|