Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

permessage-deflate server implementation #110

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ext/iodine/http.c
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,8 @@ static http_settings_s *http_settings_new(http_settings_s arg_settings) {
if ((ssize_t)arg_settings.max_clients - HTTP_BUSY_UNLESS_HAS_FDS > 0)
arg_settings.max_clients -= HTTP_BUSY_UNLESS_HAS_FDS;
}
if (!arg_settings.deflate)
arg_settings.deflate = (size_t)-1;

http_settings_s *settings = malloc(sizeof(*settings) + sizeof(void *));
*settings = arg_settings;
Expand Down
3 changes: 3 additions & 0 deletions ext/iodine/http.h
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@ struct http_settings_s {
uint8_t log;
/** a read only flag set automatically to indicate the protocol's mode. */
uint8_t is_client;
/** minimum size of a message to deflate. Defaults to -1 bytes which means no deflation. */
size_t deflate;
};

/**
Expand Down Expand Up @@ -543,6 +545,7 @@ typedef struct {
void (*on_close)(intptr_t uuid, void *udata);
/** Opaque user data. */
void *udata;
size_t deflate;
} websocket_settings_s;

/**
Expand Down
4 changes: 4 additions & 0 deletions ext/iodine/http1.c
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,10 @@ static int http1_http2websocket_server(http_s *h, websocket_settings_s *args) {
stmp = fiobj_obj2cstr(tmp);
fiobj_str_resize(tmp,
fio_base64_encode(stmp.data, fio_sha1_result(&sha1), 20));

if (args->deflate != (size_t)-1) {
http_set_header(h, HTTP_HEADER_WS_SEC_EXTENSIONS, fiobj_dup(HTTP_HVALUE_WS_DEFLATE));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to testing the deflate setting, this should be automated to test for the permessage-deflate value in the Sec-WebSocket-Extensions header.

Web app developers shouldn't have to know anything about the permessage-deflate WebSocket extension. The choice to enable or disable deflate support should be limited to the settings where the value 0 is reserved to be replaced with a "smart" default and the "disabled" value should be -1.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you are mistaking what the deflate variable within websocket_settings_s does (at least currently).

When a connection is upgraded - there needs to be something to store whether or not we want to send out the sec-websocket-extensions: permessage-deflate header. That is the job of deflate within websocket_settings_s.

It also informs the iodine websocket struct later on via the websocket_attach method within websockets.c

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, there is a place for global settings, it sets the HTTP and WebSocket behavior. For example, it limits WebSocket message sizes to avoid memory drain attacks on the server. This is usually set by the CLI (iodine_cli_parse) or the Iodine.listen method which sets the value in the iodine_http_listen function.

I believe this setting should definitely be settable using the CLI where current behavior should be preserved by setting -ws-deflate = -1.

I also believe this value should be a numerical value representing the minimum length a text message would have to be before it is compressed. Wasting CPU cycles to compress a 16 byte long message (i.e. `""updates":{}") would be regrettable.

}
http_set_header(h, HTTP_HEADER_CONNECTION, fiobj_dup(HTTP_HVALUE_WS_UPGRADE));
http_set_header(h, HTTP_HEADER_UPGRADE, fiobj_dup(HTTP_HVALUE_WEBSOCKET));
http_set_header(h, HTTP_HEADER_WS_SEC_KEY, tmp);
Expand Down
6 changes: 6 additions & 0 deletions ext/iodine/http_internal.c
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ FIOBJ HTTP_HEADER_ORIGIN;
FIOBJ HTTP_HEADER_SET_COOKIE;
FIOBJ HTTP_HEADER_UPGRADE;
FIOBJ HTTP_HEADER_WS_SEC_CLIENT_KEY;
FIOBJ HTTP_HEADER_WS_SEC_EXTENSIONS;
FIOBJ HTTP_HEADER_WS_SEC_KEY;
FIOBJ HTTP_HVALUE_BYTES;
FIOBJ HTTP_HVALUE_CLOSE;
Expand All @@ -136,6 +137,7 @@ FIOBJ HTTP_HVALUE_MAX_AGE;
FIOBJ HTTP_HVALUE_NO_CACHE;
FIOBJ HTTP_HVALUE_SSE_MIME;
FIOBJ HTTP_HVALUE_WEBSOCKET;
FIOBJ HTTP_HVALUE_WS_DEFLATE;
FIOBJ HTTP_HVALUE_WS_SEC_VERSION;
FIOBJ HTTP_HVALUE_WS_UPGRADE;
FIOBJ HTTP_HVALUE_WS_VERSION;
Expand Down Expand Up @@ -172,6 +174,7 @@ static void http_lib_cleanup(void *ignr_) {
HTTPLIB_RESET(HTTP_HEADER_SET_COOKIE);
HTTPLIB_RESET(HTTP_HEADER_UPGRADE);
HTTPLIB_RESET(HTTP_HEADER_WS_SEC_CLIENT_KEY);
HTTPLIB_RESET(HTTP_HEADER_WS_SEC_EXTENSIONS);
HTTPLIB_RESET(HTTP_HEADER_WS_SEC_KEY);
HTTPLIB_RESET(HTTP_HVALUE_BYTES);
HTTPLIB_RESET(HTTP_HVALUE_CLOSE);
Expand All @@ -182,6 +185,7 @@ static void http_lib_cleanup(void *ignr_) {
HTTPLIB_RESET(HTTP_HVALUE_NO_CACHE);
HTTPLIB_RESET(HTTP_HVALUE_SSE_MIME);
HTTPLIB_RESET(HTTP_HVALUE_WEBSOCKET);
HTTPLIB_RESET(HTTP_HVALUE_WS_DEFLATE);
HTTPLIB_RESET(HTTP_HVALUE_WS_SEC_VERSION);
HTTPLIB_RESET(HTTP_HVALUE_WS_UPGRADE);
HTTPLIB_RESET(HTTP_HVALUE_WS_VERSION);
Expand Down Expand Up @@ -213,6 +217,7 @@ static void http_lib_init(void *ignr_) {
HTTP_HEADER_SET_COOKIE = fiobj_str_new("set-cookie", 10);
HTTP_HEADER_UPGRADE = fiobj_str_new("upgrade", 7);
HTTP_HEADER_WS_SEC_CLIENT_KEY = fiobj_str_new("sec-websocket-key", 17);
HTTP_HEADER_WS_SEC_EXTENSIONS = fiobj_str_new("sec-websocket-extensions", 24);
HTTP_HEADER_WS_SEC_KEY = fiobj_str_new("sec-websocket-accept", 20);
HTTP_HVALUE_BYTES = fiobj_str_new("bytes", 5);
HTTP_HVALUE_CLOSE = fiobj_str_new("close", 5);
Expand All @@ -224,6 +229,7 @@ static void http_lib_init(void *ignr_) {
HTTP_HVALUE_NO_CACHE = fiobj_str_new("no-cache, max-age=0", 19);
HTTP_HVALUE_SSE_MIME = fiobj_str_new("text/event-stream", 17);
HTTP_HVALUE_WEBSOCKET = fiobj_str_new("websocket", 9);
HTTP_HVALUE_WS_DEFLATE = fiobj_str_new("permessage-deflate", 18);
HTTP_HVALUE_WS_SEC_VERSION = fiobj_str_new("sec-websocket-version", 21);
HTTP_HVALUE_WS_UPGRADE = fiobj_str_new("Upgrade", 7);
HTTP_HVALUE_WS_VERSION = fiobj_str_new("13", 2);
Expand Down
2 changes: 2 additions & 0 deletions ext/iodine/http_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Constants that shouldn't be accessed by the users (`fiobj_dup` required).

extern FIOBJ HTTP_HEADER_ACCEPT_RANGES;
extern FIOBJ HTTP_HEADER_WS_SEC_CLIENT_KEY;
extern FIOBJ HTTP_HEADER_WS_SEC_EXTENSIONS;
extern FIOBJ HTTP_HEADER_WS_SEC_KEY;
extern FIOBJ HTTP_HVALUE_BYTES;
extern FIOBJ HTTP_HVALUE_CLOSE;
Expand All @@ -82,6 +83,7 @@ extern FIOBJ HTTP_HVALUE_MAX_AGE;
extern FIOBJ HTTP_HVALUE_NO_CACHE;
extern FIOBJ HTTP_HVALUE_SSE_MIME;
extern FIOBJ HTTP_HVALUE_WEBSOCKET;
extern FIOBJ HTTP_HVALUE_WS_DEFLATE;
extern FIOBJ HTTP_HVALUE_WS_SEC_VERSION;
extern FIOBJ HTTP_HVALUE_WS_UPGRADE;
extern FIOBJ HTTP_HVALUE_WS_VERSION;
Expand Down
14 changes: 14 additions & 0 deletions ext/iodine/iodine.c
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ static VALUE max_msg_sym;
static VALUE method_sym;
static VALUE path_sym;
static VALUE ping_sym;
static VALUE deflate_sym;
static VALUE port_sym;
static VALUE public_sym;
static VALUE service_sym;
Expand Down Expand Up @@ -414,6 +415,8 @@ static VALUE iodine_cli_parse(VALUE self) {
FIO_CLI_INT("-max-msg -maxms incoming WebSocket message limit in Kb. "
"Default: 250Kb"),
FIO_CLI_INT("-ping websocket ping interval (1..255). Default: 40s"),
FIO_CLI_INT("-deflate minimum size of outgoing message to deflate. Default: -1. "
"-1 turns off deflation."),
FIO_CLI_PRINT_HEADER("SSL/TLS:"),
FIO_CLI_BOOL("-tls enable SSL/TLS using a self-signed certificate."),
FIO_CLI_STRING(
Expand Down Expand Up @@ -503,6 +506,9 @@ static VALUE iodine_cli_parse(VALUE self) {
if (fio_cli_get("-ping")) {
rb_hash_aset(defaults, ping_sym, INT2NUM(fio_cli_get_i("-ping")));
}
if (fio_cli_get("-deflate")) {
rb_hash_aset(defaults, deflate_sym, INT2NUM(fio_cli_get_i("-deflate")));
}
if (fio_cli_get("-redis-ping")) {
rb_hash_aset(defaults, ID2SYM(rb_intern("redis_ping_")),
INT2NUM(fio_cli_get_i("-redis-ping")));
Expand Down Expand Up @@ -659,6 +665,7 @@ Supported Settigs:
- `:public` (public folder, HTTP server only)
- `:timeout` (HTTP only)
- `:ping` (`:raw` clients and WebSockets only)
- `:deflate` (`:raw` WebSockets only)
- `:max_headers` (HTTP only)
- `:max_body` (HTTP only)
- `:max_msg` (WebSockets only)
Expand All @@ -682,6 +689,7 @@ FIO_FUNC iodine_connection_args_s iodine_connect_args(VALUE s, uint8_t is_srv) {
VALUE method = rb_hash_aref(s, method_sym);
VALUE path = rb_hash_aref(s, path_sym);
VALUE ping = rb_hash_aref(s, ping_sym);
VALUE deflate = rb_hash_aref(s, deflate_sym);
VALUE port = rb_hash_aref(s, port_sym);
VALUE r_public = rb_hash_aref(s, public_sym);
VALUE service = rb_hash_aref(s, service_sym);
Expand Down Expand Up @@ -717,6 +725,8 @@ FIO_FUNC iodine_connection_args_s iodine_connect_args(VALUE s, uint8_t is_srv) {
path = rb_hash_aref(iodine_default_args, path_sym);
if (ping == Qnil)
ping = rb_hash_aref(iodine_default_args, ping_sym);
if (deflate == Qnil)
deflate = rb_hash_aref(iodine_default_args, deflate_sym);
if (port == Qnil)
port = rb_hash_aref(iodine_default_args, port_sym);
if (r_public == Qnil) {
Expand Down Expand Up @@ -792,6 +802,9 @@ FIO_FUNC iodine_connection_args_s iodine_connect_args(VALUE s, uint8_t is_srv) {
else
r.ping = FIX2ULONG(ping);
}
if (deflate != Qnil && RB_TYPE_P(deflate, T_FIXNUM)) {
r.deflate = FIX2LONG(deflate);
}
if (port != Qnil) {
if (RB_TYPE_P(port, T_STRING)) {
char *tmp = RSTRING_PTR(port);
Expand Down Expand Up @@ -1285,6 +1298,7 @@ void Init_iodine(void) {
IODINE_MAKE_SYM(method);
IODINE_MAKE_SYM(path);
IODINE_MAKE_SYM(ping);
IODINE_MAKE_SYM(deflate);
IODINE_MAKE_SYM(port);
IODINE_MAKE_SYM(public);
IODINE_MAKE_SYM(service);
Expand Down
1 change: 1 addition & 0 deletions ext/iodine/iodine.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ typedef struct {
uint8_t timeout;
uint8_t ping;
uint8_t log;
size_t deflate;
enum {
IODINE_SERVICE_RAW,
IODINE_SERVICE_HTTP,
Expand Down
2 changes: 1 addition & 1 deletion ext/iodine/iodine_connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -900,7 +900,7 @@ void iodine_connection_init(void) {
IodineStore.add(RAWSymbol);

// define the Connection Class and it's methods
ConnectionKlass = rb_define_class_under(IodineModule, "Connection", rb_cData);
ConnectionKlass = rb_define_class_under(IodineModule, "Connection", rb_cObject);
rb_define_alloc_func(ConnectionKlass, iodine_connection_data_alloc_c);
rb_define_method(ConnectionKlass, "write", iodine_connection_write, 1);
rb_define_method(ConnectionKlass, "close", iodine_connection_close, 0);
Expand Down
23 changes: 21 additions & 2 deletions ext/iodine/iodine_http.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Feel free to copy, use and enjoy according to the license provided.
#include "iodine.h"

#include "http.h"
#include "http_internal.h"

#include <ruby/encoding.h>
#include <ruby/io.h>
Expand Down Expand Up @@ -34,6 +35,7 @@ VALUE IODINE_R_HIJACK;
VALUE IODINE_R_HIJACK_IO;
VALUE IODINE_R_HIJACK_CB;

static VALUE RACK_WS_EXTENSIONS;
static VALUE RACK_UPGRADE;
static VALUE RACK_UPGRADE_Q;
static VALUE RACK_UPGRADE_SSE;
Expand Down Expand Up @@ -193,16 +195,31 @@ static void iodine_ws_on_close(intptr_t uuid, void *udata) {
}

static void iodine_ws_attach(http_s *h, VALUE handler, VALUE env) {
http_settings_s *http_settings = (http_settings_s *)((http_fio_protocol_s *)h->private_data.flag)->settings;

VALUE io =
iodine_connection_new(.type = IODINE_CONNECTION_WEBSOCKET, .arg = NULL,
.handler = handler, .env = env, .uuid = 0);
if (io == Qnil)
return;

size_t deflate = (size_t)-1;

// check if permessage-deflate allowed
// must have header from client for extensions
// must have the permessage-deflate extension requested
// must have server authorize deflation
VALUE extension_header = rb_hash_aref(env, RACK_WS_EXTENSIONS);
char *extensions = (extension_header == Qnil ? NULL : StringValueCStr(extension_header));
if (http_settings->deflate >= 0 && extensions != NULL && strcasestr(extensions, "permessage-deflate") != NULL) {
deflate = http_settings->deflate;
}

http_upgrade2ws(h, .on_message = iodine_ws_on_message,
.on_open = iodine_ws_on_open, .on_ready = iodine_ws_on_ready,
.on_shutdown = iodine_ws_on_shutdown,
.on_close = iodine_ws_on_close, .udata = (void *)io);
.on_close = iodine_ws_on_close, .udata = (void *)io,
.deflate = deflate);
}

/* *****************************************************************************
Expand Down Expand Up @@ -944,7 +961,8 @@ intptr_t iodine_http_listen(iodine_connection_args_s args){
.tls = args.tls, .timeout = args.timeout, .ws_timeout = args.ping,
.ws_max_msg_size = args.max_msg, .max_header_size = args.max_headers,
.on_finish = free_iodine_http, .log = args.log,
.max_body_size = args.max_body, .public_folder = args.public.data);
.max_body_size = args.max_body, .public_folder = args.public.data,
.deflate = args.deflate);
if (uuid == -1)
return uuid;

Expand Down Expand Up @@ -1120,6 +1138,7 @@ void iodine_init_http(void) {
rack_set(IODINE_R_HIJACK, "rack.hijack");
rack_set(IODINE_R_HIJACK_CB, "iodine.hijack_cb");

rack_set(RACK_WS_EXTENSIONS, "HTTP_SEC_WEBSOCKET_EXTENSIONS");
rack_set(RACK_UPGRADE, "rack.upgrade");
rack_set(RACK_UPGRADE_Q, "rack.upgrade?");
rack_set_sym(RACK_UPGRADE_SSE, "sse");
Expand Down
2 changes: 1 addition & 1 deletion ext/iodine/iodine_mustache.c
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ void iodine_init_mustache(void) {
rb_global_variable(&filename_id);
rb_global_variable(&data_id);
rb_global_variable(&template_id);
VALUE tmp = rb_define_class_under(IodineModule, "Mustache", rb_cData);
VALUE tmp = rb_define_class_under(IodineModule, "Mustache", rb_cObject);
rb_define_alloc_func(tmp, iodine_mustache_data_alloc_c);
rb_define_method(tmp, "initialize", iodine_mustache_new, -1);
rb_define_method(tmp, "render", iodine_mustache_render, 1);
Expand Down
2 changes: 1 addition & 1 deletion ext/iodine/iodine_store.c
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ struct IodineStorage_s IodineStore = {
void iodine_storage_init(void) {
fio_store_capa_require(&iodine_storage, 512);
VALUE tmp =
rb_define_class_under(rb_cObject, "IodineObjectStorage", rb_cData);
rb_define_class_under(rb_cObject, "IodineObjectStorage", rb_cObject);
VALUE storage_obj =
TypedData_Wrap_Struct(tmp, &storage_type_struct, &iodine_storage);
// rb_global_variable(&iodine_storage_obj);
Expand Down
2 changes: 1 addition & 1 deletion ext/iodine/iodine_tls.c
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ void iodine_init_tls(void) {
IODINE_MAKE_SYM(private_key);
IODINE_MAKE_SYM(password);

IodineTLSClass = rb_define_class_under(IodineModule, "TLS", rb_cData);
IodineTLSClass = rb_define_class_under(IodineModule, "TLS", rb_cObject);
rb_define_alloc_func(IodineTLSClass, iodine_tls_data_alloc_c);
rb_define_method(IodineTLSClass, "initialize", iodine_tls_new, -1);
rb_define_method(IodineTLSClass, "use_certificate",
Expand Down
94 changes: 94 additions & 0 deletions ext/iodine/websocket_deflate.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
copyright: Boaz Segev, 2017-2019
license: MIT

Feel free to copy, use and enjoy according to the license specified.
*/
#ifndef H_WEBSOCKET_DEFLATE_H
/**\file

A single file WebSocket permessage-deflate wrapper

*/
#define H_WEBSOCKET_DEFLATE_H
#include <fiobj.h>
#include <stdlib.h>
#include <zlib.h>

#define WS_CHUNK 16384

z_stream *new_z_stream() {
z_stream *strm = malloc(sizeof(*strm));

*strm = (z_stream){
.zalloc = Z_NULL,
.zfree = Z_NULL,
.opaque = Z_NULL,
};

return strm;
}

z_stream *new_deflator() {
z_stream *strm = new_z_stream();

int ret = deflateInit2(strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
-MAX_WBITS, 4, Z_DEFAULT_STRATEGY);

return strm;
}

z_stream *new_inflator() {
z_stream *strm = new_z_stream();

inflateInit2(strm, -MAX_WBITS);

return strm;
}

int deflate_message(fio_str_info_s src, FIOBJ dest, z_stream *strm) {
int ret, flush;
unsigned have;
unsigned char out[WS_CHUNK];

strm->avail_in = src.len;
strm->next_in = src.data;

do {
strm->avail_out = WS_CHUNK;
strm->next_out = out;
ret = deflate(strm, Z_SYNC_FLUSH);
have = WS_CHUNK - strm->avail_out;
fiobj_str_write(dest, out, have);
} while (strm->avail_out == 0);

return Z_OK;
}

int inflate_message(fio_str_info_s src, FIOBJ dest, z_stream *strm) {
int ret;
unsigned have;
unsigned char out[WS_CHUNK];

strm->avail_in = src.len;
strm->next_in = src.data;

do {
strm->avail_out = WS_CHUNK;
strm->next_out = out;
ret = inflate(strm, Z_SYNC_FLUSH);
switch (ret) {
case Z_NEED_DICT:
ret = Z_DATA_ERROR;
case Z_DATA_ERROR:
case Z_MEM_ERROR:
(void)inflateEnd(strm);
return ret;
}
have = WS_CHUNK - strm->avail_out;
fiobj_str_write(dest, out, have);
} while (strm->avail_out == 0);

return ret;
}
#endif
Loading