diff --git a/src/main/c/CMakeLists.txt b/src/main/c/CMakeLists.txt index 9795b1c0..3b9283f0 100644 --- a/src/main/c/CMakeLists.txt +++ b/src/main/c/CMakeLists.txt @@ -56,6 +56,7 @@ set(SEASOCKS_SOURCE_FILES sha1/sha1.h StringUtil.cpp util/CrackedUri.cpp + util/FileResponse.cpp util/Json.cpp util/PathHandler.cpp util/RootPageHandler.cpp diff --git a/src/main/c/Connection.cpp b/src/main/c/Connection.cpp index a7ac96f2..ef56f08c 100644 --- a/src/main/c/Connection.cpp +++ b/src/main/c/Connection.cpp @@ -600,6 +600,9 @@ void Connection::sendHybi(uint8_t opcode, const uint8_t* webSocketResponse, size zlibContext.deflate(webSocketResponse, messageLength, compressed); + // Remove 4-byte tail end prior to transmission (see RFC 7692, section 7.2.1) + compressed.resize(compressed.size() - 4); + LS_DEBUG(_logger, "Compression result: " << messageLength << " bytes -> " << compressed.size() << " bytes"); sendHybiData(compressed.data(), compressed.size()); } else { diff --git a/src/main/c/Response.cpp b/src/main/c/Response.cpp index 60a3549a..1175f1c4 100644 --- a/src/main/c/Response.cpp +++ b/src/main/c/Response.cpp @@ -24,6 +24,7 @@ // POSSIBILITY OF SUCH DAMAGE. #include "internal/ConcreteResponse.h" +#include "seasocks/util/FileResponse.h" #include "seasocks/Response.h" @@ -69,4 +70,8 @@ std::shared_ptr Response::htmlResponse(const std::string& response) { SynchronousResponse::Headers(), true); } +std::shared_ptr Response::fileResponse(const Request &request, const std::string &filePath, const std::string &contentType, bool allowCompression, bool allowCaching) { + return std::make_shared(request, filePath, contentType, allowCompression, allowCaching); +} + } diff --git a/src/main/c/StringUtil.cpp b/src/main/c/StringUtil.cpp index edd5f9ac..4d1cf239 100644 --- a/src/main/c/StringUtil.cpp +++ b/src/main/c/StringUtil.cpp @@ -30,6 +30,7 @@ #include #include #include +#include namespace seasocks { @@ -125,6 +126,15 @@ std::string webtime(time_t time) { return buf; } +time_t webtime(std::string time) { + struct tm tm; + if (strptime(time.c_str(), "%a, %d %b %Y %H:%M:%S %Z", &tm) == nullptr) { + return -1; + } + tm.tm_isdst = -1; + return timegm(&tm); +} + std::string now() { return webtime(time(nullptr)); } diff --git a/src/main/c/seasocks/Response.h b/src/main/c/seasocks/Response.h index 0a77ca9f..6f73778c 100644 --- a/src/main/c/seasocks/Response.h +++ b/src/main/c/seasocks/Response.h @@ -25,6 +25,7 @@ #pragma once +#include "seasocks/Request.h" #include "seasocks/ResponseCode.h" #include @@ -52,6 +53,7 @@ class Response { static std::shared_ptr textResponse(const std::string& response); static std::shared_ptr jsonResponse(const std::string& response); static std::shared_ptr htmlResponse(const std::string& response); + static std::shared_ptr fileResponse(const Request &request, const std::string &filePath, const std::string &contentType, bool allowCompression, bool allowCaching); }; } diff --git a/src/main/c/seasocks/ResponseCodeDefs.h b/src/main/c/seasocks/ResponseCodeDefs.h index 53793866..a8c194ba 100644 --- a/src/main/c/seasocks/ResponseCodeDefs.h +++ b/src/main/c/seasocks/ResponseCodeDefs.h @@ -60,6 +60,9 @@ SEASOCKS_DEFINE_RESPONSECODE(402, PaymentRequired, "Payment Required") SEASOCKS_DEFINE_RESPONSECODE(403, Forbidden, "Forbidden") SEASOCKS_DEFINE_RESPONSECODE(404, NotFound, "Not Found") SEASOCKS_DEFINE_RESPONSECODE(405, MethodNotAllowed, "Method Not Allowed") +SEASOCKS_DEFINE_RESPONSECODE(412, PreconditionFailed, "Precondition Failed") +SEASOCKS_DEFINE_RESPONSECODE(416, RangeNotSatisfiable, "Range Not Satisfiable") + // more here... SEASOCKS_DEFINE_RESPONSECODE(500, InternalServerError, "Internal Server Error") diff --git a/src/main/c/seasocks/StringUtil.h b/src/main/c/seasocks/StringUtil.h index da227ce1..a3d006b4 100644 --- a/src/main/c/seasocks/StringUtil.h +++ b/src/main/c/seasocks/StringUtil.h @@ -48,6 +48,8 @@ void replace(std::string& string, const std::string& find, const std::string& re bool caseInsensitiveSame(const std::string &lhs, const std::string &rhs); std::string webtime(time_t time); +// Returns -1 on error +time_t webtime(std::string time); std::string now(); diff --git a/src/main/c/seasocks/ZlibContext.cpp b/src/main/c/seasocks/ZlibContext.cpp index acaaed46..e670de48 100644 --- a/src/main/c/seasocks/ZlibContext.cpp +++ b/src/main/c/seasocks/ZlibContext.cpp @@ -95,9 +95,6 @@ struct ZlibContext::Impl { output.insert(output.end(), buffer, buffer + sizeof(buffer) - deflateStream.avail_out); } while (deflateStream.avail_out == 0); - - // Remove 4-byte tail end prior to transmission (see RFC 7692, section 7.2.1) - output.resize(output.size() - 4); } bool inflate(std::vector &input, std::vector &output, int &zlibError) { @@ -142,4 +139,4 @@ bool ZlibContext::inflate(std::vector &input, std::vector &out return _impl->inflate(input, output, zlibError); } -} \ No newline at end of file +} diff --git a/src/main/c/seasocks/util/FileResponse.h b/src/main/c/seasocks/util/FileResponse.h new file mode 100644 index 00000000..a79d38ab --- /dev/null +++ b/src/main/c/seasocks/util/FileResponse.h @@ -0,0 +1,71 @@ +// Copyright (c) 2018, Joe Balough +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include + +#include "seasocks/Response.h" +#include "seasocks/ResponseWriter.h" +#include "seasocks/Request.h" +#include "seasocks/util/PathHandler.h" + +namespace seasocks { + +class FileResponse : public Response +{ +public: + /// Supports returning file data back to a client supporting deflate compression on the fly. + /// If compression is disabled, file resuming is supported. + /// If compression is enabled, files resumption is disabled and file size is not provided to the client. + /// These are only determinable if the file is compressed before transmission, which this Response does not do. + /// @param request Incoming request + /// @param filePath Path to file to return to client in filesystem + /// @param contentType Content-Type header value for file + /// @param allowCompression Whether to support compressing the file on the fly with deflate during transmission (disables resuming and content length) + /// @param allowCaching Whether the file contents are allowed to be cached + explicit FileResponse(const Request &request, const std::string &filePath, const std::string &contentType, bool allowCompression, bool allowCaching); + + virtual void handle(std::shared_ptr writer) override; + + /// Same as handle but does not spawn a thread + void respond(std::shared_ptr writer); + + virtual void cancel() override; + + +private: + + ResponseCode parseHeaders(const off_t fileSize, const time_t fileLastModified, bool sendCompressedData, off_t& fileTransferStart, off_t& fileTransferEnd) const; + + const Request &_request; + std::string _path; + std::string _contentType; + bool _allowCompression; + bool _allowCaching; + bool _cancelled; +}; + +} diff --git a/src/main/c/util/FileResponse.cpp b/src/main/c/util/FileResponse.cpp new file mode 100644 index 00000000..34f46c34 --- /dev/null +++ b/src/main/c/util/FileResponse.cpp @@ -0,0 +1,269 @@ +// Copyright (c) 2018, Joe Balough +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#include "seasocks/util/FileResponse.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include "seasocks/Server.h" +#include "seasocks/StringUtil.h" +#include "seasocks/ToString.h" +#include "seasocks/ZlibContext.h" + +using namespace seasocks; + +constexpr size_t ReadWriteBufferSize = 16 * 1024; + +FileResponse::FileResponse(const Request &request, const std::string &filePath, const std::string &contentType, bool allowCompression, bool allowCaching) : + _request(request), _path(filePath), _contentType(contentType), _allowCompression(allowCompression), _allowCaching(allowCaching), _cancelled(false) +{ +} + +void FileResponse::handle(std::shared_ptr writer) +{ + // Launch a thread to actually handle the request, capture this object and the writer object + std::thread t([this, writer] () mutable + { + respond(writer); + }); + t.detach(); +} + +void seasocks::FileResponse::respond(std::shared_ptr writer) +{ + Server &server = _request.server(); + ResponseCode requestResponse = ResponseCode::Ok; + std::string contentType = _contentType; + bool cacheable = _allowCaching; + bool sendCompressedData = false; + ZlibContext zlib; + + int input = ::open(_path.c_str(), O_RDONLY); + struct stat stat; + if (input == -1 || ::fstat(input, &stat) == -1) { + requestResponse = ResponseCode::NotFound; + } + + off_t size = stat.st_size; + time_t lastModified = stat.st_mtime; + + // Starting and ending position in the file to transfer + // By default, transferring the whole file + off_t start = 0; + off_t end = size; + + if (requestResponse != ResponseCode::NotFound) { + // Check if the client supports deflate compression + if (_request.hasHeader("Accept-Encoding") && _allowCompression) { + if (_request.getHeader("Accept-Encoding").find("deflate") != std::string::npos) { + sendCompressedData = true; + } + } + + // If compression is allowed, try to initialize the zlib context here + // It will throw a runtime_error if zlib was diabled in the build + if (sendCompressedData) { + try { + zlib.initialise(); + } + catch (std::runtime_error &e) + { + sendCompressedData = false; + } + } + + // Parse headers for a variety of conditions including range + // If no range headers were provided, start and end are left unmodified, + // otherwise, they are set to the start and end bytes requested + requestResponse = parseHeaders(size, lastModified, sendCompressedData, start, end); + + // Make sure the file can be seeked to the requested data + // This should really never happen so do it early to provide client a 500 if it does + if (requestResponse == ResponseCode::Ok || requestResponse == ResponseCode::PartialContent) { + if (::lseek(input, start, SEEK_SET) == -1) + { + requestResponse = ResponseCode::InternalServerError; + } + } + } + + + // Write headers back to client + server.execute([writer, requestResponse, contentType, lastModified, cacheable, sendCompressedData, start, end, size] { + writer->begin(requestResponse); + writer->header("Content-Type", contentType); + writer->header("Connection", "keep-alive"); + writer->header("Last-Modified", webtime(lastModified)); + writer->header("Vary", "Accept-Encoding"); + + if (sendCompressedData) { + writer->header("Content-Encoding", "deflate"); + } + else { + writer->header("Accept-Ranges", "bytes"); + writer->header("Content-Length", toString(end - start)); + + if (requestResponse == ResponseCode::RangeNotSatisfiable) { + // rfc7233 4.2 says Range Not Satisfiable responses should provide a special Content-Range header + writer->header("Content-Range", "bytes */" + toString(size)); + } + else { + writer->header("Content-Range", "bytes " + toString(start) + "-" + toString(end) + "/" + toString(size)); + } + } + + if (!cacheable) { + writer->header("Cache-Control", "no-store"); + writer->header("Pragma", "no-cache"); + writer->header("Expires", now()); + } + + // If the response is reporting some kind of failure, close the connection without writing anything else + if (requestResponse != ResponseCode::Ok && requestResponse != ResponseCode::PartialContent) { + writer->finish(false); + } + }); + + if (requestResponse != ResponseCode::Ok && requestResponse != ResponseCode::PartialContent) { + ::close(input); + return; + } + + std::vector compressionOutput; + compressionOutput.reserve(ReadWriteBufferSize); + auto bytesLeft = end - start; + while (bytesLeft && ! _cancelled) { + uint8_t buf[ReadWriteBufferSize]; + auto bytesRead = ::read(input, buf, std::min( (unsigned long) sizeof(buf), (unsigned long) bytesLeft)); + if (bytesRead < 0) { + // We can't send an error document as we've sent the header + break; + } + bytesLeft -= bytesRead; + // compress data if possible + if (sendCompressedData) { + zlib.deflate(buf, bytesRead, compressionOutput); + + server.execute([writer, compressionOutput] { + writer->payload(compressionOutput.data(), compressionOutput.size(), true); + }); + + compressionOutput.clear(); + } + else { + server.execute([writer, buf, bytesRead] { + writer->payload(buf, bytesRead, true); + }); + + } + } + + ::close(input); + + // All data written to client, close the connection + server.execute([writer] { + writer->finish(false); + }); +} + +void FileResponse::cancel() +{ + _cancelled = true; +} + +seasocks::ResponseCode FileResponse::parseHeaders(const off_t fileSize, const time_t fileLastModified, bool sendCompressedData, off_t& fileTransferStart, off_t& fileTransferEnd) const { + // Allowable format: ^bytes=([0-9]+)-([0-9]*)$ + // Do not support non-byte unit, multi-part range requests, or ETags + + if (_request.hasHeader("If-Modified-Since")) { + time_t ifModifiedSince = webtime(_request.getHeader("If-Modified-Since")); + if (fileLastModified <= ifModifiedSince) { + return ResponseCode::NotModified; + } + } + + if (_request.hasHeader("If-Unmodified-Since")) { + time_t ifUnmodifiedSince = webtime(_request.getHeader("If-Unmodified-Since")); + if (fileLastModified > ifUnmodifiedSince) { + return ResponseCode::PreconditionFailed; + } + } + + if (_request.hasHeader("If-Range") && !sendCompressedData) { + // If provided If-Range is not parseable, ifRange gets set to -1 which should definitely be less than the last modified time + time_t ifRange = webtime(_request.getHeader("If-Range")); + if (ifRange != fileLastModified) { + return ResponseCode::Ok; + } + } + + // Handle a Range Request + if (_request.hasHeader("Range") && !sendCompressedData) { + std::string rangeString = _request.getHeader("Range"); + + // Check for multipart range + std::regex multipartRegex("^\\S+=[0-9]+-[0-9]*, [0-9]+-[0-9]*"); + if ( std::regex_match(rangeString, multipartRegex) ) { + return ResponseCode::Ok; + } + + // Parse non-multipart range + std::regex rangeRegex("^bytes=([0-9]+)-([0-9]*)$"); + std::smatch rangeStrings; + if (! std::regex_match(rangeString, rangeStrings, rangeRegex) ) { + return ResponseCode::RangeNotSatisfiable; + } + + off_t parsedStart = atoi(rangeStrings[1].str().c_str()); + off_t parsedStop = atoi(rangeStrings[2].str().c_str()); + + // If no end specified, return data to end of file + if (rangeStrings[2].str().empty()) { + parsedStop = fileSize; + } + + // Check that start and stop are valid + if (parsedStart < 0 || parsedStart > fileSize) { + return ResponseCode::RangeNotSatisfiable; + } + if (parsedStop < parsedStart || parsedStop > fileSize) { + return ResponseCode::RangeNotSatisfiable; + } + + fileTransferStart = parsedStart; + fileTransferEnd = parsedStop; + + return ResponseCode::PartialContent; + } + + return ResponseCode::Ok; +}