#define EFL_BETA_API_SUPPORT #include #include #include #include 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)); efl_loop_quit(efl_loop_get(event->object), eina_value_int_init(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_DIALER_CONNECTED, _connected }, { EFL_NET_DIALER_EVENT_DIALER_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 *loop) { Eo *dialer; dialer = efl_add(EFL_NET_DIALER_WEBSOCKET_CLASS, loop, efl_name_set(efl_added, name), efl_event_callback_array_add(efl_added, dialer_cbs(), NULL)); if (!dialer) { efl_loop_quit(loop, eina_value_int_init(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(Eo *loop) { 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", loop); 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) { fprintf(stderr, "ERROR: could not dial '%s': %s", url, eina_error_msg_get(err)); efl_del(dialer); efl_loop_quit(loop, eina_value_int_init(EXIT_FAILURE)); 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(Eo *loop); 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(efl_loop_get(event->object))) _tests_finished(efl_loop_get(event->object)); } 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(Eo *loop) { 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, loop); 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) { fprintf(stderr, "ERROR: could not dial '%s': %s", url, eina_error_msg_get(err)); efl_del(dialer); efl_loop_quit(loop, eina_value_int_init(EXIT_FAILURE)); 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, Eo *loop); 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, efl_loop_get(event->object))) _tests_finished(efl_loop_get(event->object)); } 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 *loop) { 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, loop); 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) { fprintf(stderr, "ERROR: could not dial '%s': %s", url, eina_error_msg_get(err)); efl_del(dialer); efl_loop_quit(loop, eina_value_int_init(EXIT_FAILURE)); 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, efl_loop_get(event->object))) _tests_finished(efl_loop_get(event->object)); } 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(Eo *loop) { 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", loop); 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) { fprintf(stderr, "ERROR: could not dial '%s': %s", url, eina_error_msg_get(err)); efl_del(dialer); efl_loop_quit(loop, eina_value_int_init(EXIT_FAILURE)); 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 } }; EAPI_MAIN void efl_pause(void *data EINA_UNUSED, const Efl_Event *ev EINA_UNUSED) { } EAPI_MAIN void efl_resume(void *data EINA_UNUSED, const Efl_Event *ev EINA_UNUSED) { } EAPI_MAIN void efl_terminate(void *data EINA_UNUSED, const Efl_Event *ev EINA_UNUSED) { /* FIXME: For the moment the main loop doesn't get properly destroyed on shutdown which disallow relying on parent destroying their children */ if (pending) { efl_del(pending); pending = NULL; } if (verbose) fprintf(stderr, "INFO: main loop finished.\n"); } EAPI_MAIN void efl_main(void *data EINA_UNUSED, const Efl_Event *ev) { 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; args = ecore_getopt_parse(&options, values, 0, NULL); if (args < 0) { fputs("ERROR: Could not parse command line options.\n", stderr); goto end; } if (quit_option) goto end; args = ecore_getopt_parse_positional(&options, values, 0, NULL, args); if (args < 0) { fputs("ERROR: Could not parse positional arguments.\n", stderr); goto end; } if (case_tuples) r = _websocket_test_next_case_tuple(ev->object); else if (start_index == end_index) r = _websocket_test_index(start_index, ev->object); else r = _websocket_load_tests(ev->object); if (r) { ecore_main_loop_begin(); } if (pending) efl_del(pending); return ; end: efl_loop_quit(efl_loop_get(ev->object), eina_value_int_init(EXIT_FAILURE)); } EFL_MAIN_EX();