зеркало из https://github.com/microsoft/CCF.git
JS FFI plugin building (#2978)
This commit is contained in:
Родитель
169345ebfa
Коммит
e93d2f0beb
|
@ -348,12 +348,80 @@ set(CCF_NETWORK_TEST_ARGS -l ${TEST_HOST_LOGGING_LEVEL} --worker-threads
|
|||
${WORKER_THREADS}
|
||||
)
|
||||
|
||||
if("sgx" IN_LIST COMPILE_TARGETS)
|
||||
add_enclave_library(js_openenclave.enclave ${CCF_DIR}/src/js/openenclave.cpp)
|
||||
use_oe_mbedtls(js_openenclave.enclave)
|
||||
target_link_libraries(js_openenclave.enclave PUBLIC ccf.enclave)
|
||||
add_lvi_mitigations(js_openenclave.enclave)
|
||||
install(
|
||||
TARGETS js_openenclave.enclave
|
||||
EXPORT ccf
|
||||
DESTINATION lib
|
||||
)
|
||||
endif()
|
||||
if("virtual" IN_LIST COMPILE_TARGETS)
|
||||
add_library(js_openenclave.virtual STATIC ${CCF_DIR}/src/js/openenclave.cpp)
|
||||
add_san(js_openenclave.virtual)
|
||||
target_link_libraries(js_openenclave.virtual PUBLIC ccf.virtual)
|
||||
target_compile_options(js_openenclave.virtual PRIVATE ${COMPILE_LIBCXX})
|
||||
target_compile_definitions(
|
||||
js_openenclave.virtual PUBLIC INSIDE_ENCLAVE VIRTUAL_ENCLAVE
|
||||
_LIBCPP_HAS_THREAD_API_PTHREAD
|
||||
)
|
||||
set_property(
|
||||
TARGET js_openenclave.virtual PROPERTY POSITION_INDEPENDENT_CODE ON
|
||||
)
|
||||
use_client_mbedtls(js_openenclave.virtual)
|
||||
install(
|
||||
TARGETS js_openenclave.virtual
|
||||
EXPORT ccf
|
||||
DESTINATION lib
|
||||
)
|
||||
endif()
|
||||
|
||||
if("sgx" IN_LIST COMPILE_TARGETS)
|
||||
add_enclave_library(
|
||||
js_generic_base.enclave ${CCF_DIR}/src/apps/js_generic/js_generic_base.cpp
|
||||
)
|
||||
use_oe_mbedtls(js_generic_base.enclave)
|
||||
target_link_libraries(js_generic_base.enclave PUBLIC ccf.enclave)
|
||||
add_lvi_mitigations(js_generic_base.enclave)
|
||||
install(
|
||||
TARGETS js_generic_base.enclave
|
||||
EXPORT ccf
|
||||
DESTINATION lib
|
||||
)
|
||||
endif()
|
||||
if("virtual" IN_LIST COMPILE_TARGETS)
|
||||
add_library(
|
||||
js_generic_base.virtual STATIC
|
||||
${CCF_DIR}/src/apps/js_generic/js_generic_base.cpp
|
||||
)
|
||||
add_san(js_generic_base.virtual)
|
||||
add_warning_checks(js_generic_base.virtual)
|
||||
target_link_libraries(js_generic_base.virtual PUBLIC ccf.virtual)
|
||||
target_compile_options(js_generic_base.virtual PRIVATE ${COMPILE_LIBCXX})
|
||||
target_compile_definitions(
|
||||
js_generic_base.virtual PUBLIC INSIDE_ENCLAVE VIRTUAL_ENCLAVE
|
||||
_LIBCPP_HAS_THREAD_API_PTHREAD
|
||||
)
|
||||
set_property(
|
||||
TARGET js_generic_base.virtual PROPERTY POSITION_INDEPENDENT_CODE ON
|
||||
)
|
||||
use_client_mbedtls(js_generic_base.virtual)
|
||||
install(
|
||||
TARGETS js_generic_base.virtual
|
||||
EXPORT ccf
|
||||
DESTINATION lib
|
||||
)
|
||||
endif()
|
||||
# SNIPPET_START: JS generic application
|
||||
add_ccf_app(
|
||||
js_generic
|
||||
SRCS ${CCF_DIR}/src/apps/js_generic/js_generic.cpp
|
||||
LINK_LIBS_ENCLAVE quickjs.enclave -lgcc
|
||||
LINK_LIBS_VIRTUAL quickjs.host INSTALL_LIBS ON
|
||||
LINK_LIBS_ENCLAVE js_generic_base.enclave js_openenclave.enclave
|
||||
LINK_LIBS_VIRTUAL js_generic_base.virtual js_openenclave.virtual INSTALL_LIBS
|
||||
ON
|
||||
)
|
||||
sign_app_library(
|
||||
js_generic.enclave ${CCF_DIR}/src/apps/js_generic/oe_sign.conf
|
||||
|
|
|
@ -5,6 +5,7 @@ A CCF application is composed of the following:
|
|||
|
||||
- The :ref:`Application Entry Point <build_apps/api:Application Entry Point>` which registers the application in CCF.
|
||||
- A collection of :cpp:class:`endpoints <ccf::endpoints::Endpoint>` handling HTTP requests and grouped in a single :cpp:class:`registry <ccf::endpoints::EndpointRegistry>`. An :cpp:class:`endpoint <ccf::endpoints::Endpoint>` reads and writes to the key-value store via the :ref:`Key-Value Store API <build_apps/kv/api:Key-Value Store API>`.
|
||||
- An optional set of :ref:`JavaScript FFI Plugins <build_apps/api:JavaScript FFI Plugins>` that can be registered to extend the built-in JavaScript API surface.
|
||||
|
||||
Application Entry Point
|
||||
-----------------------
|
||||
|
@ -96,3 +97,9 @@ Historical Queries
|
|||
.. doxygenstruct:: ccf::historical::State
|
||||
:project: CCF
|
||||
:members:
|
||||
|
||||
JavaScript FFI Plugins
|
||||
----------------------
|
||||
|
||||
.. doxygenfunction:: ccfapp::get_js_plugins
|
||||
:project: CCF
|
||||
|
|
|
@ -5,16 +5,16 @@ A C++ application exposes itself to CCF by implementing:
|
|||
|
||||
.. literalinclude:: ../../include/ccf/app_interface.h
|
||||
:language: cpp
|
||||
:start-after: SNIPPET_START: rpc_handler
|
||||
:end-before: SNIPPET_END: rpc_handler
|
||||
:start-after: SNIPPET_START: app_interface
|
||||
:end-before: SNIPPET_END: app_interface
|
||||
:dedent:
|
||||
|
||||
The Logging application simply has:
|
||||
|
||||
.. literalinclude:: ../../samples/apps/logging/logging.cpp
|
||||
:language: cpp
|
||||
:start-after: SNIPPET_START: rpc_handler
|
||||
:end-before: SNIPPET_END: rpc_handler
|
||||
:start-after: SNIPPET_START: app_interface
|
||||
:end-before: SNIPPET_END: app_interface
|
||||
:dedent:
|
||||
|
||||
.. note::
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
// Licensed under the Apache 2.0 License.
|
||||
#pragma once
|
||||
|
||||
#include "js_plugin.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace ccf
|
||||
{
|
||||
|
@ -17,7 +20,7 @@ namespace ccfapp
|
|||
// Forward declaration
|
||||
struct AbstractNodeContext;
|
||||
|
||||
// SNIPPET_START: rpc_handler
|
||||
// SNIPPET_START: app_interface
|
||||
/** To be implemented by the application to be registered by CCF.
|
||||
*
|
||||
* @param network Access to the network's replicated tables
|
||||
|
@ -27,5 +30,11 @@ namespace ccfapp
|
|||
*/
|
||||
std::shared_ptr<ccf::RpcFrontend> get_rpc_handler(
|
||||
ccf::NetworkTables& network, AbstractNodeContext& context);
|
||||
// SNIPPET_END: rpc_handler
|
||||
|
||||
/** To be implemented by the application to be registered by CCF.
|
||||
*
|
||||
* @return Vector of JavaScript FFI plugins
|
||||
*/
|
||||
std::vector<ccf::js::FFIPlugin> get_js_plugins();
|
||||
// SNIPPET_END: app_interface
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the Apache 2.0 License.
|
||||
#pragma once
|
||||
|
||||
#include "ccf/js_plugin.h"
|
||||
|
||||
namespace ccf::js
|
||||
{
|
||||
extern FFIPlugin openenclave_plugin;
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
#include <quickjs/quickjs.h>
|
||||
#include <string>
|
||||
|
||||
namespace js
|
||||
namespace ccf::js
|
||||
{
|
||||
struct FFIPlugin
|
||||
{
|
|
@ -1198,11 +1198,11 @@ namespace loggingapp
|
|||
|
||||
namespace ccfapp
|
||||
{
|
||||
// SNIPPET_START: rpc_handler
|
||||
// SNIPPET_START: app_interface
|
||||
std::shared_ptr<ccf::RpcFrontend> get_rpc_handler(
|
||||
ccf::NetworkTables& nwt, ccfapp::AbstractNodeContext& context)
|
||||
{
|
||||
return make_shared<loggingapp::Logger>(nwt, context);
|
||||
}
|
||||
// SNIPPET_END: rpc_handler
|
||||
// SNIPPET_END: app_interface
|
||||
}
|
||||
|
|
|
@ -1,653 +1,20 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the Apache 2.0 License.
|
||||
#include "apps/utils/metrics_tracker.h"
|
||||
#include "ccf/app_interface.h"
|
||||
#include "ccf/historical_queries_adapter.h"
|
||||
#include "ccf/user_frontend.h"
|
||||
#include "ccf/version.h"
|
||||
#include "crypto/entropy.h"
|
||||
#include "crypto/key_wrap.h"
|
||||
#include "crypto/rsa_key_pair.h"
|
||||
#include "js/wrap.h"
|
||||
#include "kv/untyped_map.h"
|
||||
#include "named_auth_policies.h"
|
||||
|
||||
#include <memory>
|
||||
#include <quickjs/quickjs-exports.h>
|
||||
#include <quickjs/quickjs.h>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include "ccf/js_openenclave_plugin.h"
|
||||
#include "js_generic_base.h"
|
||||
|
||||
namespace ccfapp
|
||||
{
|
||||
using namespace std;
|
||||
using namespace kv;
|
||||
using namespace ccf;
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wc99-extensions"
|
||||
|
||||
class JSHandlers : public UserEndpointRegistry
|
||||
{
|
||||
private:
|
||||
NetworkTables& network;
|
||||
ccfapp::AbstractNodeContext& context;
|
||||
metrics::Tracker metrics_tracker;
|
||||
|
||||
static JSValue create_json_obj(const nlohmann::json& j, JSContext* ctx)
|
||||
{
|
||||
const auto buf = j.dump();
|
||||
return JS_ParseJSON(ctx, buf.data(), buf.size(), "<json>");
|
||||
}
|
||||
|
||||
JSValue create_caller_obj(
|
||||
ccf::endpoints::EndpointContext& endpoint_ctx, JSContext* ctx)
|
||||
{
|
||||
if (endpoint_ctx.caller == nullptr)
|
||||
{
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
auto caller = JS_NewObject(ctx);
|
||||
|
||||
if (auto jwt_ident = endpoint_ctx.try_get_caller<ccf::JwtAuthnIdentity>())
|
||||
{
|
||||
JS_SetPropertyStr(
|
||||
ctx,
|
||||
caller,
|
||||
"policy",
|
||||
JS_NewString(ctx, get_policy_name_from_ident(jwt_ident)));
|
||||
|
||||
auto jwt = JS_NewObject(ctx);
|
||||
JS_SetPropertyStr(
|
||||
ctx,
|
||||
jwt,
|
||||
"keyIssuer",
|
||||
JS_NewStringLen(
|
||||
ctx, jwt_ident->key_issuer.data(), jwt_ident->key_issuer.size()));
|
||||
JS_SetPropertyStr(
|
||||
ctx, jwt, "header", create_json_obj(jwt_ident->header, ctx));
|
||||
JS_SetPropertyStr(
|
||||
ctx, jwt, "payload", create_json_obj(jwt_ident->payload, ctx));
|
||||
JS_SetPropertyStr(ctx, caller, "jwt", jwt);
|
||||
|
||||
return caller;
|
||||
}
|
||||
else if (
|
||||
auto empty_ident =
|
||||
endpoint_ctx.try_get_caller<ccf::EmptyAuthnIdentity>())
|
||||
{
|
||||
JS_SetPropertyStr(
|
||||
ctx,
|
||||
caller,
|
||||
"policy",
|
||||
JS_NewString(ctx, get_policy_name_from_ident(empty_ident)));
|
||||
return caller;
|
||||
}
|
||||
|
||||
char const* policy_name = nullptr;
|
||||
std::string id;
|
||||
bool is_member = false;
|
||||
|
||||
if (
|
||||
auto user_cert_ident =
|
||||
endpoint_ctx.try_get_caller<ccf::UserCertAuthnIdentity>())
|
||||
{
|
||||
policy_name = get_policy_name_from_ident(user_cert_ident);
|
||||
id = user_cert_ident->user_id;
|
||||
is_member = false;
|
||||
}
|
||||
else if (
|
||||
auto member_cert_ident =
|
||||
endpoint_ctx.try_get_caller<ccf::MemberCertAuthnIdentity>())
|
||||
{
|
||||
policy_name = get_policy_name_from_ident(member_cert_ident);
|
||||
id = member_cert_ident->member_id;
|
||||
is_member = true;
|
||||
}
|
||||
else if (
|
||||
auto user_sig_ident =
|
||||
endpoint_ctx.try_get_caller<ccf::UserSignatureAuthnIdentity>())
|
||||
{
|
||||
policy_name = get_policy_name_from_ident(user_sig_ident);
|
||||
id = user_sig_ident->user_id;
|
||||
is_member = false;
|
||||
}
|
||||
else if (
|
||||
auto member_sig_ident =
|
||||
endpoint_ctx.try_get_caller<ccf::MemberSignatureAuthnIdentity>())
|
||||
{
|
||||
policy_name = get_policy_name_from_ident(member_sig_ident);
|
||||
id = member_sig_ident->member_id;
|
||||
is_member = true;
|
||||
}
|
||||
|
||||
if (policy_name == nullptr)
|
||||
{
|
||||
throw std::logic_error("Unable to convert caller info to JS object");
|
||||
}
|
||||
|
||||
// Retrieve user/member data from authenticated caller id
|
||||
nlohmann::json data = nullptr;
|
||||
ccf::ApiResult result = ccf::ApiResult::OK;
|
||||
|
||||
if (is_member)
|
||||
{
|
||||
result = get_member_data_v1(endpoint_ctx.tx, id, data);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = get_user_data_v1(endpoint_ctx.tx, id, data);
|
||||
}
|
||||
|
||||
if (result == ccf::ApiResult::InternalError)
|
||||
{
|
||||
throw std::logic_error(
|
||||
fmt::format("Failed to get data for caller {}", id));
|
||||
}
|
||||
|
||||
crypto::Pem cert;
|
||||
if (is_member)
|
||||
{
|
||||
result = get_member_cert_v1(endpoint_ctx.tx, id, cert);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = get_user_cert_v1(endpoint_ctx.tx, id, cert);
|
||||
}
|
||||
|
||||
if (result == ccf::ApiResult::InternalError)
|
||||
{
|
||||
throw std::logic_error(
|
||||
fmt::format("Failed to get certificate for caller {}", id));
|
||||
}
|
||||
|
||||
JS_SetPropertyStr(ctx, caller, "policy", JS_NewString(ctx, policy_name));
|
||||
JS_SetPropertyStr(
|
||||
ctx, caller, "id", JS_NewStringLen(ctx, id.data(), id.size()));
|
||||
JS_SetPropertyStr(ctx, caller, "data", create_json_obj(data, ctx));
|
||||
JS_SetPropertyStr(
|
||||
ctx,
|
||||
caller,
|
||||
"cert",
|
||||
JS_NewStringLen(ctx, cert.str().data(), cert.size()));
|
||||
|
||||
return caller;
|
||||
}
|
||||
|
||||
JSValue create_request_obj(
|
||||
ccf::endpoints::EndpointContext& endpoint_ctx, JSContext* ctx)
|
||||
{
|
||||
auto request = JS_NewObject(ctx);
|
||||
|
||||
auto headers = JS_NewObject(ctx);
|
||||
for (auto& [header_name, header_value] :
|
||||
endpoint_ctx.rpc_ctx->get_request_headers())
|
||||
{
|
||||
JS_SetPropertyStr(
|
||||
ctx,
|
||||
headers,
|
||||
header_name.c_str(),
|
||||
JS_NewStringLen(ctx, header_value.c_str(), header_value.size()));
|
||||
}
|
||||
JS_SetPropertyStr(ctx, request, "headers", headers);
|
||||
|
||||
const auto& request_query = endpoint_ctx.rpc_ctx->get_request_query();
|
||||
auto query_str =
|
||||
JS_NewStringLen(ctx, request_query.c_str(), request_query.size());
|
||||
JS_SetPropertyStr(ctx, request, "query", query_str);
|
||||
|
||||
auto params = JS_NewObject(ctx);
|
||||
for (auto& [param_name, param_value] :
|
||||
endpoint_ctx.rpc_ctx->get_request_path_params())
|
||||
{
|
||||
JS_SetPropertyStr(
|
||||
ctx,
|
||||
params,
|
||||
param_name.c_str(),
|
||||
JS_NewStringLen(ctx, param_value.c_str(), param_value.size()));
|
||||
}
|
||||
JS_SetPropertyStr(ctx, request, "params", params);
|
||||
|
||||
const auto& request_body = endpoint_ctx.rpc_ctx->get_request_body();
|
||||
auto body_ = JS_NewObjectClass(ctx, js::body_class_id);
|
||||
JS_SetOpaque(body_, (void*)&request_body);
|
||||
JS_SetPropertyStr(ctx, request, "body", body_);
|
||||
|
||||
JS_SetPropertyStr(
|
||||
ctx, request, "caller", create_caller_obj(endpoint_ctx, ctx));
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
void execute_request(
|
||||
const ccf::endpoints::EndpointProperties& props,
|
||||
ccf::endpoints::EndpointContext& endpoint_ctx)
|
||||
{
|
||||
if (props.mode == ccf::endpoints::Mode::Historical)
|
||||
{
|
||||
auto is_tx_committed =
|
||||
[this](ccf::View view, ccf::SeqNo seqno, std::string& error_reason) {
|
||||
return ccf::historical::is_tx_committed_v2(
|
||||
consensus, view, seqno, error_reason);
|
||||
};
|
||||
|
||||
ccf::historical::adapter_v2(
|
||||
[this, &props](
|
||||
ccf::endpoints::EndpointContext& endpoint_ctx,
|
||||
ccf::historical::StatePtr state) {
|
||||
auto tx = state->store->create_tx();
|
||||
auto tx_id = state->transaction_id;
|
||||
auto receipt = state->receipt;
|
||||
assert(receipt);
|
||||
do_execute_request(props, endpoint_ctx, tx, tx_id, receipt);
|
||||
},
|
||||
context.get_historical_state(),
|
||||
is_tx_committed)(endpoint_ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
do_execute_request(
|
||||
props, endpoint_ctx, endpoint_ctx.tx, std::nullopt, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void do_execute_request(
|
||||
const ccf::endpoints::EndpointProperties& props,
|
||||
ccf::endpoints::EndpointContext& endpoint_ctx,
|
||||
kv::Tx& target_tx,
|
||||
const std::optional<ccf::TxID>& transaction_id,
|
||||
ccf::historical::TxReceiptPtr receipt)
|
||||
{
|
||||
js::Runtime rt;
|
||||
rt.add_ccf_classdefs();
|
||||
|
||||
JS_SetModuleLoaderFunc(
|
||||
rt, nullptr, js::js_app_module_loader, &endpoint_ctx.tx);
|
||||
|
||||
js::Context ctx(rt);
|
||||
js::TxContext txctx{&target_tx, js::TxAccess::APP};
|
||||
|
||||
js::register_request_body_class(ctx);
|
||||
js::populate_global(
|
||||
&txctx,
|
||||
endpoint_ctx.rpc_ctx.get(),
|
||||
transaction_id,
|
||||
receipt,
|
||||
nullptr,
|
||||
&context.get_node_state(),
|
||||
nullptr,
|
||||
ctx);
|
||||
|
||||
JSValue export_func;
|
||||
try
|
||||
{
|
||||
auto module_val =
|
||||
js::load_app_module(ctx, props.js_module.c_str(), &endpoint_ctx.tx);
|
||||
export_func =
|
||||
ctx.function(module_val, props.js_function, props.js_module);
|
||||
}
|
||||
catch (const std::exception& exc)
|
||||
{
|
||||
endpoint_ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
exc.what());
|
||||
return;
|
||||
}
|
||||
|
||||
// Call exported function
|
||||
auto request = create_request_obj(endpoint_ctx, ctx);
|
||||
int argc = 1;
|
||||
JSValueConst* argv = (JSValueConst*)&request;
|
||||
auto val = ctx(JS_Call(ctx, export_func, JS_UNDEFINED, argc, argv));
|
||||
JS_FreeValue(ctx, request);
|
||||
JS_FreeValue(ctx, export_func);
|
||||
|
||||
if (JS_IsException(val))
|
||||
{
|
||||
js::js_dump_error(ctx);
|
||||
endpoint_ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"Exception thrown while executing.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle return value: {body, headers, statusCode}
|
||||
if (!JS_IsObject(val))
|
||||
{
|
||||
endpoint_ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"Invalid endpoint function return value (not an object).");
|
||||
return;
|
||||
}
|
||||
|
||||
// Response body (also sets a default response content-type header)
|
||||
{
|
||||
auto response_body_js = ctx(JS_GetPropertyStr(ctx, val, "body"));
|
||||
|
||||
if (!JS_IsUndefined(response_body_js))
|
||||
{
|
||||
std::vector<uint8_t> response_body;
|
||||
size_t buf_size;
|
||||
size_t buf_offset;
|
||||
JSValue typed_array_buffer = JS_GetTypedArrayBuffer(
|
||||
ctx, response_body_js, &buf_offset, &buf_size, nullptr);
|
||||
uint8_t* array_buffer;
|
||||
if (!JS_IsException(typed_array_buffer))
|
||||
{
|
||||
size_t buf_size_total;
|
||||
array_buffer =
|
||||
JS_GetArrayBuffer(ctx, &buf_size_total, typed_array_buffer);
|
||||
array_buffer += buf_offset;
|
||||
JS_FreeValue(ctx, typed_array_buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
array_buffer = JS_GetArrayBuffer(ctx, &buf_size, response_body_js);
|
||||
}
|
||||
if (array_buffer)
|
||||
{
|
||||
endpoint_ctx.rpc_ctx->set_response_header(
|
||||
http::headers::CONTENT_TYPE,
|
||||
http::headervalues::contenttype::OCTET_STREAM);
|
||||
response_body =
|
||||
std::vector<uint8_t>(array_buffer, array_buffer + buf_size);
|
||||
}
|
||||
else
|
||||
{
|
||||
const char* cstr = nullptr;
|
||||
if (JS_IsString(response_body_js))
|
||||
{
|
||||
endpoint_ctx.rpc_ctx->set_response_header(
|
||||
http::headers::CONTENT_TYPE,
|
||||
http::headervalues::contenttype::TEXT);
|
||||
cstr = JS_ToCString(ctx, response_body_js);
|
||||
}
|
||||
else
|
||||
{
|
||||
endpoint_ctx.rpc_ctx->set_response_header(
|
||||
http::headers::CONTENT_TYPE,
|
||||
http::headervalues::contenttype::JSON);
|
||||
JSValue rval =
|
||||
JS_JSONStringify(ctx, response_body_js, JS_NULL, JS_NULL);
|
||||
if (JS_IsException(rval))
|
||||
{
|
||||
js::js_dump_error(ctx);
|
||||
endpoint_ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"Invalid endpoint function return value (error during JSON "
|
||||
"conversion of body).");
|
||||
return;
|
||||
}
|
||||
cstr = JS_ToCString(ctx, rval);
|
||||
JS_FreeValue(ctx, rval);
|
||||
}
|
||||
if (!cstr)
|
||||
{
|
||||
js::js_dump_error(ctx);
|
||||
endpoint_ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"Invalid endpoint function return value (error during string "
|
||||
"conversion of body).");
|
||||
return;
|
||||
}
|
||||
std::string str(cstr);
|
||||
JS_FreeCString(ctx, cstr);
|
||||
|
||||
response_body = std::vector<uint8_t>(str.begin(), str.end());
|
||||
}
|
||||
endpoint_ctx.rpc_ctx->set_response_body(std::move(response_body));
|
||||
}
|
||||
}
|
||||
// Response headers
|
||||
{
|
||||
auto response_headers_js = ctx(JS_GetPropertyStr(ctx, val, "headers"));
|
||||
if (JS_IsObject(response_headers_js))
|
||||
{
|
||||
uint32_t prop_count = 0;
|
||||
JSPropertyEnum* props = nullptr;
|
||||
JS_GetOwnPropertyNames(
|
||||
ctx,
|
||||
&props,
|
||||
&prop_count,
|
||||
response_headers_js,
|
||||
JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY);
|
||||
for (size_t i = 0; i < prop_count; i++)
|
||||
{
|
||||
auto prop_name = props[i].atom;
|
||||
auto prop_name_cstr = ctx(JS_AtomToCString(ctx, prop_name));
|
||||
auto prop_val =
|
||||
ctx(JS_GetProperty(ctx, response_headers_js, prop_name));
|
||||
auto prop_val_cstr = JS_ToCString(ctx, prop_val);
|
||||
if (!prop_val_cstr)
|
||||
{
|
||||
endpoint_ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"Invalid endpoint function return value (header value type).");
|
||||
return;
|
||||
}
|
||||
endpoint_ctx.rpc_ctx->set_response_header(
|
||||
prop_name_cstr, prop_val_cstr);
|
||||
JS_FreeCString(ctx, prop_val_cstr);
|
||||
}
|
||||
js_free(ctx, props);
|
||||
}
|
||||
}
|
||||
|
||||
// Response status code
|
||||
{
|
||||
int response_status_code = HTTP_STATUS_OK;
|
||||
auto status_code_js = ctx(JS_GetPropertyStr(ctx, val, "statusCode"));
|
||||
if (!JS_IsUndefined(status_code_js) && !JS_IsNull(status_code_js))
|
||||
{
|
||||
if (JS_VALUE_GET_TAG(status_code_js.val) != JS_TAG_INT)
|
||||
{
|
||||
endpoint_ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"Invalid endpoint function return value (status code type).");
|
||||
return;
|
||||
}
|
||||
response_status_code = JS_VALUE_GET_INT(status_code_js.val);
|
||||
}
|
||||
endpoint_ctx.rpc_ctx->set_response_status(response_status_code);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
struct JSDynamicEndpoint : public ccf::endpoints::EndpointDefinition
|
||||
{};
|
||||
|
||||
public:
|
||||
JSHandlers(NetworkTables& network, AbstractNodeContext& context) :
|
||||
UserEndpointRegistry(context),
|
||||
network(network),
|
||||
context(context)
|
||||
{
|
||||
metrics_tracker.install_endpoint(*this);
|
||||
}
|
||||
|
||||
void instantiate_authn_policies(JSDynamicEndpoint& endpoint)
|
||||
{
|
||||
for (const auto& policy_name : endpoint.properties.authn_policies)
|
||||
{
|
||||
auto policy = get_policy_by_name(policy_name);
|
||||
if (policy == nullptr)
|
||||
{
|
||||
throw std::logic_error(
|
||||
fmt::format("Unknown auth policy: {}", policy_name));
|
||||
}
|
||||
endpoint.authn_policies.push_back(std::move(policy));
|
||||
}
|
||||
}
|
||||
|
||||
ccf::endpoints::EndpointDefinitionPtr find_endpoint(
|
||||
kv::Tx& tx, enclave::RpcContext& rpc_ctx) override
|
||||
{
|
||||
const auto method = rpc_ctx.get_method();
|
||||
const auto verb = rpc_ctx.get_request_verb();
|
||||
|
||||
auto endpoints =
|
||||
tx.ro<ccf::endpoints::EndpointsMap>(ccf::Tables::ENDPOINTS);
|
||||
|
||||
const auto key = ccf::endpoints::EndpointKey{method, verb};
|
||||
|
||||
// Look for a direct match of the given path
|
||||
const auto it = endpoints->get(key);
|
||||
if (it.has_value())
|
||||
{
|
||||
auto endpoint_def = std::make_shared<JSDynamicEndpoint>();
|
||||
endpoint_def->dispatch = key;
|
||||
endpoint_def->properties = it.value();
|
||||
instantiate_authn_policies(*endpoint_def);
|
||||
return endpoint_def;
|
||||
}
|
||||
|
||||
// If that doesn't exist, look through _all_ the endpoints to find
|
||||
// templated matches. If there is one, that's a match. More is an error,
|
||||
// none means delegate to the base class.
|
||||
{
|
||||
std::vector<ccf::endpoints::EndpointDefinitionPtr> matches;
|
||||
|
||||
endpoints->foreach_key(
|
||||
[this, &endpoints, &matches, &key, &rpc_ctx](const auto& other_key) {
|
||||
if (key.verb == other_key.verb)
|
||||
{
|
||||
const auto opt_spec =
|
||||
ccf::endpoints::parse_path_template(other_key.uri_path);
|
||||
if (opt_spec.has_value())
|
||||
{
|
||||
const auto& template_spec = opt_spec.value();
|
||||
// This endpoint has templates in its path, and the correct verb
|
||||
// - now check if template matches the current request's path
|
||||
std::smatch match;
|
||||
if (std::regex_match(
|
||||
key.uri_path, match, template_spec.template_regex))
|
||||
{
|
||||
if (matches.empty())
|
||||
{
|
||||
// Populate the request_path_params while we have the match,
|
||||
// though this will be discarded on error if we later find
|
||||
// multiple matches
|
||||
auto& path_params = rpc_ctx.get_request_path_params();
|
||||
for (size_t i = 0;
|
||||
i < template_spec.template_component_names.size();
|
||||
++i)
|
||||
{
|
||||
const auto& template_name =
|
||||
template_spec.template_component_names[i];
|
||||
const auto& template_value = match[i + 1].str();
|
||||
path_params[template_name] = template_value;
|
||||
}
|
||||
}
|
||||
|
||||
auto endpoint = std::make_shared<JSDynamicEndpoint>();
|
||||
endpoint->dispatch = other_key;
|
||||
endpoint->properties = endpoints->get(other_key).value();
|
||||
instantiate_authn_policies(*endpoint);
|
||||
matches.push_back(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (matches.size() > 1)
|
||||
{
|
||||
report_ambiguous_templated_path(key.uri_path, matches);
|
||||
}
|
||||
else if (matches.size() == 1)
|
||||
{
|
||||
return matches[0];
|
||||
}
|
||||
}
|
||||
|
||||
return ccf::endpoints::EndpointRegistry::find_endpoint(tx, rpc_ctx);
|
||||
}
|
||||
|
||||
void execute_endpoint(
|
||||
ccf::endpoints::EndpointDefinitionPtr e,
|
||||
ccf::endpoints::EndpointContext& endpoint_ctx) override
|
||||
{
|
||||
auto endpoint = dynamic_cast<const JSDynamicEndpoint*>(e.get());
|
||||
if (endpoint != nullptr)
|
||||
{
|
||||
execute_request(endpoint->properties, endpoint_ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
ccf::endpoints::EndpointRegistry::execute_endpoint(e, endpoint_ctx);
|
||||
}
|
||||
|
||||
// Since we do our own dispatch within the default handler, report the
|
||||
// supported methods here
|
||||
void build_api(nlohmann::json& document, kv::ReadOnlyTx& tx) override
|
||||
{
|
||||
UserEndpointRegistry::build_api(document, tx);
|
||||
|
||||
auto endpoints =
|
||||
tx.ro<ccf::endpoints::EndpointsMap>(ccf::Tables::ENDPOINTS);
|
||||
|
||||
endpoints->foreach([&document](const auto& key, const auto& properties) {
|
||||
const auto http_verb = key.verb.get_http_method();
|
||||
if (!http_verb.has_value())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!properties.openapi_hidden)
|
||||
{
|
||||
auto& path_op = ds::openapi::path_operation(
|
||||
ds::openapi::path(document, key.uri_path),
|
||||
http_verb.value(),
|
||||
false);
|
||||
if (!properties.openapi.empty())
|
||||
{
|
||||
for (const auto& [k, v] : properties.openapi.items())
|
||||
{
|
||||
LOG_INFO_FMT("Inserting field {}", k);
|
||||
}
|
||||
path_op.insert(
|
||||
properties.openapi.cbegin(), properties.openapi.cend());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void tick(std::chrono::milliseconds elapsed, size_t tx_count) override
|
||||
{
|
||||
metrics_tracker.tick(elapsed, tx_count);
|
||||
|
||||
ccf::UserEndpointRegistry::tick(elapsed, tx_count);
|
||||
}
|
||||
};
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
class JS : public ccf::RpcFrontend
|
||||
{
|
||||
private:
|
||||
JSHandlers js_handlers;
|
||||
|
||||
public:
|
||||
JS(NetworkTables& network, ccfapp::AbstractNodeContext& context) :
|
||||
ccf::RpcFrontend(*network.tables, js_handlers),
|
||||
js_handlers(network, context)
|
||||
{}
|
||||
};
|
||||
|
||||
std::shared_ptr<ccf::RpcFrontend> get_rpc_handler(
|
||||
NetworkTables& network, ccfapp::AbstractNodeContext& context)
|
||||
ccf::NetworkTables& network, ccfapp::AbstractNodeContext& context)
|
||||
{
|
||||
return make_shared<JS>(network, context);
|
||||
return get_rpc_handler_impl(network, context);
|
||||
}
|
||||
|
||||
std::vector<ccf::js::FFIPlugin> get_js_plugins()
|
||||
{
|
||||
return {ccf::js::openenclave_plugin};
|
||||
}
|
||||
|
||||
} // namespace ccfapp
|
||||
|
|
|
@ -0,0 +1,653 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the Apache 2.0 License.
|
||||
#include "apps/utils/metrics_tracker.h"
|
||||
#include "ccf/app_interface.h"
|
||||
#include "ccf/historical_queries_adapter.h"
|
||||
#include "ccf/user_frontend.h"
|
||||
#include "ccf/version.h"
|
||||
#include "crypto/entropy.h"
|
||||
#include "crypto/key_wrap.h"
|
||||
#include "crypto/rsa_key_pair.h"
|
||||
#include "js/wrap.h"
|
||||
#include "kv/untyped_map.h"
|
||||
#include "named_auth_policies.h"
|
||||
|
||||
#include <memory>
|
||||
#include <quickjs/quickjs-exports.h>
|
||||
#include <quickjs/quickjs.h>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace ccfapp
|
||||
{
|
||||
using namespace std;
|
||||
using namespace kv;
|
||||
using namespace ccf;
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wc99-extensions"
|
||||
|
||||
class JSHandlers : public UserEndpointRegistry
|
||||
{
|
||||
private:
|
||||
NetworkTables& network;
|
||||
ccfapp::AbstractNodeContext& context;
|
||||
metrics::Tracker metrics_tracker;
|
||||
|
||||
static JSValue create_json_obj(const nlohmann::json& j, JSContext* ctx)
|
||||
{
|
||||
const auto buf = j.dump();
|
||||
return JS_ParseJSON(ctx, buf.data(), buf.size(), "<json>");
|
||||
}
|
||||
|
||||
JSValue create_caller_obj(
|
||||
ccf::endpoints::EndpointContext& endpoint_ctx, JSContext* ctx)
|
||||
{
|
||||
if (endpoint_ctx.caller == nullptr)
|
||||
{
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
auto caller = JS_NewObject(ctx);
|
||||
|
||||
if (auto jwt_ident = endpoint_ctx.try_get_caller<ccf::JwtAuthnIdentity>())
|
||||
{
|
||||
JS_SetPropertyStr(
|
||||
ctx,
|
||||
caller,
|
||||
"policy",
|
||||
JS_NewString(ctx, get_policy_name_from_ident(jwt_ident)));
|
||||
|
||||
auto jwt = JS_NewObject(ctx);
|
||||
JS_SetPropertyStr(
|
||||
ctx,
|
||||
jwt,
|
||||
"keyIssuer",
|
||||
JS_NewStringLen(
|
||||
ctx, jwt_ident->key_issuer.data(), jwt_ident->key_issuer.size()));
|
||||
JS_SetPropertyStr(
|
||||
ctx, jwt, "header", create_json_obj(jwt_ident->header, ctx));
|
||||
JS_SetPropertyStr(
|
||||
ctx, jwt, "payload", create_json_obj(jwt_ident->payload, ctx));
|
||||
JS_SetPropertyStr(ctx, caller, "jwt", jwt);
|
||||
|
||||
return caller;
|
||||
}
|
||||
else if (
|
||||
auto empty_ident =
|
||||
endpoint_ctx.try_get_caller<ccf::EmptyAuthnIdentity>())
|
||||
{
|
||||
JS_SetPropertyStr(
|
||||
ctx,
|
||||
caller,
|
||||
"policy",
|
||||
JS_NewString(ctx, get_policy_name_from_ident(empty_ident)));
|
||||
return caller;
|
||||
}
|
||||
|
||||
char const* policy_name = nullptr;
|
||||
std::string id;
|
||||
bool is_member = false;
|
||||
|
||||
if (
|
||||
auto user_cert_ident =
|
||||
endpoint_ctx.try_get_caller<ccf::UserCertAuthnIdentity>())
|
||||
{
|
||||
policy_name = get_policy_name_from_ident(user_cert_ident);
|
||||
id = user_cert_ident->user_id;
|
||||
is_member = false;
|
||||
}
|
||||
else if (
|
||||
auto member_cert_ident =
|
||||
endpoint_ctx.try_get_caller<ccf::MemberCertAuthnIdentity>())
|
||||
{
|
||||
policy_name = get_policy_name_from_ident(member_cert_ident);
|
||||
id = member_cert_ident->member_id;
|
||||
is_member = true;
|
||||
}
|
||||
else if (
|
||||
auto user_sig_ident =
|
||||
endpoint_ctx.try_get_caller<ccf::UserSignatureAuthnIdentity>())
|
||||
{
|
||||
policy_name = get_policy_name_from_ident(user_sig_ident);
|
||||
id = user_sig_ident->user_id;
|
||||
is_member = false;
|
||||
}
|
||||
else if (
|
||||
auto member_sig_ident =
|
||||
endpoint_ctx.try_get_caller<ccf::MemberSignatureAuthnIdentity>())
|
||||
{
|
||||
policy_name = get_policy_name_from_ident(member_sig_ident);
|
||||
id = member_sig_ident->member_id;
|
||||
is_member = true;
|
||||
}
|
||||
|
||||
if (policy_name == nullptr)
|
||||
{
|
||||
throw std::logic_error("Unable to convert caller info to JS object");
|
||||
}
|
||||
|
||||
// Retrieve user/member data from authenticated caller id
|
||||
nlohmann::json data = nullptr;
|
||||
ccf::ApiResult result = ccf::ApiResult::OK;
|
||||
|
||||
if (is_member)
|
||||
{
|
||||
result = get_member_data_v1(endpoint_ctx.tx, id, data);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = get_user_data_v1(endpoint_ctx.tx, id, data);
|
||||
}
|
||||
|
||||
if (result == ccf::ApiResult::InternalError)
|
||||
{
|
||||
throw std::logic_error(
|
||||
fmt::format("Failed to get data for caller {}", id));
|
||||
}
|
||||
|
||||
crypto::Pem cert;
|
||||
if (is_member)
|
||||
{
|
||||
result = get_member_cert_v1(endpoint_ctx.tx, id, cert);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = get_user_cert_v1(endpoint_ctx.tx, id, cert);
|
||||
}
|
||||
|
||||
if (result == ccf::ApiResult::InternalError)
|
||||
{
|
||||
throw std::logic_error(
|
||||
fmt::format("Failed to get certificate for caller {}", id));
|
||||
}
|
||||
|
||||
JS_SetPropertyStr(ctx, caller, "policy", JS_NewString(ctx, policy_name));
|
||||
JS_SetPropertyStr(
|
||||
ctx, caller, "id", JS_NewStringLen(ctx, id.data(), id.size()));
|
||||
JS_SetPropertyStr(ctx, caller, "data", create_json_obj(data, ctx));
|
||||
JS_SetPropertyStr(
|
||||
ctx,
|
||||
caller,
|
||||
"cert",
|
||||
JS_NewStringLen(ctx, cert.str().data(), cert.size()));
|
||||
|
||||
return caller;
|
||||
}
|
||||
|
||||
JSValue create_request_obj(
|
||||
ccf::endpoints::EndpointContext& endpoint_ctx, JSContext* ctx)
|
||||
{
|
||||
auto request = JS_NewObject(ctx);
|
||||
|
||||
auto headers = JS_NewObject(ctx);
|
||||
for (auto& [header_name, header_value] :
|
||||
endpoint_ctx.rpc_ctx->get_request_headers())
|
||||
{
|
||||
JS_SetPropertyStr(
|
||||
ctx,
|
||||
headers,
|
||||
header_name.c_str(),
|
||||
JS_NewStringLen(ctx, header_value.c_str(), header_value.size()));
|
||||
}
|
||||
JS_SetPropertyStr(ctx, request, "headers", headers);
|
||||
|
||||
const auto& request_query = endpoint_ctx.rpc_ctx->get_request_query();
|
||||
auto query_str =
|
||||
JS_NewStringLen(ctx, request_query.c_str(), request_query.size());
|
||||
JS_SetPropertyStr(ctx, request, "query", query_str);
|
||||
|
||||
auto params = JS_NewObject(ctx);
|
||||
for (auto& [param_name, param_value] :
|
||||
endpoint_ctx.rpc_ctx->get_request_path_params())
|
||||
{
|
||||
JS_SetPropertyStr(
|
||||
ctx,
|
||||
params,
|
||||
param_name.c_str(),
|
||||
JS_NewStringLen(ctx, param_value.c_str(), param_value.size()));
|
||||
}
|
||||
JS_SetPropertyStr(ctx, request, "params", params);
|
||||
|
||||
const auto& request_body = endpoint_ctx.rpc_ctx->get_request_body();
|
||||
auto body_ = JS_NewObjectClass(ctx, js::body_class_id);
|
||||
JS_SetOpaque(body_, (void*)&request_body);
|
||||
JS_SetPropertyStr(ctx, request, "body", body_);
|
||||
|
||||
JS_SetPropertyStr(
|
||||
ctx, request, "caller", create_caller_obj(endpoint_ctx, ctx));
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
void execute_request(
|
||||
const ccf::endpoints::EndpointProperties& props,
|
||||
ccf::endpoints::EndpointContext& endpoint_ctx)
|
||||
{
|
||||
if (props.mode == ccf::endpoints::Mode::Historical)
|
||||
{
|
||||
auto is_tx_committed =
|
||||
[this](ccf::View view, ccf::SeqNo seqno, std::string& error_reason) {
|
||||
return ccf::historical::is_tx_committed_v2(
|
||||
consensus, view, seqno, error_reason);
|
||||
};
|
||||
|
||||
ccf::historical::adapter_v2(
|
||||
[this, &props](
|
||||
ccf::endpoints::EndpointContext& endpoint_ctx,
|
||||
ccf::historical::StatePtr state) {
|
||||
auto tx = state->store->create_tx();
|
||||
auto tx_id = state->transaction_id;
|
||||
auto receipt = state->receipt;
|
||||
assert(receipt);
|
||||
do_execute_request(props, endpoint_ctx, tx, tx_id, receipt);
|
||||
},
|
||||
context.get_historical_state(),
|
||||
is_tx_committed)(endpoint_ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
do_execute_request(
|
||||
props, endpoint_ctx, endpoint_ctx.tx, std::nullopt, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void do_execute_request(
|
||||
const ccf::endpoints::EndpointProperties& props,
|
||||
ccf::endpoints::EndpointContext& endpoint_ctx,
|
||||
kv::Tx& target_tx,
|
||||
const std::optional<ccf::TxID>& transaction_id,
|
||||
ccf::historical::TxReceiptPtr receipt)
|
||||
{
|
||||
js::Runtime rt;
|
||||
rt.add_ccf_classdefs();
|
||||
|
||||
JS_SetModuleLoaderFunc(
|
||||
rt, nullptr, js::js_app_module_loader, &endpoint_ctx.tx);
|
||||
|
||||
js::Context ctx(rt);
|
||||
js::TxContext txctx{&target_tx, js::TxAccess::APP};
|
||||
|
||||
js::register_request_body_class(ctx);
|
||||
js::populate_global(
|
||||
&txctx,
|
||||
endpoint_ctx.rpc_ctx.get(),
|
||||
transaction_id,
|
||||
receipt,
|
||||
nullptr,
|
||||
&context.get_node_state(),
|
||||
nullptr,
|
||||
ctx);
|
||||
|
||||
JSValue export_func;
|
||||
try
|
||||
{
|
||||
auto module_val =
|
||||
js::load_app_module(ctx, props.js_module.c_str(), &endpoint_ctx.tx);
|
||||
export_func =
|
||||
ctx.function(module_val, props.js_function, props.js_module);
|
||||
}
|
||||
catch (const std::exception& exc)
|
||||
{
|
||||
endpoint_ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
exc.what());
|
||||
return;
|
||||
}
|
||||
|
||||
// Call exported function
|
||||
auto request = create_request_obj(endpoint_ctx, ctx);
|
||||
int argc = 1;
|
||||
JSValueConst* argv = (JSValueConst*)&request;
|
||||
auto val = ctx(JS_Call(ctx, export_func, JS_UNDEFINED, argc, argv));
|
||||
JS_FreeValue(ctx, request);
|
||||
JS_FreeValue(ctx, export_func);
|
||||
|
||||
if (JS_IsException(val))
|
||||
{
|
||||
js::js_dump_error(ctx);
|
||||
endpoint_ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"Exception thrown while executing.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle return value: {body, headers, statusCode}
|
||||
if (!JS_IsObject(val))
|
||||
{
|
||||
endpoint_ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"Invalid endpoint function return value (not an object).");
|
||||
return;
|
||||
}
|
||||
|
||||
// Response body (also sets a default response content-type header)
|
||||
{
|
||||
auto response_body_js = ctx(JS_GetPropertyStr(ctx, val, "body"));
|
||||
|
||||
if (!JS_IsUndefined(response_body_js))
|
||||
{
|
||||
std::vector<uint8_t> response_body;
|
||||
size_t buf_size;
|
||||
size_t buf_offset;
|
||||
JSValue typed_array_buffer = JS_GetTypedArrayBuffer(
|
||||
ctx, response_body_js, &buf_offset, &buf_size, nullptr);
|
||||
uint8_t* array_buffer;
|
||||
if (!JS_IsException(typed_array_buffer))
|
||||
{
|
||||
size_t buf_size_total;
|
||||
array_buffer =
|
||||
JS_GetArrayBuffer(ctx, &buf_size_total, typed_array_buffer);
|
||||
array_buffer += buf_offset;
|
||||
JS_FreeValue(ctx, typed_array_buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
array_buffer = JS_GetArrayBuffer(ctx, &buf_size, response_body_js);
|
||||
}
|
||||
if (array_buffer)
|
||||
{
|
||||
endpoint_ctx.rpc_ctx->set_response_header(
|
||||
http::headers::CONTENT_TYPE,
|
||||
http::headervalues::contenttype::OCTET_STREAM);
|
||||
response_body =
|
||||
std::vector<uint8_t>(array_buffer, array_buffer + buf_size);
|
||||
}
|
||||
else
|
||||
{
|
||||
const char* cstr = nullptr;
|
||||
if (JS_IsString(response_body_js))
|
||||
{
|
||||
endpoint_ctx.rpc_ctx->set_response_header(
|
||||
http::headers::CONTENT_TYPE,
|
||||
http::headervalues::contenttype::TEXT);
|
||||
cstr = JS_ToCString(ctx, response_body_js);
|
||||
}
|
||||
else
|
||||
{
|
||||
endpoint_ctx.rpc_ctx->set_response_header(
|
||||
http::headers::CONTENT_TYPE,
|
||||
http::headervalues::contenttype::JSON);
|
||||
JSValue rval =
|
||||
JS_JSONStringify(ctx, response_body_js, JS_NULL, JS_NULL);
|
||||
if (JS_IsException(rval))
|
||||
{
|
||||
js::js_dump_error(ctx);
|
||||
endpoint_ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"Invalid endpoint function return value (error during JSON "
|
||||
"conversion of body).");
|
||||
return;
|
||||
}
|
||||
cstr = JS_ToCString(ctx, rval);
|
||||
JS_FreeValue(ctx, rval);
|
||||
}
|
||||
if (!cstr)
|
||||
{
|
||||
js::js_dump_error(ctx);
|
||||
endpoint_ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"Invalid endpoint function return value (error during string "
|
||||
"conversion of body).");
|
||||
return;
|
||||
}
|
||||
std::string str(cstr);
|
||||
JS_FreeCString(ctx, cstr);
|
||||
|
||||
response_body = std::vector<uint8_t>(str.begin(), str.end());
|
||||
}
|
||||
endpoint_ctx.rpc_ctx->set_response_body(std::move(response_body));
|
||||
}
|
||||
}
|
||||
// Response headers
|
||||
{
|
||||
auto response_headers_js = ctx(JS_GetPropertyStr(ctx, val, "headers"));
|
||||
if (JS_IsObject(response_headers_js))
|
||||
{
|
||||
uint32_t prop_count = 0;
|
||||
JSPropertyEnum* props = nullptr;
|
||||
JS_GetOwnPropertyNames(
|
||||
ctx,
|
||||
&props,
|
||||
&prop_count,
|
||||
response_headers_js,
|
||||
JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY);
|
||||
for (size_t i = 0; i < prop_count; i++)
|
||||
{
|
||||
auto prop_name = props[i].atom;
|
||||
auto prop_name_cstr = ctx(JS_AtomToCString(ctx, prop_name));
|
||||
auto prop_val =
|
||||
ctx(JS_GetProperty(ctx, response_headers_js, prop_name));
|
||||
auto prop_val_cstr = JS_ToCString(ctx, prop_val);
|
||||
if (!prop_val_cstr)
|
||||
{
|
||||
endpoint_ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"Invalid endpoint function return value (header value type).");
|
||||
return;
|
||||
}
|
||||
endpoint_ctx.rpc_ctx->set_response_header(
|
||||
prop_name_cstr, prop_val_cstr);
|
||||
JS_FreeCString(ctx, prop_val_cstr);
|
||||
}
|
||||
js_free(ctx, props);
|
||||
}
|
||||
}
|
||||
|
||||
// Response status code
|
||||
{
|
||||
int response_status_code = HTTP_STATUS_OK;
|
||||
auto status_code_js = ctx(JS_GetPropertyStr(ctx, val, "statusCode"));
|
||||
if (!JS_IsUndefined(status_code_js) && !JS_IsNull(status_code_js))
|
||||
{
|
||||
if (JS_VALUE_GET_TAG(status_code_js.val) != JS_TAG_INT)
|
||||
{
|
||||
endpoint_ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"Invalid endpoint function return value (status code type).");
|
||||
return;
|
||||
}
|
||||
response_status_code = JS_VALUE_GET_INT(status_code_js.val);
|
||||
}
|
||||
endpoint_ctx.rpc_ctx->set_response_status(response_status_code);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
struct JSDynamicEndpoint : public ccf::endpoints::EndpointDefinition
|
||||
{};
|
||||
|
||||
public:
|
||||
JSHandlers(NetworkTables& network, AbstractNodeContext& context) :
|
||||
UserEndpointRegistry(context),
|
||||
network(network),
|
||||
context(context)
|
||||
{
|
||||
metrics_tracker.install_endpoint(*this);
|
||||
}
|
||||
|
||||
void instantiate_authn_policies(JSDynamicEndpoint& endpoint)
|
||||
{
|
||||
for (const auto& policy_name : endpoint.properties.authn_policies)
|
||||
{
|
||||
auto policy = get_policy_by_name(policy_name);
|
||||
if (policy == nullptr)
|
||||
{
|
||||
throw std::logic_error(
|
||||
fmt::format("Unknown auth policy: {}", policy_name));
|
||||
}
|
||||
endpoint.authn_policies.push_back(std::move(policy));
|
||||
}
|
||||
}
|
||||
|
||||
ccf::endpoints::EndpointDefinitionPtr find_endpoint(
|
||||
kv::Tx& tx, enclave::RpcContext& rpc_ctx) override
|
||||
{
|
||||
const auto method = rpc_ctx.get_method();
|
||||
const auto verb = rpc_ctx.get_request_verb();
|
||||
|
||||
auto endpoints =
|
||||
tx.ro<ccf::endpoints::EndpointsMap>(ccf::Tables::ENDPOINTS);
|
||||
|
||||
const auto key = ccf::endpoints::EndpointKey{method, verb};
|
||||
|
||||
// Look for a direct match of the given path
|
||||
const auto it = endpoints->get(key);
|
||||
if (it.has_value())
|
||||
{
|
||||
auto endpoint_def = std::make_shared<JSDynamicEndpoint>();
|
||||
endpoint_def->dispatch = key;
|
||||
endpoint_def->properties = it.value();
|
||||
instantiate_authn_policies(*endpoint_def);
|
||||
return endpoint_def;
|
||||
}
|
||||
|
||||
// If that doesn't exist, look through _all_ the endpoints to find
|
||||
// templated matches. If there is one, that's a match. More is an error,
|
||||
// none means delegate to the base class.
|
||||
{
|
||||
std::vector<ccf::endpoints::EndpointDefinitionPtr> matches;
|
||||
|
||||
endpoints->foreach_key(
|
||||
[this, &endpoints, &matches, &key, &rpc_ctx](const auto& other_key) {
|
||||
if (key.verb == other_key.verb)
|
||||
{
|
||||
const auto opt_spec =
|
||||
ccf::endpoints::parse_path_template(other_key.uri_path);
|
||||
if (opt_spec.has_value())
|
||||
{
|
||||
const auto& template_spec = opt_spec.value();
|
||||
// This endpoint has templates in its path, and the correct verb
|
||||
// - now check if template matches the current request's path
|
||||
std::smatch match;
|
||||
if (std::regex_match(
|
||||
key.uri_path, match, template_spec.template_regex))
|
||||
{
|
||||
if (matches.empty())
|
||||
{
|
||||
// Populate the request_path_params while we have the match,
|
||||
// though this will be discarded on error if we later find
|
||||
// multiple matches
|
||||
auto& path_params = rpc_ctx.get_request_path_params();
|
||||
for (size_t i = 0;
|
||||
i < template_spec.template_component_names.size();
|
||||
++i)
|
||||
{
|
||||
const auto& template_name =
|
||||
template_spec.template_component_names[i];
|
||||
const auto& template_value = match[i + 1].str();
|
||||
path_params[template_name] = template_value;
|
||||
}
|
||||
}
|
||||
|
||||
auto endpoint = std::make_shared<JSDynamicEndpoint>();
|
||||
endpoint->dispatch = other_key;
|
||||
endpoint->properties = endpoints->get(other_key).value();
|
||||
instantiate_authn_policies(*endpoint);
|
||||
matches.push_back(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (matches.size() > 1)
|
||||
{
|
||||
report_ambiguous_templated_path(key.uri_path, matches);
|
||||
}
|
||||
else if (matches.size() == 1)
|
||||
{
|
||||
return matches[0];
|
||||
}
|
||||
}
|
||||
|
||||
return ccf::endpoints::EndpointRegistry::find_endpoint(tx, rpc_ctx);
|
||||
}
|
||||
|
||||
void execute_endpoint(
|
||||
ccf::endpoints::EndpointDefinitionPtr e,
|
||||
ccf::endpoints::EndpointContext& endpoint_ctx) override
|
||||
{
|
||||
auto endpoint = dynamic_cast<const JSDynamicEndpoint*>(e.get());
|
||||
if (endpoint != nullptr)
|
||||
{
|
||||
execute_request(endpoint->properties, endpoint_ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
ccf::endpoints::EndpointRegistry::execute_endpoint(e, endpoint_ctx);
|
||||
}
|
||||
|
||||
// Since we do our own dispatch within the default handler, report the
|
||||
// supported methods here
|
||||
void build_api(nlohmann::json& document, kv::ReadOnlyTx& tx) override
|
||||
{
|
||||
UserEndpointRegistry::build_api(document, tx);
|
||||
|
||||
auto endpoints =
|
||||
tx.ro<ccf::endpoints::EndpointsMap>(ccf::Tables::ENDPOINTS);
|
||||
|
||||
endpoints->foreach([&document](const auto& key, const auto& properties) {
|
||||
const auto http_verb = key.verb.get_http_method();
|
||||
if (!http_verb.has_value())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!properties.openapi_hidden)
|
||||
{
|
||||
auto& path_op = ds::openapi::path_operation(
|
||||
ds::openapi::path(document, key.uri_path),
|
||||
http_verb.value(),
|
||||
false);
|
||||
if (!properties.openapi.empty())
|
||||
{
|
||||
for (const auto& [k, v] : properties.openapi.items())
|
||||
{
|
||||
LOG_INFO_FMT("Inserting field {}", k);
|
||||
}
|
||||
path_op.insert(
|
||||
properties.openapi.cbegin(), properties.openapi.cend());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void tick(std::chrono::milliseconds elapsed, size_t tx_count) override
|
||||
{
|
||||
metrics_tracker.tick(elapsed, tx_count);
|
||||
|
||||
ccf::UserEndpointRegistry::tick(elapsed, tx_count);
|
||||
}
|
||||
};
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
class JS : public ccf::RpcFrontend
|
||||
{
|
||||
private:
|
||||
JSHandlers js_handlers;
|
||||
|
||||
public:
|
||||
JS(NetworkTables& network, ccfapp::AbstractNodeContext& context) :
|
||||
ccf::RpcFrontend(*network.tables, js_handlers),
|
||||
js_handlers(network, context)
|
||||
{}
|
||||
};
|
||||
|
||||
std::shared_ptr<ccf::RpcFrontend> get_rpc_handler_impl(
|
||||
NetworkTables& network, ccfapp::AbstractNodeContext& context)
|
||||
{
|
||||
return make_shared<JS>(network, context);
|
||||
}
|
||||
} // namespace ccfapp
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the Apache 2.0 License.
|
||||
#include "ccf/app_interface.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace ccfapp
|
||||
{
|
||||
std::shared_ptr<ccf::RpcFrontend> get_rpc_handler_impl(
|
||||
ccf::NetworkTables& network, ccfapp::AbstractNodeContext& context);
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
#include "ds/oversized.h"
|
||||
#include "enclave_time.h"
|
||||
#include "interface.h"
|
||||
#include "js/wrap.h"
|
||||
#include "node/entities.h"
|
||||
#include "node/historical_queries.h"
|
||||
#include "node/network_state.h"
|
||||
|
@ -126,6 +127,8 @@ namespace enclave
|
|||
rpc_map->register_frontend<ccf::ActorsType::nodes>(
|
||||
std::make_unique<ccf::NodeRpcFrontend>(network, *context));
|
||||
|
||||
ccf::js::register_ffi_plugins(ccfapp::get_js_plugins());
|
||||
|
||||
node->initialize(
|
||||
consensus_config,
|
||||
rpc_map,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// Licensed under the Apache 2.0 License.
|
||||
#include "js/wrap.h"
|
||||
|
||||
namespace js
|
||||
namespace ccf::js
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wc99-extensions"
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
#include <quickjs/quickjs.h>
|
||||
|
||||
namespace js
|
||||
namespace ccf::js
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wc99-extensions"
|
||||
|
@ -485,4 +485,4 @@ namespace js
|
|||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the Apache 2.0 License.
|
||||
#include "ccf/app_interface.h"
|
||||
#include "ccf/js_plugin.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace ccfapp
|
||||
{
|
||||
std::vector<ccf::js::FFIPlugin> __attribute__((weak)) get_js_plugins()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
}
|
|
@ -13,10 +13,12 @@
|
|||
#else
|
||||
# include <openenclave/host_verify.h>
|
||||
#endif
|
||||
#include "ccf/js_openenclave_plugin.h"
|
||||
#include "ccf/js_plugin.h"
|
||||
#include "ccf/version.h"
|
||||
#include "js/plugin.h"
|
||||
#include "js/wrap.h"
|
||||
|
||||
namespace js
|
||||
namespace ccf::js
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wc99-extensions"
|
|
@ -8,7 +8,7 @@
|
|||
#include "enclave/rpc_context.h"
|
||||
#include "js/conv.cpp"
|
||||
#include "js/crypto.cpp"
|
||||
#include "js/oe.cpp"
|
||||
#include "js/no_plugins.cpp"
|
||||
#include "kv/untyped_map.h"
|
||||
#include "node/jwt.h"
|
||||
#include "node/rpc/call_types.h"
|
||||
|
@ -19,7 +19,7 @@
|
|||
#include <quickjs/quickjs-exports.h>
|
||||
#include <quickjs/quickjs.h>
|
||||
|
||||
namespace js
|
||||
namespace ccf::js
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wc99-extensions"
|
||||
|
@ -59,9 +59,12 @@ namespace js
|
|||
ffi_plugins.push_back(plugin);
|
||||
}
|
||||
|
||||
void register_ffi_plugins()
|
||||
void register_ffi_plugins(const std::vector<FFIPlugin>& plugins)
|
||||
{
|
||||
register_ffi_plugin(openenclave_plugin);
|
||||
for (const auto& plugin : plugins)
|
||||
{
|
||||
register_ffi_plugin(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
static JSValue js_kv_map_has(
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
#pragma once
|
||||
|
||||
#include "ccf/historical_queries_interface.h"
|
||||
#include "ccf/js_plugin.h"
|
||||
#include "ccf/tx.h"
|
||||
#include "ds/logger.h"
|
||||
#include "enclave/rpc_context.h"
|
||||
#include "js/plugin.h"
|
||||
#include "kv/kv_types.h"
|
||||
#include "node/network_state.h"
|
||||
#include "node/rpc/node_interface.h"
|
||||
|
@ -15,7 +15,7 @@
|
|||
#include <quickjs/quickjs-exports.h>
|
||||
#include <quickjs/quickjs.h>
|
||||
|
||||
namespace js
|
||||
namespace ccf::js
|
||||
{
|
||||
extern JSClassID kv_class_id;
|
||||
extern JSClassID kv_map_handle_class_id;
|
||||
|
@ -46,7 +46,7 @@ namespace js
|
|||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wc99-extensions"
|
||||
|
||||
void register_ffi_plugins();
|
||||
void register_ffi_plugins(const std::vector<ccf::js::FFIPlugin>& plugins);
|
||||
void register_class_ids();
|
||||
void register_request_body_class(JSContext* ctx);
|
||||
void populate_global(
|
||||
|
@ -208,4 +208,4 @@ namespace js
|
|||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -333,7 +333,6 @@ namespace ccf
|
|||
get_subject_alternative_names();
|
||||
|
||||
js::register_class_ids();
|
||||
js::register_ffi_plugins();
|
||||
self_signed_node_cert = create_self_signed_node_cert();
|
||||
accept_node_tls_connections();
|
||||
open_frontend(ActorsType::nodes);
|
||||
|
|
Загрузка…
Ссылка в новой задаче