oxen-storage-server/httpserver/onion_processing.cpp
2021-01-07 15:12:15 +11:00

353 lines
11 KiB
C++

#include "channel_encryption.hpp"
#include "oxen_logger.h"
#include "request_handler.h"
#include "service_node.h"
#include <lokimq/base64.h>
#include <nlohmann/json.hpp>
/// This is only included because of `parse_combined_payload`,
/// in the future it will be moved
#include "http_connection.h"
#include <charconv>
#include <variant>
using nlohmann::json;
namespace oxen {
/// The request is to be forwarded to another SS node
struct RelayToNodeInfo {
/// Inner ciphertext for next node
std::string ciphertext;
// Key to be forwarded to next node for decryption
std::string ephemeral_key;
// Next node's ed25519 key
std::string next_node;
};
/// The request is to be forwarded to some non-SS server
/// that supports our protocol (e.g. Session File Server)
struct RelayToServerInfo {
// Result of decryption (intact)
std::string payload;
// Server's address
std::string host;
// Request's target
std::string target;
};
/// We are the final destination for this request
struct FinalDesitnationInfo {
std::string body;
};
enum class ProcessCiphertextError {
INVALID_CIPHERTEXT,
INVALID_JSON,
};
using ParsedInfo = std::variant<RelayToNodeInfo, RelayToServerInfo,
FinalDesitnationInfo, ProcessCiphertextError>;
static auto
process_ciphertext_v1(const ChannelEncryption<std::string>& decryptor,
const std::string& ciphertext,
const std::string& ephem_key) -> ParsedInfo {
std::string plaintext;
try {
if (!lokimq::is_base64(ciphertext))
throw std::runtime_error{"cipher text is not base64 encoded"};
const std::string ciphertext_bin = lokimq::from_base64(ciphertext);
plaintext = decryptor.decrypt_gcm(ciphertext_bin, ephem_key);
} catch (const std::exception& e) {
OXEN_LOG(debug, "Error decrypting an onion request: {}", e.what());
return ProcessCiphertextError::INVALID_CIPHERTEXT;
}
OXEN_LOG(debug, "onion request decrypted: (len: {})", plaintext.size());
try {
const json inner_json = json::parse(plaintext, nullptr, true);
if (inner_json.find("body") != inner_json.end()) {
auto body = inner_json.at("body").get_ref<const std::string&>();
OXEN_LOG(debug, "Found body: <{}>", body);
return FinalDesitnationInfo{body};
} else if (inner_json.find("host") != inner_json.end()) {
const auto& host =
inner_json.at("host").get_ref<const std::string&>();
const auto& target =
inner_json.at("target").get_ref<const std::string&>();
return RelayToServerInfo{plaintext, host, target};
} else {
// We fall back to forwarding a request to the next node
const auto& ciphertext =
inner_json.at("ciphertext").get_ref<const std::string&>();
const auto& dest =
inner_json.at("destination").get_ref<const std::string&>();
const auto& ekey =
inner_json.at("ephemeral_key").get_ref<const std::string&>();
return RelayToNodeInfo{ciphertext, ekey, dest};
}
} catch (std::exception& e) {
OXEN_LOG(debug, "Error parsing inner JSON in onion request: {}",
e.what());
return ProcessCiphertextError::INVALID_JSON;
}
}
static auto
process_ciphertext_v2(const ChannelEncryption<std::string>& decryptor,
const std::string& ciphertext,
const std::string& ephem_key) -> ParsedInfo {
std::string plaintext;
try {
plaintext = decryptor.decrypt_gcm(ciphertext, ephem_key);
} catch (const std::exception& e) {
OXEN_LOG(debug, "Error decrypting an onion request: {}", e.what());
return ProcessCiphertextError::INVALID_CIPHERTEXT;
}
OXEN_LOG(debug, "onion request decrypted: (len: {})", plaintext.size());
const auto parsed = parse_combined_payload(plaintext);
try {
const json inner_json = json::parse(parsed.json, nullptr, true);
/// Kind of unfortunate that we use "headers" (which is empty)
/// to identify we are the final destination...
if (inner_json.find("headers") != inner_json.end()) {
OXEN_LOG(trace, "Found body: <{}>", parsed.ciphertext);
/// In v2 the body is parsed.ciphertext
return FinalDesitnationInfo{parsed.ciphertext};
} else if (inner_json.find("host") != inner_json.end()) {
const auto& host =
inner_json.at("host").get_ref<const std::string&>();
const auto& target =
inner_json.at("target").get_ref<const std::string&>();
return RelayToServerInfo{plaintext, host, target};
} else {
// We fall back to forwarding a request to the next node
const auto& dest =
inner_json.at("destination").get_ref<const std::string&>();
const auto& ekey =
inner_json.at("ephemeral_key").get_ref<const std::string&>();
return RelayToNodeInfo{parsed.ciphertext, ekey, dest};
}
} catch (std::exception& e) {
OXEN_LOG(debug, "Error parsing inner JSON in onion request: {}",
e.what());
return ProcessCiphertextError::INVALID_JSON;
}
}
static auto gateway_timeout() -> oxen::Response {
return oxen::Response{Status::GATEWAY_TIMEOUT, "Request time out"};
}
static auto make_status(std::string_view status) -> oxen::Status {
int code;
auto res =
std::from_chars(status.data(), status.data() + status.size(), code);
if (res.ec == std::errc::invalid_argument ||
res.ec == std::errc::result_out_of_range) {
return Status::INTERNAL_SERVER_ERROR;
}
switch (code) {
case 200:
return Status::OK;
case 400:
return Status::BAD_REQUEST;
case 403:
return Status::FORBIDDEN;
case 406:
return Status::NOT_ACCEPTABLE;
case 421:
return Status::MISDIRECTED_REQUEST;
case 432:
return Status::INVALID_POW;
case 500:
return Status::INTERNAL_SERVER_ERROR;
case 502:
return Status::BAD_GATEWAY;
case 503:
return Status::SERVICE_UNAVAILABLE;
case 504:
return Status::GATEWAY_TIMEOUT;
default:
return Status::INTERNAL_SERVER_ERROR;
}
}
static void relay_to_node(const ServiceNode& service_node,
const RelayToNodeInfo& info,
std::function<void(oxen::Response)> cb, int req_idx,
bool v2) {
const auto& dest = info.next_node;
const auto& payload = info.ciphertext;
const auto& ekey = info.ephemeral_key;
auto dest_node = service_node.find_node_by_ed25519_pk(dest);
if (!dest_node) {
auto msg = fmt::format("Next node not found: {}", dest);
OXEN_LOG(warn, "{}", msg);
auto res = oxen::Response{Status::BAD_GATEWAY, std::move(msg)};
cb(std::move(res));
return;
}
nlohmann::json req_body;
req_body["ciphertext"] = payload;
req_body["ephemeral_key"] = ekey;
auto on_response = [cb, &service_node](bool success,
std::vector<std::string> data) {
// Processing the result we got from upstream
if (!success) {
OXEN_LOG(debug, "[Onion request] Request time out");
cb(gateway_timeout());
return;
}
// We only expect a two-part message
if (data.size() != 2) {
OXEN_LOG(debug, "[Onion request] Incorrect number of messages: {}",
data.size());
cb(oxen::Response{Status::INTERNAL_SERVER_ERROR,
"Incorrect number of messages from gateway"});
return;
}
/// We use http status codes (for now)
if (data[0] != "200") {
OXEN_LOG(debug, "Onion request relay failed with: {}", data[1]);
}
cb(oxen::Response{make_status(data[0]), std::move(data[1])});
};
OXEN_LOG(debug, "send_onion_to_sn, sn: {} reqidx: {}", *dest_node, req_idx);
if (v2) {
service_node.send_onion_to_sn_v2(*dest_node, payload, ekey,
on_response);
} else {
service_node.send_onion_to_sn_v1(*dest_node, payload, ekey,
on_response);
}
}
void RequestHandler::process_onion_req(const std::string& ciphertext,
const std::string& ephem_key,
std::function<void(oxen::Response)> cb,
bool v2) {
if (!service_node_.snode_ready()) {
auto msg =
fmt::format("Snode not ready: {}",
service_node_.own_address().pubkey_ed25519_hex());
cb(oxen::Response{Status::SERVICE_UNAVAILABLE, std::move(msg)});
return;
}
OXEN_LOG(debug, "process_onion_req, v2: {}", v2);
static int counter = 0;
ParsedInfo res;
if (v2) {
res =
process_ciphertext_v2(this->channel_cipher_, ciphertext, ephem_key);
} else {
res =
process_ciphertext_v1(this->channel_cipher_, ciphertext, ephem_key);
}
if (const auto info = std::get_if<FinalDesitnationInfo>(&res)) {
OXEN_LOG(debug, "We are the final destination in the onion request!");
this->process_onion_exit(
ephem_key, info->body,
[this, ephem_key, cb = std::move(cb)](oxen::Response res) {
auto wrapped_res = this->wrap_proxy_response(
res, ephem_key, true /* use aes gcm */);
cb(std::move(wrapped_res));
});
return;
} else if (const auto info = std::get_if<RelayToNodeInfo>(&res)) {
relay_to_node(this->service_node_, *info, std::move(cb), counter++, v2);
} else if (const auto info = std::get_if<RelayToServerInfo>(&res)) {
OXEN_LOG(debug, "We are to forward the request to url: {}{}",
info->host, info->target);
const auto& target = info->target;
// Forward the request to url but only if it ends in `/lsrpc`
if ((target.rfind("/lsrpc") == target.size() - 6) &&
(target.find('?') == std::string::npos)) {
this->process_onion_to_url(info->host, target, info->payload,
std::move(cb));
} else {
auto res = oxen::Response{Status::BAD_REQUEST, "Invalid url"};
auto wrapped_res = this->wrap_proxy_response(res, ephem_key, true);
cb(std::move(wrapped_res));
}
} else if (const auto error = std::get_if<ProcessCiphertextError>(&res)) {
switch (*error) {
case ProcessCiphertextError::INVALID_CIPHERTEXT: {
// Should this error be propagated back to the client? (No, if we
// couldn't decrypt, we probably won't be able to encrypt either.)
cb(oxen::Response{Status::BAD_REQUEST, "Invalid ciphertext"});
break;
}
case ProcessCiphertextError::INVALID_JSON: {
auto res = oxen::Response{Status::BAD_REQUEST, "Invalid json"};
auto wrapped_res = this->wrap_proxy_response(res, ephem_key, true);
cb(std::move(wrapped_res));
break;
}
}
} else {
OXEN_LOG(error, "UNKNOWN VARIANT");
}
}
} // namespace oxen