Some intial WebSockets code
This commit is contained in:
parent
931fc441f6
commit
5885d1aa69
|
@ -22,12 +22,13 @@ set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
|
|||
# Dependencies
|
||||
find_package (PkgConfig REQUIRED)
|
||||
pkg_check_modules (dependencies REQUIRED jansson)
|
||||
pkg_check_modules (libssl REQUIRED libssl libcrypto)
|
||||
find_package (LibEV REQUIRED)
|
||||
find_package (LibMagic REQUIRED)
|
||||
|
||||
set (project_libraries ${dependencies_LIBRARIES}
|
||||
set (project_libraries ${dependencies_LIBRARIES} ${libssl_LIBRARIES}
|
||||
${LIBEV_LIBRARIES} ${LIBMAGIC_LIBRARIES})
|
||||
include_directories (${dependencies_INCLUDE_DIRS}
|
||||
include_directories (${dependencies_INCLUDE_DIRS} ${libssl_INCLUDE_DIRS}
|
||||
${LIBEV_INCLUDE_DIRS} ${LIBMAGIC_INCLUDE_DIRS})
|
||||
|
||||
# Generate a configuration file
|
||||
|
|
2
README
2
README
|
@ -11,6 +11,8 @@ gist of it is actually very simple -- run some stuff on new git commits.
|
|||
|
||||
`acid' will provide a JSON-RPC 2.0 service for frontends over FastCGI, SCGI, or
|
||||
WebSockets, as well as a webhook endpoint for notifications about new commits.
|
||||
The daemon is supposed to be "firewalled" by a normal HTTP server and it will
|
||||
not provide TLS support to secure the communications.
|
||||
|
||||
`acid' will be able to tell you about build results via e-mail and/or IRC.
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
#define print_status_data ((void *) LOG_INFO)
|
||||
#define print_debug_data ((void *) LOG_DEBUG)
|
||||
|
||||
#define LIBERTY_WANT_SSL
|
||||
|
||||
#include "config.h"
|
||||
#include "liberty/liberty.c"
|
||||
|
||||
|
@ -36,6 +38,10 @@
|
|||
#include <jansson.h>
|
||||
#include <magic.h>
|
||||
|
||||
// FIXME: don't include the implementation, include the header and compile
|
||||
// the implementation separately
|
||||
#include "http-parser/http_parser.c"
|
||||
|
||||
// --- Extensions to liberty ---------------------------------------------------
|
||||
|
||||
// These should be incorporated into the library ASAP
|
||||
|
@ -106,6 +112,48 @@ str_pack_u64 (struct str *self, uint64_t x)
|
|||
str_append_data (self, tmp, sizeof tmp);
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static void
|
||||
base64_encode (const void *data, size_t len, struct str *output)
|
||||
{
|
||||
const char *alphabet =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
const uint8_t *p = data;
|
||||
size_t n_groups = len / 3;
|
||||
size_t tail = len - n_groups * 3;
|
||||
uint32_t group;
|
||||
|
||||
for (; n_groups--; p += 3)
|
||||
{
|
||||
group = p[0] << 16 | p[1] << 8 | p[2];
|
||||
str_append_c (output, alphabet[(group >> 18) & 63]);
|
||||
str_append_c (output, alphabet[(group >> 12) & 63]);
|
||||
str_append_c (output, alphabet[(group >> 6) & 63]);
|
||||
str_append_c (output, alphabet[ group & 63]);
|
||||
}
|
||||
|
||||
switch (tail)
|
||||
{
|
||||
case 2:
|
||||
group = p[0] << 16 | p[1] << 8;
|
||||
str_append_c (output, alphabet[(group >> 18) & 63]);
|
||||
str_append_c (output, alphabet[(group >> 12) & 63]);
|
||||
str_append_c (output, alphabet[(group >> 6) & 63]);
|
||||
str_append_c (output, '=');
|
||||
break;
|
||||
case 1:
|
||||
group = p[0] << 16;
|
||||
str_append_c (output, alphabet[(group >> 18) & 63]);
|
||||
str_append_c (output, alphabet[(group >> 12) & 63]);
|
||||
str_append_c (output, '=');
|
||||
str_append_c (output, '=');
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- libev helpers -----------------------------------------------------------
|
||||
|
||||
static bool
|
||||
|
@ -989,6 +1037,438 @@ scgi_parser_push (struct scgi_parser *self,
|
|||
return false;
|
||||
}
|
||||
|
||||
// --- WebSockets --------------------------------------------------------------
|
||||
|
||||
#define WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
|
||||
#define SEC_WS_KEY "Sec-WebSocket-Key"
|
||||
#define SEC_WS_ACCEPT "Sec-WebSocket-Accept"
|
||||
#define SEC_WS_PROTOCOL "Sec-WebSocket-Protocol"
|
||||
#define SEC_WS_VERSION "Sec-WebSocket-Version"
|
||||
|
||||
static char *
|
||||
ws_encode_response_key (const char *key)
|
||||
{
|
||||
char *response_key = xstrdup_printf ("%s" WS_GUID, key);
|
||||
unsigned char hash[SHA_DIGEST_LENGTH];
|
||||
SHA1 ((unsigned char *) response_key, strlen (response_key), hash);
|
||||
free (response_key);
|
||||
|
||||
struct str base64;
|
||||
str_init (&base64);
|
||||
base64_encode (hash, sizeof hash, &base64);
|
||||
return str_steal (&base64);
|
||||
}
|
||||
|
||||
enum ws_status
|
||||
{
|
||||
// These names aren't really standard, just somewhat descriptive.
|
||||
// The RFC isn't really much cleaner about their meaning.
|
||||
|
||||
WS_STATUS_NORMAL = 1000,
|
||||
WS_STATUS_GOING_AWAY = 1001,
|
||||
WS_STATUS_PROTOCOL = 1002,
|
||||
WS_STATUS_UNACCEPTABLE = 1003,
|
||||
WS_STATUS_INCONSISTENT = 1007,
|
||||
WS_STATUS_POLICY = 1008,
|
||||
WS_STATUS_TOO_BIG = 1009,
|
||||
WS_STATUS_EXTENSION = 1010,
|
||||
WS_STATUS_UNEXPECTED = 1011,
|
||||
|
||||
// Reserved for internal usage
|
||||
WS_STATUS_MISSING = 1005,
|
||||
WS_STATUS_ABNORMAL = 1006,
|
||||
WS_STATUS_TLS = 1015
|
||||
};
|
||||
|
||||
// - - Frame parser - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
enum ws_parser_state
|
||||
{
|
||||
WS_PARSER_FIXED, ///< Parsing fixed length part
|
||||
WS_PARSER_PAYLOAD_LEN_16, ///< Parsing extended payload length
|
||||
WS_PARSER_PAYLOAD_LEN_64, ///< Parsing extended payload length
|
||||
WS_PARSER_MASK, ///< Parsing masking-key
|
||||
WS_PARSER_PAYLOAD ///< Parsing payload
|
||||
};
|
||||
|
||||
enum ws_opcode
|
||||
{
|
||||
// Non-control
|
||||
WS_OPCODE_CONT = 0,
|
||||
WS_OPCODE_TEXT = 1,
|
||||
WS_OPCODE_BINARY = 2,
|
||||
|
||||
// Control
|
||||
WS_OPCODE_CLOSE = 8,
|
||||
WS_OPCODE_PING = 9,
|
||||
WS_OPCODE_PONG = 10
|
||||
};
|
||||
|
||||
struct ws_parser
|
||||
{
|
||||
struct str input; ///< External input buffer
|
||||
enum ws_parser_state state; ///< Parsing state
|
||||
|
||||
unsigned is_fin : 1; ///< Final frame of a message?
|
||||
unsigned is_masked : 1; ///< Is the frame masked?
|
||||
unsigned reserved_1 : 1; ///< Reserved
|
||||
unsigned reserved_2 : 1; ///< Reserved
|
||||
unsigned reserved_3 : 1; ///< Reserved
|
||||
enum ws_opcode opcode; ///< Opcode
|
||||
uint32_t mask; ///< Frame mask
|
||||
uint64_t payload_len; ///< Payload length
|
||||
|
||||
// TODO: it wouldn't be half bad if there was a callback to just validate
|
||||
// the frame header (such as the maximum payload length)
|
||||
|
||||
/// Callback for when a message is successfully parsed.
|
||||
/// The actual payload is stored in "input", of length "payload_len".
|
||||
bool (*on_frame) (void *user_data, const struct ws_parser *self);
|
||||
|
||||
void *user_data; ///< User data for callbacks
|
||||
};
|
||||
|
||||
static void
|
||||
ws_parser_init (struct ws_parser *self)
|
||||
{
|
||||
memset (self, 0, sizeof *self);
|
||||
str_init (&self->input);
|
||||
}
|
||||
|
||||
static void
|
||||
ws_parser_free (struct ws_parser *self)
|
||||
{
|
||||
str_free (&self->input);
|
||||
}
|
||||
|
||||
static void
|
||||
ws_parser_demask (struct ws_parser *self)
|
||||
{
|
||||
// Yes, this could be made faster. For example by reading the mask in
|
||||
// native byte ordering and applying it directly here.
|
||||
|
||||
uint64_t end = self->payload_len & ~(uint64_t) 3;
|
||||
for (uint64_t i = 0; i < end; i += 4)
|
||||
{
|
||||
self->input.str[i + 3] ^= self->mask & 0xFF;
|
||||
self->input.str[i + 2] ^= (self->mask >> 8) & 0xFF;
|
||||
self->input.str[i + 1] ^= (self->mask >> 16) & 0xFF;
|
||||
self->input.str[i ] ^= (self->mask >> 24) & 0xFF;
|
||||
}
|
||||
|
||||
switch (self->payload_len - end)
|
||||
{
|
||||
case 3:
|
||||
self->input.str[end + 2] ^= (self->mask >> 8) & 0xFF;
|
||||
case 2:
|
||||
self->input.str[end + 1] ^= (self->mask >> 16) & 0xFF;
|
||||
case 1:
|
||||
self->input.str[end ] ^= (self->mask >> 24) & 0xFF;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
ws_parser_push (struct ws_parser *self, const void *data, size_t len)
|
||||
{
|
||||
str_append_data (&self->input, data, len);
|
||||
|
||||
struct msg_unpacker unpacker;
|
||||
msg_unpacker_init (&unpacker, self->input.str, self->input.len);
|
||||
|
||||
while (true)
|
||||
switch (self->state)
|
||||
{
|
||||
uint8_t u8;
|
||||
uint16_t u16;
|
||||
|
||||
case WS_PARSER_FIXED:
|
||||
if (self->input.len < 2)
|
||||
return true;
|
||||
|
||||
(void) msg_unpacker_u8 (&unpacker, &u8);
|
||||
self->is_fin = (u8 >> 7) & 1;
|
||||
self->reserved_1 = (u8 >> 6) & 1;
|
||||
self->reserved_2 = (u8 >> 5) & 1;
|
||||
self->reserved_3 = (u8 >> 4) & 1;
|
||||
self->opcode = u8 & 15;
|
||||
|
||||
(void) msg_unpacker_u8 (&unpacker, &u8);
|
||||
self->is_masked = (u8 >> 7) & 1;
|
||||
self->payload_len = u8 & 127;
|
||||
|
||||
if (self->payload_len == 127)
|
||||
self->state = WS_PARSER_PAYLOAD_LEN_64;
|
||||
else if (self->payload_len == 126)
|
||||
self->state = WS_PARSER_PAYLOAD_LEN_16;
|
||||
else
|
||||
self->state = self->is_masked
|
||||
? WS_PARSER_MASK
|
||||
: WS_PARSER_PAYLOAD;
|
||||
|
||||
str_remove_slice (&self->input, 0, 2);
|
||||
break;
|
||||
|
||||
case WS_PARSER_PAYLOAD_LEN_16:
|
||||
if (self->input.len < 2)
|
||||
return true;
|
||||
|
||||
(void) msg_unpacker_u16 (&unpacker, &u16);
|
||||
self->payload_len = u16;
|
||||
|
||||
self->state = self->is_masked
|
||||
? WS_PARSER_MASK
|
||||
: WS_PARSER_PAYLOAD;
|
||||
str_remove_slice (&self->input, 0, 2);
|
||||
break;
|
||||
|
||||
case WS_PARSER_PAYLOAD_LEN_64:
|
||||
if (self->input.len < 8)
|
||||
return true;
|
||||
|
||||
(void) msg_unpacker_u64 (&unpacker, &self->payload_len);
|
||||
|
||||
self->state = self->is_masked
|
||||
? WS_PARSER_MASK
|
||||
: WS_PARSER_PAYLOAD;
|
||||
str_remove_slice (&self->input, 0, 8);
|
||||
break;
|
||||
|
||||
case WS_PARSER_MASK:
|
||||
if (self->input.len < 4)
|
||||
return true;
|
||||
|
||||
(void) msg_unpacker_u32 (&unpacker, &self->mask);
|
||||
|
||||
self->state = WS_PARSER_PAYLOAD;
|
||||
str_remove_slice (&self->input, 0, 4);
|
||||
break;
|
||||
|
||||
case WS_PARSER_PAYLOAD:
|
||||
if (self->input.len < self->payload_len)
|
||||
return true;
|
||||
|
||||
if (self->is_masked)
|
||||
ws_parser_demask (self);
|
||||
if (!self->on_frame (self->user_data, self))
|
||||
return false;
|
||||
|
||||
self->state = WS_PARSER_FIXED;
|
||||
str_reset (&self->input);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// TODO: something to build frames for data
|
||||
|
||||
// - - Server handler - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// WebSockets aren't CGI-compatible, therefore we must handle the initial HTTP
|
||||
// handshake ourselves. Luckily it's not too much of a bother with http-parser.
|
||||
// Typically there will be a normal HTTP server in front of us, proxying the
|
||||
// requests based on the URI.
|
||||
|
||||
enum ws_handler_state
|
||||
{
|
||||
WS_HANDLER_HTTP, ///< Parsing HTTP
|
||||
WS_HANDLER_WEBSOCKETS ///< Parsing WebSockets frames
|
||||
};
|
||||
|
||||
struct ws_handler
|
||||
{
|
||||
enum ws_handler_state state; ///< State
|
||||
|
||||
http_parser hp; ///< HTTP parser
|
||||
bool parsing_header_value; ///< Parsing header value or field?
|
||||
struct str field; ///< Field part buffer
|
||||
struct str value; ///< Value part buffer
|
||||
struct str_map headers; ///< HTTP Headers
|
||||
struct str url; ///< Request URL
|
||||
|
||||
struct ws_parser parser; ///< Protocol frame parser
|
||||
|
||||
/// Called upon reception of a single full message
|
||||
bool (*on_message) (void *user_data, const void *data, size_t len);
|
||||
|
||||
/// Write a chunk of data to the stream
|
||||
void (*write_cb) (void *user_data, const void *data, size_t len);
|
||||
|
||||
// TODO: close_cb
|
||||
|
||||
void *user_data; ///< User data for callbacks
|
||||
};
|
||||
|
||||
static bool
|
||||
ws_handler_on_frame (void *user_data, const struct ws_parser *parser)
|
||||
{
|
||||
struct ws_handler *self = user_data;
|
||||
// TODO: handle pings and what not
|
||||
// TODO: validate the message
|
||||
|
||||
return self->on_message (self->user_data,
|
||||
self->parser.input.str, self->parser.payload_len);
|
||||
}
|
||||
|
||||
static void
|
||||
ws_handler_init (struct ws_handler *self)
|
||||
{
|
||||
memset (self, 0, sizeof *self);
|
||||
|
||||
http_parser_init (&self->hp, HTTP_REQUEST);
|
||||
self->hp.data = self;
|
||||
|
||||
str_init (&self->field);
|
||||
str_init (&self->value);
|
||||
str_map_init (&self->headers);
|
||||
self->headers.free = free;
|
||||
// TODO: set headers.key_strxfrm?
|
||||
str_init (&self->url);
|
||||
|
||||
ws_parser_init (&self->parser);
|
||||
self->parser.on_frame = ws_handler_on_frame;
|
||||
}
|
||||
|
||||
static void
|
||||
ws_handler_free (struct ws_handler *self)
|
||||
{
|
||||
str_free (&self->field);
|
||||
str_free (&self->value);
|
||||
str_map_free (&self->headers);
|
||||
str_free (&self->url);
|
||||
ws_parser_free (&self->parser);
|
||||
}
|
||||
|
||||
static void
|
||||
ws_handler_on_header_read (struct ws_handler *self)
|
||||
{
|
||||
// TODO: some headers can appear more than once, concatenate their values;
|
||||
// for example "Sec-WebSocket-Version"
|
||||
str_map_set (&self->headers, self->field.str, self->value.str);
|
||||
}
|
||||
|
||||
static int
|
||||
ws_handler_on_header_field (http_parser *parser, const char *at, size_t len)
|
||||
{
|
||||
struct ws_handler *self = parser->data;
|
||||
if (self->parsing_header_value)
|
||||
{
|
||||
ws_handler_on_header_read (self);
|
||||
str_reset (&self->field);
|
||||
str_reset (&self->value);
|
||||
}
|
||||
str_append_data (&self->field, at, len);
|
||||
self->parsing_header_value = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
ws_handler_on_header_value (http_parser *parser, const char *at, size_t len)
|
||||
{
|
||||
struct ws_handler *self = parser->data;
|
||||
str_append_data (&self->value, at, len);
|
||||
self->parsing_header_value = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
ws_handler_on_headers_complete (http_parser *parser)
|
||||
{
|
||||
// Just return 1 to tell the parser we don't want to parse any body;
|
||||
// the parser should have found an upgrade request for WebSockets
|
||||
(void) parser;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
ws_handler_on_url (http_parser *parser, const char *at, size_t len)
|
||||
{
|
||||
struct ws_handler *self = parser->data;
|
||||
str_append_data (&self->value, at, len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
ws_handler_finish_handshake (struct ws_handler *self)
|
||||
{
|
||||
// TODO: probably factor this block out into its own function
|
||||
// TODO: check if everything seems to be right
|
||||
if (self->hp.method != HTTP_GET
|
||||
|| self->hp.http_major != 1
|
||||
|| self->hp.http_minor != 1)
|
||||
; // TODO: error (maybe send a frame depending on conditions)
|
||||
|
||||
const char *upgrade = str_map_find (&self->headers, "Upgrade");
|
||||
|
||||
const char *key = str_map_find (&self->headers, SEC_WS_KEY);
|
||||
const char *version = str_map_find (&self->headers, SEC_WS_VERSION);
|
||||
const char *protocol = str_map_find (&self->headers, SEC_WS_PROTOCOL);
|
||||
|
||||
struct str response;
|
||||
str_init (&response);
|
||||
str_append (&response, "HTTP/1.1 101 Switching Protocols\r\n");
|
||||
str_append (&response, "Upgrade: websocket\r\n");
|
||||
str_append (&response, "Connection: Upgrade\r\n");
|
||||
|
||||
// TODO: prepare the rest of the headers
|
||||
|
||||
// TODO: we should ideally check that this is a 16-byte base64-encoded
|
||||
// value; do we also have to strip surrounding whitespace?
|
||||
char *response_key = ws_encode_response_key (key);
|
||||
str_append_printf (&response, SEC_WS_ACCEPT ": %s\r\n", response_key);
|
||||
free (response_key);
|
||||
|
||||
str_append (&response, "\r\n");
|
||||
self->write_cb (self->user_data, response.str, response.len);
|
||||
str_free (&response);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
ws_handler_push (struct ws_handler *self, const void *data, size_t len)
|
||||
{
|
||||
if (self->state == WS_HANDLER_WEBSOCKETS)
|
||||
return ws_parser_push (&self->parser, data, len);
|
||||
|
||||
// The handshake hasn't been done yet, process HTTP headers
|
||||
static const http_parser_settings http_settings =
|
||||
{
|
||||
.on_header_field = ws_handler_on_header_field,
|
||||
.on_header_value = ws_handler_on_header_value,
|
||||
.on_headers_complete = ws_handler_on_headers_complete,
|
||||
.on_url = ws_handler_on_url,
|
||||
};
|
||||
|
||||
size_t n_parsed = http_parser_execute (&self->hp,
|
||||
&http_settings, data, len);
|
||||
|
||||
if (self->hp.upgrade)
|
||||
{
|
||||
if (len - n_parsed)
|
||||
{
|
||||
// TODO: error: the handshake hasn't been finished, yet there
|
||||
// is more data to process after the headers
|
||||
}
|
||||
|
||||
if (!ws_handler_finish_handshake (self))
|
||||
return false;
|
||||
|
||||
self->state = WS_HANDLER_WEBSOCKETS;
|
||||
return true;
|
||||
}
|
||||
else if (n_parsed != len || HTTP_PARSER_ERRNO (&self->hp) != HPE_OK)
|
||||
{
|
||||
// TODO: error
|
||||
// print_debug (..., http_errno_description
|
||||
// (HTTP_PARSER_ERRNO (&self->hp));
|
||||
}
|
||||
|
||||
// TODO: make double sure to handle the case of !upgrade
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Server ------------------------------------------------------------------
|
||||
|
||||
static struct config_item g_config_table[] =
|
||||
|
@ -1736,7 +2216,7 @@ client_scgi_write (void *user_data, const void *data, size_t len)
|
|||
static void
|
||||
client_scgi_close (void *user_data)
|
||||
{
|
||||
// XXX: this rather really means "close me [the request]"
|
||||
// NOTE: this rather really means "close me [the request]"
|
||||
struct client *client = user_data;
|
||||
client_remove (client);
|
||||
}
|
||||
|
@ -1815,23 +2295,56 @@ static struct client_impl g_client_scgi =
|
|||
|
||||
// - - WebSockets - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
struct client_ws
|
||||
{
|
||||
struct ws_handler handler; ///< WebSockets connection handler
|
||||
};
|
||||
|
||||
static void
|
||||
client_ws_write (void *user_data, const void *data, size_t len)
|
||||
{
|
||||
struct client *client = user_data;
|
||||
client_write (client, data, len);
|
||||
}
|
||||
|
||||
static bool
|
||||
client_ws_on_message (void *user_data, const void *data, size_t len)
|
||||
{
|
||||
struct client *client = user_data;
|
||||
struct client_ws *self = client->impl_data;
|
||||
|
||||
// TODO: do something about the message
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
client_ws_init (struct client *client)
|
||||
{
|
||||
// TODO
|
||||
struct client_ws *self = xmalloc (sizeof *self);
|
||||
client->impl_data = self;
|
||||
|
||||
ws_handler_init (&self->handler);
|
||||
self->handler.write_cb = client_ws_write;
|
||||
self->handler.on_message = client_ws_on_message;
|
||||
self->handler.user_data = client;
|
||||
// TODO: configure the handler some more, e.g. regarding the protocol
|
||||
}
|
||||
|
||||
static void
|
||||
client_ws_destroy (struct client *client)
|
||||
{
|
||||
// TODO
|
||||
struct client_ws *self = client->impl_data;
|
||||
client->impl_data = NULL;
|
||||
|
||||
ws_handler_free (&self->handler);
|
||||
free (self);
|
||||
}
|
||||
|
||||
static bool
|
||||
client_ws_push (struct client *client, const void *data, size_t len)
|
||||
{
|
||||
// TODO: first push everything into a http_parser, then after a protocol
|
||||
// upgrade start parsing the WebSocket frames themselves
|
||||
struct client_ws *self = client->impl_data;
|
||||
return ws_handler_push (&self->handler, data, len);
|
||||
}
|
||||
|
||||
static struct client_impl g_client_ws =
|
||||
|
|
Loading…
Reference in New Issue