-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: preparation for basic http api
The goal is to provide very basic support for simple commands, fancy stuff like pipelining, blocking commands won't work. 1. Added optional registration for /api handler. 2. Implemented parsing of post body. 3. Added basic formatting routine for the response. It does not cover all the commands but should suffice for basic usage. The API is a POST method and the body of the request should contain command arguments formatted as json array. For example, `'["set", "foo", "bar", "ex", "100"]'`. The response is a json object with either `result` field holding the response of the command or `error` field containing the error message sent by the server. See `test_http` test in tests/dragonfly/connection_test.py for more details. Signed-off-by: Roman Gershman <[email protected]>
- Loading branch information
Showing
8 changed files
with
280 additions
and
32 deletions.
There are no files selected for viewing
Submodule helio
updated
4 files
+5 −1 | util/fibers/listener_interface.cc | |
+20 −9 | util/http/http_handler.cc | |
+22 −5 | util/http/http_handler.h | |
+27 −21 | util/http/http_main.cc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
// Copyright 2024, DragonflyDB authors. All rights reserved. | ||
// See LICENSE for licensing terms. | ||
// | ||
|
||
#include "server/http_api.h" | ||
|
||
#include "base/logging.h" | ||
#include "core/flatbuffers.h" | ||
#include "facade/conn_context.h" | ||
#include "facade/reply_builder.h" | ||
#include "server/main_service.h" | ||
#include "util/http/http_common.h" | ||
|
||
namespace dfly { | ||
using namespace util; | ||
using namespace std; | ||
namespace h2 = boost::beast::http; | ||
using facade::CapturingReplyBuilder; | ||
|
||
namespace { | ||
|
||
bool IsValidReq(flexbuffers::Reference req) { | ||
if (!req.IsVector()) { | ||
return false; | ||
} | ||
|
||
auto vec = req.AsVector(); | ||
if (vec.size() == 0) { | ||
return false; | ||
} | ||
|
||
for (size_t i = 0; i < vec.size(); ++i) { | ||
if (!vec[i].IsString()) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
// Escape a string so that it is legal to print it in JSON text. | ||
std::string JsonEscape(string_view input) { | ||
auto hex_digit = [](int c) -> char { return c < 10 ? c + '0' : c - 10 + 'a'; }; | ||
|
||
string out; | ||
out.reserve(input.size() + 2); | ||
out.push_back('\"'); | ||
|
||
auto* p = reinterpret_cast<const unsigned char*>(input.begin()); | ||
auto* e = reinterpret_cast<const unsigned char*>(input.end()); | ||
|
||
while (p < e) { | ||
if (*p == '\\' || *p == '\"') { | ||
out.push_back('\\'); | ||
out.push_back(*p++); | ||
} else if (*p <= 0x1f) { | ||
switch (*p) { | ||
case '\b': | ||
out.append("\\b"); | ||
p++; | ||
break; | ||
case '\f': | ||
out.append("\\f"); | ||
p++; | ||
break; | ||
case '\n': | ||
out.append("\\n"); | ||
p++; | ||
break; | ||
case '\r': | ||
out.append("\\r"); | ||
p++; | ||
break; | ||
case '\t': | ||
out.append("\\t"); | ||
p++; | ||
break; | ||
default: | ||
// this condition captures non readable chars with value < 32, | ||
// so size = 1 byte (e.g control chars). | ||
out.append("\\u00"); | ||
out.push_back(hex_digit((*p & 0xf0) >> 4)); | ||
out.push_back(hex_digit(*p & 0xf)); | ||
p++; | ||
} | ||
} else { | ||
out.push_back(*p++); | ||
} | ||
} | ||
|
||
out.push_back('\"'); | ||
return out; | ||
} | ||
|
||
struct CaptureVisitor { | ||
CaptureVisitor() { | ||
str = R"({"result":)"; | ||
} | ||
|
||
void operator()(monostate) { | ||
} | ||
|
||
void operator()(long v) { | ||
absl::StrAppend(&str, v); | ||
} | ||
|
||
void operator()(double v) { | ||
absl::StrAppend(&str, v); | ||
} | ||
|
||
void operator()(const CapturingReplyBuilder::SimpleString& ss) { | ||
absl::StrAppend(&str, "\"", ss, "\""); | ||
} | ||
|
||
void operator()(const CapturingReplyBuilder::BulkString& bs) { | ||
absl::StrAppend(&str, JsonEscape(bs)); | ||
} | ||
|
||
void operator()(CapturingReplyBuilder::Null) { | ||
absl::StrAppend(&str, "null"); | ||
} | ||
|
||
void operator()(CapturingReplyBuilder::Error err) { | ||
str = absl::StrCat(R"({"error": ")", err.first); | ||
} | ||
|
||
void operator()(facade::OpStatus status) { | ||
absl::StrAppend(&str, "\"", facade::StatusToMsg(status), "\""); | ||
} | ||
|
||
void operator()(const CapturingReplyBuilder::StrArrPayload& sa) { | ||
absl::StrAppend(&str, "not_implemented"); | ||
} | ||
|
||
void operator()(const unique_ptr<CapturingReplyBuilder::CollectionPayload>& cp) { | ||
if (!cp) { | ||
absl::StrAppend(&str, "null"); | ||
return; | ||
} | ||
if (cp->len == 0 && cp->type == facade::RedisReplyBuilder::ARRAY) { | ||
absl::StrAppend(&str, "[]"); | ||
return; | ||
} | ||
|
||
absl::StrAppend(&str, "["); | ||
for (auto& pl : cp->arr) { | ||
visit(*this, std::move(pl)); | ||
} | ||
} | ||
|
||
void operator()(facade::SinkReplyBuilder::MGetResponse resp) { | ||
absl::StrAppend(&str, "not_implemented"); | ||
} | ||
|
||
void operator()(const CapturingReplyBuilder::ScoredArray& sarr) { | ||
absl::StrAppend(&str, "["); | ||
for (const auto& [key, score] : sarr.arr) { | ||
absl::StrAppend(&str, "{", JsonEscape(key), ":", score, "},"); | ||
} | ||
if (sarr.arr.size() > 0) { | ||
str.pop_back(); | ||
} | ||
absl::StrAppend(&str, "]"); | ||
} | ||
|
||
string str; | ||
}; | ||
|
||
} // namespace | ||
|
||
void HttpAPI(const http::QueryArgs& args, HttpRequest&& req, Service* service, | ||
HttpContext* http_cntx) { | ||
auto& body = req.body(); | ||
|
||
flexbuffers::Builder fbb; | ||
flatbuffers::Parser parser; | ||
flexbuffers::Reference doc; | ||
bool success = parser.ParseFlexBuffer(body.c_str(), nullptr, &fbb); | ||
if (success) { | ||
fbb.Finish(); | ||
doc = flexbuffers::GetRoot(fbb.GetBuffer()); | ||
if (!IsValidReq(doc)) { | ||
success = false; | ||
} | ||
} | ||
|
||
if (!success) { | ||
auto response = http::MakeStringResponse(h2::status::bad_request); | ||
http::SetMime(http::kTextMime, &response); | ||
response.body() = "Failed to parse json\r\n"; | ||
http_cntx->Invoke(std::move(response)); | ||
return; | ||
} | ||
|
||
vector<string> cmd_args; | ||
flexbuffers::Vector vec = doc.AsVector(); | ||
for (size_t i = 0; i < vec.size(); ++i) { | ||
cmd_args.push_back(vec[i].AsString().c_str()); | ||
} | ||
vector<facade::MutableSlice> cmd_slices(cmd_args.size()); | ||
for (size_t i = 0; i < cmd_args.size(); ++i) { | ||
cmd_slices[i] = absl::MakeSpan(cmd_args[i]); | ||
} | ||
|
||
facade::ConnectionContext* context = (facade::ConnectionContext*)http_cntx->user_data(); | ||
DCHECK(context); | ||
|
||
facade::CapturingReplyBuilder reply_builder; | ||
auto* prev = context->Inject(&reply_builder); | ||
// TODO: to finish this. | ||
service->DispatchCommand(absl::MakeSpan(cmd_slices), context); | ||
facade::CapturingReplyBuilder::Payload payload = reply_builder.Take(); | ||
|
||
context->Inject(prev); | ||
auto response = http::MakeStringResponse(); | ||
http::SetMime(http::kJsonMime, &response); | ||
|
||
CaptureVisitor visitor; | ||
std::visit(visitor, std::move(payload)); | ||
visitor.str.append("}\r\n"); | ||
response.body() = visitor.str; | ||
http_cntx->Invoke(std::move(response)); | ||
} | ||
|
||
} // namespace dfly |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// Copyright 2024, DragonflyDB authors. All rights reserved. | ||
// See LICENSE for licensing terms. | ||
// | ||
|
||
#pragma once | ||
|
||
#include "util/http/http_handler.h" | ||
|
||
namespace dfly { | ||
class Service; | ||
using HttpRequest = util::HttpListenerBase::RequestType; | ||
|
||
void HttpAPI(const util::http::QueryArgs& args, HttpRequest&& req, Service* service, | ||
util::HttpContext* send); | ||
|
||
} // namespace dfly |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.