--- /dev/null
+#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;
+}
--- /dev/null
+#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;
+}
--- /dev/null
+#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"