summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGustavo Sverzut Barbieri <barbieri@profusion.mobi>2016-08-30 00:28:00 -0300
committerGustavo Sverzut Barbieri <barbieri@profusion.mobi>2016-09-02 00:08:50 -0300
commite12afd772cef959ae1e58746f3bb41b26da30744 (patch)
tree82a23663fb1c47e45706b0fdeb0cc669743bb3c7
parent57e765a758243ae326db1d8393242717b9fa2b04 (diff)
downloadefl-devs/barbieri/websocket.tar.gz
efl_net_dialer_websocket: EFL now does WebSocket!devs/barbieri/websocket
The Efl.Net.Dialer.Websocket is just like other Efl.Net.Dialers: you can dial, you can close, monitor connected/address resolved and so on. And you can use WebSocket primitives and events such as text_send(), binary_send(), ping() and close_request() (since WebSockets use a close process where you should state a close reason). See efl_net_dialer_websocket_example.c Even if WebSocket is a message-based protocol (like "packets" from UDP), you can use efl_net_dialer_websocket_streaming_mode_set() to tell it to handle text or binary messages as a stream. Then all the Efl.Io.Reader and Efl.Io.Writer APIs work as expected, see efl_io_copier_example.c updates.
-rw-r--r--src/Makefile_Ecore_Con.am2
-rw-r--r--src/examples/ecore/.gitignore2
-rw-r--r--src/examples/ecore/Makefile.am14
-rw-r--r--src/examples/ecore/efl_io_copier_example.c56
-rw-r--r--src/examples/ecore/efl_net_dialer_websocket_autobahntestee.c693
-rw-r--r--src/examples/ecore/efl_net_dialer_websocket_example.c384
-rw-r--r--src/lib/ecore_con/Ecore_Con_Eo.h1
-rw-r--r--src/lib/ecore_con/ecore_con_url.c13
-rw-r--r--src/lib/ecore_con/efl_net_dialer_websocket.c1667
-rw-r--r--src/lib/ecore_con/efl_net_dialer_websocket.eo309
10 files changed, 3136 insertions, 5 deletions
diff --git a/src/Makefile_Ecore_Con.am b/src/Makefile_Ecore_Con.am
index ceb2e90692..ae7166d9e2 100644
--- a/src/Makefile_Ecore_Con.am
+++ b/src/Makefile_Ecore_Con.am
@@ -12,6 +12,7 @@ ecore_con_eolian_files = \
lib/ecore_con/efl_net_dialer.eo \
lib/ecore_con/efl_net_dialer_tcp.eo \
lib/ecore_con/efl_net_dialer_http.eo \
+ lib/ecore_con/efl_net_dialer_websocket.eo \
lib/ecore_con/efl_net_server.eo \
lib/ecore_con/efl_net_server_fd.eo \
lib/ecore_con/efl_net_server_tcp.eo \
@@ -75,6 +76,7 @@ lib/ecore_con/efl_net_socket_tcp.c \
lib/ecore_con/efl_net_dialer.c \
lib/ecore_con/efl_net_dialer_tcp.c \
lib/ecore_con/efl_net_dialer_http.c \
+lib/ecore_con/efl_net_dialer_websocket.c \
lib/ecore_con/efl_net_server.c \
lib/ecore_con/efl_net_server_fd.c \
lib/ecore_con/efl_net_server_tcp.c
diff --git a/src/examples/ecore/.gitignore b/src/examples/ecore/.gitignore
index 38a72c7e24..5a26815e1d 100644
--- a/src/examples/ecore/.gitignore
+++ b/src/examples/ecore/.gitignore
@@ -51,3 +51,5 @@
/efl_io_queue_example
/efl_net_server_example
/efl_net_dialer_http_example
+/efl_net_dialer_websocket_example
+/efl_net_dialer_websocket_autobahntestee
diff --git a/src/examples/ecore/Makefile.am b/src/examples/ecore/Makefile.am
index 16a8693319..79c1e9c58e 100644
--- a/src/examples/ecore/Makefile.am
+++ b/src/examples/ecore/Makefile.am
@@ -81,7 +81,9 @@ efl_io_copier_example \
efl_io_copier_simple_example \
efl_io_queue_example \
efl_net_server_example \
-efl_net_dialer_http_example
+efl_net_dialer_http_example \
+efl_net_dialer_websocket_example \
+efl_net_dialer_websocket_autobahntestee
ECORE_COMMON_LDADD = \
$(top_builddir)/src/lib/ecore/libecore.la \
@@ -297,6 +299,12 @@ efl_net_server_example_LDADD = $(ECORE_CON_COMMON_LDADD)
efl_net_dialer_http_example_SOURCES = efl_net_dialer_http_example.c
efl_net_dialer_http_example_LDADD = $(ECORE_CON_COMMON_LDADD)
+efl_net_dialer_websocket_example_SOURCES = efl_net_dialer_websocket_example.c
+efl_net_dialer_websocket_example_LDADD = $(ECORE_CON_COMMON_LDADD)
+
+efl_net_dialer_websocket_autobahntestee_SOURCES = efl_net_dialer_websocket_autobahntestee.c
+efl_net_dialer_websocket_autobahntestee_LDADD = $(ECORE_CON_COMMON_LDADD)
+
SRCS = \
ecore_animator_example.c \
ecore_buffer_example.c \
@@ -347,7 +355,9 @@ efl_io_copier_example.c \
efl_io_copier_simple_example.c \
efl_io_queue_example.c \
efl_net_server_example.c \
-efl_net_dialer_http_example.c
+efl_net_dialer_http_example.c \
+efl_net_dialer_websocket_example.c \
+efl_net_dialer_websocket_autobahntestee.c
DATA_FILES = red.png Makefile.examples
diff --git a/src/examples/ecore/efl_io_copier_example.c b/src/examples/ecore/efl_io_copier_example.c
index 943c94eaef..a63377333d 100644
--- a/src/examples/ecore/efl_io_copier_example.c
+++ b/src/examples/ecore/efl_io_copier_example.c
@@ -71,7 +71,7 @@ static void
_dialer_error(void *data EINA_UNUSED, const Efl_Event *event)
{
const Eina_Error *perr = event->info;
- fprintf(stderr, "INFO: error: %d\n", *perr);
+ fprintf(stderr, "INFO: error: %d '%s'\n", *perr, eina_error_msg_get(*perr));
retval = EXIT_FAILURE;
/* no need to quit as copier will get a "eos" event and emit "done" */
}
@@ -217,7 +217,7 @@ static void
_copier_error(void *data EINA_UNUSED, const Efl_Event *event)
{
const Eina_Error *perr = event->info;
- fprintf(stderr, "INFO: error: %d\n", *perr);
+ fprintf(stderr, "INFO: error: %d '%s'\n", *perr, eina_error_msg_get(*perr));
retval = EXIT_FAILURE;
ecore_main_loop_quit();
}
@@ -332,6 +332,7 @@ static const Ecore_Getopt options = {
":stdin: to read from stdin.\n"
"tcp://IP:PORT to connect using TCP and an IPv4 (A.B.C.D:PORT) or IPv6 ([A:B:C:D::E]:PORT).\n"
"http://address to do a GET request\n"
+ "ws://address or wss:// to do WebSocket request (must send some data once connected)\n"
"",
"input-file"),
ECORE_GETOPT_STORE_METAVAR_STR(0, NULL,
@@ -342,6 +343,7 @@ static const Ecore_Getopt options = {
":none: to not use a destination object.\n"
"tcp://IP:PORT to connect using TCP and an IPv4 (A.B.C.D:PORT) or IPv6 ([A:B:C:D::E]:PORT).\n"
"http://address to do a PUT request\n"
+ "ws://address or wss:// to do WebSocket request\n"
"",
"output-file"),
ECORE_GETOPT_SENTINEL
@@ -472,6 +474,31 @@ main(int argc, char **argv)
goto end_input;
}
}
+ else if (strncmp(input_fname, "ws://", strlen("ws://")) == 0 ||
+ strncmp(input_fname, "wss://", strlen("wss://")) == 0)
+ {
+ Eina_Error err;
+
+ input = efl_add(EFL_NET_DIALER_WEBSOCKET_CLASS, ecore_main_loop_get(),
+ efl_net_dialer_websocket_streaming_mode_set(efl_self, EFL_NET_DIALER_WEBSOCKET_STREAMING_MODE_TEXT),
+ efl_event_callback_array_add(efl_self, input_cbs(), NULL), /* optional */
+ efl_event_callback_array_add(efl_self, dialer_cbs(), NULL) /* optional */
+ );
+ if (!input)
+ {
+ fprintf(stderr, "ERROR: could not create WebSocket Dialer.\n");
+ retval = EXIT_FAILURE;
+ goto end;
+ }
+
+ err = efl_net_dialer_dial(input, input_fname);
+ if (err)
+ {
+ fprintf(stderr, "ERROR: could not WebSocket dial %s: %s\n",
+ input_fname, eina_error_msg_get(err));
+ goto end_input;
+ }
+ }
else
{
/* regular file, open with flags: read-only and close-on-exec */
@@ -605,6 +632,31 @@ main(int argc, char **argv)
goto end_output;
}
}
+ else if (strncmp(output_fname, "ws://", strlen("ws://")) == 0 ||
+ strncmp(output_fname, "wss://", strlen("wss://")) == 0)
+ {
+ Eina_Error err;
+
+ output = efl_add(EFL_NET_DIALER_WEBSOCKET_CLASS, ecore_main_loop_get(),
+ efl_net_dialer_websocket_streaming_mode_set(efl_self, EFL_NET_DIALER_WEBSOCKET_STREAMING_MODE_TEXT),
+ efl_event_callback_array_add(efl_self, output_cbs(), NULL), /* optional */
+ efl_event_callback_array_add(efl_self, dialer_cbs(), NULL) /* optional */
+ );
+ if (!output)
+ {
+ fprintf(stderr, "ERROR: could not create WebSocket Dialer.\n");
+ retval = EXIT_FAILURE;
+ goto end_input;
+ }
+
+ err = efl_net_dialer_dial(output, output_fname);
+ if (err)
+ {
+ fprintf(stderr, "ERROR: could not WebSocket dial %s: %s\n",
+ output_fname, eina_error_msg_get(err));
+ goto end_output;
+ }
+ }
else
{
/* regular file, open with flags: write-only, close-on-exec,
diff --git a/src/examples/ecore/efl_net_dialer_websocket_autobahntestee.c b/src/examples/ecore/efl_net_dialer_websocket_autobahntestee.c
new file mode 100644
index 0000000000..92595b7f70
--- /dev/null
+++ b/src/examples/ecore/efl_net_dialer_websocket_autobahntestee.c
@@ -0,0 +1,693 @@
+#define EFL_BETA_API_SUPPORT 1
+#define EFL_EO_API_SUPPORT 1
+#include <Ecore.h>
+#include <Ecore_Con.h>
+#include <Ecore_Getopt.h>
+#include <fcntl.h>
+#include <ctype.h>
+
+static int retval = EXIT_SUCCESS;
+static char *address = NULL;
+static char *agent = "efl_net_dialer_websocket";
+static unsigned int start_index = 0;
+static unsigned int end_index = UINT32_MAX;
+static unsigned int current_index = 0;
+static Eina_Bool no_report_update = EINA_FALSE;
+static Eina_List *case_tuples = NULL;
+static Eina_Bool verbose = 0;
+
+static Eo *pending = NULL;
+
+/* https://www.w3.org/International/questions/qa-forms-utf-8 */
+static Eina_Bool
+_utf8_check(const char *text)
+{
+ const unsigned char * bytes = (const unsigned char *)text;
+ while (*bytes)
+ {
+ const unsigned char c = bytes[0];
+
+ /* ascii: [\x09\x0A\x0D\x20-\x7E] */
+ if (((c >= 0x20) && (c <= 0x7e)) ||
+ (c == 0x09) || (c == 0x0a) || (c == 0x0d))
+ {
+ bytes += 1;
+ continue;
+ }
+
+ /* autobahnsuite says 0x7f is valid */
+ if (c == 0x7f)
+ {
+ bytes += 1;
+ continue;
+ }
+
+#define VALUE_BYTE_CHECK(x) ((x >= 0x80) && (x <= 0xbf))
+
+ /* non-overlong 2-byte: [\xC2-\xDF][\x80-\xBF] */
+ if ((c >= 0xc2) && (c <= 0xdf))
+ {
+ if (VALUE_BYTE_CHECK(bytes[1]))
+ {
+ bytes += 2;
+ continue;
+ }
+ }
+
+ /* excluding overlongs: \xE0[\xA0-\xBF][\x80-\xBF] */
+ if (c == 0xe0)
+ {
+ const unsigned char d = bytes[1];
+ if ((d >= 0xa0) && (d <= 0xbf))
+ {
+ if (VALUE_BYTE_CHECK(bytes[2]))
+ {
+ bytes += 3;
+ continue;
+ }
+ }
+ }
+
+ /* straight 3-byte: [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} */
+ if (((c >= 0xe1) && (c <= 0xec)) ||
+ (c == 0xee) || (c == 0xef))
+ {
+ if (VALUE_BYTE_CHECK(bytes[1]) && VALUE_BYTE_CHECK(bytes[2]))
+ {
+ bytes += 3;
+ continue;
+ }
+ }
+
+ /* excluding surrogates: \xED[\x80-\x9F][\x80-\xBF] */
+ if (c == 0xed)
+ {
+ const unsigned char d = bytes[1];
+ if ((d >= 0x80) && (d <= 0x9f))
+ {
+ if (VALUE_BYTE_CHECK(bytes[2]))
+ {
+ bytes += 3;
+ continue;
+ }
+ }
+ }
+
+ /* planes 1-3: \xF0[\x90-\xBF][\x80-\xBF]{2} */
+ if (c == 0xf0)
+ {
+ const unsigned char d = bytes[1];
+ if ((d >= 0x90) && (d <= 0xbf))
+ {
+ if (VALUE_BYTE_CHECK(bytes[2]) && VALUE_BYTE_CHECK(bytes[3]))
+ {
+ bytes += 4;
+ continue;
+ }
+ }
+ }
+ /* planes 4-15: [\xF1-\xF3][\x80-\xBF]{3} */
+ if ((c >= 0xf1) && (c <= 0xf3))
+ {
+ if (VALUE_BYTE_CHECK(bytes[1]) && VALUE_BYTE_CHECK(bytes[2]) && VALUE_BYTE_CHECK(bytes[3]))
+ {
+ bytes += 4;
+ continue;
+ }
+ }
+
+ /* plane 16: \xF4[\x80-\x8F][\x80-\xBF]{2} */
+ if (c == 0xf4)
+ {
+ const unsigned char d = bytes[1];
+ if ((d >= 0x80) && (d <= 0x8f))
+ {
+ if (VALUE_BYTE_CHECK(bytes[2]) && VALUE_BYTE_CHECK(bytes[3]))
+ {
+ bytes += 4;
+ continue;
+ }
+ }
+ }
+
+ if (verbose) fprintf(stderr, "INFO: failed unicode byte #%zd '%s'\n", (const char*)bytes - text, text);
+ return EINA_FALSE;
+ }
+
+ return EINA_TRUE;
+}
+
+static void
+_ws_pong(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Eo *dialer = event->object;
+ const char *text = event->info;
+ if (!verbose) return;
+ fprintf(stderr, "INFO: %s got PONG: %s\n",
+ efl_name_get(dialer), text);
+}
+
+static void
+_ws_closed_reason(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Eo *dialer = event->object;
+ Efl_Net_Dialer_Websocket_Closed_Reason *reason = event->info;
+
+ if (!_utf8_check(reason->message))
+ {
+ efl_net_dialer_websocket_close_request(dialer, EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_PROTOCOL_ERROR, "invalid UTF-8");
+ if (verbose) fprintf(stderr, "INFO: %s got CLOSE with invalid UTF-8\n", efl_name_get(dialer));
+ }
+
+ if (!verbose) return;
+ fprintf(stderr, "INFO: %s got CLOSE: %4d '%s'\n",
+ efl_name_get(dialer), reason->reason, reason->message);
+}
+
+static void
+_ws_message_text(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Eo *dialer = event->object;
+ const char *text = event->info;
+
+ if (!verbose) return;
+ fprintf(stderr, "INFO: %s got TEXT %zd bytes:\n%s\n",
+ efl_name_get(dialer), strlen(text), text);
+}
+
+static void
+_ws_message_binary(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Eo *dialer = event->object;
+ const Eina_Slice *slice = event->info;
+ size_t i;
+
+ if (!verbose) return;
+ fprintf(stderr, "INFO: %s got BINARY %zd bytes\n",
+ efl_name_get(dialer), slice->len);
+
+ for (i = 0; i < slice->len; i++)
+ {
+ const int c = slice->bytes[i];
+ if (isprint(c))
+ fprintf(stderr, " %#4x(%c)", c, c);
+ else
+ fprintf(stderr, " %#4x", c);
+ }
+
+ fprintf(stderr, "\n");
+}
+
+static void
+_closed(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Eo *dialer = event->object;
+
+ if (!verbose) return;
+ fprintf(stderr, "INFO: %s closed\n", efl_name_get(dialer));
+}
+
+static void
+_eos(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Eo *dialer = event->object;
+
+ if (!verbose) return;
+ fprintf(stderr, "INFO: %s eos\n", efl_name_get(dialer));
+}
+
+static void
+_connected(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Eo *dialer = event->object;
+
+ if (!verbose) return;
+ fprintf(stderr, "INFO: %s connected %s\n",
+ efl_name_get(dialer),
+ efl_net_dialer_address_dial_get(dialer));
+}
+
+static void
+_error(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Eo *dialer = event->object;
+ const Eina_Error *perr = event->info;
+ fprintf(stderr, "ERROR: %s error: %d '%s'\n",
+ efl_name_get(dialer), *perr, eina_error_msg_get(*perr));
+ retval = EXIT_FAILURE;
+}
+
+static void
+_del(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Eo *dialer = event->object;
+ if (pending == dialer)
+ pending = NULL;
+}
+
+EFL_CALLBACKS_ARRAY_DEFINE(dialer_cbs,
+ { EFL_NET_DIALER_WEBSOCKET_EVENT_PONG, _ws_pong },
+ { EFL_NET_DIALER_WEBSOCKET_EVENT_CLOSED_REASON, _ws_closed_reason },
+ { EFL_NET_DIALER_WEBSOCKET_EVENT_MESSAGE_TEXT, _ws_message_text },
+ { EFL_NET_DIALER_WEBSOCKET_EVENT_MESSAGE_BINARY, _ws_message_binary },
+ { EFL_NET_DIALER_EVENT_CONNECTED, _connected },
+ { EFL_NET_DIALER_EVENT_ERROR, _error },
+ { EFL_IO_CLOSER_EVENT_CLOSED, _closed },
+ { EFL_IO_READER_EVENT_EOS, _eos },
+ { EFL_EVENT_DEL, _del });
+
+static Eo *
+_websocket_new(const char *name)
+{
+ Eo *dialer;
+
+ dialer = efl_add(EFL_NET_DIALER_WEBSOCKET_CLASS, ecore_main_loop_get(),
+ efl_name_set(efl_self, name),
+ efl_event_callback_array_add(efl_self, dialer_cbs(), NULL));
+ if (!dialer)
+ {
+ retval = EXIT_FAILURE;
+ fprintf(stderr, "ERROR: could not create WebSockets dialer '%s'\n", name);
+ return NULL;
+ }
+
+ pending = dialer;
+
+ return dialer;
+}
+
+static void
+_closed_quit(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Eo *dialer = event->object;
+ efl_del(dialer);
+ ecore_main_loop_quit();
+}
+
+static void
+_tests_finished(void)
+{
+ Eo *dialer;
+ char url[4096];
+ int len;
+ Eina_Error err;
+
+ if (no_report_update)
+ {
+ if (verbose)
+ fprintf(stderr, "INFO: tests finished, user required to not update the reports\n");
+ ecore_main_loop_quit();
+ return;
+ }
+
+ case_tuples = eina_list_remove(case_tuples, case_tuples);
+ len = snprintf(url, sizeof(url), "%s/updateReports?agent=%s",
+ address, agent);
+ if (len < 0)
+ {
+ fprintf(stderr, "ERROR: could not create URL "
+ "'%s/updateReports?agent=%s': %s",
+ address, agent, strerror(errno));
+ ecore_main_loop_quit();
+ return;
+ }
+ else if ((size_t)len > sizeof(url))
+ {
+ fprintf(stderr, "ERROR: could not create URL "
+ "'%s/updateReports?agent=%s': no space.",
+ address, agent);
+ ecore_main_loop_quit();
+ return;
+ }
+
+ dialer = _websocket_new("update-reports");
+ if (!dialer)
+ {
+ ecore_main_loop_quit();
+ return;
+ }
+
+ efl_event_callback_add(dialer, EFL_IO_CLOSER_EVENT_CLOSED, _closed_quit, NULL);
+
+ err = efl_net_dialer_dial(dialer, url);
+ if (err != 0)
+ {
+ retval = EXIT_FAILURE;
+ fprintf(stderr, "ERROR: could not dial '%s': %s",
+ url, eina_error_msg_get(err));
+ efl_del(dialer);
+ ecore_main_loop_quit();
+ return;
+ }
+
+ if (!verbose) return;
+ fprintf(stderr, "INFO: %s '%s'\n",
+ efl_name_get(dialer), efl_net_dialer_address_dial_get(dialer));
+}
+
+static void
+_echo_text(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Eo *dialer = event->object;
+ const char *text = event->info;
+
+ if (!_utf8_check(text))
+ {
+ if (verbose) fprintf(stderr, "INFO: invalid UTF-8 sequence '%s'. Close the connection.\n", text);
+ efl_net_dialer_websocket_close_request(dialer, EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_INCONSISTENT_DATA, "invalid UTF-8");
+ return;
+ }
+
+ efl_net_dialer_websocket_text_send(dialer, text);
+}
+
+static void
+_echo_binary(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Eo *dialer = event->object;
+ const Eina_Slice *slice = event->info;
+ efl_net_dialer_websocket_binary_send(dialer, *slice);
+}
+
+static Eina_Bool _websocket_test_next_case_tuple(void);
+
+static void
+_test_next_case_closed(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Eo *dialer = event->object;
+ efl_del(dialer);
+
+ if (!_websocket_test_next_case_tuple())
+ _tests_finished();
+}
+
+EFL_CALLBACKS_ARRAY_DEFINE(_test_next_case_tuple_cbs,
+ { EFL_NET_DIALER_WEBSOCKET_EVENT_MESSAGE_TEXT, _echo_text },
+ { EFL_NET_DIALER_WEBSOCKET_EVENT_MESSAGE_BINARY, _echo_binary },
+ { EFL_IO_CLOSER_EVENT_CLOSED, _test_next_case_closed });
+
+static Eina_Bool
+_websocket_test_next_case_tuple(void)
+{
+ Eo *dialer;
+ char url[4096];
+ char name[256];
+ char *str;
+ int len;
+ Eina_Error err;
+
+ if (!case_tuples)
+ return EINA_FALSE;
+
+ str = case_tuples->data;
+ case_tuples = eina_list_remove_list(case_tuples, case_tuples);
+ len = snprintf(url, sizeof(url), "%s/runCase?casetuple=%s&agent=%s",
+ address, str, agent);
+ if (len < 0)
+ {
+ fprintf(stderr, "ERROR: could not create URL "
+ "'%s/runCase?casetuple=%s&agent=%s': %s",
+ address, str, agent, strerror(errno));
+ free(str);
+ return EINA_FALSE;
+ }
+ else if ((size_t)len > sizeof(url))
+ {
+ fprintf(stderr, "ERROR: could not create URL "
+ "'%s/runCase?casetuple=%s&agent=%s': no space.",
+ address, str, agent);
+ free(str);
+ return EINA_FALSE;
+ }
+
+ snprintf(name, sizeof(name), "test_case=%s", str);
+ free(str);
+
+ dialer = _websocket_new(name);
+ if (!dialer) return EINA_FALSE;
+
+ efl_event_callback_array_add(dialer, _test_next_case_tuple_cbs(), NULL);
+
+ err = efl_net_dialer_dial(dialer, url);
+ if (err != 0)
+ {
+ retval = EXIT_FAILURE;
+ fprintf(stderr, "ERROR: could not dial '%s': %s",
+ url, eina_error_msg_get(err));
+ efl_del(dialer);
+ return EINA_FALSE;
+ }
+
+ fprintf(stderr, "TEST: %s '%s'\n", efl_name_get(dialer), efl_net_dialer_address_dial_get(dialer));
+
+ return EINA_TRUE;
+}
+
+static Eina_Bool _websocket_test_index(unsigned int idx);
+
+static void
+_test_index_closed(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Eo *dialer = event->object;
+ efl_del(dialer);
+
+ if (!_websocket_test_index(current_index + 1))
+ _tests_finished();
+}
+
+EFL_CALLBACKS_ARRAY_DEFINE(_test_index_cbs,
+ { EFL_NET_DIALER_WEBSOCKET_EVENT_MESSAGE_TEXT, _echo_text },
+ { EFL_NET_DIALER_WEBSOCKET_EVENT_MESSAGE_BINARY, _echo_binary },
+ { EFL_IO_CLOSER_EVENT_CLOSED, _test_index_closed });
+
+static Eina_Bool
+_websocket_test_index(unsigned int idx)
+{
+ Eo *dialer;
+ char url[4096];
+ char name[64];
+ int len;
+ Eina_Error err;
+
+ if (idx > end_index)
+ return EINA_FALSE;
+
+ len = snprintf(url, sizeof(url), "%s/runCase?case=%u&agent=%s",
+ address, idx, agent);
+ if (len < 0)
+ {
+ fprintf(stderr, "ERROR: could not create URL "
+ "'%s/runCase?case=%u&agent=%s': %s",
+ address, idx, agent, strerror(errno));
+ return EINA_FALSE;
+ }
+ else if ((size_t)len > sizeof(url))
+ {
+ fprintf(stderr, "ERROR: could not create URL "
+ "'%s/runCase?case=%u&agent=%s': no space.",
+ address, idx, agent);
+ return EINA_FALSE;
+ }
+
+ snprintf(name, sizeof(name), "test_case=%u", idx);
+
+ dialer = _websocket_new(name);
+ if (!dialer) return EINA_FALSE;
+
+ efl_event_callback_array_add(dialer, _test_index_cbs(), NULL);
+
+ err = efl_net_dialer_dial(dialer, url);
+ if (err != 0)
+ {
+ retval = EXIT_FAILURE;
+ fprintf(stderr, "ERROR: could not dial '%s': %s",
+ url, eina_error_msg_get(err));
+ efl_del(dialer);
+ return EINA_FALSE;
+ }
+
+ current_index = idx;
+
+ fprintf(stderr, "TEST: %s '%s'\n", efl_name_get(dialer), efl_net_dialer_address_dial_get(dialer));
+
+ return EINA_TRUE;
+}
+
+static void
+_load_tests_text(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ const char *text = event->info;
+ unsigned int n = strtoul(text, NULL, 10);
+
+ if (start_index == 0)
+ start_index = 1;
+ else if (start_index > n)
+ start_index = n;
+
+ if (end_index == 0 || end_index > n)
+ end_index = n;
+
+ if (!verbose) return;
+ fprintf(stderr, "INFO: test count: %u, start_index=%u, end_index=%u\n",
+ n, start_index, end_index);
+}
+
+static void
+_load_tests_closed(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Eo *dialer = event->object;
+ efl_del(dialer);
+
+ if (!_websocket_test_index(start_index))
+ _tests_finished();
+}
+
+EFL_CALLBACKS_ARRAY_DEFINE(_load_tests_cbs,
+ { EFL_NET_DIALER_WEBSOCKET_EVENT_MESSAGE_TEXT, _load_tests_text },
+ { EFL_IO_CLOSER_EVENT_CLOSED, _load_tests_closed });
+
+static Eina_Bool
+_websocket_load_tests(void)
+{
+ Eo *dialer;
+ char url[4096];
+ int len;
+ Eina_Error err;
+
+ len = snprintf(url, sizeof(url), "%s/getCaseCount", address);
+ if (len < 0)
+ {
+ fprintf(stderr, "ERROR: could not create URL '%s/getCaseCount': %s",
+ address, strerror(errno));
+ return EINA_FALSE;
+ }
+ else if ((size_t)len > sizeof(url))
+ {
+ fprintf(stderr, "ERROR: could not create URL '%s/getCaseCount': no space.",
+ address);
+ return EINA_FALSE;
+ }
+
+ dialer = _websocket_new("get-case-count");
+ if (!dialer) return EINA_FALSE;
+
+ efl_event_callback_array_add(dialer, _load_tests_cbs(), NULL);
+
+ err = efl_net_dialer_dial(dialer, url);
+ if (err != 0)
+ {
+ retval = EXIT_FAILURE;
+ fprintf(stderr, "ERROR: could not dial '%s': %s",
+ url, eina_error_msg_get(err));
+ efl_del(dialer);
+ return EINA_FALSE;
+ }
+
+ if (verbose) fprintf(stderr, "INFO: %s '%s'\n", efl_name_get(dialer), efl_net_dialer_address_dial_get(dialer));
+
+ return EINA_TRUE;
+}
+
+static const Ecore_Getopt options = {
+ "efl_net_dialer_websocket_autobahntestee", /* program name */
+ NULL, /* usage line */
+ "1", /* version */
+ "(C) 2016 Enlightenment Project", /* copyright */
+ "BSD 2-Clause", /* license */
+ /* long description, may be multiline and contain \n */
+ "Use Efl_Net_Dialer_Websocket to implement a testee client for the Autobahn Test Suite."
+ "\n"
+ "Autobahn Test Suite http://autobahn.ws/testsuite provides a fully automated test suite to verify client and server implementations of the WebSocket Protocol for specification conformance and implementation robustness."
+ "\n"
+ "This is a client to talk to their test server, that should be executed as:\n"
+ " wstest -m fuzzingserver\n"
+ "\n",
+ EINA_FALSE,
+ {
+ ECORE_GETOPT_STORE_UINT('s', "start-index", "when running batch, specifies the start (first) index"),
+ ECORE_GETOPT_STORE_UINT('e', "end-index", "when running batch, specifies the end (last) index"),
+ ECORE_GETOPT_STORE_TRUE('n', "no-report-update", "do not trigger autobahn to update report"),
+ ECORE_GETOPT_STORE_TRUE('v', "verbose", "print messages"),
+ ECORE_GETOPT_APPEND('t', "test-case", "the test-case tuple such as '1.2.8'", ECORE_GETOPT_TYPE_STR),
+ ECORE_GETOPT_STORE_STR('a', "agent", "the agent identifier"),
+ ECORE_GETOPT_VERSION('V', "version"),
+ ECORE_GETOPT_COPYRIGHT('C', "copyright"),
+ ECORE_GETOPT_LICENSE('L', "license"),
+ ECORE_GETOPT_HELP('h', "help"),
+ ECORE_GETOPT_STORE_METAVAR_STR(0, NULL,
+ "The address (URL) to dial, such as ws://127.0.0.1:9001", "address"),
+ ECORE_GETOPT_SENTINEL
+ }
+};
+
+int
+main(int argc, char **argv)
+{
+ Eina_Bool quit_option = EINA_FALSE;
+ Ecore_Getopt_Value values[] = {
+ ECORE_GETOPT_VALUE_UINT(start_index),
+ ECORE_GETOPT_VALUE_UINT(end_index),
+ ECORE_GETOPT_VALUE_BOOL(no_report_update),
+ ECORE_GETOPT_VALUE_BOOL(verbose),
+ ECORE_GETOPT_VALUE_LIST(case_tuples),
+ ECORE_GETOPT_VALUE_STR(agent),
+
+ /* standard block to provide version, copyright, license and help */
+ ECORE_GETOPT_VALUE_BOOL(quit_option), /* -V/--version quits */
+ ECORE_GETOPT_VALUE_BOOL(quit_option), /* -C/--copyright quits */
+ ECORE_GETOPT_VALUE_BOOL(quit_option), /* -L/--license quits */
+ ECORE_GETOPT_VALUE_BOOL(quit_option), /* -h/--help quits */
+
+ /* positional argument */
+ ECORE_GETOPT_VALUE_STR(address),
+
+ ECORE_GETOPT_VALUE_NONE /* sentinel */
+ };
+ int args;
+ Eina_Bool r;
+
+ ecore_init();
+ ecore_con_init();
+ ecore_con_url_init();
+
+ args = ecore_getopt_parse(&options, values, argc, argv);
+ if (args < 0)
+ {
+ fputs("ERROR: Could not parse command line options.\n", stderr);
+ retval = EXIT_FAILURE;
+ goto end;
+ }
+
+ if (quit_option) goto end;
+
+ args = ecore_getopt_parse_positional(&options, values, argc, argv, args);
+ if (args < 0)
+ {
+ fputs("ERROR: Could not parse positional arguments.\n", stderr);
+ retval = EXIT_FAILURE;
+ goto end;
+ }
+
+ if (case_tuples)
+ r = _websocket_test_next_case_tuple();
+ else if (start_index == end_index)
+ r = _websocket_test_index(start_index);
+ else
+ r = _websocket_load_tests();
+
+ if (r)
+ {
+ ecore_main_loop_begin();
+ if (verbose) fprintf(stderr, "INFO: main loop finished. retval=%d\n", retval);
+ }
+
+ if (pending)
+ efl_del(pending);
+
+ end:
+ ecore_con_url_shutdown();
+ ecore_con_shutdown();
+ ecore_shutdown();
+
+ return retval;
+}
diff --git a/src/examples/ecore/efl_net_dialer_websocket_example.c b/src/examples/ecore/efl_net_dialer_websocket_example.c
new file mode 100644
index 0000000000..fa7bb2b7c2
--- /dev/null
+++ b/src/examples/ecore/efl_net_dialer_websocket_example.c
@@ -0,0 +1,384 @@
+#define EFL_BETA_API_SUPPORT 1
+#define EFL_EO_API_SUPPORT 1
+#include <Ecore.h>
+#include <Ecore_Con.h>
+#include <Ecore_Getopt.h>
+#include <fcntl.h>
+#include <ctype.h>
+
+static int retval = EXIT_SUCCESS;
+
+static int lines_text = 0;
+static int lines_binary = 0;
+
+static void
+_dummy_send(Eo *dialer, Eina_Bool text, size_t lines)
+{
+ size_t len = lines * 80;
+ char *buf = malloc(len + 1);
+ const size_t az_range = 'Z' - 'A';
+ size_t i;
+
+ for (i = 0; i < lines; i++) {
+ char *ln = buf + i * 80;
+ uint8_t chr;
+
+ snprintf(ln, 11, "%9zd ", i + 1);
+ if (text)
+ chr = (i % az_range) + 'A';
+ else
+ chr = i & 0xff;
+ memset(ln + 10, chr, 69);
+ ln[79] = '\n';
+ }
+ buf[len] = '\0';
+
+ if (text)
+ efl_net_dialer_websocket_text_send(dialer, buf);
+ else
+ {
+ Eina_Slice slice = {.mem = buf, .len = len};
+ efl_net_dialer_websocket_binary_send(dialer, slice);
+ }
+ free(buf);
+}
+
+static void
+_ws_pong(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ fprintf(stderr, "INFO: got PONG: %s\n", (const char *)event->info);
+
+ efl_net_dialer_websocket_close_request(event->object,
+ EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_NORMAL,
+ "close it!");
+}
+
+static void
+_ws_closed_reason(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Efl_Net_Dialer_Websocket_Closed_Reason *reason = event->info;
+ fprintf(stderr, "INFO: got CLOSE: %4d '%s'\n",
+ reason->reason, reason->message);
+}
+
+static void
+_ws_message_text(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ fprintf(stderr, "INFO: got TEXT:\n%s\n", (const char *)event->info);
+
+ if (lines_text < 5)
+ _dummy_send(event->object, EINA_TRUE, ++lines_text);
+ else
+ _dummy_send(event->object, EINA_FALSE, ++lines_binary);
+}
+
+static void
+_ws_message_binary(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ const Eina_Slice *slice = event->info;
+ size_t i;
+
+ fprintf(stderr, "INFO: got BINARY %zd bytes\n", slice->len);
+
+ for (i = 0; i < slice->len; i++)
+ {
+ const int c = slice->bytes[i];
+ if (isprint(c))
+ fprintf(stderr, " %#4x(%c)", c, c);
+ else
+ fprintf(stderr, " %#4x", c);
+ }
+
+ fprintf(stderr, "\n");
+
+ if (lines_binary < 5)
+ _dummy_send(event->object, EINA_FALSE, ++lines_binary);
+ else
+ efl_net_dialer_websocket_ping(event->object, "will close on pong");
+}
+
+static void
+_closed(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ fprintf(stderr, "INFO: closed %s\n",
+ efl_name_get(event->object));
+ ecore_main_loop_quit();
+}
+
+static void
+_eos(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ fprintf(stderr, "INFO: eos %s\n",
+ efl_name_get(event->object));
+}
+
+static void
+_connected(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ Eina_Stringshare *protocol;
+ Eina_Iterator *itr;
+ fprintf(stderr, "INFO: connected %s\n",
+ efl_net_dialer_address_dial_get(event->object));
+
+ itr = efl_net_dialer_websocket_response_protocols_get(event->object);
+ EINA_ITERATOR_FOREACH(itr, protocol)
+ fprintf(stderr, "INFO: server protocol: %s\n", protocol);
+ eina_iterator_free(itr);
+
+ _dummy_send(event->object, EINA_TRUE, ++lines_text);
+}
+
+static void
+_resolved(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ fprintf(stderr, "INFO: resolved %s => %s\n",
+ efl_net_dialer_address_dial_get(event->object),
+ efl_net_socket_address_remote_get(event->object));
+}
+
+static void
+_error(void *data EINA_UNUSED, const Efl_Event *event)
+{
+ const Eina_Error *perr = event->info;
+ fprintf(stderr, "INFO: error: %d '%s'\n", *perr, eina_error_msg_get(*perr));
+ retval = EXIT_FAILURE;
+ ecore_main_loop_quit();
+}
+
+EFL_CALLBACKS_ARRAY_DEFINE(dialer_cbs,
+ { EFL_NET_DIALER_WEBSOCKET_EVENT_PONG, _ws_pong },
+ { EFL_NET_DIALER_WEBSOCKET_EVENT_CLOSED_REASON, _ws_closed_reason },
+ { EFL_NET_DIALER_WEBSOCKET_EVENT_MESSAGE_TEXT, _ws_message_text },
+ { EFL_NET_DIALER_WEBSOCKET_EVENT_MESSAGE_BINARY, _ws_message_binary },
+ { EFL_NET_DIALER_EVENT_CONNECTED, _connected },
+ { EFL_NET_DIALER_EVENT_RESOLVED, _resolved },
+ { EFL_NET_DIALER_EVENT_ERROR, _error },
+ { EFL_IO_CLOSER_EVENT_CLOSED, _closed },
+ { EFL_IO_READER_EVENT_EOS, _eos });
+
+
+static const char *authentication_method_choices[] = {
+ "none",
+ "basic",
+ "digest",
+ "negotiate",
+ "ntlm",
+ "ntlm_winbind",
+ "any_safe",
+ "any",
+ NULL,
+};
+
+static Efl_Net_Http_Authentication_Method
+_parse_authentication_method(const char *str)
+{
+ if (strcmp(str, "basic") == 0)
+ return EFL_NET_HTTP_AUTHENTICATION_METHOD_BASIC;
+ if (strcmp(str, "digest") == 0)
+ return EFL_NET_HTTP_AUTHENTICATION_METHOD_DIGEST;
+ if (strcmp(str, "negotiate") == 0)
+ return EFL_NET_HTTP_AUTHENTICATION_METHOD_NEGOTIATE;
+ if (strcmp(str, "ntlm") == 0)
+ return EFL_NET_HTTP_AUTHENTICATION_METHOD_NTLM;
+ if (strcmp(str, "ntlm_winbind") == 0)
+ return EFL_NET_HTTP_AUTHENTICATION_METHOD_NTLM_WINBIND;
+ if (strcmp(str, "any_safe") == 0)
+ return EFL_NET_HTTP_AUTHENTICATION_METHOD_ANY_SAFE;
+ if (strcmp(str, "any") == 0)
+ return EFL_NET_HTTP_AUTHENTICATION_METHOD_ANY;
+
+ return EFL_NET_HTTP_AUTHENTICATION_METHOD_NONE;
+}
+
+static const Ecore_Getopt options = {
+ "efl_net_dialer_websocket_example", /* program name */
+ NULL, /* usage line */
+ "1", /* version */
+ "(C) 2016 Enlightenment Project", /* copyright */
+ "BSD 2-Clause", /* license */
+ /* long description, may be multiline and contain \n */
+ "Example of Efl_Net_Dialer_Websocket usage in message-based mode.\n"
+ "In this example couple of text and binary messages are sent to the server, "
+ "as well as a ping. On pong the websocket is closed."
+ "\n"
+ "For the EFL I/O interfaces example, see efl_io_copier_example.c"
+ "\n",
+ EINA_FALSE,
+ {
+ ECORE_GETOPT_APPEND('p', "websocket-protocol", "WebSocket protocol to request", ECORE_GETOPT_TYPE_STR),
+ ECORE_GETOPT_STORE_STR('U', "username", "Authentication username"),
+ ECORE_GETOPT_STORE_STR('P', "password", "Authentication password"),
+ ECORE_GETOPT_CHOICE('A', "authentication-method", "Authentication method", authentication_method_choices),
+ ECORE_GETOPT_STORE_BOOL('R', "authentication-restricted", "Authentication method must be restricted"),
+ ECORE_GETOPT_STORE_BOOL('r', "allow-redirects", "allow redirections by following 'Location:' headers"),
+ ECORE_GETOPT_STORE_DOUBLE('t', "connect-timeout", "timeout in seconds for the connection phase"),
+ ECORE_GETOPT_APPEND('H', "header", "Add custom headers. Format must be 'Key: Value'", ECORE_GETOPT_TYPE_STR),
+ ECORE_GETOPT_STORE_STR('X', "proxy", "Set a specific proxy for the connection"),
+ ECORE_GETOPT_STORE_STR('c', "cookie-jar", "Set the cookie-jar file to read/save cookies from. Empty means an in-memory cookie-jar"),
+
+ ECORE_GETOPT_VERSION('V', "version"),
+ ECORE_GETOPT_COPYRIGHT('C', "copyright"),
+ ECORE_GETOPT_LICENSE('L', "license"),
+ ECORE_GETOPT_HELP('h', "help"),
+ ECORE_GETOPT_STORE_METAVAR_STR(0, NULL,
+ "The address (URL) to dial, such as wss://echo.websocket.org", "address"),
+ ECORE_GETOPT_SENTINEL
+ }
+};
+
+int
+main(int argc, char **argv)
+{
+ char *address = NULL;
+ char *username = NULL;
+ char *password = NULL;
+ char *authentication_method_str = "basic";
+ char *proxy = NULL;
+ char *cookie_jar = NULL;
+ Eina_Bool authentication_restricted = EINA_FALSE;
+ Eina_Bool allow_redirects = EINA_TRUE;
+ double timeout_dial = 30.0;
+ Eina_List *headers = NULL;
+ Eina_List *protocols = NULL;
+ Eina_Bool quit_option = EINA_FALSE;
+ Ecore_Getopt_Value values[] = {
+ ECORE_GETOPT_VALUE_LIST(protocols),
+ ECORE_GETOPT_VALUE_STR(username),
+ ECORE_GETOPT_VALUE_STR(password),
+ ECORE_GETOPT_VALUE_STR(authentication_method_str),
+ ECORE_GETOPT_VALUE_BOOL(authentication_restricted),
+ ECORE_GETOPT_VALUE_BOOL(allow_redirects),
+ ECORE_GETOPT_VALUE_DOUBLE(timeout_dial),
+ ECORE_GETOPT_VALUE_LIST(headers),
+ ECORE_GETOPT_VALUE_STR(proxy),
+ ECORE_GETOPT_VALUE_STR(cookie_jar),
+
+ /* standard block to provide version, copyright, license and help */
+ ECORE_GETOPT_VALUE_BOOL(quit_option), /* -V/--version quits */
+ ECORE_GETOPT_VALUE_BOOL(quit_option), /* -C/--copyright quits */
+ ECORE_GETOPT_VALUE_BOOL(quit_option), /* -L/--license quits */
+ ECORE_GETOPT_VALUE_BOOL(quit_option), /* -h/--help quits */
+
+ /* positional argument */
+ ECORE_GETOPT_VALUE_STR(address),
+
+ ECORE_GETOPT_VALUE_NONE /* sentinel */
+ };
+ int args;
+ Eo *dialer, *loop;
+ Efl_Net_Http_Authentication_Method authentication_method;
+ Efl_Net_Http_Header *header;
+ Eina_Iterator *itr;
+ Eina_Error err;
+ char *str;
+
+ ecore_init();
+ ecore_con_init();
+ ecore_con_url_init();
+
+ args = ecore_getopt_parse(&options, values, argc, argv);
+ if (args < 0)
+ {
+ fputs("ERROR: Could not parse command line options.\n", stderr);
+ retval = EXIT_FAILURE;
+ goto end;
+ }
+
+ if (quit_option) goto end;
+
+ loop = ecore_main_loop_get();
+
+ args = ecore_getopt_parse_positional(&options, values, argc, argv, args);
+ if (args < 0)
+ {
+ fputs("ERROR: Could not parse positional arguments.\n", stderr);
+ retval = EXIT_FAILURE;
+ goto end;
+ }
+
+ authentication_method = _parse_authentication_method(authentication_method_str);
+
+ if (cookie_jar && cookie_jar[0] == ' ')
+ cookie_jar = "";
+
+ dialer = efl_add(EFL_NET_DIALER_WEBSOCKET_CLASS, loop,
+ efl_name_set(efl_self, "dialer"),
+ efl_net_dialer_websocket_authentication_set(efl_self, username, password, authentication_method, authentication_restricted),
+ efl_net_dialer_websocket_allow_redirects_set(efl_self, allow_redirects),
+ efl_net_dialer_websocket_cookie_jar_set(efl_self, cookie_jar),
+ efl_net_dialer_proxy_set(efl_self, proxy),
+ efl_net_dialer_timeout_dial_set(efl_self, timeout_dial),
+ efl_event_callback_array_add(efl_self, dialer_cbs(), NULL));
+ if (!dialer)
+ {
+ retval = EXIT_FAILURE;
+ fprintf(stderr, "ERROR: could not create WebSockets dialer\n");
+ goto end;
+ }
+
+ EINA_LIST_FREE(headers, str)
+ {
+ char *p = strchr(str, ':');
+ if (p)
+ {
+ p[0] = '\0';
+ p++;
+ while ((p[0] != '\0') && isspace(p[0]))
+ p++;
+ }
+ efl_net_dialer_websocket_request_header_add(dialer, str, p);
+ free(str);
+ }
+
+ EINA_LIST_FREE(protocols, str)
+ {
+ efl_net_dialer_websocket_request_protocol_add(dialer, str);
+ free(str);
+ }
+
+ err = efl_net_dialer_dial(dialer, address);
+ if (err != 0)
+ {
+ retval = EXIT_FAILURE;
+ fprintf(stderr, "ERROR: could not dial '%s': %s",
+ address, eina_error_msg_get(err));
+ goto no_mainloop;
+ }
+
+ fprintf(stderr,
+ "INFO: dialed %s\n"
+ "INFO: - allow_redirects=%d\n"
+ "INFO: - cookie_jar=%s\n"
+ "INFO: - timeout_dial=%fs\n"
+ "INFO: - proxy=%s\n"
+ "INFO: - request headers:\n",
+ efl_net_dialer_address_dial_get(dialer),
+ efl_net_dialer_websocket_allow_redirects_get(dialer),
+ efl_net_dialer_websocket_cookie_jar_get(dialer),
+ efl_net_dialer_timeout_dial_get(dialer),
+ efl_net_dialer_proxy_get(dialer));
+ itr = efl_net_dialer_websocket_request_headers_get(dialer);
+ EINA_ITERATOR_FOREACH(itr, header)
+ fprintf(stderr, "INFO: %s: %s\n", header->key, header->value);
+ eina_iterator_free(itr);
+
+ fprintf(stderr, "INFO: - request protocols:\n");
+ itr = efl_net_dialer_websocket_request_protocols_get(dialer);
+ EINA_ITERATOR_FOREACH(itr, str)
+ fprintf(stderr, "INFO: %s\n", str);
+ eina_iterator_free(itr);
+
+ ecore_main_loop_begin();
+
+ fprintf(stderr, "INFO: main loop finished.\n");
+
+ no_mainloop:
+ efl_del(dialer);
+
+ end:
+ ecore_con_url_shutdown();
+ ecore_con_shutdown();
+ ecore_shutdown();
+
+ return retval;
+}
diff --git a/src/lib/ecore_con/Ecore_Con_Eo.h b/src/lib/ecore_con/Ecore_Con_Eo.h
index 22f1e70b49..6028d600fb 100644
--- a/src/lib/ecore_con/Ecore_Con_Eo.h
+++ b/src/lib/ecore_con/Ecore_Con_Eo.h
@@ -73,3 +73,4 @@ extern Eina_Error EFL_NET_HTTP_ERROR_USE_SSL_FAILED;
extern Eina_Error EFL_NET_HTTP_ERROR_WRITE_ERROR;
#include "efl_net_dialer_http.eo.h"
+#include "efl_net_dialer_websocket.eo.h"
diff --git a/src/lib/ecore_con/ecore_con_url.c b/src/lib/ecore_con/ecore_con_url.c
index 5da1847404..5996c7c699 100644
--- a/src/lib/ecore_con/ecore_con_url.c
+++ b/src/lib/ecore_con/ecore_con_url.c
@@ -30,6 +30,7 @@
#include "Ecore_Con.h"
#include "ecore_con_private.h"
#include "ecore_con_url_curl.h"
+#include "Emile.h"
#define MY_CLASS EFL_NETWORK_URL_CLASS
@@ -114,11 +115,20 @@ EAPI int
ecore_con_url_init(void)
{
if (++_init_count > 1) return _init_count;
- if (!ecore_init()) return --_init_count;
+ if (!ecore_init()) goto ecore_init_failed;
+ if (!emile_init()) goto emile_init_failed;
+ if (!emile_cipher_init()) goto emile_cipher_init_failed;
ECORE_CON_EVENT_URL_DATA = ecore_event_type_new();
ECORE_CON_EVENT_URL_COMPLETE = ecore_event_type_new();
ECORE_CON_EVENT_URL_PROGRESS = ecore_event_type_new();
return _init_count;
+
+ emile_cipher_init_failed:
+ emile_shutdown();
+ emile_init_failed:
+ ecore_shutdown();
+ ecore_init_failed:
+ return --_init_count;
}
EAPI int
@@ -139,6 +149,7 @@ ecore_con_url_shutdown(void)
EINA_LIST_FREE(_fd_hd_list, fd_handler)
ecore_main_fd_handler_del(fd_handler);
_c_shutdown();
+ emile_shutdown(); /* no emile_cipher_shutdown(), handled here */
ecore_shutdown();
return 0;
}
diff --git a/src/lib/ecore_con/efl_net_dialer_websocket.c b/src/lib/ecore_con/efl_net_dialer_websocket.c
new file mode 100644
index 0000000000..59593b95d7
--- /dev/null
+++ b/src/lib/ecore_con/efl_net_dialer_websocket.c
@@ -0,0 +1,1667 @@
+#define EFL_NET_DIALER_WEBSOCKET_PROTECTED 1
+#define EFL_NET_DIALER_PROTECTED 1
+#define EFL_NET_SOCKET_PROTECTED 1
+#define EFL_IO_READER_PROTECTED 1
+#define EFL_IO_WRITER_PROTECTED 1
+#define EFL_IO_CLOSER_PROTECTED 1
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#include "Ecore.h"
+#include "Ecore_Con.h"
+#include "ecore_con_private.h"
+#include "Emile.h"
+
+/*
+ * NOTE:
+ *
+ * Test this code against Autobahnsuite: http://autobahn.ws/testsuite/
+ * See src/examples/ecore/efl_net_dialer_websocket_autobahntestee
+ *
+ * Known failed tests:
+ *
+ * - Cases 6.4.x: fail fast.
+ *
+ * STATUS: WONTFIX
+ *
+ * These are non-strict and requires UTF-8 to be checked per frame,
+ * something we do not do as it's left to the user and the user
+ * only gets full frames. At the end user will fail, but taking
+ * more data then it should.
+ *
+ *
+ * - Case 6.7.1: message is UTF-8 null byte (1 byte = \x00).
+ *
+ * STATUS: WONTFIX
+ *
+ * We handle text messages as NULL terminated strings and when
+ * sending back, we do not include such null terminator in the
+ * payload. To do so we'd need to use a cumbersome API that
+ * specifies the size of the string.
+ *
+ */
+
+
+/* just to check curl_version_info_data and warn on old versions */
+#include "ecore_con_url_curl.h"
+
+#define MY_CLASS EFL_NET_DIALER_WEBSOCKET_CLASS
+
+typedef enum _Efl_Net_Dialer_Websocket_Opcode {
+ EFL_NET_DIALER_WEBSOCKET_OPCODE_CONTINUATION = 0x0,
+ EFL_NET_DIALER_WEBSOCKET_OPCODE_TEXT = 0x1,
+ EFL_NET_DIALER_WEBSOCKET_OPCODE_BINARY = 0x2,
+ EFL_NET_DIALER_WEBSOCKET_OPCODE_CLOSE = 0x8,
+ EFL_NET_DIALER_WEBSOCKET_OPCODE_PING = 0x9,
+ EFL_NET_DIALER_WEBSOCKET_OPCODE_PONG = 0xa,
+} Efl_Net_Dialer_Websocket_Opcode;
+
+static inline Eina_Bool
+_efl_net_dialer_websocket_opcode_control_check(Efl_Net_Dialer_Websocket_Opcode opcode)
+{
+ switch (opcode)
+ {
+ case EFL_NET_DIALER_WEBSOCKET_OPCODE_CONTINUATION:
+ case EFL_NET_DIALER_WEBSOCKET_OPCODE_TEXT:
+ case EFL_NET_DIALER_WEBSOCKET_OPCODE_BINARY:
+ return EINA_FALSE;
+ default:
+ return EINA_TRUE;
+ }
+}
+
+static inline Eina_Bool
+_efl_net_dialer_websocket_close_reason_check(Efl_Net_Dialer_Websocket_Close_Reason r)
+{
+ switch (r)
+ {
+ case EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_NORMAL:
+ case EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_GOING_AWAY:
+ case EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_PROTOCOL_ERROR:
+ case EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_UNEXPECTED_DATA:
+ case EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_INCONSISTENT_DATA:
+ case EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_POLICY_VIOLATION:
+ case EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_TOO_BIG:
+ case EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_MISSING_EXTENSION:
+ case EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_SERVER_ERROR:
+ case EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_IANA_REGISTRY_START:
+ case EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_IANA_REGISTRY_END:
+ case EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_PRIVATE_START:
+ case EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_PRIVATE_END:
+ return EINA_TRUE;
+ case EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_NO_REASON:
+ case EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_ABRUPTLY:
+ return EINA_FALSE;
+ }
+
+ if ((r >= EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_IANA_REGISTRY_START) &&
+ (r <= EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_IANA_REGISTRY_END))
+ return EINA_TRUE;
+
+ if ((r >= EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_PRIVATE_START) &&
+ (r <= EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_PRIVATE_END))
+ return EINA_TRUE;
+
+ return EINA_FALSE;
+}
+
+/*
+ * WebSocket is a framed protocol in the format:
+ *
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-------+-+-------------+-------------------------------+
+ * |F|R|R|R| opcode|M| Payload len | Extended payload length |
+ * |I|S|S|S| (4) |A| (7) | (16/64) |
+ * |N|V|V|V| |S| | (if payload len==126/127) |
+ * | |1|2|3| |K| | |
+ * +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
+ * | Extended payload length continued, if payload len == 127 |
+ * + - - - - - - - - - - - - - - - +-------------------------------+
+ * | |Masking-key, if MASK set to 1 |
+ * +-------------------------------+-------------------------------+
+ * | Masking-key (continued) | Payload Data |
+ * +-------------------------------- - - - - - - - - - - - - - - - +
+ * : Payload Data continued ... :
+ * + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ * | Payload Data continued ... |
+ * +---------------------------------------------------------------+
+ *
+ * See https://tools.ietf.org/html/rfc6455#section-5.2
+ */
+typedef struct _Efl_Net_Dialer_Websocket_Frame_Header {
+ /* first byte: fin + opcode */
+ uint8_t opcode : 4;
+ uint8_t _reserved : 3;
+ uint8_t fin : 1;
+
+ /* second byte: mask + payload length */
+ uint8_t payload_len : 7; /* if 126, uses extra 2 bytes (uint16_t)
+ * if 127, uses extra 8 bytes (uint64_t)
+ * if <=125 is self-contained
+ */
+ uint8_t mask : 1; /* if 1, uses 4 extra bytes */
+} Efl_Net_Dialer_Websocket_Frame_Header;
+
+typedef struct _Efl_Net_Dialer_Websocket_Frame {
+ EINA_INLIST;
+ size_t len; /* total frame size to send */
+ Efl_Net_Dialer_Websocket_Frame_Header header;
+} Efl_Net_Dialer_Websocket_Frame;
+
+typedef struct _Efl_Net_Dialer_Websocket_Pending_Read {
+ EINA_INLIST;
+ size_t len;
+ uint8_t *bytes;
+} Efl_Net_Dialer_Websocket_Pending_Read;
+
+typedef struct _Efl_Net_Dialer_Websocket_Data {
+ Eo *http;
+ Eo *close_timer;
+ Eina_Promise *job;
+ Eina_Stringshare *address_dial; /* must rewrite ws->http, wss->https */
+ Eina_Stringshare *address_remote; /* must rewrite ws->http, wss->https */
+ struct {
+ Eina_List *requested;
+ Eina_List *received;
+ } protocols;
+ char accept_key[29]; /* 28 + \0 */
+ struct {
+ struct {
+ uint64_t total_len;
+ uint64_t used_len;
+ uint8_t *payload;
+ Efl_Net_Dialer_Websocket_Opcode opcode;
+ Eina_Bool fin;
+ } current;
+
+ struct {
+ uint64_t total_len;
+ uint64_t used_len;
+ uint8_t *payload;
+ Efl_Net_Dialer_Websocket_Opcode opcode;
+ } fragmented;
+
+ Efl_Net_Dialer_Websocket_Pending_Read *pending_read;
+ size_t pending_read_offset;
+
+ /* for current frame */
+ uint8_t tmpbuf[sizeof(Efl_Net_Dialer_Websocket_Frame_Header) + sizeof(uint64_t)];
+ uint8_t done; /* of tmpbuf, for header */
+ uint8_t needed; /* of tmpbuf, for header */
+ } recv;
+ struct {
+ Efl_Net_Dialer_Websocket_Frame *pending;
+ size_t offset;
+ } send;
+ Efl_Net_Dialer_Websocket_Streaming_Mode streaming_mode;
+ Eina_Bool connected;
+ Eina_Bool close_requested;
+ Eina_Bool can_read;
+ Eina_Bool can_write;
+} Efl_Net_Dialer_Websocket_Data;
+
+static void _efl_net_dialer_websocket_job_schedule(Eo *o, Efl_Net_Dialer_Websocket_Data *pd);
+
+static void
+_efl_net_dialer_websocket_send_pending_remove(Efl_Net_Dialer_Websocket_Data *pd)
+{
+ Efl_Net_Dialer_Websocket_Frame *f = pd->send.pending;
+ Eina_Inlist *lst = EINA_INLIST_GET(f);
+
+ lst = eina_inlist_remove(lst, lst);
+ pd->send.pending = EINA_INLIST_CONTAINER_GET(lst, Efl_Net_Dialer_Websocket_Frame);
+
+ free(f);
+ pd->send.offset = 0;
+}
+
+static uint8_t *
+_efl_net_dialer_websocket_frame_bytes_get(Efl_Net_Dialer_Websocket_Frame *f)
+{
+ return (uint8_t *)&f->header;
+}
+
+static uint8_t *
+_efl_net_dialer_websocket_frame_mask_get(Efl_Net_Dialer_Websocket_Frame *f)
+{
+ uint8_t *bytes = _efl_net_dialer_websocket_frame_bytes_get(f)
+ + sizeof(Efl_Net_Dialer_Websocket_Frame_Header);
+
+ if (f->header.payload_len == 127)
+ return bytes + sizeof(uint64_t);
+ else if (f->header.payload_len == 126)
+ return bytes + sizeof(uint16_t);
+ else
+ return bytes;
+}
+
+static uint8_t *
+_efl_net_dialer_websocket_frame_payload_get(Efl_Net_Dialer_Websocket_Frame *f)
+{
+ return _efl_net_dialer_websocket_frame_mask_get(f) + 4;
+}
+
+static Efl_Net_Dialer_Websocket_Frame *
+_efl_net_dialer_websocket_send_pending_add(Eo *o, Efl_Net_Dialer_Websocket_Data *pd, Efl_Net_Dialer_Websocket_Opcode opcode, size_t payload_len)
+{
+ uint8_t *bytes;
+ size_t len = payload_len + 4 /* sizeof mask */;
+ Efl_Net_Dialer_Websocket_Frame *f;
+ Eina_Inlist *lst;
+ int i;
+
+ if (payload_len > UINT16_MAX) len += sizeof(uint64_t);
+ else if (payload_len > 125) len += sizeof(uint16_t);
+
+ f = malloc(sizeof(Efl_Net_Dialer_Websocket_Frame) + len);
+ EINA_SAFETY_ON_NULL_RETURN_VAL(f, NULL);
+
+ f->len = sizeof(Efl_Net_Dialer_Websocket_Frame_Header) + len;
+ f->header.opcode = opcode;
+ f->header.fin = 1;
+ f->header.mask = 1;
+ f->header.payload_len = ((payload_len > UINT16_MAX) ? 127 :
+ ((payload_len > 125) ? 126 : payload_len));
+
+ bytes = _efl_net_dialer_websocket_frame_bytes_get(f)
+ + sizeof(Efl_Net_Dialer_Websocket_Frame_Header);
+
+ if (f->header.payload_len == 127)
+ {
+ uint64_t plen = payload_len;
+#ifndef WORDS_BIGENDIAN
+ plen = eina_swap64(plen);
+#endif
+ memcpy(bytes, &plen, sizeof(plen));
+ bytes += sizeof(plen);
+ }
+ else if (f->header.payload_len == 126)
+ {
+ uint16_t plen = payload_len;
+#ifndef WORDS_BIGENDIAN
+ plen = eina_swap16(plen);
+#endif
+ memcpy(bytes, &plen, sizeof(plen));
+ bytes += sizeof(plen);
+ }
+
+ for (i = 0; i < 4; i++, bytes++)
+ *bytes = rand() & 0xff;
+
+ lst = EINA_INLIST_GET(pd->send.pending);
+ lst = eina_inlist_append(lst, EINA_INLIST_GET(f));
+ pd->send.pending = EINA_INLIST_CONTAINER_GET(lst, Efl_Net_Dialer_Websocket_Frame);
+
+ DBG("opcode=%#x, payload_len=%zd, f=%p len=%zd", opcode, payload_len, f, f->len);
+
+ /* say we can't write to throttle writers until we actually write */
+ efl_io_writer_can_write_set(o, EINA_FALSE);
+
+ if (efl_io_writer_can_write_get(pd->http))
+ _efl_net_dialer_websocket_job_schedule(o, pd);
+
+ return f;
+}
+
+static void
+_efl_net_dialer_websocket_frame_write(Efl_Net_Dialer_Websocket_Frame *f, size_t offset, const void *mem, size_t len)
+{
+ uint8_t *mask = _efl_net_dialer_websocket_frame_mask_get(f);
+ uint8_t *payload = _efl_net_dialer_websocket_frame_payload_get(f);
+ const uint8_t *input = mem;
+ uint8_t *o;
+
+ for (o = payload + offset; o < payload + offset + len; o++, input++)
+ *o = *input ^ mask[(o - payload) & 0x3];
+}
+
+static void
+_efl_net_dialer_websocket_send(Eo *o, Efl_Net_Dialer_Websocket_Data *pd, Efl_Net_Dialer_Websocket_Opcode opcode, const void *mem, size_t len)
+{
+ Efl_Net_Dialer_Websocket_Frame *f;
+
+ f = _efl_net_dialer_websocket_send_pending_add(o, pd, opcode, len);
+ EINA_SAFETY_ON_NULL_RETURN(f);
+
+ _efl_net_dialer_websocket_frame_write(f, 0, mem, len);
+}
+
+static void
+_efl_net_dialer_websocket_job_send(Eo *o, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ Eina_Slice slice = {
+ .bytes = _efl_net_dialer_websocket_frame_bytes_get(pd->send.pending) + pd->send.offset,
+ .len = pd->send.pending->len - pd->send.offset,
+ };
+ Eina_Error err = efl_io_writer_write(pd->http, &slice, NULL);
+
+ pd->send.offset += slice.len;
+
+ if (pd->send.offset == pd->send.pending->len)
+ {
+ _efl_net_dialer_websocket_send_pending_remove(pd);
+ if (!pd->close_requested)
+ efl_io_writer_can_write_set(o, EINA_TRUE);
+ }
+
+ if ((err) && (err != EAGAIN))
+ {
+ ERR("could not write to HTTP socket #%d '%s'", err, eina_error_msg_get(err));
+ efl_event_callback_call(o, EFL_NET_DIALER_EVENT_ERROR, &err);
+ }
+}
+
+static void
+_efl_net_dialer_websocket_recv_frame_steal_and_queue(Eo *o, Efl_Net_Dialer_Websocket_Data *pd, uint8_t **p_payload, size_t len)
+{
+ Efl_Net_Dialer_Websocket_Pending_Read *pr;
+ Eina_Inlist *lst;
+
+ if (len == 0) return;
+
+ pr = malloc(sizeof(Efl_Net_Dialer_Websocket_Pending_Read));
+ EINA_SAFETY_ON_NULL_RETURN(pr);
+
+ pr->len = len;
+ pr->bytes = *p_payload;
+
+ lst = EINA_INLIST_GET(pd->recv.pending_read);
+ lst = eina_inlist_append(lst, EINA_INLIST_GET(pr));
+ pd->recv.pending_read = EINA_INLIST_CONTAINER_GET(lst, Efl_Net_Dialer_Websocket_Pending_Read);
+
+ *p_payload = NULL;
+
+ efl_io_reader_can_read_set(o, EINA_TRUE);
+}
+
+#define _efl_net_dialer_websocket_close_protocol_error(o, message) \
+ do \
+ { \
+ if (!efl_io_closer_closed_get(o)) \
+ { \
+ WRN("dialer=%p closing with PROTOCOL ERROR (%d)", o, EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_PROTOCOL_ERROR); \
+ efl_net_dialer_websocket_close_request(o, EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_PROTOCOL_ERROR, message); \
+ } \
+ } \
+ while (0)
+
+static Eina_Bool
+_efl_net_dialer_websocket_job_dispatch_frame_validate(Eo *o, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ if ((pd->close_requested) && (pd->recv.current.opcode != EFL_NET_DIALER_WEBSOCKET_OPCODE_CLOSE))
+ {
+ DBG("dialer=%p dropped frame opcode=%#x: close requested", o, pd->recv.current.opcode);
+ return EINA_FALSE;
+ }
+
+ if ((!pd->recv.current.fin) &&
+ _efl_net_dialer_websocket_opcode_control_check(pd->recv.current.opcode))
+ WRN("dialer=%p server sent forbidden fragmented control frame opcode=%#x.",
+ o, pd->recv.current.opcode);
+ else if ((pd->recv.current.opcode == EFL_NET_DIALER_WEBSOCKET_OPCODE_CONTINUATION) &&
+ (pd->recv.fragmented.opcode == 0))
+ WRN("dialer=%p server sent continuation frame after non-fragmentable frame", o);
+ else
+ return EINA_TRUE;
+
+ _efl_net_dialer_websocket_close_protocol_error(o, NULL);
+ return EINA_FALSE;
+}
+
+static void
+_efl_net_dialer_websocket_job_dispatch_frame_text(Eo *o, Efl_Net_Dialer_Websocket_Data *pd, uint8_t **p_payload, size_t len)
+{
+ char *text = (char *)*p_payload;
+
+ /* len == 0 may contain an old payload to be reused with realloc() */
+ if (len == 0) text = "";
+
+ efl_event_callback_call(o, EFL_NET_DIALER_WEBSOCKET_EVENT_MESSAGE_TEXT, text);
+ if (pd->streaming_mode == EFL_NET_DIALER_WEBSOCKET_STREAMING_MODE_TEXT)
+ _efl_net_dialer_websocket_recv_frame_steal_and_queue(o, pd, p_payload, len);
+}
+
+static void
+_efl_net_dialer_websocket_job_dispatch_frame_binary(Eo *o, Efl_Net_Dialer_Websocket_Data *pd, uint8_t **p_payload, size_t len)
+{
+ Eina_Slice slice = {
+ .bytes = *p_payload,
+ .len = len,
+ };
+ efl_event_callback_call(o, EFL_NET_DIALER_WEBSOCKET_EVENT_MESSAGE_BINARY, &slice);
+ if (pd->streaming_mode == EFL_NET_DIALER_WEBSOCKET_STREAMING_MODE_BINARY)
+ _efl_net_dialer_websocket_recv_frame_steal_and_queue(o, pd, p_payload, len);
+}
+
+static void
+_efl_net_dialer_websocket_job_dispatch_frame(Eo *o, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ DBG("frame opcode=%#x fin=%d, length=%" PRIu64,
+ pd->recv.current.opcode, pd->recv.current.fin, pd->recv.current.used_len);
+
+ if (!_efl_net_dialer_websocket_job_dispatch_frame_validate(o, pd))
+ return;
+
+ switch (pd->recv.current.opcode)
+ {
+ case EFL_NET_DIALER_WEBSOCKET_OPCODE_CONTINUATION:
+ if (pd->recv.current.fin)
+ {
+ if (pd->recv.fragmented.opcode == EFL_NET_DIALER_WEBSOCKET_OPCODE_TEXT)
+ _efl_net_dialer_websocket_job_dispatch_frame_text(o, pd, &pd->recv.current.payload, pd->recv.current.used_len);
+ else if (pd->recv.fragmented.opcode == EFL_NET_DIALER_WEBSOCKET_OPCODE_BINARY)
+ _efl_net_dialer_websocket_job_dispatch_frame_binary(o, pd, &pd->recv.current.payload, pd->recv.current.used_len);
+
+ memset(&pd->recv.fragmented, 0, sizeof(pd->recv.fragmented));
+ }
+ else
+ {
+ pd->recv.fragmented.payload = pd->recv.current.payload;
+ pd->recv.fragmented.used_len = pd->recv.current.used_len;
+ pd->recv.fragmented.total_len = pd->recv.current.total_len;
+ pd->recv.current.payload = NULL;
+ pd->recv.current.used_len = 0;
+ pd->recv.current.total_len = 0;
+ }
+ break;
+
+ case EFL_NET_DIALER_WEBSOCKET_OPCODE_TEXT:
+ if (pd->recv.current.fin)
+ _efl_net_dialer_websocket_job_dispatch_frame_text(o, pd, &pd->recv.current.payload, pd->recv.current.used_len);
+ else
+ {
+ pd->recv.fragmented.payload = pd->recv.current.payload;
+ pd->recv.fragmented.used_len = pd->recv.current.used_len;
+ pd->recv.fragmented.total_len = pd->recv.current.total_len;
+ pd->recv.fragmented.opcode = pd->recv.current.opcode;
+
+ pd->recv.current.payload = NULL;
+ pd->recv.current.used_len = 0;
+ pd->recv.current.total_len = 0;
+ pd->recv.current.opcode = 0;
+ pd->recv.current.fin = 0;
+ }
+ break;
+
+ case EFL_NET_DIALER_WEBSOCKET_OPCODE_BINARY:
+ if (pd->recv.current.fin)
+ _efl_net_dialer_websocket_job_dispatch_frame_binary(o, pd, &pd->recv.current.payload, pd->recv.current.used_len);
+ else
+ {
+ pd->recv.fragmented.payload = pd->recv.current.payload;
+ pd->recv.fragmented.used_len = pd->recv.current.used_len;
+ pd->recv.fragmented.total_len = pd->recv.current.total_len;
+ pd->recv.fragmented.opcode = pd->recv.current.opcode;
+
+ pd->recv.current.payload = NULL;
+ pd->recv.current.used_len = 0;
+ pd->recv.current.total_len = 0;
+ pd->recv.current.opcode = 0;
+ pd->recv.current.fin = 0;
+ }
+ break;
+
+ case EFL_NET_DIALER_WEBSOCKET_OPCODE_CLOSE:
+ {
+ Efl_Net_Dialer_Websocket_Closed_Reason reason = {
+ .reason = EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_NO_REASON,
+ .message = "",
+ };
+
+ if (pd->recv.current.used_len >= sizeof(uint16_t))
+ {
+ uint16_t r;
+ memcpy(&r, pd->recv.current.payload, sizeof(uint16_t));
+#ifndef WORDS_BIGENDIAN
+ r = eina_swap16(r);
+#endif
+ if (!_efl_net_dialer_websocket_close_reason_check(r))
+ {
+ WRN("dialer=%p invalid CLOSE reason: %hu", o, r);
+ _efl_net_dialer_websocket_close_protocol_error(o, "invalid close reason");
+ r = EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_PROTOCOL_ERROR;
+ }
+ reason.reason = r;
+ reason.message = (const char *)pd->recv.current.payload + sizeof(uint16_t);
+ }
+ else if ((pd->recv.current.used_len > 0) &&
+ (pd->recv.current.used_len < sizeof(uint16_t)))
+ {
+ WRN("dialer=%p invalid CLOSE payload length: %" PRIu64, o, pd->recv.current.used_len);
+ _efl_net_dialer_websocket_close_protocol_error(o, "invalid close payload length");
+ }
+
+ efl_event_callback_call(o, EFL_NET_DIALER_WEBSOCKET_EVENT_CLOSED_REASON, &reason);
+ if ((!pd->recv.pending_read) &&
+ (pd->streaming_mode != EFL_NET_DIALER_WEBSOCKET_STREAMING_MODE_DISABLED))
+ efl_event_callback_call(o, EFL_IO_READER_EVENT_EOS, NULL);
+
+ if (!pd->close_requested)
+ efl_io_closer_close(o);
+ else
+ efl_event_callback_call(o, EFL_IO_CLOSER_EVENT_CLOSED, NULL);
+ if (pd->close_timer)
+ {
+ efl_del(pd->close_timer);
+ pd->close_timer = NULL;
+ }
+ break;
+ }
+
+ case EFL_NET_DIALER_WEBSOCKET_OPCODE_PING:
+ DBG("got PING from server '%.*s', echo as PONG.", (int)pd->recv.current.used_len, pd->recv.current.payload);
+ _efl_net_dialer_websocket_send(o, pd, EFL_NET_DIALER_WEBSOCKET_OPCODE_PONG, pd->recv.current.payload, pd->recv.current.used_len);
+ break;
+
+ case EFL_NET_DIALER_WEBSOCKET_OPCODE_PONG:
+ {
+ char *text = (char *)pd->recv.current.payload;
+ if (pd->recv.current.used_len == 0) text = "";
+ efl_event_callback_call(o, EFL_NET_DIALER_WEBSOCKET_EVENT_PONG, text);
+ break;
+ }
+
+ default:
+ WRN("dialer=%p unexpected WebSocket opcode: %#x.", o, pd->recv.current.opcode);
+ _efl_net_dialer_websocket_close_protocol_error(o, "unexpected opcode");
+ }
+}
+
+static void
+_efl_net_dialer_websocket_job_receive(Eo *o, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ const uint8_t fh_len = sizeof(Efl_Net_Dialer_Websocket_Frame_Header);
+ Eina_Error err;
+
+ while ((pd->recv.done < pd->recv.needed) &&
+ (efl_io_reader_can_read_get(pd->http)))
+ {
+ uint64_t frame_len;
+ Eina_Rw_Slice rw_slice = {
+ .bytes = pd->recv.tmpbuf + pd->recv.done,
+ .len = pd->recv.needed - pd->recv.done,
+ };
+
+ err = efl_io_reader_read(pd->http, &rw_slice);
+ EINA_SAFETY_ON_TRUE_GOTO(err != 0, error);
+ pd->recv.done += rw_slice.len;
+
+ if (pd->recv.done != pd->recv.needed)
+ continue;
+
+ if (pd->recv.needed == fh_len)
+ {
+ Efl_Net_Dialer_Websocket_Frame_Header fh;
+ memcpy(&fh, pd->recv.tmpbuf, pd->recv.needed);
+
+ pd->recv.current.opcode = fh.opcode;
+ pd->recv.current.fin = fh.fin;
+
+ if (fh._reserved)
+ {
+ WRN("dialer=%p server sent reserved bits %#x, opcode=%#x, but no extension was negotiated.",
+ o, fh._reserved, fh.opcode);
+ _efl_net_dialer_websocket_close_protocol_error(o, "reserved bits not negotiated");
+ }
+
+ if (fh.mask)
+ {
+ WRN("dialer=%p server masked frame opcode=%#x.", o, fh.opcode);
+ _efl_net_dialer_websocket_close_protocol_error(o, "server sent masked frame");
+ }
+
+ if (fh.payload_len == 127)
+ {
+ if (_efl_net_dialer_websocket_opcode_control_check(fh.opcode))
+ {
+ WRN("dialer=%p server sent forbidden 64-bit control frame %#x.", o, fh.opcode);
+ _efl_net_dialer_websocket_close_protocol_error(o, "control frames are limited to 125 bytes");
+ }
+ pd->recv.needed += sizeof(uint64_t);
+ continue;
+ }
+ else if (fh.payload_len == 126)
+ {
+ if (_efl_net_dialer_websocket_opcode_control_check(fh.opcode))
+ {
+ WRN("dialer=%p server sent forbidden 16-bit control frame %#x.", o, fh.opcode);
+ _efl_net_dialer_websocket_close_protocol_error(o, "control frames are limited to 125 bytes");
+ }
+ pd->recv.needed += sizeof(uint16_t);
+ continue;
+ }
+ else
+ frame_len = fh.payload_len;
+ }
+ else if (pd->recv.needed == fh_len + sizeof(uint64_t))
+ {
+ uint64_t len;
+
+ memcpy(&len, pd->recv.tmpbuf + fh_len, sizeof(len));
+#ifndef WORDS_BIGENDIAN
+ len = eina_swap64(len);
+#endif
+ frame_len = len;
+ }
+ else if (pd->recv.needed == fh_len + sizeof(uint16_t))
+ {
+ uint16_t len;
+
+ memcpy(&len, pd->recv.tmpbuf + fh_len, sizeof(len));
+#ifndef WORDS_BIGENDIAN
+ len = eina_swap16(len);
+#endif
+ frame_len = len;
+ }
+ else
+ {
+ CRI("shouldn't reach done=%u, needed=%u", pd->recv.done, pd->recv.needed);
+ frame_len = 0;
+ }
+
+ if (pd->recv.current.opcode == EFL_NET_DIALER_WEBSOCKET_OPCODE_CONTINUATION)
+ {
+ if (pd->recv.fragmented.opcode == 0)
+ {
+ WRN("dialer=%p server sent continuation frame after non-fragmentable frame", o);
+ _efl_net_dialer_websocket_close_protocol_error(o, "nothing to continue");
+ }
+ if (pd->recv.current.payload)
+ free(pd->recv.current.payload);
+ pd->recv.current.payload = pd->recv.fragmented.payload;
+ pd->recv.current.used_len = pd->recv.fragmented.used_len;
+ pd->recv.current.total_len = pd->recv.fragmented.total_len;
+ pd->recv.fragmented.payload = NULL;
+ pd->recv.fragmented.used_len = 0;
+ pd->recv.fragmented.total_len = 0;
+ }
+ else if ((!_efl_net_dialer_websocket_opcode_control_check(pd->recv.current.opcode)) &&
+ (pd->recv.fragmented.opcode != 0))
+ {
+ WRN("dialer=%p server sent non-control frame opcode=%#x while continuation was expected", o, pd->recv.current.opcode);
+ _efl_net_dialer_websocket_close_protocol_error(o, "expected continuation or control frames");
+ }
+
+ if (frame_len > 0)
+ {
+ void *tmp = realloc(pd->recv.current.payload, pd->recv.current.total_len + frame_len + 1);
+ err = errno;
+ EINA_SAFETY_ON_NULL_GOTO(tmp, error);
+ pd->recv.current.payload = tmp;
+ pd->recv.current.total_len += frame_len;
+ }
+ }
+
+ if ((!efl_io_reader_can_read_get(pd->http)) &&
+ (pd->recv.done < pd->recv.needed))
+ return; /* more header to do */
+
+ while ((pd->recv.current.used_len < pd->recv.current.total_len) &&
+ (efl_io_reader_can_read_get(pd->http)))
+ {
+ Eina_Rw_Slice rw_slice = {
+ .bytes = pd->recv.current.payload + pd->recv.current.used_len,
+ .len = pd->recv.current.total_len - pd->recv.current.used_len,
+ };
+
+ err = efl_io_reader_read(pd->http, &rw_slice);
+ EINA_SAFETY_ON_TRUE_GOTO(err != 0, error);
+ pd->recv.current.used_len += rw_slice.len;
+ }
+ if (pd->recv.current.total_len > 0)
+ pd->recv.current.payload[pd->recv.current.used_len] = '\0';
+
+ if ((!efl_io_reader_can_read_get(pd->http)) &&
+ (pd->recv.current.used_len < pd->recv.current.total_len))
+ return; /* more payload to do */
+
+ _efl_net_dialer_websocket_job_dispatch_frame(o, pd);
+ pd->recv.done = 0;
+ pd->recv.needed = sizeof(Efl_Net_Dialer_Websocket_Frame_Header);
+ /* no need to free payload here, let realloc() reuse it */
+ pd->recv.current.used_len = 0;
+ pd->recv.current.total_len = 0;
+ return;
+
+ error:
+ ERR("could not receive data header=%u/%u, payload=%" PRIu64 "/%" PRIu64 ": %s",
+ pd->recv.done, pd->recv.needed,
+ pd->recv.current.used_len, pd->recv.current.total_len,
+ eina_error_msg_get(err));
+ efl_event_callback_call(o, EFL_NET_DIALER_EVENT_ERROR, &err);
+}
+
+static void
+_efl_net_dialer_websocket_job(void *data, void *value EINA_UNUSED)
+{
+ Eo *o = data;
+ Efl_Net_Dialer_Websocket_Data *pd = efl_data_scope_get(o, MY_CLASS);
+
+ pd->job = NULL;
+
+ efl_ref(o);
+
+ if (efl_io_reader_can_read_get(pd->http))
+ _efl_net_dialer_websocket_job_receive(o, pd);
+
+ if (efl_io_writer_can_write_get(pd->http) && pd->send.pending)
+ _efl_net_dialer_websocket_job_send(o, pd);
+
+ if (efl_io_reader_can_read_get(pd->http))
+ _efl_net_dialer_websocket_job_schedule(o, pd);
+
+ efl_unref(o);
+}
+
+static void
+_efl_net_dialer_websocket_job_schedule(Eo *o, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ Eo *loop;
+
+ if (pd->job) return;
+
+ loop = efl_loop_user_loop_get(o);
+ if (!loop) return;
+ pd->job = efl_loop_job(loop, o);
+ eina_promise_then(pd->job, _efl_net_dialer_websocket_job, NULL, o);
+}
+
+static void
+_efl_net_dialer_websocket_http_can_read_changed(void *data, const Efl_Event *event EINA_UNUSED)
+{
+ Eo *o = data;
+ Efl_Net_Dialer_Websocket_Data *pd = efl_data_scope_get(o, MY_CLASS);
+ if (efl_io_reader_can_read_get(pd->http))
+ _efl_net_dialer_websocket_job_schedule(o, pd);
+}
+
+static void
+_efl_net_dialer_websocket_http_can_write_changed(void *data, const Efl_Event *event EINA_UNUSED)
+{
+ Eo *o = data;
+ Efl_Net_Dialer_Websocket_Data *pd = efl_data_scope_get(o, MY_CLASS);
+ if (efl_io_writer_can_write_get(pd->http))
+ _efl_net_dialer_websocket_job_schedule(o, pd);
+}
+
+static void
+_efl_net_dialer_websocket_http_closed(void *data, const Efl_Event *event EINA_UNUSED)
+{
+ Eo *o = data;
+ Efl_Net_Dialer_Websocket_Data *pd = efl_data_scope_get(o, MY_CLASS);
+ if (!pd->close_requested)
+ {
+ Efl_Net_Dialer_Websocket_Closed_Reason reason = {
+ .reason = EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_ABRUPTLY,
+ .message = "",
+ };
+ pd->close_requested = EINA_TRUE;
+ efl_event_callback_call(o, EFL_NET_DIALER_WEBSOCKET_EVENT_CLOSED_REASON, &reason);
+ }
+ efl_event_callback_call(o, EFL_IO_CLOSER_EVENT_CLOSED, NULL);
+}
+
+static void
+_efl_net_dialer_websocket_http_error(void *data, const Efl_Event *event)
+{
+ Eo *o = data;
+ Efl_Net_Dialer_Websocket_Data *pd = efl_data_scope_get(o, MY_CLASS);
+ Eina_Error *perr = event->info;
+ if ((pd->close_requested) && (*perr == EFL_NET_HTTP_ERROR_RECV_ERROR))
+ return;
+ efl_event_callback_call(o, EFL_NET_DIALER_EVENT_ERROR, perr);
+}
+
+static void
+_efl_net_dialer_websocket_http_headers_done(void *data, const Efl_Event *event EINA_UNUSED)
+{
+ Eo *o = data;
+ Efl_Net_Dialer_Websocket_Data *pd = efl_data_scope_get(o, MY_CLASS);
+ Eina_Bool accepted = EINA_FALSE;
+ Eina_Bool upgraded = EINA_FALSE;
+ Eina_Bool connection_websocket = EINA_FALSE;
+ Efl_Net_Http_Status status;
+ const Efl_Net_Http_Header *header;
+ Eina_Stringshare *str;
+ Eina_Iterator *itr;
+
+ status = efl_net_dialer_http_response_status_get(pd->http);
+ if (status != EFL_NET_HTTP_STATUS_SWITCHING_PROTOCOLS)
+ {
+ Eina_Error err = EFL_NET_HTTP_ERROR_COULDNT_CONNECT;
+ if ((status >= 300) && (status < 400))
+ return;
+ WRN("failed WebSocket handshake: HTTP status=%d, expected=%d",
+ status, EFL_NET_HTTP_STATUS_SWITCHING_PROTOCOLS);
+ efl_event_callback_call(o, EFL_NET_DIALER_EVENT_ERROR, &err);
+ return;
+ }
+
+ EINA_LIST_FREE(pd->protocols.received, str)
+ eina_stringshare_del(str);
+
+ itr = efl_net_dialer_http_response_headers_get(pd->http);
+ EINA_ITERATOR_FOREACH(itr, header)
+ {
+ if (!header->key) continue;
+ if (strcasecmp(header->key, "Connection") == 0)
+ upgraded = strcasecmp(header->value, "upgrade") == 0;
+ else if (strcasecmp(header->key, "Upgrade") == 0)
+ connection_websocket = strcasecmp(header->value, "websocket") == 0;
+ else if (strcasecmp(header->key, "Sec-WebSocket-Accept") == 0)
+ accepted = strcmp(header->value, pd->accept_key) == 0; /* case matters */
+ else if (strcasecmp(header->key, "Sec-WebSocket-Protocol") == 0)
+ {
+ char **strv = eina_str_split(header->value, ",", 0);
+ if (strv)
+ {
+ int i;
+ for (i = 0; strv[i] != NULL; i++)
+ pd->protocols.received = eina_list_append(pd->protocols.received, eina_stringshare_add(strv[i]));
+ free(strv[0]);
+ free(strv);
+ }
+ }
+ }
+ eina_iterator_free(itr);
+ efl_net_dialer_http_response_headers_clear(pd->http);
+
+ if ((!upgraded) || (!connection_websocket) || (!accepted))
+ {
+ Eina_Error err = EFL_NET_HTTP_ERROR_COULDNT_CONNECT;
+ WRN("failed WebSocket handshake: upgraded=%d, connection_websocket=%d, accepted=%d",
+ upgraded, connection_websocket, accepted);
+ efl_event_callback_call(o, EFL_NET_DIALER_EVENT_ERROR, &err);
+ return;
+ }
+
+ str = efl_net_socket_address_remote_get(pd->http);
+ if (str)
+ {
+ char *tmp = NULL;
+ /* if ws:// or wss:// were used, rewrite-back */
+ if (strncasecmp(pd->address_dial, "ws://", strlen("ws://")) == 0)
+ {
+ tmp = malloc(strlen(str) + strlen("ws://") - strlen("http://") + 1);
+ if (tmp)
+ {
+ memcpy(tmp, "ws://", strlen("ws://"));
+ memcpy(tmp + strlen("ws://"),
+ str + strlen("http://"),
+ strlen(str) - strlen("http://") + 1);
+ str = tmp;
+ }
+ }
+ else if (strncasecmp(pd->address_dial, "wss://", strlen("wss://")) == 0)
+ {
+ tmp = malloc(strlen(str) + strlen("wss://") - strlen("https://") + 1);
+ if (tmp)
+ {
+ memcpy(tmp, "wss://", strlen("wss://"));
+ memcpy(tmp + strlen("wss://"),
+ str + strlen("https://"),
+ strlen(str) - strlen("https://") + 1);
+ str = tmp;
+ }
+ }
+
+ efl_net_socket_address_remote_set(o, str);
+ free(tmp);
+ }
+
+ efl_net_dialer_connected_set(o, EINA_TRUE);
+ efl_io_writer_can_write_set(o, EINA_TRUE);
+}
+
+EFL_CALLBACKS_ARRAY_DEFINE(_efl_net_dialer_websocket_http_cbs,
+ {EFL_IO_READER_EVENT_CAN_READ_CHANGED, _efl_net_dialer_websocket_http_can_read_changed},
+ {EFL_IO_WRITER_EVENT_CAN_WRITE_CHANGED, _efl_net_dialer_websocket_http_can_write_changed},
+ {EFL_IO_CLOSER_EVENT_CLOSED, _efl_net_dialer_websocket_http_closed},
+ {EFL_NET_DIALER_EVENT_ERROR, _efl_net_dialer_websocket_http_error},
+ {EFL_NET_DIALER_HTTP_EVENT_HEADERS_DONE, _efl_net_dialer_websocket_http_headers_done});
+
+EOLIAN static Efl_Object *
+_efl_net_dialer_websocket_efl_object_constructor(Eo *o, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ pd->recv.needed = sizeof(Efl_Net_Dialer_Websocket_Frame_Header);
+
+ o = efl_constructor(efl_super(o, MY_CLASS));
+ EINA_SAFETY_ON_NULL_RETURN_VAL(o, NULL);
+
+ pd->http = efl_add(EFL_NET_DIALER_HTTP_CLASS, o,
+ efl_net_dialer_http_version_set(efl_self, EFL_NET_HTTP_VERSION_V1_1),
+ efl_net_dialer_http_method_set(efl_self, "GET"),
+ efl_net_dialer_http_primary_mode_set(efl_self, EFL_NET_DIALER_HTTP_PRIMARY_MODE_UPLOAD),
+ efl_event_callback_array_add(efl_self, _efl_net_dialer_websocket_http_cbs(), o));
+ EINA_SAFETY_ON_NULL_RETURN_VAL(pd->http, NULL);
+
+ return o;
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_efl_object_destructor(Eo *o, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ Eina_Stringshare *str;
+
+ efl_event_callback_array_del(pd->http, _efl_net_dialer_websocket_http_cbs(), o);
+ if (pd->close_timer)
+ {
+ efl_del(pd->close_timer);
+ pd->close_timer = NULL;
+ }
+
+ if (!efl_io_closer_closed_get(pd->http))
+ efl_io_closer_close(pd->http);
+ efl_del(pd->http);
+ pd->http = NULL;
+
+ if (pd->job)
+ {
+ eina_promise_cancel(pd->job);
+ pd->job = NULL;
+ }
+
+ efl_destructor(efl_super(o, MY_CLASS));
+
+ eina_stringshare_replace(&pd->address_dial, NULL);
+ eina_stringshare_replace(&pd->address_remote, NULL);
+ EINA_LIST_FREE(pd->protocols.requested, str)
+ eina_stringshare_del(str);
+ EINA_LIST_FREE(pd->protocols.received, str)
+ eina_stringshare_del(str);
+
+ free(pd->recv.current.payload);
+ pd->recv.current.payload = NULL;
+ free(pd->recv.fragmented.payload);
+ pd->recv.fragmented.payload = NULL;
+
+ while (pd->send.pending)
+ _efl_net_dialer_websocket_send_pending_remove(pd);
+}
+
+static void
+_efl_net_dialer_websocket_protocols_add(Efl_Net_Dialer_Websocket_Data *pd)
+{
+ const Eina_List *lst;
+ Eina_Stringshare *str;
+ char *protocols;
+ size_t len = 0;
+ size_t offset = 0;
+
+ EINA_LIST_FOREACH(pd->protocols.requested, lst, str)
+ {
+ if (len) len += strlen(", ");
+ len += eina_stringshare_strlen(str);
+ }
+
+ if (len == 0) return;
+ protocols = malloc(len + 1);
+ EINA_SAFETY_ON_NULL_RETURN(protocols);
+ EINA_LIST_FOREACH(pd->protocols.requested, lst, str)
+ {
+ if (offset)
+ {
+ memcpy(protocols + offset, ", ", strlen(", "));
+ offset += strlen(", ");
+ }
+ memcpy(protocols + offset, str, eina_stringshare_strlen(str));
+ offset += eina_stringshare_strlen(str);
+ }
+ protocols[offset] = '\0';
+
+ efl_net_dialer_http_request_header_add(pd->http, "Sec-WebSocket-Protocol", protocols);
+ free(protocols);
+}
+
+static void
+_efl_net_dialer_websocket_key_add(Efl_Net_Dialer_Websocket_Data *pd)
+{
+ Eina_Binbuf *binbuf_key;
+ Eina_Strbuf *strbuf_key_base64;
+ uint8_t key[16];
+ const Eina_Slice guid_slice = EINA_SLICE_STR_LITERAL("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
+ Eina_Slice ro_slice;
+ Eina_Rw_Slice rw_slice;
+ uint8_t sha1hash[20];
+ Eina_Bool ret;
+ size_t i;
+
+ for (i = 0; i < sizeof(key); i++)
+ key[i] = rand() & 0xff;
+
+ binbuf_key = eina_binbuf_manage_new(key, sizeof(key), EINA_TRUE);
+ EINA_SAFETY_ON_NULL_RETURN(binbuf_key);
+
+ strbuf_key_base64 = emile_base64_encode(binbuf_key);
+ eina_binbuf_free(binbuf_key);
+
+ EINA_SAFETY_ON_NULL_RETURN(strbuf_key_base64);
+
+ efl_net_dialer_http_request_header_add(pd->http, "Sec-WebSocket-Key", eina_strbuf_string_get(strbuf_key_base64));
+
+ /* accept_key = base64(sha1(base64(random(16)) + guid)) */
+ /* 1) base64(random(16)) + guid */
+ eina_strbuf_append_slice(strbuf_key_base64, guid_slice);
+
+ /* emile_binbuf_sha1() operates on binbuf, convert */
+ ro_slice = eina_strbuf_slice_get(strbuf_key_base64);
+ binbuf_key = eina_binbuf_manage_new(ro_slice.mem, ro_slice.len, EINA_TRUE);
+ EINA_SAFETY_ON_NULL_GOTO(binbuf_key, free_strbuf);
+ /* 2) sha1(base64(random(16)) + guid) */
+ ret = emile_binbuf_sha1(binbuf_key, sha1hash);
+ eina_binbuf_free(binbuf_key);
+ eina_strbuf_free(strbuf_key_base64);
+
+ EINA_SAFETY_ON_FALSE_RETURN(ret);
+
+ /* 3) base64(sha1(base64(random(16)) + guid)) */
+ binbuf_key = eina_binbuf_manage_new(sha1hash, sizeof(sha1hash), EINA_TRUE);
+ EINA_SAFETY_ON_NULL_RETURN(binbuf_key);
+
+ strbuf_key_base64 = emile_base64_encode(binbuf_key);
+ eina_binbuf_free(binbuf_key);
+ EINA_SAFETY_ON_NULL_RETURN(strbuf_key_base64);
+
+ ro_slice = eina_strbuf_slice_get(strbuf_key_base64);
+
+ rw_slice.mem = pd->accept_key;
+ rw_slice.len = sizeof(pd->accept_key) - 1;
+ rw_slice = eina_rw_slice_copy(rw_slice, ro_slice);
+ rw_slice.bytes[rw_slice.len] = '\0';
+ eina_strbuf_free(strbuf_key_base64);
+
+ EINA_SAFETY_ON_TRUE_RETURN(rw_slice.len < sizeof(pd->accept_key) - 1);
+ return;
+
+ free_strbuf:
+ eina_strbuf_free(strbuf_key_base64);
+}
+
+static void
+_efl_net_dialer_websocket_request_headers_websocket_add(Efl_Net_Dialer_Websocket_Data *pd)
+{
+ efl_net_dialer_http_request_header_add(pd->http, "Upgrade", "websocket");
+ efl_net_dialer_http_request_header_add(pd->http, "Connection", "Upgrade");
+ efl_net_dialer_http_request_header_add(pd->http, "Expect", "101");
+ efl_net_dialer_http_request_header_add(pd->http, "Transfer-Encoding", "");
+ efl_net_dialer_http_request_header_add(pd->http, "Sec-WebSocket-Version", "13");
+
+ _efl_net_dialer_websocket_protocols_add(pd);
+ _efl_net_dialer_websocket_key_add(pd);
+}
+
+EOLIAN static Eina_Error
+_efl_net_dialer_websocket_efl_net_dialer_dial(Eo *o, Efl_Net_Dialer_Websocket_Data *pd, const char *address)
+{
+ const char *http_url = address;
+ char *tmp = NULL;
+ Eina_Error err;
+
+ EINA_SAFETY_ON_NULL_RETURN_VAL(address, EINVAL);
+
+ if (_c_init())
+ {
+ curl_version_info_data *cver = _c->curl_version_info(CURLVERSION_FOURTH);
+ static Eina_Bool did_once = EINA_FALSE;
+ /*
+ * CURL 7.50.2 fixed bug
+ * https://github.com/curl/curl/pull/899 that prevented
+ * WebSocket with initial small frames (ie: 2 bytes) to
+ * be processed, this was clear with autobahn test suite
+ * Case 1.1.1
+ *
+ * commit 7bda07b0466a192e082f32d363d1b1ce1881d483
+ * Author: Michael Kaufmann <mail@michael-kaufmann.ch>
+ * Date: Tue Jun 28 10:57:30 2016 +0200
+ *
+ * HTTP: stop parsing headers when switching to unknown protocols
+ *
+ * - unknown protocols probably won't send more headers (e.g. WebSocket)
+ * - improved comments and moved them to the correct case statements
+ *
+ * Closes #899
+ */
+ if ((!did_once) && (cver) && (cver->version_num < 0x073202))
+ {
+ WRN("Your version of CURL='%s' is too old. >=7.50.2 is required for WebSockets to work properly", cver->version);
+ did_once = EINA_TRUE;
+ }
+ }
+
+ efl_net_dialer_address_dial_set(o, address);
+
+ /* rewrite ws:// -> http://, wss:// -> https:// */
+ if (strncasecmp(address, "ws://", strlen("ws://")) == 0) {
+ tmp = malloc(strlen(address) + strlen("http://") - strlen("ws://") + 1);
+ EINA_SAFETY_ON_NULL_RETURN_VAL(tmp, ENOMEM);
+ memcpy(tmp, "http://", strlen("http://"));
+ memcpy(tmp + strlen("http://"),
+ address + strlen("ws://"),
+ strlen(address) - strlen("ws://") + 1);
+ http_url = tmp;
+ } else if (strncasecmp(address, "wss://", strlen("wss://")) == 0) {
+ tmp = malloc(strlen(address) + strlen("https://") - strlen("wss://") + 1);
+ EINA_SAFETY_ON_NULL_RETURN_VAL(tmp, ENOMEM);
+ memcpy(tmp, "https://", strlen("https://"));
+ memcpy(tmp + strlen("https://"),
+ address + strlen("wss://"),
+ strlen(address) - strlen("wss://") + 1);
+ http_url = tmp;
+ }
+
+ _efl_net_dialer_websocket_request_headers_websocket_add(pd);
+ err = efl_net_dialer_dial(pd->http, http_url);
+ free(tmp);
+ return err;
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_efl_net_dialer_address_dial_set(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd, const char *address)
+{
+ eina_stringshare_replace(&pd->address_dial, address);
+}
+
+EOLIAN static const char *
+_efl_net_dialer_websocket_efl_net_dialer_address_dial_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ return pd->address_dial;
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_efl_net_dialer_connected_set(Eo *o, Efl_Net_Dialer_Websocket_Data *pd, Eina_Bool connected)
+{
+ if (pd->connected == connected) return;
+ pd->connected = connected;
+ if (connected)
+ efl_event_callback_call(o, EFL_NET_DIALER_EVENT_CONNECTED, NULL);
+}
+
+EOLIAN static Eina_Bool
+_efl_net_dialer_websocket_efl_net_dialer_connected_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ return pd->connected;
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_efl_net_dialer_proxy_set(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd, const char *proxy_url)
+{
+ efl_net_dialer_proxy_set(pd->http, proxy_url);
+}
+
+EOLIAN static const char *
+_efl_net_dialer_websocket_efl_net_dialer_proxy_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ return efl_net_dialer_proxy_get(pd->http);
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_efl_net_dialer_timeout_dial_set(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd, double seconds)
+{
+ efl_net_dialer_timeout_dial_set(pd->http, seconds);
+}
+
+EOLIAN static double
+_efl_net_dialer_websocket_efl_net_dialer_timeout_dial_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ return efl_net_dialer_timeout_dial_get(pd->http);
+}
+
+EOLIAN static const char *
+_efl_net_dialer_websocket_efl_net_socket_address_local_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ return efl_net_socket_address_local_get(pd->http);
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_efl_net_socket_address_remote_set(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd, const char *address)
+{
+ if (eina_stringshare_replace(&pd->address_remote, address))
+ efl_event_callback_call(o, EFL_NET_DIALER_EVENT_RESOLVED, NULL);
+}
+
+EOLIAN static const char *
+_efl_net_dialer_websocket_efl_net_socket_address_remote_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ return pd->address_remote;
+}
+
+static void
+_efl_net_dialer_websocket_recv_pending_read_remove(Eo *o, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ Efl_Net_Dialer_Websocket_Pending_Read *pr = pd->recv.pending_read;
+ Eina_Inlist *lst = EINA_INLIST_GET(pr);
+
+ lst = eina_inlist_remove(lst, lst);
+ pd->recv.pending_read = EINA_INLIST_CONTAINER_GET(lst, Efl_Net_Dialer_Websocket_Pending_Read);
+
+ free(pr->bytes);
+ free(pr);
+
+ pd->recv.pending_read_offset = 0;
+ efl_io_reader_can_read_set(o, !!pd->recv.pending_read);
+}
+
+EOLIAN static Eina_Error
+_efl_net_dialer_websocket_efl_io_reader_read(Eo *o, Efl_Net_Dialer_Websocket_Data *pd, Eina_Rw_Slice *rw_slice)
+{
+ size_t todo;
+
+ EINA_SAFETY_ON_NULL_RETURN_VAL(rw_slice, EINVAL);
+
+ if (!pd->recv.pending_read) return EAGAIN;
+
+ todo = rw_slice->len;
+ while ((pd->recv.pending_read) && (todo > 0))
+ {
+ Eina_Slice src = {
+ .bytes = pd->recv.pending_read->bytes + pd->recv.pending_read_offset,
+ .len = pd->recv.pending_read->len - pd->recv.pending_read_offset,
+ };
+ Eina_Rw_Slice dst = {
+ .bytes = rw_slice->bytes + rw_slice->len - todo,
+ .len = todo,
+ };
+ dst = eina_rw_slice_copy(dst, src);
+ todo -= dst.len;
+ pd->recv.pending_read_offset += dst.len;
+ if (pd->recv.pending_read_offset == pd->recv.pending_read->len)
+ _efl_net_dialer_websocket_recv_pending_read_remove(o, pd);
+ }
+
+ rw_slice->len -= todo;
+
+ if ((pd->close_requested) && (!pd->recv.pending_read))
+ efl_event_callback_call(o, EFL_IO_READER_EVENT_EOS, NULL);
+
+ return 0;
+}
+
+EOLIAN static Eina_Bool
+_efl_net_dialer_websocket_efl_io_reader_can_read_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ return pd->can_read;
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_efl_io_reader_can_read_set(Eo *o, Efl_Net_Dialer_Websocket_Data *pd, Eina_Bool can_read)
+{
+ if (pd->streaming_mode == EFL_NET_DIALER_WEBSOCKET_STREAMING_MODE_DISABLED) return;
+ if (pd->can_read == can_read) return;
+ pd->can_read = can_read;
+ efl_event_callback_call(o, EFL_IO_READER_EVENT_CAN_READ_CHANGED, NULL);
+}
+
+EOLIAN static Eina_Bool
+_efl_net_dialer_websocket_efl_io_reader_eos_get(Eo *o, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ return !pd->can_read && efl_io_closer_closed_get(o);
+}
+
+EOLIAN static Eina_Error
+_efl_net_dialer_websocket_efl_io_writer_write(Eo *o, Efl_Net_Dialer_Websocket_Data *pd, Eina_Slice *slice, Eina_Slice *remaining)
+{
+ Efl_Net_Dialer_Websocket_Opcode opcode;
+
+ EINA_SAFETY_ON_NULL_RETURN_VAL(slice, EINVAL);
+ EINA_SAFETY_ON_TRUE_RETURN_VAL(efl_io_closer_closed_get(o), EINVAL);
+ EINA_SAFETY_ON_TRUE_RETURN_VAL(pd->streaming_mode == EFL_NET_DIALER_WEBSOCKET_STREAMING_MODE_DISABLED, EINVAL);
+
+ if (pd->streaming_mode == EFL_NET_DIALER_WEBSOCKET_STREAMING_MODE_TEXT)
+ opcode = EFL_NET_DIALER_WEBSOCKET_OPCODE_TEXT;
+ else
+ opcode = EFL_NET_DIALER_WEBSOCKET_OPCODE_BINARY;
+
+ if (remaining)
+ {
+ remaining->mem = NULL;
+ remaining->len = 0;
+ }
+
+ _efl_net_dialer_websocket_send(o, pd, opcode, slice->mem, slice->len);
+ return 0;
+}
+
+EOLIAN static Eina_Bool
+_efl_net_dialer_websocket_efl_io_writer_can_write_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ return pd->can_write;
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_efl_io_writer_can_write_set(Eo *o, Efl_Net_Dialer_Websocket_Data *pd, Eina_Bool can_write)
+{
+ if (pd->streaming_mode == EFL_NET_DIALER_WEBSOCKET_STREAMING_MODE_DISABLED) return;
+ if (can_write && efl_io_closer_closed_get(o))
+ can_write = EINA_FALSE;
+ if (pd->can_write == can_write) return;
+ pd->can_write = can_write;
+ efl_event_callback_call(o, EFL_IO_WRITER_EVENT_CAN_WRITE_CHANGED, NULL);
+}
+
+EOLIAN static Eina_Error
+_efl_net_dialer_websocket_efl_io_closer_close(Eo *o, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ if (pd->close_requested) return 0;
+ efl_net_dialer_websocket_close_request(o, EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_NORMAL, NULL);
+ return 0;
+}
+
+EOLIAN static Eina_Bool
+_efl_net_dialer_websocket_efl_io_closer_closed_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ return pd->close_requested || efl_io_closer_closed_get(pd->http);
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_streaming_mode_set(Eo *o, Efl_Net_Dialer_Websocket_Data *pd, Efl_Net_Dialer_Websocket_Streaming_Mode streaming_mode)
+{
+ if (streaming_mode == EFL_NET_DIALER_WEBSOCKET_STREAMING_MODE_DISABLED)
+ {
+ while (pd->recv.pending_read)
+ _efl_net_dialer_websocket_recv_pending_read_remove(o, pd);
+ }
+
+ pd->streaming_mode = streaming_mode;
+}
+
+EOLIAN static Efl_Net_Dialer_Websocket_Streaming_Mode
+_efl_net_dialer_websocket_streaming_mode_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ return pd->streaming_mode;
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_user_agent_set(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd, const char *user_agent)
+{
+ efl_net_dialer_http_user_agent_set(pd->http, user_agent);
+}
+
+EOLIAN static const char *
+_efl_net_dialer_websocket_user_agent_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ return efl_net_dialer_http_user_agent_get(pd->http);
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_authentication_set(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd, const char *username, const char *password, Efl_Net_Http_Authentication_Method method, Eina_Bool restricted)
+{
+ efl_net_dialer_http_authentication_set(pd->http, username, password, method, restricted);
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_authentication_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd, const char **username, const char **password, Efl_Net_Http_Authentication_Method *method, Eina_Bool *restricted)
+{
+ efl_net_dialer_http_authentication_get(pd->http, username, password, method, restricted);
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_allow_redirects_set(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd, Eina_Bool allow_redirects)
+{
+ efl_net_dialer_http_allow_redirects_set(pd->http, allow_redirects);
+}
+
+EOLIAN static Eina_Bool
+_efl_net_dialer_websocket_allow_redirects_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ return efl_net_dialer_http_allow_redirects_get(pd->http);
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_cookie_jar_set(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd, const char *path)
+{
+ efl_net_dialer_http_cookie_jar_set(pd->http, path);
+}
+
+EOLIAN static const char *
+_efl_net_dialer_websocket_cookie_jar_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ return efl_net_dialer_http_cookie_jar_get(pd->http);
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_ping(Eo *o, Efl_Net_Dialer_Websocket_Data *pd, const char *reason)
+{
+ size_t len;
+ EINA_SAFETY_ON_TRUE_RETURN(pd->close_requested);
+ if (!reason) reason = "";
+
+ len = strlen(reason);
+ if (len > 125)
+ {
+ WRN("reason is over 125 bytes! chopped '%s'", reason);
+ len = 125;
+ }
+ _efl_net_dialer_websocket_send(o, pd, EFL_NET_DIALER_WEBSOCKET_OPCODE_PING,
+ reason, len);
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_text_send(Eo *o, Efl_Net_Dialer_Websocket_Data *pd, const char *text)
+{
+ EINA_SAFETY_ON_TRUE_RETURN(pd->close_requested);
+ if (!text) text = "";
+ _efl_net_dialer_websocket_send(o, pd, EFL_NET_DIALER_WEBSOCKET_OPCODE_TEXT,
+ text, strlen(text));
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_binary_send(Eo *o, Efl_Net_Dialer_Websocket_Data *pd, const Eina_Slice blob)
+{
+ EINA_SAFETY_ON_TRUE_RETURN(pd->close_requested);
+ _efl_net_dialer_websocket_send(o, pd, EFL_NET_DIALER_WEBSOCKET_OPCODE_BINARY,
+ blob.mem, blob.len);
+}
+
+static void
+_efl_net_dialer_websocket_close_request_timeout(void *data, const Efl_Event *event)
+{
+ Eo *o = data;
+ Efl_Net_Dialer_Websocket_Data *pd = efl_data_scope_get(o, MY_CLASS);
+
+ efl_del(event->object);
+ pd->close_timer = NULL;
+ DBG("server did not close the TCP socket, timeout");
+ efl_event_callback_call(o, EFL_IO_CLOSER_EVENT_CLOSED, NULL);
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_close_request(Eo *o, Efl_Net_Dialer_Websocket_Data *pd, Efl_Net_Dialer_Websocket_Close_Reason reason, const char *message)
+{
+ Efl_Net_Dialer_Websocket_Frame *f;
+ uint16_t r = reason;
+ size_t len;
+ EINA_SAFETY_ON_TRUE_RETURN(pd->close_requested);
+
+ if (pd->close_timer)
+ {
+ efl_loop_timer_interval_set(pd->close_timer, 2.0);
+ efl_loop_timer_reset(pd->close_timer);
+ }
+ else
+ {
+ pd->close_timer = efl_add(EFL_LOOP_TIMER_CLASS, efl_loop_user_loop_get(o),
+ efl_loop_timer_interval_set(efl_self, 2.0),
+ efl_event_callback_add(efl_self, EFL_LOOP_TIMER_EVENT_TICK, _efl_net_dialer_websocket_close_request_timeout, o));
+ }
+ efl_io_writer_can_write_set(o, EINA_FALSE);
+
+ if ((!reason) || (reason == EFL_NET_DIALER_WEBSOCKET_CLOSE_REASON_NO_REASON))
+ {
+ f = _efl_net_dialer_websocket_send_pending_add(o, pd, EFL_NET_DIALER_WEBSOCKET_OPCODE_CLOSE, 0);
+ EINA_SAFETY_ON_NULL_RETURN(f);
+ pd->close_requested = EINA_TRUE;
+ return;
+ }
+
+ if (!message) message = "";
+
+#ifndef WORDS_BIGENDIAN
+ r = eina_swap16(r);
+#endif
+
+ len = strlen(message);
+ if (len > 123) /* 2 for code */
+ {
+ WRN("message is over 123 bytes! chopped '%s'", message);
+ len = 123;
+ }
+
+ f = _efl_net_dialer_websocket_send_pending_add(o, pd, EFL_NET_DIALER_WEBSOCKET_OPCODE_CLOSE, sizeof(r) + len);
+ EINA_SAFETY_ON_NULL_RETURN(f);
+
+ _efl_net_dialer_websocket_frame_write(f, 0, &r, sizeof(r));
+ _efl_net_dialer_websocket_frame_write(f, sizeof(r), message, len);
+ pd->close_requested = EINA_TRUE;
+}
+
+typedef struct _Efl_Net_Dialer_Websocket_Blacklist_Header {
+ const char *header;
+ size_t len;
+} Efl_Net_Dialer_Websocket_Blacklist_Header;
+
+static const Efl_Net_Dialer_Websocket_Blacklist_Header _efl_net_dialer_websocket_blacklisted_headers[] = {
+#define _M(x) {x, sizeof(x) - 1}
+ _M("Upgrade"),
+ _M("Connection"),
+ _M("Expect"),
+ _M("Transfer-Encoding"),
+ _M("Sec-WebSocket-Key"),
+ _M("Sec-WebSocket-Protocol"),
+ _M("Sec-WebSocket-Version"),
+#undef _M
+ {NULL, 0}
+};
+
+static Eina_Bool
+_efl_net_dialer_websocket_blacklisted_header_check(const char *key)
+{
+ const Efl_Net_Dialer_Websocket_Blacklist_Header *bh;
+ size_t len;
+
+ len = strlen(key);
+ for (bh = _efl_net_dialer_websocket_blacklisted_headers;
+ bh->header != NULL;
+ bh++)
+ {
+ if ((bh->len == len) && (memcmp(bh->header, key, len) == 0))
+ return EINA_TRUE;
+ }
+
+ return EINA_FALSE;
+}
+
+
+EOLIAN static void
+_efl_net_dialer_websocket_request_header_add(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd, const char *key, const char *value)
+{
+ EINA_SAFETY_ON_NULL_RETURN(key);
+ EINA_SAFETY_ON_NULL_RETURN(value);
+
+ if (_efl_net_dialer_websocket_blacklisted_header_check(key))
+ {
+ ERR("HTTP header conflicts with WebSocket: %s: %s", key, value);
+ return;
+ }
+
+ efl_net_dialer_http_request_header_add(pd->http, key, value);
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_request_headers_clear(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+
+ efl_net_dialer_http_request_headers_clear(pd->http);
+ _efl_net_dialer_websocket_request_headers_websocket_add(pd);
+}
+
+typedef struct _Eina_Iterator_Filtered_Header
+{
+ Eina_Iterator iterator;
+ Eina_Iterator *full;
+} Eina_Iterator_Filtered_Header;
+
+static Eina_Bool
+eina_iterator_filtered_header_next(Eina_Iterator_Filtered_Header *it, void **data)
+{
+ Efl_Net_Http_Header *header;
+ EINA_ITERATOR_FOREACH(it->full, header)
+ {
+ if (!header->key) continue;
+ if (!_efl_net_dialer_websocket_blacklisted_header_check(header->key))
+ {
+ *data = header;
+ return EINA_TRUE;
+ }
+ }
+ return EINA_FALSE;
+}
+
+static const struct filtered *
+eina_iterator_filtered_header_get_container(Eina_Iterator_Filtered_Header *it)
+{
+ return eina_iterator_container_get(it->full);
+}
+
+static void
+eina_iterator_filtered_header_free(Eina_Iterator_Filtered_Header *it)
+{
+ eina_iterator_free(it->full);
+ free(it);
+}
+
+EOLIAN static Eina_Iterator *
+_efl_net_dialer_websocket_request_headers_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ Eina_Iterator_Filtered_Header *it;
+
+ it = calloc(1, sizeof(Eina_Iterator_Filtered_Header));
+ EINA_SAFETY_ON_NULL_RETURN_VAL(it, NULL);
+
+ it->full = efl_net_dialer_http_request_headers_get(pd->http);
+ EINA_SAFETY_ON_NULL_GOTO(it->full, error);
+
+ EINA_MAGIC_SET(&it->iterator, EINA_MAGIC_ITERATOR);
+ it->iterator.version = EINA_ITERATOR_VERSION;
+ it->iterator.next = FUNC_ITERATOR_NEXT(eina_iterator_filtered_header_next);
+ it->iterator.get_container = FUNC_ITERATOR_GET_CONTAINER(eina_iterator_filtered_header_get_container);
+ it->iterator.free = FUNC_ITERATOR_FREE(eina_iterator_filtered_header_free);
+
+ return &it->iterator;
+
+ error:
+ free(it);
+ return NULL;
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_request_protocol_add(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd, const char *protocol)
+{
+ EINA_SAFETY_ON_NULL_RETURN(protocol);
+ protocol = eina_stringshare_add(protocol);
+ pd->protocols.requested = eina_list_append(pd->protocols.requested, protocol);
+}
+
+EOLIAN static Eina_Iterator *
+_efl_net_dialer_websocket_request_protocols_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ return eina_list_iterator_new(pd->protocols.requested);
+}
+
+EOLIAN static void
+_efl_net_dialer_websocket_request_protocols_clear(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ Eina_Stringshare *str;
+ EINA_LIST_FREE(pd->protocols.requested, str)
+ eina_stringshare_del(str);
+}
+
+EOLIAN static Eina_Iterator *
+_efl_net_dialer_websocket_response_protocols_get(Eo *o EINA_UNUSED, Efl_Net_Dialer_Websocket_Data *pd)
+{
+ return eina_list_iterator_new(pd->protocols.received);
+}
+
+#include "efl_net_dialer_websocket.eo.c"
diff --git a/src/lib/ecore_con/efl_net_dialer_websocket.eo b/src/lib/ecore_con/efl_net_dialer_websocket.eo
new file mode 100644
index 0000000000..0d560caa20
--- /dev/null
+++ b/src/lib/ecore_con/efl_net_dialer_websocket.eo
@@ -0,0 +1,309 @@
+import eina_types;
+import efl_net_http_types;
+
+enum Efl.Net.Dialer.Websocket.Streaming_Mode {
+ [[How to map WebSocket to EFL I/O Interfaces.]]
+ disabled, [[@Efl.Io.Writer.write and @Efl.Io.Reader.read will fail by returning ENOSTR]]
+ binary, [[@Efl.Io.Writer.write will result in @Efl.Net.Dialer.Websocket.binary_send]]
+ text, [[@Efl.Io.Writer.write will result in @Efl.Net.Dialer.Websocket.text_send]]
+}
+
+enum Efl.Net.Dialer.Websocket.Close_Reason {
+ [[Registered reasons for the CLOSE (opcode=0x8).
+
+ These are the well known reasons, with some ranges being defined
+ using "_start" and "end" suffixes.
+
+ See https://tools.ietf.org/html/rfc6455#section-7.4.1
+ ]]
+ normal = 1000, [[indicates a normal closure, meaning that the purpose for which the connection was established has been fulfilled.]]
+ going_away = 1001, [[indicates that an endpoint is "going away", such as a server going down or a browser having navigated away from a page.]]
+ protocol_error = 1002, [[indicates that an endpoint is terminating the connection due to a protocol error.]]
+ no_reason = 1005, [[reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint. It is designated for use in applications expecting a status code to indicate that no status code was actually present.]]
+ abruptly = 1006, [[reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint. It is designated for use in applications expecting a status code to indicate that the connection was closed abnormally, e.g., without sending or receiving a Close control frame.]]
+ unexpected_data = 1003, [[indicates that an endpoint is terminating the connection because it has received a type of data it cannot accept (e.g., an endpoint that understands only text data MAY send this if it receives a binary message).]]
+ inconsistent_data = 1007, [[indicates that an endpoint is terminating the connection because it has received data within a message that was not consistent with the type of the message (e.g., non-UTF-8 data within a text message).]]
+ policy_violation = 1008, [[indicates that an endpoint is terminating the connection because it has received a message that violates its policy. This is a generic status code that can be returned when there is no other more suitable status code (e.g., 1003 or 1009) or if there is a need to hide specific details about the policy.]]
+ too_big = 1009, [[indicates that an endpoint is terminating the connection because it has received a message that is too big for it to process.]]
+ missing_extension = 1010, [[indicates that an endpoint (client) is terminating the connection because it has expected the server to negotiate one or more extension, but the server didn't return them in the response message of the WebSocket handshake. The list of extensions that are needed SHOULD appear in the reason part of the Close frame. Note that this status code is not used by the server, because it can fail the WebSocket handshake instead.]]
+ server_error = 1011, [[indicates that a server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request.]]
+ iana_registry_start = 3000, [[IANA registry starts at 3000]]
+ iana_registry_end = 3999, [[IANA registry ends at 3999]]
+ private_start = 4000, [[Applications can use range 4000-4999]]
+ private_end = 4999, [[Applications can use range 4000-4999]]
+}
+
+struct Efl.Net.Dialer.Websocket.Closed_Reason {
+ reason: Efl.Net.Dialer.Websocket.Close_Reason;
+ message: string;
+}
+
+class Efl.Net.Dialer.Websocket (Efl.Loop_User, Efl.Net.Dialer, Efl.Io.Reader, Efl.Io.Writer) { /* TODO: reader/writer should be from dialer->socket, but are being missed somehow... */
+ [[WebSocket Dialer (Client).
+
+ The WebSocket Protocol (https://tools.ietf.org/html/rfc6455) is
+ a message-based protocol over HTTP, this allows it to leverage
+ on authentication, cookies, proxies and SSL/TLS.
+
+ It's worth to note that although it uses the HTTP dialer, it's
+ not a subclass and thus not all HTTP features are exposed as the
+ WebSocket has strict requirements that must be respected.
+
+ @since 1.19
+ ]]
+
+ methods {
+ ping {
+ [[Send a PING (opcode=0x9) to the server.
+
+ The server should reply with a PONG, that will be
+ emitted as "pong" event.
+ ]]
+
+ params {
+ reason: string @optional;
+ }
+ }
+
+ text_send {
+ [[Send an UTF-8 TEXT (opcode=0x1) to the server.
+
+ The text goes in a message will be delivered as a single
+ entity to the remote peer.
+
+ The text is copied into a local buffer, no references
+ are kept after this method returns.
+ ]]
+ params {
+ text: string;
+ }
+ }
+
+ binary_send {
+ [[Send a binary blob (opcode=0x2) to the server.
+
+ The slice describing the blob goes in a message will be
+ delivered as a single entity to the remote peer.
+
+ The memory is copied into a local buffer, no references
+ are kept after this method returns.
+ ]]
+ params {
+ blob: const(Eina.Slice);
+ }
+ }
+
+ close_request {
+ [[Request (opcode=0x8) the server to terminate the connection.
+
+ Unlike @Efl.Io.Closer.close, this won't abruptly close
+ the connection, rather will queue a message requesting
+ the server to gracefully close it.
+
+ After this method is called you should consider the
+ object in "closing" state, no more messages can be sent
+ (@.text_send, @.binary_send and @.ping will fail).
+
+ The object will be automatically closed with
+ @Efl.Io.Closer.close once the serve replies with his own
+ close message, that will be reported as "closed,reason".
+ ]]
+ params {
+ reason: Efl.Net.Dialer.Websocket.Close_Reason;
+ message: string @optional;
+ }
+ }
+
+ request_protocol_add {
+ [[Add a new WebSocket protocol to the request.
+
+ This should be set before dialing.
+ ]]
+ params {
+ protocol: string;
+ }
+ }
+
+ request_protocols_get {
+ [[Return an iterator to the requested WebSocket protocols]]
+ return: free(own(iterator<string>), eina_iterator_free) @warn_unused;
+ }
+
+ request_protocols_clear {
+ [[Clear all request protocols]]
+ }
+
+ response_protocols_get {
+ [[Return an iterator to the server-replied (response) WebSocket protocols it supports]]
+ return: free(own(iterator<string>), eina_iterator_free) @warn_unused;
+ }
+
+ @property streaming_mode {
+ [[Configure how to map streaming APIs to WebSocket.
+
+ WebSocket is a message-based protocol with these send
+ via @.text_send and @.binary_send and delivered via
+ events such as "message,text" and "message,binary".
+
+ However this class can operate in streaming mode,
+ mapping each @Efl.Io.Writer.write to a @.binary_send if
+ streaming_mode is set to
+ @Efl.Net.Dialer.Websocket.Streaming_Mode.binary, of
+ @.text_send if
+ @Efl.Net.Dialer.Websocket.Streaming_Mode.text
+
+ @Efl.Io.Reader.read may consume less then the whole
+ received message, in this case the rest of the message
+ is kept for the next read call. (Note this differs from
+ SOCK_SEQPACKET + read(2)).
+
+ By default, streaming is disabled
+ (@Efl.Net.Dialer.Websocket.Streaming_Mode.disabled).
+ ]]
+ get { }
+ set { }
+ values {
+ streaming_mode: Efl.Net.Dialer.Websocket.Streaming_Mode;
+ }
+ }
+
+ @property user_agent {
+ [[The User-Agent to specify.
+
+ This should be set before dialing.
+ ]]
+ get { }
+ set { }
+ values {
+ user_agent: string;
+ }
+ }
+
+ @property authentication {
+ [[HTTP authentication to use.
+
+ This should be set before dialing.
+ ]]
+ get { }
+ set { }
+ values {
+ username: string;
+ password: string;
+ method: Efl.Net.Http.Authentication_Method @optional; [[authentication method to use, defaults to @Efl.Net.Http.Authentication_Method.basic]]
+ restricted: bool @optional; [[restrict method]]
+ }
+ }
+
+ @property allow_redirects {
+ [[Allow HTTP redirects to be followed.
+
+ This should be set before dialing.
+ ]]
+ get { }
+ set { }
+ values {
+ allow_redirects: bool;
+ }
+ }
+
+ request_header_add {
+ [[Add a HTTP request header 'key: value'.
+
+ See @.request_headers_clear
+
+ WebSocket won't allow the following headers to be added
+ as they conflict with its own operation:
+
+ - Content-Length
+ - Content-Type
+ - Transfer-Encoding
+ - Connection
+ - Upgrade
+ - Expect
+ - Sec-WebSocket-Version
+ - Sec-WebSocket-Key
+
+ This should be called before dialing.
+ ]]
+ params {
+ @in key: string;
+ @in value: string;
+ }
+ }
+
+ request_headers_clear {
+ [[Clear all request headers.
+
+ See @.request_header_add
+
+ This should be called before dialing.
+ ]]
+ }
+
+ request_headers_get {
+ [[Return an iterator to the key-value pairs for request headers]]
+ return: free(own(iterator<Efl.Net.Http.Header>), eina_iterator_free) @warn_unused;
+ }
+
+ @property cookie_jar {
+ [[This property sets the filename where to read and write cookies.
+
+ By setting a file to load and persist cookies to, the
+ internal cookie system will be activated, automatically
+ handling HTTP headers such as 'Set-cookie:' and sending
+ the appropriate cookies for a server.
+
+ If a new, empty session is to be used, start with an
+ empty or non-existent file such as created with
+ mkstemp() or tmpfile(). An alternative is to use an
+ empty string ("") to keep it in memory.
+
+ If it is desired to start from a pre-existent cookie jar
+ but do not want to modify that, first copy that file and
+ then pass the new, temporary file.
+
+ Likewise, if it's desired to fill some cookies to the
+ system, create a cookie jar and pass its path to this
+ property.
+
+ \@note that whenever this property is set, even if to the
+ same value, it will flush all cookies to the previously
+ set file, then erase all known cookies, then use the new
+ file (if any).
+ ]]
+ get { }
+ set { }
+ values {
+ path: string;
+ }
+ }
+ }
+
+ events {
+ message,text: string; [[Received a text string message (opcode=0x1)]]
+ message,binary: const(Eina.Slice)*; [[Received a binary message (opcode=0x2)]]
+ pong: string; [[Received a pong (opcode=0xA) with optional message/reason]]
+ closed,reason: Efl.Net.Dialer.Websocket.Closed_Reason; [[Received a request to close the connection. It may be a reply/confirmation from a local request, see @.close_request, or some server-generated reason. After this point, no more messages are allowed to be sent and no more will be received. @Efl.Io.Closer.close will be called.]]
+ }
+
+ implements {
+ Efl.Object.constructor;
+ Efl.Object.destructor;
+ Efl.Net.Dialer.dial;
+ Efl.Net.Dialer.address_dial;
+ Efl.Net.Dialer.connected;
+ Efl.Net.Dialer.proxy;
+ Efl.Net.Dialer.timeout_dial;
+ Efl.Net.Socket.address_local.get;
+ Efl.Net.Socket.address_remote;
+ Efl.Io.Reader.read;
+ Efl.Io.Reader.can_read.get;
+ Efl.Io.Reader.can_read.set;
+ Efl.Io.Reader.eos.get;
+ Efl.Io.Writer.write;
+ Efl.Io.Writer.can_write.get;
+ Efl.Io.Writer.can_write.set;
+ Efl.Io.Closer.close;
+ Efl.Io.Closer.closed.get;
+ }
+}