From 7a129c17ba8347a8e6a53463c04647ec6b5a0973 Mon Sep 17 00:00:00 2001 From: Sergey Lyubka <valenok@gmail.com> Date: Sun, 19 Jan 2014 16:32:43 +0000 Subject: [PATCH] Added http client with unit tests --- mongoose.c | 205 ++++++++++++++++++++++++++++++++++------------------ mongoose.h | 12 ++- unit_test.c | 152 ++++++++++++++++++++------------------ 3 files changed, 226 insertions(+), 143 deletions(-) diff --git a/mongoose.c b/mongoose.c index 41ddefcd7..4218a7d83 100644 --- a/mongoose.c +++ b/mongoose.c @@ -318,7 +318,7 @@ enum connection_flags { CONN_HEADERS_SENT = 8, // User callback has sent HTTP headers CONN_BUFFER = 16, // CGI only. Holds data send until CGI prints // all HTTP headers - CONN_CONNECTED = 32, // HTTP client has connected + CONN_CONNECTING = 32, // HTTP client is doing non-blocking connect() CONN_LONG_RUNNING = 64 // Long-running URI handlers }; @@ -1154,6 +1154,7 @@ static void open_cgi_endpoint(struct connection *conn, const char *prog) { conn->endpoint_type = EP_CGI; conn->endpoint.cgi_sock = fds[0]; spool(&conn->remote_iobuf, cgi_status, sizeof(cgi_status) - 1); + conn->mg_conn.status_code = 200; conn->flags |= CONN_BUFFER; } else { closesocket(fds[0]); @@ -1193,19 +1194,24 @@ static void read_from_cgi(struct connection *conn) { } } memcpy(io->buf + 9, status, 3); + conn->mg_conn.status_code = atoi(status); conn->flags &= ~CONN_BUFFER; } } } -static void forward_post_data(struct connection *conn) { - struct iobuf *io = &conn->local_iobuf; - int n = send(conn->endpoint.cgi_sock, io->buf, io->len, 0); - if (n > 0) { +static void discard_leading_iobuf_bytes(struct iobuf *io, int n) { + if (n >= 0 && n <= io->len) { memmove(io->buf, io->buf + n, io->len - n); io->len -= n; } } + +static void forward_post_data(struct connection *conn) { + struct iobuf *io = &conn->local_iobuf; + int n = send(conn->endpoint.cgi_sock, io->buf, io->len, 0); + discard_leading_iobuf_bytes(io, n); +} #endif // !NO_CGI // 'sa' must be an initialized address to bind to @@ -1331,9 +1337,10 @@ static struct connection *accept_new_connection(struct mg_server *server) { } static void close_conn(struct connection *conn) { - DBG(("%p %d %d", conn, conn->flags, conn->endpoint_type)); LINKED_LIST_REMOVE(&conn->link); closesocket(conn->client_sock); + close_local_endpoint(conn); + DBG(("%p %d %d", conn, conn->flags, conn->endpoint_type)); free(conn->request); // It's OK to free(NULL), ditto below free(conn->path_info); free(conn->remote_iobuf.buf); @@ -1840,8 +1847,7 @@ static int deliver_websocket_frame(struct connection *conn) { if (conn->endpoint.uh->handler(&conn->mg_conn)) { conn->flags |= CONN_SPOOL_DONE; } - memmove(buf, buf + frame_len, buf_len - frame_len); - conn->local_iobuf.len -= frame_len; + discard_leading_iobuf_bytes(&conn->local_iobuf, frame_len); } return buffered; @@ -1938,8 +1944,7 @@ static void write_to_socket(struct connection *conn) { if (is_error(n)) { conn->flags |= CONN_CLOSE; } else if (n > 0) { - memmove(io->buf, io->buf + n, io->len - n); - io->len -= n; + discard_leading_iobuf_bytes(io, n); conn->num_bytes_sent += n; } @@ -2641,8 +2646,7 @@ static void forward_put_data(struct connection *conn) { struct iobuf *io = &conn->local_iobuf; int n = write(conn->endpoint.fd, io->buf, io->len); if (n > 0) { - memmove(io->buf, io->buf + n, io->len - n); - io->len -= n; + discard_leading_iobuf_bytes(io, n); conn->cl -= n; if (conn->cl <= 0) { close_local_endpoint(conn); @@ -3312,7 +3316,6 @@ static void handle_lsp_request(struct connection *conn, const char *path, #endif // USE_LUA static void open_local_endpoint(struct connection *conn) { - const char *cl_hdr = mg_get_header(&conn->mg_conn, "Content-Length"); #ifndef NO_FILESYSTEM static const char lua_pat[] = LUA_SCRIPT_PATTERN; file_stat_t st; @@ -3322,8 +3325,6 @@ static void open_local_endpoint(struct connection *conn) { const char *dir_lst = conn->server->config_options[ENABLE_DIRECTORY_LISTING]; #endif - conn->mg_conn.content_len = cl_hdr == NULL ? 0 : (int) to64(cl_hdr); - // Call URI handler if one is registered for this URI conn->endpoint.uh = find_uri_handler(conn->server, conn->mg_conn.uri); if (conn->endpoint.uh != NULL) { @@ -3425,7 +3426,7 @@ static int is_valid_uri(const char *uri) { return uri[0] == '/' || (uri[0] == '*' && uri[1] == '\0'); } -static void process_request(struct connection *conn) { +static void try_http_parse_and_set_content_length(struct connection *conn) { struct iobuf *io = &conn->local_iobuf; if (conn->request_len == 0 && @@ -3435,14 +3436,24 @@ static void process_request(struct connection *conn) { // become invalid. conn->request = (char *) malloc(conn->request_len); memcpy(conn->request, io->buf, conn->request_len); - DBG(("%p ==> [%.*s]", conn, conn->request_len, conn->request)); - memmove(io->buf, io->buf + conn->request_len, io->len - conn->request_len); - io->len -= conn->request_len; + DBG(("%p [%.*s]", conn, conn->request_len, conn->request)); + discard_leading_iobuf_bytes(io, conn->request_len); conn->request_len = parse_http_message(conn->request, conn->request_len, &conn->mg_conn); + if (conn->request_len > 0) { + const char *cl_hdr = mg_get_header(&conn->mg_conn, "Content-Length"); + conn->cl = cl_hdr == NULL ? 0 : to64(cl_hdr); + conn->mg_conn.content_len = (long int) conn->cl; + } } +} - DBG(("%p %d %d [%.*s]", conn, conn->request_len, io->len, io->len, io->buf)); +static void process_request(struct connection *conn) { + struct iobuf *io = &conn->local_iobuf; + + try_http_parse_and_set_content_length(conn); + DBG(("%p %d %d %d [%.*s]", conn, conn->request_len, io->len, conn->flags, + io->len, io->buf)); if (conn->request_len < 0 || (conn->request_len > 0 && !is_valid_uri(conn->mg_conn.uri))) { send_http_error(conn, 400, NULL); @@ -3475,48 +3486,96 @@ static void process_request(struct connection *conn) { #endif } +static void call_http_client_handler(struct connection *conn, int code) { + conn->mg_conn.status_code = code; + // For responses without Content-Lengh, use the whole buffer + if (conn->cl == 0 && code == MG_DOWNLOAD_SUCCESS) { + conn->mg_conn.content_len = conn->local_iobuf.len; + } + conn->mg_conn.content = conn->local_iobuf.buf; + if (conn->handler(&conn->mg_conn) || code == MG_CONNECT_FAILURE || + code == MG_DOWNLOAD_FAILURE) { + conn->flags |= CONN_CLOSE; + } + discard_leading_iobuf_bytes(&conn->local_iobuf, conn->mg_conn.content_len); + conn->flags = conn->mg_conn.status_code = 0; + conn->cl = conn->num_bytes_sent = conn->request_len = 0; + free(conn->request); + conn->request = NULL; +} + +static void process_response(struct connection *conn) { + struct iobuf *io = &conn->local_iobuf; + + try_http_parse_and_set_content_length(conn); + DBG(("%p %d %d [%.*s]", conn, conn->request_len, io->len, + io->len > 40 ? 40 : io->len, io->buf)); + if (conn->request_len < 0 || + (conn->request_len == 0 && io->len > MAX_REQUEST_SIZE)) { + call_http_client_handler(conn, MG_DOWNLOAD_FAILURE); + } + if (io->len >= conn->cl) { + call_http_client_handler(conn, MG_DOWNLOAD_SUCCESS); + } +} + +static void callback_http_client_on_connect(struct connection *conn) { + int ok; + socklen_t len = sizeof(ok); + + if (getsockopt(conn->client_sock, SOL_SOCKET, SO_ERROR, (char *) &ok, + &len) == 0 && ok == 0) { + conn->mg_conn.status_code = MG_CONNECT_SUCCESS; + } + conn->flags &= ~CONN_CONNECTING; + if (conn->handler(&conn->mg_conn) || ok != 0) { + conn->flags |= CONN_CLOSE; + } +} + static void read_from_socket(struct connection *conn) { char buf[IOBUF_SIZE]; - int ok, n = 0; - socklen_t len = sizeof(ok); + int n = 0; - if (conn->endpoint_type == EP_CLIENT) { - conn->mg_conn.wsbits = 1; - if (!(conn->flags & CONN_CONNECTED) && - getsockopt(conn->client_sock, SOL_SOCKET, SO_ERROR, - (char *) &ok, &len) < 0) { - conn->mg_conn.wsbits = 0; - } - conn->handler(&conn->mg_conn); - conn->flags |= CONN_CLOSE | CONN_CONNECTED; - } else { - if (conn->ssl != NULL) { + if (conn->endpoint_type == EP_CLIENT && conn->flags & CONN_CONNECTING) { + callback_http_client_on_connect(conn); + return; + } + + if (conn->ssl != NULL) { #ifdef USE_SSL - if (conn->flags & CONN_SSL_HANDS_SHAKEN) { - n = SSL_read(conn->ssl, buf, sizeof(buf)); - } else { - if (SSL_accept(conn->ssl) == 1) { - conn->flags |= CONN_SSL_HANDS_SHAKEN; - } - return; - } -#endif + if (conn->flags & CONN_SSL_HANDS_SHAKEN) { + n = SSL_read(conn->ssl, buf, sizeof(buf)); } else { - n = recv(conn->client_sock, buf, sizeof(buf), 0); + if (SSL_accept(conn->ssl) == 1) { + conn->flags |= CONN_SSL_HANDS_SHAKEN; + } + return; } +#endif + } else { + n = recv(conn->client_sock, buf, sizeof(buf), 0); + } - DBG(("%p %d", conn, n)); - if (is_error(n)) { - conn->flags |= CONN_CLOSE; - } else if (n > 0) { - spool(&conn->local_iobuf, buf, n); + DBG(("%p %d %d (1)", conn, n, conn->flags)); + if (is_error(n)) { + if (conn->endpoint_type == EP_CLIENT && conn->local_iobuf.len > 0) { + call_http_client_handler(conn, MG_DOWNLOAD_SUCCESS); + } + conn->flags |= CONN_CLOSE; + } else if (n > 0) { + spool(&conn->local_iobuf, buf, n); + if (conn->endpoint_type == EP_CLIENT) { + process_response(conn); + } else { process_request(conn); } } + DBG(("%p %d %d (2)", conn, n, conn->flags)); } -int mg_connect(struct mg_server *server, const char *host, - int port, mg_handler_t handler, void *param) { +int mg_connect(struct mg_server *server, const char *host, int port, + mg_handler_t handler, void *param) { sock_t sock = INVALID_SOCKET; struct sockaddr_in sin; struct hostent *he = NULL; @@ -3544,10 +3603,16 @@ int mg_connect(struct mg_server *server, const char *host, conn->handler = handler; conn->mg_conn.server_param = server->server_data; conn->mg_conn.connection_param = param; + conn->birth_time = conn->last_activity_time = time(NULL); + conn->flags = CONN_CONNECTING; + conn->mg_conn.status_code = MG_CONNECT_FAILURE; LINKED_LIST_ADD_TO_FRONT(&server->active_connections, &conn->link); + DBG(("%p %s:%d", conn, host, port)); if (connect_ret_val == 0) { - conn->flags = CONN_CONNECTED; + conn->mg_conn.status_code = MG_CONNECT_SUCCESS; + conn->flags &= ~CONN_CONNECTING; + conn->mg_conn.content = conn->local_iobuf.buf; handler(&conn->mg_conn); } @@ -3593,17 +3658,11 @@ static void log_access(const struct connection *conn, const char *path) { } #endif -static void gobble_prior_post_data(struct iobuf *io, int len) { - if (len > 0 && len <= io->len) { - memmove(io->buf, io->buf + len, io->len - len); - io->len -= len; - } -} - static void close_local_endpoint(struct connection *conn) { // Must be done before free() int keep_alive = should_keep_alive(&conn->mg_conn) && (conn->endpoint_type == EP_FILE || conn->endpoint_type == EP_USER); + DBG(("%p %d %d %d", conn, conn->endpoint_type, keep_alive, conn->flags)); switch (conn->endpoint_type) { case EP_PUT: close(conn->endpoint.fd); break; @@ -3613,17 +3672,16 @@ static void close_local_endpoint(struct connection *conn) { } #ifndef NO_LOGGING - if (conn->mg_conn.status_code != 400) { + if (conn->mg_conn.status_code > 0 && conn->endpoint_type != EP_CLIENT && + conn->mg_conn.status_code != 400) { log_access(conn, conn->server->config_options[ACCESS_LOG_FILE]); } #endif - if (conn->endpoint_type == EP_USER) { - gobble_prior_post_data(&conn->local_iobuf, conn->mg_conn.content_len); - } - + // Gobble possible POST data sent to the URI handler + discard_leading_iobuf_bytes(&conn->local_iobuf, conn->mg_conn.content_len); conn->endpoint_type = EP_NONE; - conn->flags = 0; + conn->flags = conn->mg_conn.status_code = 0; conn->cl = conn->num_bytes_sent = conn->request_len = 0; free(conn->request); conn->request = NULL; @@ -3690,6 +3748,9 @@ unsigned int mg_poll_server(struct mg_server *server, int milliseconds) { LINKED_LIST_FOREACH(&server->active_connections, lp, tmp) { conn = LINKED_LIST_ENTRY(lp, struct connection, link); add_to_set(conn->client_sock, &read_set, &max_fd); + if (conn->endpoint_type == EP_CLIENT && (conn->flags & CONN_CONNECTING)) { + add_to_set(conn->client_sock, &write_set, &max_fd); + } if (conn->endpoint_type == EP_FILE) { transfer_file_data(conn); } else if (conn->endpoint_type == EP_CGI) { @@ -3730,10 +3791,14 @@ unsigned int mg_poll_server(struct mg_server *server, int milliseconds) { read_from_cgi(conn); } #endif - if (FD_ISSET(conn->client_sock, &write_set) && - !(conn->flags & CONN_BUFFER)) { - conn->last_activity_time = current_time; - write_to_socket(conn); + if (FD_ISSET(conn->client_sock, &write_set)) { + if (conn->endpoint_type == EP_CLIENT && + (conn->flags & CONN_CONNECTING)) { + read_from_socket(conn); + } else if (!(conn->flags & CONN_BUFFER)) { + conn->last_activity_time = current_time; + write_to_socket(conn); + } } } } @@ -3767,7 +3832,7 @@ void mg_destroy_server(struct mg_server **server) { closesocket((*server)->ctl[0]); closesocket((*server)->ctl[1]); LINKED_LIST_FOREACH(&(*server)->active_connections, lp, tmp) { - free(LINKED_LIST_ENTRY(lp, struct connection, link)); + close_conn(LINKED_LIST_ENTRY(lp, struct connection, link)); } LINKED_LIST_FOREACH(&(*server)->uri_handlers, lp, tmp) { free(LINKED_LIST_ENTRY(lp, struct uri_handler, link)->uri); @@ -3980,7 +4045,7 @@ const char *mg_set_option(struct mg_server *server, const char *name, free(server->config_options[ind]); } server->config_options[ind] = mg_strdup(value); - DBG(("%s => %s", name, value)); + DBG(("%s [%s]", name, value)); if (ind == LISTENING_PORT) { if (server->listening_sock != INVALID_SOCKET) { diff --git a/mongoose.h b/mongoose.h index 02e165dff..b4998c5b6 100644 --- a/mongoose.h +++ b/mongoose.h @@ -46,11 +46,11 @@ struct mg_connection { } http_headers[30]; char *content; // POST (or websocket message) data, or NULL - int content_len; // content length + long int content_len; // content length int is_websocket; // Connection is a websocket connection int status_code; // HTTP status code for HTTP error handler - unsigned char wsbits; // First byte of the websocket frame + int wsbits; // First byte of the websocket frame void *server_param; // Parameter passed to mg_add_uri_handler() void *connection_param; // Placeholder for connection-specific data }; @@ -101,6 +101,14 @@ char *mg_md5(char buf[33], ...); int mg_authorize_digest(struct mg_connection *c, FILE *fp); void mg_send_digest_auth_request(struct mg_connection *conn); +// HTTP client interface +enum { + MG_CONNECT_SUCCESS, MG_CONNECT_FAILURE, + MG_DOWNLOAD_SUCCESS, MG_DOWNLOAD_FAILURE +}; +int mg_connect(struct mg_server *server, const char *host, int port, + mg_handler_t handler, void *param); + #ifdef __cplusplus } #endif // __cplusplus diff --git a/unit_test.c b/unit_test.c index 392a5acec..6c9f945bc 100644 --- a/unit_test.c +++ b/unit_test.c @@ -1,5 +1,5 @@ // Unit test for the mongoose web server. -// g++ -W -Wall -pedantic unit_test.c -o t && ./t +// g++ -W -Wall -pedantic -g unit_test.c && ./a.out #define USE_WEBSOCKET @@ -28,6 +28,7 @@ static int static_num_tests = 0; +#if 0 // Connects to host:port, and sends formatted request to it. Returns // malloc-ed reply and reply length, or NULL on error. Reply contains // everything including headers, not just the message body. @@ -73,6 +74,7 @@ static char *wget(const char *host, int port, int *len, const char *fmt, ...) { return reply; } +#endif static char *read_file(const char *path, int *size) { FILE *fp; @@ -361,78 +363,42 @@ static const char *test_next_option(void) { static int cb1(struct mg_connection *conn) { // We're not sending HTTP headers here, to make testing easier + //mg_printf(conn, "HTTP/1.0 200 OK\r\n\r\n%s %s %s", mg_printf(conn, "%s %s %s", conn->server_param == NULL ? "?" : (char *) conn->server_param, conn->connection_param == NULL ? "?" : "!", conn->remote_ip); return 1; } -static const char *test_regular_file(void) { - static const char *fname = "mongoose.c"; - int reply_len, file_len; - char *reply, *file_data; - file_stat_t st; - - ASSERT(stat(fname, &st) == 0); - ASSERT(st.st_size > 0); - - ASSERT((file_data = read_file(fname, &file_len)) != NULL); - ASSERT(file_len == st.st_size); - - reply = wget("127.0.0.1", atoi(HTTP_PORT), &reply_len, - "GET /%s.c HTTP/1.0\r\n\r\n", fname); - ASSERT(reply != NULL); - ASSERT(reply_len > 0); - // TODO(lsm): test headers and content - - free(reply); - free(file_data); - - return NULL; -} - -static const char *test_server_param(void) { - int reply_len; - char *reply; - - reply = wget("127.0.0.1", atoi(HTTP_PORT), &reply_len, "%s", - "GET /cb1 HTTP/1.0\r\n\r\n"); - ASSERT(reply != NULL); - ASSERT(reply_len == 15); - // cb1() does not send HTTP headers - ASSERT(memcmp(reply, "foo ? 127.0.0.1", 5) == 0); - //printf("%d [%.*s]\n", reply_len, reply_len, reply); - free(reply); - - return NULL; -} - static int error_handler(struct mg_connection *conn) { - mg_printf(conn, "error: %d", conn->status_code); + mg_printf(conn, "HTTP/1.0 404 NF\r\n\r\nERR: %d", conn->status_code); return 1; } -static const char *test_error_handler(void) { - int reply_len; - char *reply; - - reply = wget("127.0.0.1", atoi(HTTP_PORT), &reply_len, "%s", - "GET /non_exist HTTP/1.0\r\n\r\n"); - ASSERT(reply != NULL); - ASSERT(reply_len == 10); - ASSERT(memcmp(reply, "error: 404", 10) == 0); - free(reply); - - return NULL; +static int ts1(struct mg_connection *conn) { + if (conn->status_code == MG_CONNECT_SUCCESS) { + mg_printf(conn, "%s", "GET /cb1 HTTP/1.0\r\n\r\n"); + return 0; + } else if (conn->status_code == MG_DOWNLOAD_SUCCESS) { + sprintf((char *) conn->connection_param, "%.*s", + (int) conn->content_len, conn->content); + } + return 1; } -static void *server_thread(void *param) { - int i; - for (i = 0; i < 10; i++) mg_poll_server((struct mg_server *) param, 1); - return NULL; +static int ts2(struct mg_connection *conn) { + if (conn->status_code == MG_CONNECT_SUCCESS) { + mg_printf(conn, "%s", "GET /non_exist HTTP/1.0\r\n\r\n"); + return 0; + } else if (conn->status_code == MG_DOWNLOAD_SUCCESS) { + sprintf((char *) conn->connection_param, "%s %.*s", + conn->uri, (int) conn->content_len, conn->content); + } + return 1; } static const char *test_server(void) { + char buf1[100] = "", buf2[100] = ""; struct mg_server *server = mg_create_server((void *) "foo"); ASSERT(server != NULL); @@ -440,34 +406,78 @@ static const char *test_server(void) { ASSERT(mg_set_option(server, "document_root", ".") == NULL); mg_add_uri_handler(server, "/cb1", cb1); mg_set_http_error_handler(server, error_handler); - mg_start_thread(server_thread, server); - RUN_TEST(test_regular_file); - RUN_TEST(test_server_param); - RUN_TEST(test_error_handler); - // TODO(lsm): come up with a better way of thread sync - sleep(1); + ASSERT(mg_connect(server, "127.0.0.1", atoi(HTTP_PORT), ts1, buf1) == 1); + ASSERT(mg_connect(server, "127.0.0.1", atoi(HTTP_PORT), ts2, buf2) == 1); - ASSERT(strcmp(static_config_options[URL_REWRITES * 2], "url_rewrites") == 0); + { int i; for (i = 0; i < 50; i++) mg_poll_server(server, 0); } + ASSERT(strcmp(buf1, "foo ? 127.0.0.1") == 0); + ASSERT(strcmp(buf2, "404 ERR: 404") == 0); + ASSERT(strcmp(static_config_options[URL_REWRITES * 2], "url_rewrites") == 0); mg_destroy_server(&server); ASSERT(server == NULL); return NULL; } +// This http client sends two requests using one connection static int cb2(struct mg_connection *conn) { - (void) conn; - return 0; + const char *file1 = "mongoose.h", *file2 = "mongoose.c", *file_name = file2; + char *buf = (char *) conn->connection_param, *file_data; + int file_len, ret_val = 1; + + switch (conn->status_code) { + case MG_CONNECT_SUCCESS: + mg_printf(conn, "GET /%s HTTP/1.1\r\n\r\n", file1); + strcat(buf, "a"); + ret_val = 0; + break; + case MG_CONNECT_FAILURE: strcat(buf, "b"); break; + case MG_DOWNLOAD_FAILURE: strcat(buf, "c"); break; + case MG_DOWNLOAD_SUCCESS: + if (!strcmp(buf, "a")) { + // Send second request + mg_printf(conn, "GET /%s HTTP/1.1\r\n\r\n", file2); + file_name = file1; + } + if ((file_data = read_file(file_name, &file_len)) != NULL) { + if (file_len == conn->content_len && + memcmp(file_data, conn->content, file_len) == 0) { + strcat(buf, "d"); + } else { + strcat(buf, "f"); + } + free(file_data); + } + ret_val = !strcmp(buf, "ad") ? 0 : 1; + break; + default: strcat(buf, "e"); break; + } + + return ret_val; +} + +static int cb3(struct mg_connection *conn) { + sprintf((char *) conn->connection_param, "%d", conn->status_code); + return 1; } static const char *test_mg_connect(void) { - char buf[1000]; - struct mg_server *server = mg_create_server(buf); - //mg_connect(server, "127.0.0.1", atoi(HTTP_PORT), cb2); + char buf2[40] = "", buf3[40] = ""; + struct mg_server *server = mg_create_server(NULL); + ASSERT(mg_set_option(server, "listening_port", LISTENING_ADDR) == NULL); ASSERT(mg_set_option(server, "document_root", ".") == NULL); - mg_add_uri_handler(server, "/cb1", cb2); + ASSERT(mg_connect(server, "", 0, NULL, NULL) == 0); + ASSERT(mg_connect(server, "127.0.0.1", atoi(HTTP_PORT), cb2, buf2) == 1); + ASSERT(mg_connect(server, "127.0.0.1", 1, cb3, buf3) == 1); + + { int i; for (i = 0; i < 50; i++) mg_poll_server(server, 0); } + + ASSERT(strcmp(buf2, "add") == 0); + ASSERT(strcmp(buf3, "1") == 0); mg_destroy_server(&server); + return NULL; } -- GitLab