зеркало из https://github.com/microsoft/CCF.git
Initial minimal cut of HTTP support (#396)
This commit is contained in:
Родитель
6127202f5e
Коммит
a0b3a23a48
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,439 @@
|
|||
/* Copyright Joyent, Inc. and other Node contributors. All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
#ifndef http_parser_h
|
||||
#define http_parser_h
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Also update SONAME in the Makefile whenever you change these. */
|
||||
#define HTTP_PARSER_VERSION_MAJOR 2
|
||||
#define HTTP_PARSER_VERSION_MINOR 9
|
||||
#define HTTP_PARSER_VERSION_PATCH 2
|
||||
|
||||
#include <stddef.h>
|
||||
#if defined(_WIN32) && !defined(__MINGW32__) && \
|
||||
(!defined(_MSC_VER) || _MSC_VER<1600) && !defined(__WINE__)
|
||||
#include <BaseTsd.h>
|
||||
typedef __int8 int8_t;
|
||||
typedef unsigned __int8 uint8_t;
|
||||
typedef __int16 int16_t;
|
||||
typedef unsigned __int16 uint16_t;
|
||||
typedef __int32 int32_t;
|
||||
typedef unsigned __int32 uint32_t;
|
||||
typedef __int64 int64_t;
|
||||
typedef unsigned __int64 uint64_t;
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
|
||||
/* Compile with -DHTTP_PARSER_STRICT=0 to make less checks, but run
|
||||
* faster
|
||||
*/
|
||||
#ifndef HTTP_PARSER_STRICT
|
||||
# define HTTP_PARSER_STRICT 1
|
||||
#endif
|
||||
|
||||
/* Maximium header size allowed. If the macro is not defined
|
||||
* before including this header then the default is used. To
|
||||
* change the maximum header size, define the macro in the build
|
||||
* environment (e.g. -DHTTP_MAX_HEADER_SIZE=<value>). To remove
|
||||
* the effective limit on the size of the header, define the macro
|
||||
* to a very large number (e.g. -DHTTP_MAX_HEADER_SIZE=0x7fffffff)
|
||||
*/
|
||||
#ifndef HTTP_MAX_HEADER_SIZE
|
||||
# define HTTP_MAX_HEADER_SIZE (80*1024)
|
||||
#endif
|
||||
|
||||
typedef struct http_parser http_parser;
|
||||
typedef struct http_parser_settings http_parser_settings;
|
||||
|
||||
|
||||
/* Callbacks should return non-zero to indicate an error. The parser will
|
||||
* then halt execution.
|
||||
*
|
||||
* The one exception is on_headers_complete. In a HTTP_RESPONSE parser
|
||||
* returning '1' from on_headers_complete will tell the parser that it
|
||||
* should not expect a body. This is used when receiving a response to a
|
||||
* HEAD request which may contain 'Content-Length' or 'Transfer-Encoding:
|
||||
* chunked' headers that indicate the presence of a body.
|
||||
*
|
||||
* Returning `2` from on_headers_complete will tell parser that it should not
|
||||
* expect neither a body nor any futher responses on this connection. This is
|
||||
* useful for handling responses to a CONNECT request which may not contain
|
||||
* `Upgrade` or `Connection: upgrade` headers.
|
||||
*
|
||||
* http_data_cb does not return data chunks. It will be called arbitrarily
|
||||
* many times for each string. E.G. you might get 10 callbacks for "on_url"
|
||||
* each providing just a few characters more data.
|
||||
*/
|
||||
typedef int (*http_data_cb) (http_parser*, const char *at, size_t length);
|
||||
typedef int (*http_cb) (http_parser*);
|
||||
|
||||
|
||||
/* Status Codes */
|
||||
#define HTTP_STATUS_MAP(XX) \
|
||||
XX(100, CONTINUE, Continue) \
|
||||
XX(101, SWITCHING_PROTOCOLS, Switching Protocols) \
|
||||
XX(102, PROCESSING, Processing) \
|
||||
XX(200, OK, OK) \
|
||||
XX(201, CREATED, Created) \
|
||||
XX(202, ACCEPTED, Accepted) \
|
||||
XX(203, NON_AUTHORITATIVE_INFORMATION, Non-Authoritative Information) \
|
||||
XX(204, NO_CONTENT, No Content) \
|
||||
XX(205, RESET_CONTENT, Reset Content) \
|
||||
XX(206, PARTIAL_CONTENT, Partial Content) \
|
||||
XX(207, MULTI_STATUS, Multi-Status) \
|
||||
XX(208, ALREADY_REPORTED, Already Reported) \
|
||||
XX(226, IM_USED, IM Used) \
|
||||
XX(300, MULTIPLE_CHOICES, Multiple Choices) \
|
||||
XX(301, MOVED_PERMANENTLY, Moved Permanently) \
|
||||
XX(302, FOUND, Found) \
|
||||
XX(303, SEE_OTHER, See Other) \
|
||||
XX(304, NOT_MODIFIED, Not Modified) \
|
||||
XX(305, USE_PROXY, Use Proxy) \
|
||||
XX(307, TEMPORARY_REDIRECT, Temporary Redirect) \
|
||||
XX(308, PERMANENT_REDIRECT, Permanent Redirect) \
|
||||
XX(400, BAD_REQUEST, Bad Request) \
|
||||
XX(401, UNAUTHORIZED, Unauthorized) \
|
||||
XX(402, PAYMENT_REQUIRED, Payment Required) \
|
||||
XX(403, FORBIDDEN, Forbidden) \
|
||||
XX(404, NOT_FOUND, Not Found) \
|
||||
XX(405, METHOD_NOT_ALLOWED, Method Not Allowed) \
|
||||
XX(406, NOT_ACCEPTABLE, Not Acceptable) \
|
||||
XX(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required) \
|
||||
XX(408, REQUEST_TIMEOUT, Request Timeout) \
|
||||
XX(409, CONFLICT, Conflict) \
|
||||
XX(410, GONE, Gone) \
|
||||
XX(411, LENGTH_REQUIRED, Length Required) \
|
||||
XX(412, PRECONDITION_FAILED, Precondition Failed) \
|
||||
XX(413, PAYLOAD_TOO_LARGE, Payload Too Large) \
|
||||
XX(414, URI_TOO_LONG, URI Too Long) \
|
||||
XX(415, UNSUPPORTED_MEDIA_TYPE, Unsupported Media Type) \
|
||||
XX(416, RANGE_NOT_SATISFIABLE, Range Not Satisfiable) \
|
||||
XX(417, EXPECTATION_FAILED, Expectation Failed) \
|
||||
XX(421, MISDIRECTED_REQUEST, Misdirected Request) \
|
||||
XX(422, UNPROCESSABLE_ENTITY, Unprocessable Entity) \
|
||||
XX(423, LOCKED, Locked) \
|
||||
XX(424, FAILED_DEPENDENCY, Failed Dependency) \
|
||||
XX(426, UPGRADE_REQUIRED, Upgrade Required) \
|
||||
XX(428, PRECONDITION_REQUIRED, Precondition Required) \
|
||||
XX(429, TOO_MANY_REQUESTS, Too Many Requests) \
|
||||
XX(431, REQUEST_HEADER_FIELDS_TOO_LARGE, Request Header Fields Too Large) \
|
||||
XX(451, UNAVAILABLE_FOR_LEGAL_REASONS, Unavailable For Legal Reasons) \
|
||||
XX(500, INTERNAL_SERVER_ERROR, Internal Server Error) \
|
||||
XX(501, NOT_IMPLEMENTED, Not Implemented) \
|
||||
XX(502, BAD_GATEWAY, Bad Gateway) \
|
||||
XX(503, SERVICE_UNAVAILABLE, Service Unavailable) \
|
||||
XX(504, GATEWAY_TIMEOUT, Gateway Timeout) \
|
||||
XX(505, HTTP_VERSION_NOT_SUPPORTED, HTTP Version Not Supported) \
|
||||
XX(506, VARIANT_ALSO_NEGOTIATES, Variant Also Negotiates) \
|
||||
XX(507, INSUFFICIENT_STORAGE, Insufficient Storage) \
|
||||
XX(508, LOOP_DETECTED, Loop Detected) \
|
||||
XX(510, NOT_EXTENDED, Not Extended) \
|
||||
XX(511, NETWORK_AUTHENTICATION_REQUIRED, Network Authentication Required) \
|
||||
|
||||
enum http_status
|
||||
{
|
||||
#define XX(num, name, string) HTTP_STATUS_##name = num,
|
||||
HTTP_STATUS_MAP(XX)
|
||||
#undef XX
|
||||
};
|
||||
|
||||
|
||||
/* Request Methods */
|
||||
#define HTTP_METHOD_MAP(XX) \
|
||||
XX(0, DELETE, DELETE) \
|
||||
XX(1, GET, GET) \
|
||||
XX(2, HEAD, HEAD) \
|
||||
XX(3, POST, POST) \
|
||||
XX(4, PUT, PUT) \
|
||||
/* pathological */ \
|
||||
XX(5, CONNECT, CONNECT) \
|
||||
XX(6, OPTIONS, OPTIONS) \
|
||||
XX(7, TRACE, TRACE) \
|
||||
/* WebDAV */ \
|
||||
XX(8, COPY, COPY) \
|
||||
XX(9, LOCK, LOCK) \
|
||||
XX(10, MKCOL, MKCOL) \
|
||||
XX(11, MOVE, MOVE) \
|
||||
XX(12, PROPFIND, PROPFIND) \
|
||||
XX(13, PROPPATCH, PROPPATCH) \
|
||||
XX(14, SEARCH, SEARCH) \
|
||||
XX(15, UNLOCK, UNLOCK) \
|
||||
XX(16, BIND, BIND) \
|
||||
XX(17, REBIND, REBIND) \
|
||||
XX(18, UNBIND, UNBIND) \
|
||||
XX(19, ACL, ACL) \
|
||||
/* subversion */ \
|
||||
XX(20, REPORT, REPORT) \
|
||||
XX(21, MKACTIVITY, MKACTIVITY) \
|
||||
XX(22, CHECKOUT, CHECKOUT) \
|
||||
XX(23, MERGE, MERGE) \
|
||||
/* upnp */ \
|
||||
XX(24, MSEARCH, M-SEARCH) \
|
||||
XX(25, NOTIFY, NOTIFY) \
|
||||
XX(26, SUBSCRIBE, SUBSCRIBE) \
|
||||
XX(27, UNSUBSCRIBE, UNSUBSCRIBE) \
|
||||
/* RFC-5789 */ \
|
||||
XX(28, PATCH, PATCH) \
|
||||
XX(29, PURGE, PURGE) \
|
||||
/* CalDAV */ \
|
||||
XX(30, MKCALENDAR, MKCALENDAR) \
|
||||
/* RFC-2068, section 19.6.1.2 */ \
|
||||
XX(31, LINK, LINK) \
|
||||
XX(32, UNLINK, UNLINK) \
|
||||
/* icecast */ \
|
||||
XX(33, SOURCE, SOURCE) \
|
||||
|
||||
enum http_method
|
||||
{
|
||||
#define XX(num, name, string) HTTP_##name = num,
|
||||
HTTP_METHOD_MAP(XX)
|
||||
#undef XX
|
||||
};
|
||||
|
||||
|
||||
enum http_parser_type { HTTP_REQUEST, HTTP_RESPONSE, HTTP_BOTH };
|
||||
|
||||
|
||||
/* Flag values for http_parser.flags field */
|
||||
enum flags
|
||||
{ F_CHUNKED = 1 << 0
|
||||
, F_CONNECTION_KEEP_ALIVE = 1 << 1
|
||||
, F_CONNECTION_CLOSE = 1 << 2
|
||||
, F_CONNECTION_UPGRADE = 1 << 3
|
||||
, F_TRAILING = 1 << 4
|
||||
, F_UPGRADE = 1 << 5
|
||||
, F_SKIPBODY = 1 << 6
|
||||
, F_CONTENTLENGTH = 1 << 7
|
||||
};
|
||||
|
||||
|
||||
/* Map for errno-related constants
|
||||
*
|
||||
* The provided argument should be a macro that takes 2 arguments.
|
||||
*/
|
||||
#define HTTP_ERRNO_MAP(XX) \
|
||||
/* No error */ \
|
||||
XX(OK, "success") \
|
||||
\
|
||||
/* Callback-related errors */ \
|
||||
XX(CB_message_begin, "the on_message_begin callback failed") \
|
||||
XX(CB_url, "the on_url callback failed") \
|
||||
XX(CB_header_field, "the on_header_field callback failed") \
|
||||
XX(CB_header_value, "the on_header_value callback failed") \
|
||||
XX(CB_headers_complete, "the on_headers_complete callback failed") \
|
||||
XX(CB_body, "the on_body callback failed") \
|
||||
XX(CB_message_complete, "the on_message_complete callback failed") \
|
||||
XX(CB_status, "the on_status callback failed") \
|
||||
XX(CB_chunk_header, "the on_chunk_header callback failed") \
|
||||
XX(CB_chunk_complete, "the on_chunk_complete callback failed") \
|
||||
\
|
||||
/* Parsing-related errors */ \
|
||||
XX(INVALID_EOF_STATE, "stream ended at an unexpected time") \
|
||||
XX(HEADER_OVERFLOW, \
|
||||
"too many header bytes seen; overflow detected") \
|
||||
XX(CLOSED_CONNECTION, \
|
||||
"data received after completed connection: close message") \
|
||||
XX(INVALID_VERSION, "invalid HTTP version") \
|
||||
XX(INVALID_STATUS, "invalid HTTP status code") \
|
||||
XX(INVALID_METHOD, "invalid HTTP method") \
|
||||
XX(INVALID_URL, "invalid URL") \
|
||||
XX(INVALID_HOST, "invalid host") \
|
||||
XX(INVALID_PORT, "invalid port") \
|
||||
XX(INVALID_PATH, "invalid path") \
|
||||
XX(INVALID_QUERY_STRING, "invalid query string") \
|
||||
XX(INVALID_FRAGMENT, "invalid fragment") \
|
||||
XX(LF_EXPECTED, "LF character expected") \
|
||||
XX(INVALID_HEADER_TOKEN, "invalid character in header") \
|
||||
XX(INVALID_CONTENT_LENGTH, \
|
||||
"invalid character in content-length header") \
|
||||
XX(UNEXPECTED_CONTENT_LENGTH, \
|
||||
"unexpected content-length header") \
|
||||
XX(INVALID_CHUNK_SIZE, \
|
||||
"invalid character in chunk size header") \
|
||||
XX(INVALID_CONSTANT, "invalid constant string") \
|
||||
XX(INVALID_INTERNAL_STATE, "encountered unexpected internal state")\
|
||||
XX(STRICT, "strict mode assertion failed") \
|
||||
XX(PAUSED, "parser is paused") \
|
||||
XX(UNKNOWN, "an unknown error occurred")
|
||||
|
||||
|
||||
/* Define HPE_* values for each errno value above */
|
||||
#define HTTP_ERRNO_GEN(n, s) HPE_##n,
|
||||
enum http_errno {
|
||||
HTTP_ERRNO_MAP(HTTP_ERRNO_GEN)
|
||||
};
|
||||
#undef HTTP_ERRNO_GEN
|
||||
|
||||
|
||||
/* Get an http_errno value from an http_parser */
|
||||
#define HTTP_PARSER_ERRNO(p) ((enum http_errno) (p)->http_errno)
|
||||
|
||||
|
||||
struct http_parser {
|
||||
/** PRIVATE **/
|
||||
unsigned int type : 2; /* enum http_parser_type */
|
||||
unsigned int flags : 8; /* F_* values from 'flags' enum; semi-public */
|
||||
unsigned int state : 7; /* enum state from http_parser.c */
|
||||
unsigned int header_state : 7; /* enum header_state from http_parser.c */
|
||||
unsigned int index : 7; /* index into current matcher */
|
||||
unsigned int lenient_http_headers : 1;
|
||||
|
||||
uint32_t nread; /* # bytes read in various scenarios */
|
||||
uint64_t content_length; /* # bytes in body (0 if no Content-Length header) */
|
||||
|
||||
/** READ-ONLY **/
|
||||
unsigned short http_major;
|
||||
unsigned short http_minor;
|
||||
unsigned int status_code : 16; /* responses only */
|
||||
unsigned int method : 8; /* requests only */
|
||||
unsigned int http_errno : 7;
|
||||
|
||||
/* 1 = Upgrade header was present and the parser has exited because of that.
|
||||
* 0 = No upgrade header present.
|
||||
* Should be checked when http_parser_execute() returns in addition to
|
||||
* error checking.
|
||||
*/
|
||||
unsigned int upgrade : 1;
|
||||
|
||||
/** PUBLIC **/
|
||||
void *data; /* A pointer to get hook to the "connection" or "socket" object */
|
||||
};
|
||||
|
||||
|
||||
struct http_parser_settings {
|
||||
http_cb on_message_begin;
|
||||
http_data_cb on_url;
|
||||
http_data_cb on_status;
|
||||
http_data_cb on_header_field;
|
||||
http_data_cb on_header_value;
|
||||
http_cb on_headers_complete;
|
||||
http_data_cb on_body;
|
||||
http_cb on_message_complete;
|
||||
/* When on_chunk_header is called, the current chunk length is stored
|
||||
* in parser->content_length.
|
||||
*/
|
||||
http_cb on_chunk_header;
|
||||
http_cb on_chunk_complete;
|
||||
};
|
||||
|
||||
|
||||
enum http_parser_url_fields
|
||||
{ UF_SCHEMA = 0
|
||||
, UF_HOST = 1
|
||||
, UF_PORT = 2
|
||||
, UF_PATH = 3
|
||||
, UF_QUERY = 4
|
||||
, UF_FRAGMENT = 5
|
||||
, UF_USERINFO = 6
|
||||
, UF_MAX = 7
|
||||
};
|
||||
|
||||
|
||||
/* Result structure for http_parser_parse_url().
|
||||
*
|
||||
* Callers should index into field_data[] with UF_* values iff field_set
|
||||
* has the relevant (1 << UF_*) bit set. As a courtesy to clients (and
|
||||
* because we probably have padding left over), we convert any port to
|
||||
* a uint16_t.
|
||||
*/
|
||||
struct http_parser_url {
|
||||
uint16_t field_set; /* Bitmask of (1 << UF_*) values */
|
||||
uint16_t port; /* Converted UF_PORT string */
|
||||
|
||||
struct {
|
||||
uint16_t off; /* Offset into buffer in which field starts */
|
||||
uint16_t len; /* Length of run in buffer */
|
||||
} field_data[UF_MAX];
|
||||
};
|
||||
|
||||
|
||||
/* Returns the library version. Bits 16-23 contain the major version number,
|
||||
* bits 8-15 the minor version number and bits 0-7 the patch level.
|
||||
* Usage example:
|
||||
*
|
||||
* unsigned long version = http_parser_version();
|
||||
* unsigned major = (version >> 16) & 255;
|
||||
* unsigned minor = (version >> 8) & 255;
|
||||
* unsigned patch = version & 255;
|
||||
* printf("http_parser v%u.%u.%u\n", major, minor, patch);
|
||||
*/
|
||||
unsigned long http_parser_version(void);
|
||||
|
||||
void http_parser_init(http_parser *parser, enum http_parser_type type);
|
||||
|
||||
|
||||
/* Initialize http_parser_settings members to 0
|
||||
*/
|
||||
void http_parser_settings_init(http_parser_settings *settings);
|
||||
|
||||
|
||||
/* Executes the parser. Returns number of parsed bytes. Sets
|
||||
* `parser->http_errno` on error. */
|
||||
size_t http_parser_execute(http_parser *parser,
|
||||
const http_parser_settings *settings,
|
||||
const char *data,
|
||||
size_t len);
|
||||
|
||||
|
||||
/* If http_should_keep_alive() in the on_headers_complete or
|
||||
* on_message_complete callback returns 0, then this should be
|
||||
* the last message on the connection.
|
||||
* If you are the server, respond with the "Connection: close" header.
|
||||
* If you are the client, close the connection.
|
||||
*/
|
||||
int http_should_keep_alive(const http_parser *parser);
|
||||
|
||||
/* Returns a string version of the HTTP method. */
|
||||
const char *http_method_str(enum http_method m);
|
||||
|
||||
/* Returns a string version of the HTTP status code. */
|
||||
const char *http_status_str(enum http_status s);
|
||||
|
||||
/* Return a string name of the given error */
|
||||
const char *http_errno_name(enum http_errno err);
|
||||
|
||||
/* Return a string description of the given error */
|
||||
const char *http_errno_description(enum http_errno err);
|
||||
|
||||
/* Initialize all http_parser_url members to 0 */
|
||||
void http_parser_url_init(struct http_parser_url *u);
|
||||
|
||||
/* Parse a URL; return nonzero on failure */
|
||||
int http_parser_parse_url(const char *buf, size_t buflen,
|
||||
int is_connect,
|
||||
struct http_parser_url *u);
|
||||
|
||||
/* Pause or un-pause the parser; a nonzero value pauses */
|
||||
void http_parser_pause(http_parser *parser, int paused);
|
||||
|
||||
/* Checks if this is the final chunk of the body. */
|
||||
int http_body_is_final(const http_parser *parser);
|
||||
|
||||
/* Change the maximum header size provided at compile time. */
|
||||
void http_parser_set_max_header_size(uint32_t size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
182
CMakeLists.txt
182
CMakeLists.txt
|
@ -125,6 +125,10 @@ if(BUILD_TESTS)
|
|||
use_client_mbedtls(channels_test)
|
||||
target_link_libraries(channels_test PRIVATE secp256k1.host)
|
||||
|
||||
add_unit_test(http_test
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/enclave/test/http.cpp)
|
||||
target_link_libraries(http_test PRIVATE http_parser.host)
|
||||
|
||||
if(NOT PBFT)
|
||||
add_unit_test(frontend_test
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/node/rpc/test/frontend_test.cpp)
|
||||
|
@ -236,105 +240,113 @@ if(BUILD_TESTS)
|
|||
${CMAKE_SOURCE_DIR}/tests/raft_scenarios ${CMAKE_SOURCE_DIR})
|
||||
set_property(TEST raft_scenario_test PROPERTY LABELS raft_scenario)
|
||||
|
||||
## Member client end to end tests
|
||||
add_e2e_test(
|
||||
NAME member_client_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/memberclient.py
|
||||
)
|
||||
|
||||
## Logging client end to end test
|
||||
add_e2e_test(
|
||||
NAME logging_client_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/loggingclient.py
|
||||
)
|
||||
|
||||
if (NOT SAN)
|
||||
if (NOT HTTP)
|
||||
## Member client end to end tests
|
||||
add_e2e_test(
|
||||
NAME connections
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/connections.py
|
||||
NAME member_client_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/memberclient.py
|
||||
)
|
||||
endif()
|
||||
|
||||
## Storing signed votes test
|
||||
add_e2e_test(
|
||||
NAME voting_history_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/votinghistory.py)
|
||||
|
||||
## Lua Logging client end to end test
|
||||
add_e2e_test(
|
||||
NAME lua_logging_client_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/loggingclient.py
|
||||
ADDITIONAL_ARGS
|
||||
--app-script ${CMAKE_SOURCE_DIR}/src/apps/logging/logging.lua)
|
||||
|
||||
## Lua sample app (tx regulator) end to end test
|
||||
add_e2e_test(
|
||||
NAME lua_txregulator_test
|
||||
PYTHON_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/samples/apps/txregulator/tests/txregulatorclient.py
|
||||
ADDITIONAL_ARGS
|
||||
--app-script ${CMAKE_CURRENT_SOURCE_DIR}/samples/apps/txregulator/app/txregulator.lua
|
||||
--datafile ${CMAKE_CURRENT_SOURCE_DIR}/samples/apps/txregulator/dataset/sample_data.csv)
|
||||
|
||||
if(QUOTES_ENABLED)
|
||||
## Logging client end to end test
|
||||
add_e2e_test(
|
||||
NAME governance_tests
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/governance.py
|
||||
NAME logging_client_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/loggingclient.py
|
||||
)
|
||||
|
||||
if (NOT SAN)
|
||||
add_e2e_test(
|
||||
NAME connections
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/connections.py
|
||||
)
|
||||
endif()
|
||||
|
||||
## Storing signed votes test
|
||||
add_e2e_test(
|
||||
NAME voting_history_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/votinghistory.py)
|
||||
|
||||
## Lua Logging client end to end test
|
||||
add_e2e_test(
|
||||
NAME lua_logging_client_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/loggingclient.py
|
||||
ADDITIONAL_ARGS
|
||||
--oesign ${OESIGN}
|
||||
)
|
||||
--app-script ${CMAKE_SOURCE_DIR}/src/apps/logging/logging.lua)
|
||||
|
||||
## Lua sample app (tx regulator) end to end test
|
||||
add_e2e_test(
|
||||
NAME add_node_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/addnode.py
|
||||
)
|
||||
|
||||
add_e2e_test(
|
||||
NAME code_update_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/code_update.py
|
||||
NAME lua_txregulator_test
|
||||
PYTHON_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/samples/apps/txregulator/tests/txregulatorclient.py
|
||||
ADDITIONAL_ARGS
|
||||
--oesign ${OESIGN}
|
||||
--oeconfpath ${CMAKE_CURRENT_SOURCE_DIR}/src/apps/logging/oe_sign.conf
|
||||
--oesignkeypath ${CMAKE_CURRENT_SOURCE_DIR}/src/apps/sample_key.pem
|
||||
--election-timeout 1500
|
||||
)
|
||||
endif()
|
||||
--app-script ${CMAKE_CURRENT_SOURCE_DIR}/samples/apps/txregulator/app/txregulator.lua
|
||||
--datafile ${CMAKE_CURRENT_SOURCE_DIR}/samples/apps/txregulator/dataset/sample_data.csv)
|
||||
|
||||
add_e2e_test(
|
||||
NAME end_to_end_logging
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/e2e_logging.py
|
||||
)
|
||||
if(QUOTES_ENABLED)
|
||||
add_e2e_test(
|
||||
NAME governance_tests
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/governance.py
|
||||
ADDITIONAL_ARGS
|
||||
--oesign ${OESIGN}
|
||||
)
|
||||
|
||||
add_e2e_test(
|
||||
NAME end_to_end_scenario
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/e2e_scenarios.py
|
||||
ADDITIONAL_ARGS
|
||||
--scenario ${CMAKE_SOURCE_DIR}/tests/simple_logging_scenario.json
|
||||
)
|
||||
add_e2e_test(
|
||||
NAME add_node_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/addnode.py
|
||||
)
|
||||
|
||||
add_e2e_test(
|
||||
NAME election_tests
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/election.py
|
||||
ADDITIONAL_ARGS
|
||||
--election-timeout 2000
|
||||
)
|
||||
add_e2e_test(
|
||||
NAME code_update_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/code_update.py
|
||||
ADDITIONAL_ARGS
|
||||
--oesign ${OESIGN}
|
||||
--oeconfpath ${CMAKE_CURRENT_SOURCE_DIR}/src/apps/logging/oe_sign.conf
|
||||
--oesignkeypath ${CMAKE_CURRENT_SOURCE_DIR}/src/apps/sample_key.pem
|
||||
--election-timeout 1500
|
||||
)
|
||||
endif()
|
||||
|
||||
add_e2e_test(
|
||||
NAME recovery_tests
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/recovery.py
|
||||
ADDITIONAL_ARGS
|
||||
${RECOVERY_ARGS}
|
||||
)
|
||||
|
||||
add_e2e_test(
|
||||
NAME schema_tests
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/schema.py
|
||||
ADDITIONAL_ARGS
|
||||
-p libloggingenc
|
||||
--schema-dir ${CMAKE_SOURCE_DIR}/sphinx/source/schemas
|
||||
add_e2e_test(
|
||||
NAME end_to_end_logging
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/e2e_logging.py
|
||||
)
|
||||
|
||||
if (BUILD_SMALLBANK)
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/samples/apps/smallbank/smallbank.cmake)
|
||||
add_e2e_test(
|
||||
NAME end_to_end_scenario
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/e2e_scenarios.py
|
||||
ADDITIONAL_ARGS
|
||||
--scenario ${CMAKE_SOURCE_DIR}/tests/simple_logging_scenario.json
|
||||
)
|
||||
|
||||
add_e2e_test(
|
||||
NAME election_tests
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/election.py
|
||||
ADDITIONAL_ARGS
|
||||
--election-timeout 2000
|
||||
)
|
||||
|
||||
add_e2e_test(
|
||||
NAME recovery_tests
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/recovery.py
|
||||
ADDITIONAL_ARGS
|
||||
${RECOVERY_ARGS}
|
||||
)
|
||||
|
||||
add_e2e_test(
|
||||
NAME schema_tests
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/schema.py
|
||||
ADDITIONAL_ARGS
|
||||
-p libloggingenc
|
||||
--schema-dir ${CMAKE_SOURCE_DIR}/sphinx/source/schemas
|
||||
)
|
||||
|
||||
if (BUILD_SMALLBANK)
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/samples/apps/smallbank/smallbank.cmake)
|
||||
endif()
|
||||
|
||||
else()
|
||||
add_e2e_test(
|
||||
NAME end_to_end_http
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/e2e_http.py
|
||||
)
|
||||
endif()
|
||||
|
||||
else()
|
||||
|
|
|
@ -80,6 +80,15 @@
|
|||
"commitHash": "60e35eb58a0700fd3e0e973c07d3e4b38a8f9502"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": {
|
||||
"type": "git",
|
||||
"git": {
|
||||
"repositoryUrl": "https://github.com/nodejs/http-parser",
|
||||
"commitHash": "5c17dad400e45c5a442a63f250fff2638d144682"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"Version": 1
|
||||
|
|
|
@ -69,6 +69,11 @@ if (USE_NULL_ENCRYPTOR)
|
|||
add_definitions(-DUSE_NULL_ENCRYPTOR)
|
||||
endif()
|
||||
|
||||
option(HTTP "Enable HTTP Support" OFF)
|
||||
if (HTTP)
|
||||
add_definitions(-DHTTP)
|
||||
endif()
|
||||
|
||||
option(SAN "Enable Address and Undefined Behavior Sanitizers" OFF)
|
||||
option(DISABLE_QUOTE_VERIFICATION "Disable quote verification" OFF)
|
||||
option(BUILD_END_TO_END_TESTS "Build end to end tests" ON)
|
||||
|
@ -212,6 +217,9 @@ set(LUA_SOURCES
|
|||
${LUA_DIR}/lvm.c
|
||||
${LUA_DIR}/lzio.c)
|
||||
|
||||
set(HTTP_PARSER_SOURCES
|
||||
${CCF_DIR}/3rdparty/http-parser/http_parser.c)
|
||||
|
||||
set(OE_MBEDTLS_LIBRARIES
|
||||
"${OE_LIB_DIR}/enclave/libmbedtls.a"
|
||||
"${OE_LIB_DIR}/enclave/libmbedx509.a"
|
||||
|
@ -408,6 +416,7 @@ function(add_enclave_lib name app_oe_conf_path enclave_sign_key_path)
|
|||
-Wl,-Bstatic,-Bsymbolic,--export-dynamic,-pie
|
||||
${PARSED_ARGS_LINK_LIBS}
|
||||
${ENCLAVE_LIBS}
|
||||
http_parser.enclave
|
||||
)
|
||||
set_property(TARGET ${name} PROPERTY POSITION_INDEPENDENT_CODE ON)
|
||||
sign_app_library(${name} ${app_oe_conf_path} ${enclave_sign_key_path})
|
||||
|
@ -460,6 +469,7 @@ function(add_enclave_lib name app_oe_conf_path enclave_sign_key_path)
|
|||
lua.host
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
secp256k1.host
|
||||
http_parser.host
|
||||
)
|
||||
enable_coverage(${virt_name})
|
||||
use_client_mbedtls(${virt_name})
|
||||
|
@ -573,6 +583,12 @@ add_library(lua.host STATIC ${LUA_SOURCES})
|
|||
target_compile_definitions(lua.host PRIVATE NO_IO)
|
||||
set_property(TARGET lua.host PROPERTY POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
# HTTP parser
|
||||
add_enclave_library_c(http_parser.enclave "${HTTP_PARSER_SOURCES}")
|
||||
set_property(TARGET http_parser.enclave PROPERTY POSITION_INDEPENDENT_CODE ON)
|
||||
add_enclave_library_c(http_parser.host "${HTTP_PARSER_SOURCES}")
|
||||
set_property(TARGET http_parser.host PROPERTY POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
# Common test args for Python scripts starting up CCF networks
|
||||
set(CCF_NETWORK_TEST_ARGS
|
||||
${TEST_IGNORE_QUOTE}
|
||||
|
@ -646,6 +662,14 @@ function(add_e2e_test)
|
|||
PROPERTY
|
||||
LABELS end_to_end
|
||||
)
|
||||
if (HTTP)
|
||||
set_property(
|
||||
TEST ${PARSED_ARGS_NAME}
|
||||
APPEND
|
||||
PROPERTY
|
||||
ENVIRONMENT "HTTP=ON"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ public:
|
|||
nlohmann::json sj;
|
||||
sj["req"] = j;
|
||||
sj["sig"] = sig_contents;
|
||||
|
||||
return gen_rpc_raw(sj, {j["id"]});
|
||||
}
|
||||
};
|
|
@ -383,7 +383,7 @@ namespace raft
|
|||
{
|
||||
if (timeout_elapsed >= request_timeout)
|
||||
{
|
||||
LOG_DEBUG_FMT("Sending periodic updates to followers");
|
||||
LOG_TRACE_FMT("Sending periodic updates to followers");
|
||||
using namespace std::chrono_literals;
|
||||
timeout_elapsed = 0ms;
|
||||
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the Apache 2.0 License.
|
||||
#pragma once
|
||||
|
||||
#include "tlsendpoint.h"
|
||||
|
||||
#include <http-parser/http_parser.h>
|
||||
|
||||
namespace enclave
|
||||
{
|
||||
namespace http
|
||||
{
|
||||
// TODO: Split into a request formatter class
|
||||
std::vector<uint8_t> post(const std::string& body)
|
||||
{
|
||||
auto req = fmt::format(
|
||||
"POST / HTTP/1.1\r\n"
|
||||
"Content-Type: application/json\r\n"
|
||||
"Content-Length: {}\r\n\r\n{}",
|
||||
body.size(),
|
||||
body);
|
||||
return std::vector<uint8_t>(req.begin(), req.end());
|
||||
}
|
||||
|
||||
std::vector<uint8_t> post_header(const std::vector<uint8_t>& body)
|
||||
{
|
||||
auto req = fmt::format(
|
||||
"POST / HTTP/1.1\r\n"
|
||||
"Content-Type: application/json\r\n"
|
||||
"Content-Length: {}\r\n\r\n{}",
|
||||
body.size());
|
||||
return std::vector<uint8_t>(req.begin(), req.end());
|
||||
}
|
||||
|
||||
class MsgProcessor
|
||||
{
|
||||
public:
|
||||
virtual void msg(std::vector<uint8_t> m) = 0;
|
||||
};
|
||||
|
||||
enum State
|
||||
{
|
||||
DONE,
|
||||
IN_MESSAGE
|
||||
};
|
||||
|
||||
static int on_msg(http_parser* parser);
|
||||
static int on_msg_end(http_parser* parser);
|
||||
static int on_req(http_parser* parser, const char* at, size_t length);
|
||||
|
||||
class Parser
|
||||
{
|
||||
private:
|
||||
http_parser parser;
|
||||
http_parser_settings settings;
|
||||
MsgProcessor& proc;
|
||||
State state = DONE;
|
||||
std::vector<uint8_t> buf;
|
||||
|
||||
public:
|
||||
Parser(http_parser_type type, MsgProcessor& proc_) : proc(proc_)
|
||||
{
|
||||
http_parser_settings_init(&settings);
|
||||
settings.on_body = on_req;
|
||||
settings.on_message_begin = on_msg;
|
||||
settings.on_message_complete = on_msg_end;
|
||||
http_parser_init(&parser, type);
|
||||
parser.data = this;
|
||||
}
|
||||
|
||||
size_t execute(const uint8_t* data, size_t size)
|
||||
{
|
||||
auto parsed =
|
||||
http_parser_execute(&parser, &settings, (const char*)data, size);
|
||||
LOG_TRACE_FMT("Parsed {} bytes", parsed);
|
||||
auto err = HTTP_PARSER_ERRNO(&parser);
|
||||
if (err)
|
||||
throw std::runtime_error(fmt::format(
|
||||
"HTTP parsing failed: {}: {}",
|
||||
http_errno_name(err),
|
||||
http_errno_description(err)));
|
||||
// TODO: check for http->upgrade to support websockets
|
||||
return parsed;
|
||||
}
|
||||
|
||||
void append(const char* at, size_t length)
|
||||
{
|
||||
if (state == IN_MESSAGE)
|
||||
{
|
||||
LOG_TRACE_FMT("Appending chunk [{}]", std::string(at, at + length));
|
||||
std::copy(at, at + length, std::back_inserter(buf));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Receiving content outside of message");
|
||||
}
|
||||
}
|
||||
|
||||
void new_message()
|
||||
{
|
||||
if (state == DONE)
|
||||
{
|
||||
LOG_TRACE_FMT("Entering new message");
|
||||
state = IN_MESSAGE;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Entering new message when previous message isn't complete");
|
||||
}
|
||||
}
|
||||
|
||||
void end_message()
|
||||
{
|
||||
if (state == IN_MESSAGE)
|
||||
{
|
||||
LOG_TRACE_FMT("Done with message");
|
||||
proc.msg(std::move(buf));
|
||||
state = DONE;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Ending message, but not in a message");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class ResponseHeaderEmitter
|
||||
{
|
||||
public:
|
||||
static std::vector<uint8_t> emit(const std::vector<uint8_t> data)
|
||||
{
|
||||
if (data.size() == 0)
|
||||
{
|
||||
auto hdr = fmt::format("HTTP/1.1 204 No Content\r\n");
|
||||
return std::vector<uint8_t>(hdr.begin(), hdr.end());
|
||||
}
|
||||
else
|
||||
{
|
||||
auto hdr = fmt::format(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: "
|
||||
"application/json\r\nContent-Length: {}\r\n\r\n",
|
||||
data.size());
|
||||
return std::vector<uint8_t>(hdr.begin(), hdr.end());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class RequestHeaderEmitter
|
||||
{
|
||||
public:
|
||||
static std::vector<uint8_t> emit(const std::vector<uint8_t> data)
|
||||
{
|
||||
return http::post_header(data);
|
||||
}
|
||||
};
|
||||
|
||||
static int on_msg(http_parser* parser)
|
||||
{
|
||||
Parser* p = reinterpret_cast<Parser*>(parser->data);
|
||||
p->new_message();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int on_msg_end(http_parser* parser)
|
||||
{
|
||||
Parser* p = reinterpret_cast<Parser*>(parser->data);
|
||||
p->end_message();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int on_req(http_parser* parser, const char* at, size_t length)
|
||||
{
|
||||
Parser* p = reinterpret_cast<Parser*>(parser->data);
|
||||
p->append(at, length);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
template <class E>
|
||||
class HTTPEndpoint : public TLSEndpoint, public http::MsgProcessor
|
||||
{
|
||||
protected:
|
||||
http::Parser p;
|
||||
|
||||
public:
|
||||
HTTPEndpoint(
|
||||
size_t session_id,
|
||||
ringbuffer::AbstractWriterFactory& writer_factory,
|
||||
std::unique_ptr<tls::Context> ctx)
|
||||
{
|
||||
LOG_FAIL_FMT("FAIL");
|
||||
assert(false);
|
||||
}
|
||||
|
||||
void recv(const uint8_t* data, size_t size)
|
||||
{
|
||||
recv_buffered(data, size);
|
||||
|
||||
LOG_TRACE_FMT("recv called with {} bytes", size);
|
||||
auto buf = read(4096, false); // TODO: retry if more was pending
|
||||
LOG_TRACE_FMT("read got {}", buf.size());
|
||||
|
||||
if (buf.size() == 0)
|
||||
return;
|
||||
|
||||
LOG_TRACE_FMT(
|
||||
"Going to parse {} bytes: [{}]",
|
||||
buf.size(),
|
||||
std::string(buf.begin(), buf.end()));
|
||||
|
||||
if (p.execute(buf.data(), buf.size()) == 0)
|
||||
return;
|
||||
}
|
||||
|
||||
virtual void msg(std::vector<uint8_t> m)
|
||||
{
|
||||
if (m.size() > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!handle_data(m))
|
||||
close();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// On any exception, close the connection.
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void send(const std::vector<uint8_t>& data)
|
||||
{
|
||||
send_buffered(E::emit(data));
|
||||
if (data.size() > 0)
|
||||
send_buffered(data);
|
||||
flush();
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
HTTPEndpoint<http::RequestHeaderEmitter>::HTTPEndpoint(
|
||||
size_t session_id,
|
||||
ringbuffer::AbstractWriterFactory& writer_factory,
|
||||
std::unique_ptr<tls::Context> ctx) :
|
||||
TLSEndpoint(session_id, writer_factory, std::move(ctx)),
|
||||
p(HTTP_RESPONSE, *this)
|
||||
{}
|
||||
|
||||
template <>
|
||||
HTTPEndpoint<http::ResponseHeaderEmitter>::HTTPEndpoint(
|
||||
size_t session_id,
|
||||
ringbuffer::AbstractWriterFactory& writer_factory,
|
||||
std::unique_ptr<tls::Context> ctx) :
|
||||
TLSEndpoint(session_id, writer_factory, std::move(ctx)),
|
||||
p(HTTP_REQUEST, *this)
|
||||
{}
|
||||
}
|
|
@ -3,11 +3,18 @@
|
|||
#pragma once
|
||||
|
||||
#include "enclavetypes.h"
|
||||
#include "http.h"
|
||||
#include "tlsframedendpoint.h"
|
||||
|
||||
namespace enclave
|
||||
{
|
||||
class RPCClient : public FramedTLSEndpoint
|
||||
#ifdef HTTP
|
||||
using ClientEndpoint = HTTPEndpoint<http::RequestHeaderEmitter>;
|
||||
#else
|
||||
using ClientEndpoint = FramedTLSEndpoint;
|
||||
#endif
|
||||
|
||||
class RPCClient : public ClientEndpoint
|
||||
{
|
||||
using HandleDataCallback =
|
||||
std::function<bool(const std::vector<uint8_t>& data)>;
|
||||
|
@ -20,7 +27,7 @@ namespace enclave
|
|||
size_t session_id,
|
||||
ringbuffer::AbstractWriterFactory& writer_factory,
|
||||
std::unique_ptr<tls::Context> ctx) :
|
||||
FramedTLSEndpoint(session_id, writer_factory, move(ctx))
|
||||
ClientEndpoint(session_id, writer_factory, move(ctx))
|
||||
{}
|
||||
|
||||
void connect(
|
||||
|
|
|
@ -2,12 +2,19 @@
|
|||
// Licensed under the Apache 2.0 License.
|
||||
#pragma once
|
||||
|
||||
#include "http.h"
|
||||
#include "rpcmap.h"
|
||||
#include "tlsframedendpoint.h"
|
||||
|
||||
namespace enclave
|
||||
{
|
||||
class RPCEndpoint : public FramedTLSEndpoint
|
||||
#ifdef HTTP
|
||||
using ServerEndpoint = HTTPEndpoint<http::ResponseHeaderEmitter>;
|
||||
#else
|
||||
using ServerEndpoint = FramedTLSEndpoint;
|
||||
#endif
|
||||
|
||||
class RPCEndpoint : public ServerEndpoint
|
||||
{
|
||||
private:
|
||||
std::shared_ptr<RpcMap> rpc_map;
|
||||
|
@ -22,7 +29,7 @@ namespace enclave
|
|||
size_t session_id,
|
||||
ringbuffer::AbstractWriterFactory& writer_factory,
|
||||
std::unique_ptr<tls::Context> ctx) :
|
||||
FramedTLSEndpoint(session_id, writer_factory, move(ctx)),
|
||||
ServerEndpoint(session_id, writer_factory, move(ctx)),
|
||||
rpc_map(rpc_map_),
|
||||
session_id(session_id)
|
||||
{}
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the Apache 2.0 License.
|
||||
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||
#include "../http.h"
|
||||
|
||||
#include <doctest/doctest.h>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
|
||||
std::vector<uint8_t> post(const std::string& body)
|
||||
{
|
||||
auto req = fmt::format(
|
||||
"POST / HTTP/1.1\r\n"
|
||||
"Content-Type: application/json\r\n"
|
||||
"Content-Length: {}\r\n\r\n{}",
|
||||
body.size(),
|
||||
body);
|
||||
return std::vector<uint8_t>(req.begin(), req.end());
|
||||
}
|
||||
|
||||
class StubProc : public enclave::http::MsgProcessor
|
||||
{
|
||||
std::queue<std::vector<uint8_t>> chunks;
|
||||
|
||||
public:
|
||||
virtual void msg(std::vector<uint8_t> m)
|
||||
{
|
||||
chunks.emplace(m);
|
||||
}
|
||||
|
||||
void expect(std::vector<std::string> msgs)
|
||||
{
|
||||
for (auto s : msgs)
|
||||
{
|
||||
if (!chunks.empty())
|
||||
{
|
||||
CHECK(std::string(chunks.front().begin(), chunks.front().end()) == s);
|
||||
chunks.pop();
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK_MESSAGE(false, fmt::format("Did not contain: {}", s));
|
||||
}
|
||||
}
|
||||
CHECK(chunks.size() == 0);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CASE("Complete request")
|
||||
{
|
||||
std::string r("{}");
|
||||
|
||||
StubProc sp;
|
||||
enclave::http::Parser p(HTTP_REQUEST, sp);
|
||||
|
||||
auto req = post(r);
|
||||
auto parsed = p.execute(req.data(), req.size());
|
||||
|
||||
sp.expect({r});
|
||||
}
|
||||
|
||||
TEST_CASE("Parsing error")
|
||||
{
|
||||
std::string r("{}");
|
||||
|
||||
StubProc sp;
|
||||
enclave::http::Parser p(HTTP_REQUEST, sp);
|
||||
|
||||
auto req = post(r);
|
||||
req[6] = '\n';
|
||||
CHECK_THROWS_WITH(
|
||||
p.execute(req.data(), req.size()),
|
||||
"HTTP parsing failed: HPE_INVALID_HEADER_TOKEN: invalid character in "
|
||||
"header");
|
||||
sp.expect({});
|
||||
}
|
||||
|
||||
TEST_CASE("Partial request")
|
||||
{
|
||||
std::string r("{}");
|
||||
|
||||
StubProc sp;
|
||||
enclave::http::Parser p(HTTP_REQUEST, sp);
|
||||
|
||||
auto req = post(r);
|
||||
size_t offset = 10;
|
||||
|
||||
auto parsed = p.execute(req.data(), req.size() - offset);
|
||||
CHECK(parsed == req.size() - offset);
|
||||
parsed = p.execute(req.data() + req.size() - offset, offset);
|
||||
CHECK(parsed == offset);
|
||||
|
||||
sp.expect({r});
|
||||
}
|
||||
|
||||
TEST_CASE("Partial body")
|
||||
{
|
||||
std::string r("{\"a_json_key\": \"a_json_value\"}");
|
||||
|
||||
StubProc sp;
|
||||
enclave::http::Parser p(HTTP_REQUEST, sp);
|
||||
|
||||
auto req = post(r);
|
||||
size_t offset = 10;
|
||||
|
||||
auto parsed = p.execute(req.data(), req.size() - offset);
|
||||
CHECK(parsed == req.size() - offset);
|
||||
|
||||
parsed = p.execute(req.data() + req.size() - offset, offset);
|
||||
CHECK(parsed == offset);
|
||||
|
||||
sp.expect({r});
|
||||
}
|
||||
|
||||
TEST_CASE("Multiple requests")
|
||||
{
|
||||
std::string r0("{\"a_json_key\": \"a_json_value\"}");
|
||||
std::string r1("{\"another_json_key\": \"another_json_value\"}");
|
||||
|
||||
StubProc sp;
|
||||
enclave::http::Parser p(HTTP_REQUEST, sp);
|
||||
|
||||
auto req = post(r0);
|
||||
auto req1 = post(r1);
|
||||
std::copy(req1.begin(), req1.end(), std::back_inserter(req));
|
||||
|
||||
auto parsed = p.execute(req.data(), req.size());
|
||||
CHECK(parsed == req.size());
|
||||
|
||||
sp.expect({r0, r1});
|
||||
}
|
|
@ -29,6 +29,7 @@ namespace enclave
|
|||
|
||||
std::vector<uint8_t> pending_write;
|
||||
std::vector<uint8_t> pending_read;
|
||||
// Decrypted data, read through mbedtls
|
||||
std::vector<uint8_t> read_buffer;
|
||||
|
||||
std::unique_ptr<tls::Context> ctx;
|
||||
|
@ -73,7 +74,7 @@ namespace enclave
|
|||
std::vector<uint8_t> read(size_t up_to, bool exact = false)
|
||||
{
|
||||
LOG_TRACE_FMT("Requesting {} bytes", up_to);
|
||||
// This will return en empty vector if the connection isn't
|
||||
// This will return an empty vector if the connection isn't
|
||||
// ready, but it will not block on the handshake.
|
||||
do_handshake();
|
||||
|
||||
|
@ -131,7 +132,9 @@ namespace enclave
|
|||
data.resize(offset);
|
||||
|
||||
if (!exact)
|
||||
{
|
||||
return data;
|
||||
}
|
||||
|
||||
read_buffer = move(data);
|
||||
return {};
|
||||
|
|
|
@ -331,7 +331,7 @@ namespace ccf
|
|||
std::lock_guard<SpinLock> guard(lock);
|
||||
sm.expect(State::pending);
|
||||
|
||||
auto j = jsonrpc::unpack(data, jsonrpc::Pack::MsgPack);
|
||||
auto j = jsonrpc::unpack(data, jsonrpc::Pack::Text);
|
||||
|
||||
// Check that the response is valid.
|
||||
try
|
||||
|
@ -409,7 +409,7 @@ namespace ccf
|
|||
join_rpc.params.quote = quote;
|
||||
|
||||
auto join_req =
|
||||
jsonrpc::pack(nlohmann::json(join_rpc), jsonrpc::Pack::MsgPack);
|
||||
jsonrpc::pack(nlohmann::json(join_rpc), jsonrpc::Pack::Text);
|
||||
|
||||
LOG_INFO_FMT("Sending join request");
|
||||
|
||||
|
|
|
@ -900,6 +900,10 @@ namespace ccf
|
|||
bool is_forwarded,
|
||||
SignedReq& signed_request)
|
||||
{
|
||||
#ifdef HTTP
|
||||
return true; // TODO: use Authorize header
|
||||
#endif
|
||||
|
||||
if (!client_signatures)
|
||||
return false;
|
||||
|
||||
|
|
|
@ -336,9 +336,11 @@ namespace ccf
|
|||
if (!check_member_active(args.tx, args.caller_id))
|
||||
return jsonrpc::error(jsonrpc::CCFErrorCodes::INSUFFICIENT_RIGHTS);
|
||||
|
||||
#ifndef HTTP
|
||||
if (args.signed_request.sig.empty())
|
||||
return jsonrpc::error(
|
||||
jsonrpc::CCFErrorCodes::RPC_NOT_SIGNED, "Votes must be signed");
|
||||
#endif
|
||||
|
||||
const auto vote = args.params.get<Vote>();
|
||||
auto proposals = args.tx.get_view(this->network.proposals);
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the Apache 2.0 License.
|
||||
import os
|
||||
import getpass
|
||||
import time
|
||||
import logging
|
||||
import multiprocessing
|
||||
import shutil
|
||||
from random import seed
|
||||
import infra.ccf
|
||||
import infra.proc
|
||||
import infra.jsonrpc
|
||||
import infra.notification
|
||||
import infra.net
|
||||
import e2e_args
|
||||
|
||||
from loguru import logger as LOG
|
||||
|
||||
|
||||
def run(args):
|
||||
hosts = ["localhost"]
|
||||
|
||||
with infra.notification.notification_server(args.notify_server) as notifications:
|
||||
|
||||
with infra.ccf.network(
|
||||
hosts, args.build_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb
|
||||
) as network:
|
||||
primary, _ = network.start_and_join(args)
|
||||
with primary.user_client(format="json") as c:
|
||||
c.rpc("LOG_record", {"id": 42, "msg": "Hello"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
args = e2e_args.cli_args()
|
||||
args.package = "libloggingenc"
|
||||
notify_server_host = "localhost"
|
||||
args.notify_server = (
|
||||
notify_server_host
|
||||
+ ":"
|
||||
+ str(infra.net.probably_free_local_port(notify_server_host))
|
||||
)
|
||||
run(args)
|
|
@ -426,14 +426,59 @@ class Network:
|
|||
proposal and make members vote to transition the network to state
|
||||
OPEN.
|
||||
"""
|
||||
result = self.propose(1, node, "open_network")
|
||||
self.vote_using_majority(node, result[1]["id"])
|
||||
self.check_for_service(node)
|
||||
if os.getenv("HTTP"):
|
||||
with node.member_client() as mc:
|
||||
script = """
|
||||
tables = ...
|
||||
return Calls:call("open_network")
|
||||
"""
|
||||
r = mc.rpc("propose", {"parameter": None, "script": {"text": script}})
|
||||
with node.member_client(2) as mc2:
|
||||
script = """
|
||||
tables, changes = ...
|
||||
return true
|
||||
"""
|
||||
r = mc2.rpc(
|
||||
"vote",
|
||||
{"ballot": {"text": script}, "id": r.result["id"]},
|
||||
signed=True,
|
||||
)
|
||||
time.sleep(3)
|
||||
else:
|
||||
result = self.propose(1, node, "open_network")
|
||||
self.vote_using_majority(node, result[1]["id"])
|
||||
self.check_for_service(node)
|
||||
|
||||
def add_users(self, node, users):
|
||||
for u in users:
|
||||
result = self.propose(1, node, "add_user", f"--user-cert=user{u}_cert.pem")
|
||||
result = self.vote_using_majority(node, result[1]["id"])
|
||||
if os.getenv("HTTP"):
|
||||
with node.member_client() as mc:
|
||||
for u in users:
|
||||
user_cert = []
|
||||
with open(f"user{u}_cert.pem") as cert:
|
||||
user_cert = [ord(c) for c in cert.read()]
|
||||
script = """
|
||||
tables, user_cert = ...
|
||||
return Calls:call("new_user", user_cert)
|
||||
"""
|
||||
r = mc.rpc(
|
||||
"propose", {"parameter": user_cert, "script": {"text": script}}
|
||||
)
|
||||
with node.member_client(2) as mc2:
|
||||
script = """
|
||||
tables, changes = ...
|
||||
return true
|
||||
"""
|
||||
r = mc2.rpc(
|
||||
"vote",
|
||||
{"ballot": {"text": script}, "id": r.result["id"]},
|
||||
signed=True,
|
||||
)
|
||||
else:
|
||||
for u in users:
|
||||
result = self.propose(
|
||||
1, node, "add_user", f"--user-cert=user{u}_cert.pem"
|
||||
)
|
||||
result = self.vote_using_majority(node, result[1]["id"])
|
||||
|
||||
def stop_all_nodes(self):
|
||||
for node in self.nodes:
|
||||
|
|
|
@ -10,6 +10,9 @@ import json
|
|||
import logging
|
||||
import time
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import base64
|
||||
from enum import IntEnum
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
@ -315,6 +318,131 @@ class FramedTLSJSONRPCClient:
|
|||
return self.response(id)
|
||||
|
||||
|
||||
# We use curl for now because we still use SNI to route to frontends
|
||||
# and that's difficult to force in Python clients, whereas curl conveniently
|
||||
# exposes --resolver
|
||||
# We probably will keep this around in a limited fashion later still, because
|
||||
# the resulting logs nicely illustrate manual usage in a way using requests doesn't
|
||||
class CurlClient:
|
||||
def __init__(
|
||||
self,
|
||||
host,
|
||||
port,
|
||||
server_hostname,
|
||||
cert,
|
||||
key,
|
||||
cafile,
|
||||
version,
|
||||
format,
|
||||
description,
|
||||
):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.server_hostname = server_hostname
|
||||
self.cert = cert
|
||||
self.key = key
|
||||
self.cafile = cafile
|
||||
self.version = version
|
||||
self.format = format
|
||||
self.stream = Stream(version, format=format)
|
||||
self.pending = {}
|
||||
|
||||
def signed_request(self, method, params):
|
||||
r = self.stream.request(method, params)
|
||||
with tempfile.NamedTemporaryFile() as nf:
|
||||
msg = getattr(r, "to_{}".format(self.format))()
|
||||
LOG.debug("Going to send {}".format(msg))
|
||||
nf.write(msg)
|
||||
nf.flush()
|
||||
dgst = subprocess.run(
|
||||
["openssl", "dgst", "-sha256", "-sign", "member1_privk.pem", nf.name],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
subprocess.run(["cat", nf.name], check=True)
|
||||
cmd = [
|
||||
"curl",
|
||||
"-v",
|
||||
"-k",
|
||||
f"https://{self.server_hostname}:{self.port}/",
|
||||
"-H",
|
||||
"Content-Type: application/json",
|
||||
"-H",
|
||||
f"Authorize: {base64.b64encode(dgst.stdout).decode()}",
|
||||
"--resolve",
|
||||
f"{self.server_hostname}:{self.port}:{self.host}",
|
||||
"--data-binary",
|
||||
f"@{nf.name}",
|
||||
]
|
||||
if self.cafile:
|
||||
cmd.extend(["--cacert", self.cafile])
|
||||
if self.key:
|
||||
cmd.extend(["--key", self.key])
|
||||
if self.cert:
|
||||
cmd.extend(["--cert", self.cert])
|
||||
LOG.debug(f"Running: {' '.join(cmd)}")
|
||||
rc = subprocess.run(cmd, capture_output=True)
|
||||
LOG.debug(f"Received {rc.stdout.decode()}")
|
||||
if rc.returncode != 0:
|
||||
LOG.debug(f"ERR {rc.stderr.decode()}")
|
||||
self.stream.update(rc.stdout)
|
||||
return r.id
|
||||
|
||||
def request(self, method, params):
|
||||
r = self.stream.request(method, params)
|
||||
with tempfile.NamedTemporaryFile() as nf:
|
||||
msg = getattr(r, "to_{}".format(self.format))()
|
||||
LOG.debug("Going to send {}".format(msg))
|
||||
nf.write(msg)
|
||||
nf.flush()
|
||||
cmd = [
|
||||
"curl",
|
||||
"-k",
|
||||
f"https://{self.server_hostname}:{self.port}/",
|
||||
"-H",
|
||||
"Content-Type: application/json",
|
||||
"--resolve",
|
||||
f"{self.server_hostname}:{self.port}:{self.host}",
|
||||
"--data-binary",
|
||||
f"@{nf.name}",
|
||||
]
|
||||
if self.cafile:
|
||||
cmd.extend(["--cacert", self.cafile])
|
||||
if self.key:
|
||||
cmd.extend(["--key", self.key])
|
||||
if self.cert:
|
||||
cmd.extend(["--cert", self.cert])
|
||||
LOG.debug(f"Running: {' '.join(cmd)}")
|
||||
rc = subprocess.run(cmd, capture_output=True)
|
||||
LOG.debug(f"Received {rc.stdout.decode()}")
|
||||
if rc.returncode != 0:
|
||||
LOG.debug(f"ERR {rc.stderr.decode()}")
|
||||
self.stream.update(rc.stdout)
|
||||
return r.id
|
||||
|
||||
def response(self, id):
|
||||
return self.stream.response(id)
|
||||
|
||||
def do(self, method, params, expected_result=None, expected_error_code=None):
|
||||
id = self.request(method, params)
|
||||
r = self.response(id)
|
||||
|
||||
if expected_result is not None:
|
||||
assert expected_result == r.result
|
||||
|
||||
if expected_error_code is not None:
|
||||
assert expected_error_code.value == r.error["code"]
|
||||
return r
|
||||
|
||||
def rpc(self, method, params, signed=False):
|
||||
if signed:
|
||||
id = self.signed_request(method, params)
|
||||
return self.response(id)
|
||||
else:
|
||||
id = self.request(method, params)
|
||||
return self.response(id)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def client(
|
||||
host,
|
||||
|
@ -324,19 +452,25 @@ def client(
|
|||
key=None,
|
||||
cafile=None,
|
||||
version="2.0",
|
||||
format="msgpack",
|
||||
format="json" if os.getenv("HTTP") else "msgpack",
|
||||
description=None,
|
||||
log_file=None,
|
||||
):
|
||||
c = FramedTLSJSONRPCClient(
|
||||
host, port, server_hostname, cert, key, cafile, version, format, description
|
||||
)
|
||||
|
||||
if log_file is not None:
|
||||
c.rpc_loggers += (RPCFileLogger(log_file),)
|
||||
|
||||
c.connect()
|
||||
try:
|
||||
if os.getenv("HTTP"):
|
||||
c = CurlClient(
|
||||
host, port, server_hostname, cert, key, cafile, version, format, description
|
||||
)
|
||||
yield c
|
||||
finally:
|
||||
c.disconnect()
|
||||
else:
|
||||
c = FramedTLSJSONRPCClient(
|
||||
host, port, server_hostname, cert, key, cafile, version, format, description
|
||||
)
|
||||
|
||||
if log_file is not None:
|
||||
c.rpc_loggers += (RPCFileLogger(log_file),)
|
||||
|
||||
c.connect()
|
||||
try:
|
||||
yield c
|
||||
finally:
|
||||
c.disconnect()
|
||||
|
|
Загрузка…
Ссылка в новой задаче