Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

server support partial ranges and byteranges #476

Merged
merged 3 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions include/cinatra/coro_http_request.hpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,113 @@
#pragma once
#include <charconv>

#include "async_simple/coro/Lazy.h"
#include "define.h"
#include "http_parser.hpp"
#include "ws_define.h"

namespace cinatra {
inline std::vector<std::string_view> split_sv(std::string_view s,
std::string_view delimiter) {
size_t start = 0;
size_t end = s.find_first_of(delimiter);

std::vector<std::string_view> output;

while (end <= std::string_view::npos) {
output.emplace_back(s.substr(start, end - start));

if (end == std::string_view::npos)
break;

start = end + 1;
end = s.find_first_of(delimiter, start);
}

return output;
}

inline std::string_view trim_sv(std::string_view v) {
v.remove_prefix((std::min)(v.find_first_not_of(" "), v.size()));
v.remove_suffix((std::min)(v.size() - v.find_last_not_of(" ") - 1, v.size()));
return v;
}

inline std::vector<std::pair<int, int>> parse_ranges(std::string_view range_str,
size_t file_size,
bool& is_valid) {
range_str = trim_sv(range_str);
if (range_str.empty()) {
return {{0, file_size - 1}};
}

if (range_str.find("--") != std::string_view::npos) {
is_valid = false;
return {};
}

if (range_str == "-") {
return {{0, file_size - 1}};
}

std::vector<std::pair<int, int>> vec;
auto ranges = split_sv(range_str, ",");
for (auto range : ranges) {
auto sub_range = split_sv(range, "-");
auto fist_range = trim_sv(sub_range[0]);

int start = 0;
if (fist_range.empty()) {
start = -1;
}
else {
auto [ptr, ec] = std::from_chars(
fist_range.data(), fist_range.data() + fist_range.size(), start);
if (ec != std::errc{}) {
is_valid = false;
return {};
}
}

int end = 0;
if (sub_range.size() == 1) {
end = file_size - 1;
}
else {
auto second_range = trim_sv(sub_range[1]);
if (second_range.empty()) {
end = file_size - 1;
}
else {
auto [ptr, ec] =
std::from_chars(second_range.data(),
second_range.data() + second_range.size(), end);
if (ec != std::errc{}) {
is_valid = false;
return {};
}
}
}

if (start > 0 && (start >= file_size || start == end)) {
// out of range
is_valid = false;
return {};
}

if (end > 0 && end >= file_size) {
end = file_size - 1;
}

if (start == -1) {
start = file_size - end;
end = file_size - 1;
}

vec.push_back({start, end});
}
return vec;
}
class coro_http_connection;
class coro_http_request {
public:
Expand Down
181 changes: 171 additions & 10 deletions include/cinatra/coro_http_server.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,12 @@ class coro_http_server {
coro_http_response &resp) -> async_simple::coro::Lazy<void> {
std::string_view extension = get_extension(file_name);
std::string_view mime = get_mime_type(extension);
auto range_str = req.get_header_value("Range");

if (auto it = static_file_cache_.find(file_name);
it != static_file_cache_.end()) {
auto range_header =
build_range_header(mime, file_name, fs::file_size(file_name));
auto range_header = build_range_header(
mime, file_name, std::to_string(fs::file_size(file_name)));
resp.set_delay(true);
std::string &body = it->second;
std::array<asio::const_buffer, 2> arr{asio::buffer(range_header),
Expand All @@ -269,7 +270,10 @@ class coro_http_server {
co_return;
}

if (format_type_ == file_resp_format_type::chunked) {
size_t file_size = fs::file_size(file_name);

if (format_type_ == file_resp_format_type::chunked &&
range_str.empty()) {
resp.set_format_type(format_type::chunked);
bool ok;
if (ok = co_await resp.get_conn()->begin_chunked(); !ok) {
Expand Down Expand Up @@ -298,8 +302,84 @@ class coro_http_server {
}
}
else {
auto range_header = build_range_header(
mime, file_name, coro_io::coro_file::file_size(file_name));
auto pos = range_str.find('=');
if (pos != std::string_view::npos) {
range_str = range_str.substr(pos + 1);
bool is_valid = true;
auto ranges =
parse_ranges(range_str, fs::file_size(file_name), is_valid);
if (!is_valid) {
resp.set_status(status_type::range_not_satisfiable);
co_return;
}

assert(!ranges.empty());

if (ranges.size() == 1) {
// single part
auto [start, end] = ranges[0];
in_file.seek(start, SEEK_SET);
size_t part_size = end + 1 - start;
int status = (part_size == file_size) ? 200 : 206;
std::string content_range = "Content-Range: bytes ";
content_range.append(std::to_string(start))
.append("-")
.append(std::to_string(end))
.append("/")
.append(std::to_string(file_size))
.append(CRCF);
auto range_header = build_range_header(
mime, file_name, std::to_string(part_size), status,
content_range);
resp.set_delay(true);
bool r = co_await req.get_conn()->write_data(range_header);
if (!r) {
co_return;
}

co_await send_single_part(in_file, content, req, resp,
part_size);
}
else {
// multipart ranges
resp.set_delay(true);
std::string file_size_str = std::to_string(file_size);
size_t content_len = 0;
std::vector<std::string> multi_heads = build_part_heads(
ranges, mime, file_size_str, content_len);
auto range_header = build_multiple_range_header(content_len);
bool r = co_await req.get_conn()->write_data(range_header);
if (!r) {
co_return;
}

for (int i = 0; i < ranges.size(); i++) {
std::string &part_header = multi_heads[i];
r = co_await req.get_conn()->write_data(part_header);
if (!r) {
co_return;
}

auto [start, end] = ranges[i];
in_file.seek(start, SEEK_SET);
size_t part_size = end + 1 - start;

std::string_view more = CRCF;
if (i == ranges.size() - 1) {
more = MULTIPART_END;
}
r = co_await send_single_part(in_file, content, req, resp,
part_size, more);
if (!r) {
co_return;
}
}
}
co_return;
}

auto range_header = build_range_header(mime, file_name,
std::to_string(file_size));
resp.set_delay(true);
bool r = co_await req.get_conn()->write_data(range_header);
if (!r) {
Expand Down Expand Up @@ -485,20 +565,101 @@ class coro_http_server {
}
}

std::string build_multiple_range_header(size_t content_len) {
std::string header_str = "HTTP/1.1 206 Partial Content\r\n";
header_str.append("Content-Length: ");
header_str.append(std::to_string(content_len)).append(CRCF);
header_str.append("Content-Type: multipart/byteranges; boundary=");
header_str.append(BOUNDARY).append(TWO_CRCF);
return header_str;
}

std::vector<std::string> build_part_heads(auto &ranges, std::string_view mime,
std::string_view file_size_str,
size_t &content_len) {
std::vector<std::string> multi_heads;
for (auto [start, end] : ranges) {
std::string part_header = "--";
part_header.append(BOUNDARY).append(CRCF);
part_header.append("Content-Type: ").append(mime).append(CRCF);
part_header.append("Content-Range: ").append("bytes ");
part_header.append(std::to_string(start))
.append("-")
.append(std::to_string(end))
.append("/")
.append(file_size_str)
.append(TWO_CRCF);
content_len += part_header.size();
multi_heads.push_back(std::move(part_header));
size_t part_size = end + 1 - start + CRCF.size();
content_len += part_size;
}
content_len += (BOUNDARY.size() + 4);
return multi_heads;
}

std::string build_range_header(std::string_view mime,
std::string_view filename, size_t file_size) {
std::string header_str =
"HTTP/1.1 200 OK\r\nAccess-Control-Allow-origin: "
"*\r\nAccept-Ranges: bytes\r\n";
std::string_view filename,
std::string_view file_size_str,
int status = 200,
std::string_view content_range = "") {
std::string header_str = "HTTP/1.1 ";
header_str.append(std::to_string(status));
header_str.append(
" OK\r\nAccess-Control-Allow-origin: "
"*\r\nAccept-Ranges: bytes\r\n");
if (!content_range.empty()) {
header_str.append(content_range);
}
header_str.append("Content-Disposition: attachment;filename=");
header_str.append(filename).append("\r\n");
header_str.append("Connection: keep-alive\r\n");
header_str.append("Content-Type: ").append(mime).append("\r\n");
header_str.append("Content-Length: ");
header_str.append(std::to_string(file_size)).append("\r\n\r\n");
header_str.append(file_size_str).append("\r\n\r\n");
return header_str;
}

async_simple::coro::Lazy<bool> send_single_part(auto &in_file, auto &content,
auto &req, auto &resp,
size_t part_size,
std::string_view more = "") {
while (true) {
size_t read_size = (std::min)(part_size, chunked_size_);
if (read_size == 0) {
break;
}
auto [ec, size] = co_await in_file.async_read(content.data(), read_size);
if (ec) {
resp.set_status(status_type::no_content);
co_await resp.get_conn()->reply();
co_return false;
}

part_size -= read_size;

bool r = true;
if (more.empty()) {
r = co_await req.get_conn()->write_data(
std::string_view(content.data(), size));
}
else {
std::array<asio::const_buffer, 2> arr{
asio::buffer(content.data(), size), asio::buffer(more)};
auto [ec, _] = co_await req.get_conn()->async_write(arr);
if (ec) {
r = false;
}
}

if (!r) {
co_return false;
}
}

co_return true;
}

private:
std::unique_ptr<coro_io::io_context_pool> pool_;
asio::io_context *out_ctx_ = nullptr;
Expand Down
3 changes: 1 addition & 2 deletions include/cinatra/define.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,7 @@ inline const std::string CSESSIONID = "CSESSIONID";
const static inline std::string CRCF = "\r\n";
const static inline std::string TWO_CRCF = "\r\n\r\n";
const static inline std::string BOUNDARY = "--CinatraBoundary2B8FAF4A80EDB307";
const static inline std::string MULTIPART_END =
CRCF + "--" + BOUNDARY + "--" + TWO_CRCF;
const static inline std::string MULTIPART_END = CRCF + "--" + BOUNDARY + "--";

inline std::unordered_map<std::string, std::string> g_content_type_map = {
{".css", "text/css"},
Expand Down
Loading
Loading