diff options
Diffstat (limited to 'pjlib-util/src')
-rw-r--r-- | pjlib-util/src/pjlib-util-test/http_client.c | 765 | ||||
-rw-r--r-- | pjlib-util/src/pjlib-util-test/test.c | 4 | ||||
-rw-r--r-- | pjlib-util/src/pjlib-util-test/test.h | 2 | ||||
-rw-r--r-- | pjlib-util/src/pjlib-util/errno.c | 7 | ||||
-rw-r--r-- | pjlib-util/src/pjlib-util/http_client.c | 1006 |
5 files changed, 1784 insertions, 0 deletions
diff --git a/pjlib-util/src/pjlib-util-test/http_client.c b/pjlib-util/src/pjlib-util-test/http_client.c new file mode 100644 index 00000000..d179bbf3 --- /dev/null +++ b/pjlib-util/src/pjlib-util-test/http_client.c @@ -0,0 +1,765 @@ +/* $Id$ */ +/* + * Copyright (C) 2008-2010 Teluu Inc. (http://www.teluu.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "test.h" + +#if INCLUDE_HTTP_CLIENT_TEST + +#define THIS_FILE "test_http" +//#define VERBOSE +#define STR_PREC(s) (int)s.slen, s.ptr +#define USE_LOCAL_SERVER + +#include <pjlib.h> +#include <pjlib-util.h> + +#define ACTION_REPLY 0 +#define ACTION_IGNORE -1 + +static struct server_t +{ + pj_sock_t sock; + pj_uint16_t port; + pj_thread_t *thread; + + /* Action: + * 0: reply with the response in resp. + * -1: ignore query (to simulate timeout). + * other: reply with that error + */ + int action; + pj_bool_t send_content_length; + unsigned data_size; + unsigned buf_size; +} g_server; + +static pj_bool_t thread_quit; +static pj_timer_heap_t *timer_heap; +static pj_ioqueue_t *ioqueue; +static pj_pool_t *pool; +static pj_http_req *http_req; +static pj_bool_t test_cancel = PJ_FALSE; +static pj_size_t total_size; +static pj_size_t send_size = 0; +static pj_status_t sstatus; +static pj_sockaddr_in addr; +static int counter = 0; + +static int server_thread(void *p) +{ + struct server_t *srv = (struct server_t*)p; + pj_sock_t newsock; + + while (!thread_quit) { + char *pkt = pj_pool_alloc(pool, srv->buf_size); + pj_ssize_t pkt_len; + int rc; + pj_fd_set_t rset; + pj_time_val timeout = {0, 100}; + + rc = pj_sock_accept(srv->sock, &newsock, NULL, NULL); + if (rc != 0) + continue; + + PJ_FD_ZERO(&rset); + PJ_FD_SET(newsock, &rset); + rc = pj_sock_select(newsock+1, &rset, NULL, NULL, &timeout); + if (rc != 1) + continue; + + pkt_len = srv->buf_size; + do { + rc = pj_sock_recv(newsock, pkt, &pkt_len, 0); + if (rc != 0) { + app_perror("Server error receiving packet", rc); + continue; + } + rc = pj_sock_select(newsock+1, &rset, NULL, NULL, &timeout); + if (rc < 1) + break; + } while(1); + + /* Simulate network RTT */ + pj_thread_sleep(50); + + if (srv->action == ACTION_IGNORE) { + continue; + } else if (srv->action == ACTION_REPLY) { + unsigned send_size = 0, ctr = 0; + pj_ansi_sprintf(pkt, "HTTP/1.0 200 OK\r\n"); + if (srv->send_content_length) { + pj_ansi_sprintf(pkt + pj_ansi_strlen(pkt), + "Content-Length: %d\r\n", + srv->data_size); + } + pj_ansi_sprintf(pkt + pj_ansi_strlen(pkt), "\r\n"); + pkt_len = pj_ansi_strlen(pkt); + pj_sock_send(newsock, pkt, &pkt_len, 0); + while (send_size < srv->data_size) { + pkt_len = srv->data_size - send_size; + if (pkt_len > (signed)srv->buf_size) + pkt_len = srv->buf_size; + send_size += pkt_len; + pj_create_random_string(pkt, pkt_len); + pj_ansi_sprintf(pkt, "\nPacket: %d", ++ctr); + pkt[pj_ansi_strlen(pkt)] = '\n'; + pj_sock_send(newsock, pkt, &pkt_len, 0); + } + pj_sock_close(newsock); + } + } + + return 0; +} + +static void on_data_read(pj_http_req *hreq, void *data, pj_size_t size) +{ + PJ_UNUSED_ARG(hreq); + PJ_UNUSED_ARG(data); + + PJ_LOG(5, (THIS_FILE, "\nData received: %d bytes\n", size)); + if (size > 0) { +#ifdef VERBOSE + printf("%.*s\n", (int)size, (char *)data); +#endif + } +} + +static void on_send_data(pj_http_req *hreq, + void **data, pj_size_t *size) +{ + char *sdata; + pj_size_t sendsz = 8397; + + PJ_UNUSED_ARG(hreq); + + if (send_size + sendsz > total_size) { + sendsz = total_size - send_size; + } + send_size += sendsz; + + sdata = pj_pool_alloc(pool, sendsz); + pj_create_random_string(sdata, sendsz); + pj_ansi_sprintf(sdata, "\nSegment #%d\n", ++counter); + *data = sdata; + *size = sendsz; + + PJ_LOG(5, (THIS_FILE, "\nSending data progress: %d out of %d bytes\n", + send_size, total_size)); +} + + +static void on_complete(pj_http_req *hreq, pj_status_t status, + const pj_http_resp *resp) +{ + PJ_UNUSED_ARG(hreq); + + if (status == PJ_ECANCELLED) { + PJ_LOG(5, (THIS_FILE, "Request cancelled\n")); + return; + } else if (status == PJ_ETIMEDOUT) { + PJ_LOG(5, (THIS_FILE, "Request timed out!\n")); + return; + } else if (status != PJ_SUCCESS && status != PJ_EPENDING) { + PJ_LOG(3, (THIS_FILE, "Error %d\n", status)); + return; + } + PJ_LOG(5, (THIS_FILE, "\nData completed: %d bytes\n", resp->size)); + if (resp->size > 0 && resp->data) { +#ifdef VERBOSE + printf("%.*s\n", (int)resp->size, (char *)resp->data); +#endif + } +} + +static void on_response(pj_http_req *hreq, const pj_http_resp *resp) +{ + pj_size_t i; + + PJ_UNUSED_ARG(hreq); + PJ_UNUSED_ARG(resp); + PJ_UNUSED_ARG(i); + +#ifdef VERBOSE + printf("%.*s, %.*s, %.*s\n", STR_PREC(resp->version), + STR_PREC(resp->status_code), STR_PREC(resp->reason)); + for (i = 0; i < resp->headers.count; i++) { + printf("%.*s : %.*s\n", + STR_PREC(resp->headers.header[i].name), + STR_PREC(resp->headers.header[i].value)); + } +#endif + + if (test_cancel) { + pj_http_req_cancel(hreq, PJ_TRUE); + test_cancel = PJ_FALSE; + } +} + + +pj_status_t parse_url(const char *url) +{ + pj_str_t surl; + pj_http_url hurl; + pj_status_t status; + + pj_cstr(&surl, url); + status = pj_http_req_parse_url(&surl, &hurl); +#ifdef VERBOSE + if (!status) { + printf("URL: %s\nProtocol: %.*s\nHost: %.*s\nPort: %d\nPath: %.*s\n\n", + url, STR_PREC(hurl.protocol), STR_PREC(hurl.host), + hurl.port, STR_PREC(hurl.path)); + } else { + } +#endif + return status; +} + +int parse_url_test() +{ + /* Simple URL without '/' in the end */ + if (parse_url("http://www.google.com.sg") != PJ_SUCCESS) + return -11; + /* Simple URL with port number but without '/' in the end */ + if (parse_url("http://www.example.com:8080") != PJ_SUCCESS) + return -13; + /* URL with path */ + if (parse_url("http://127.0.0.1:280/Joomla/index.php?option=com_content&task=view&id=5&Itemid=6") + != PJ_SUCCESS) + return -15; + /* URL with port and path */ + if (parse_url("http://teluu.com:81/about-us/") != PJ_SUCCESS) + return -17; + /* unsupported protocol */ + if (parse_url("ftp://www.teluu.com") != PJ_ENOTSUP) + return -19; + /* invalid format */ + if (parse_url("http:/teluu.com/about-us/") != PJLIB_UTIL_EHTTPINURL) + return -21; + /* invalid port number */ + if (parse_url("http://teluu.com:xyz/") != PJLIB_UTIL_EHTTPINPORT) + return -23; + + return 0; +} + +/* + * GET request scenario 1: using on_response() and on_data_read() + * Server replies with content-length. Application cancels the + * request upon receiving the response, then start it again. + */ +int http_client_test1() +{ + pj_str_t url; + pj_http_req_callback hcb; + pj_http_req_param param; + + pj_bzero(&hcb, sizeof(hcb)); + hcb.on_complete = &on_complete; + hcb.on_data_read = &on_data_read; + hcb.on_response = &on_response; + pj_http_req_param_default(¶m); + + /* Create pool, timer, and ioqueue */ + pool = pj_pool_create(mem, NULL, 8192, 4096, NULL); + if (pj_timer_heap_create(pool, 16, &timer_heap)) + return -31; + if (pj_ioqueue_create(pool, 16, &ioqueue)) + return -32; + +#ifdef USE_LOCAL_SERVER + + pj_cstr(&url, "http://127.0.0.1:8080/about-us/"); + thread_quit = PJ_FALSE; + g_server.action = ACTION_REPLY; + g_server.send_content_length = PJ_TRUE; + g_server.data_size = 2970; + g_server.port = 8080; + g_server.buf_size = 1024; + + sstatus = pj_sock_socket(pj_AF_INET(), pj_SOCK_STREAM(), 0, + &g_server.sock); + if (sstatus != PJ_SUCCESS) + return -41; + + pj_sockaddr_in_init(&addr, NULL, (pj_uint16_t)g_server.port); + + sstatus = pj_sock_bind(g_server.sock, &addr, sizeof(addr)); + if (sstatus != PJ_SUCCESS) + return -43; + + sstatus = pj_sock_listen(g_server.sock, 8); + if (sstatus != PJ_SUCCESS) + return -45; + + sstatus = pj_thread_create(pool, NULL, &server_thread, &g_server, + 0, 0, &g_server.thread); + if (sstatus != PJ_SUCCESS) + return -47; + +#else + pj_cstr(&url, "http://www.teluu.com/about-us/"); +#endif + + if (pj_http_req_create(pool, &url, timer_heap, ioqueue, + ¶m, &hcb, &http_req)) + return -33; + + test_cancel = PJ_TRUE; + if (pj_http_req_start(http_req)) + return -35; + + while (pj_http_req_is_running(http_req)) { + pj_time_val delay = {0, 50}; + pj_ioqueue_poll(ioqueue, &delay); + pj_timer_heap_poll(timer_heap, NULL); + } + + if (pj_http_req_start(http_req)) + return -37; + + while (pj_http_req_is_running(http_req)) { + pj_time_val delay = {0, 50}; + pj_ioqueue_poll(ioqueue, &delay); + pj_timer_heap_poll(timer_heap, NULL); + } + +#ifdef USE_LOCAL_SERVER + thread_quit = PJ_TRUE; + pj_sock_close(g_server.sock); +#endif + + pj_http_req_destroy(http_req); + pj_ioqueue_destroy(ioqueue); + pj_timer_heap_destroy(timer_heap); + pj_pool_release(pool); + + return PJ_SUCCESS; +} + +/* + * GET request scenario 2: using on_complete() to get the + * complete data. Server does not reply with content-length. + * Request timed out, application sets a longer timeout, then + * then restart the request. + */ +int http_client_test2() +{ + pj_str_t url; + pj_http_req_callback hcb; + pj_http_req_param param; + pj_time_val timeout; + + pj_bzero(&hcb, sizeof(hcb)); + hcb.on_complete = &on_complete; + hcb.on_response = &on_response; + pj_http_req_param_default(¶m); + + /* Create pool, timer, and ioqueue */ + pool = pj_pool_create(mem, NULL, 8192, 4096, NULL); + if (pj_timer_heap_create(pool, 16, &timer_heap)) + return -41; + if (pj_ioqueue_create(pool, 16, &ioqueue)) + return -42; + +#ifdef USE_LOCAL_SERVER + + pj_cstr(&url, "http://127.0.0.1:380"); + param.timeout.sec = 0; + param.timeout.msec = 2000; + + thread_quit = PJ_FALSE; + g_server.action = ACTION_IGNORE; + g_server.send_content_length = PJ_FALSE; + g_server.data_size = 4173; + g_server.port = 380; + g_server.buf_size = 1024; + + sstatus = pj_sock_socket(pj_AF_INET(), pj_SOCK_STREAM(), 0, + &g_server.sock); + if (sstatus != PJ_SUCCESS) + return -41; + + pj_sockaddr_in_init(&addr, NULL, (pj_uint16_t)g_server.port); + + sstatus = pj_sock_bind(g_server.sock, &addr, sizeof(addr)); + if (sstatus != PJ_SUCCESS) + return -43; + + sstatus = pj_sock_listen(g_server.sock, 8); + if (sstatus != PJ_SUCCESS) + return -45; + + sstatus = pj_thread_create(pool, NULL, &server_thread, &g_server, + 0, 0, &g_server.thread); + if (sstatus != PJ_SUCCESS) + return -47; + +#else + pj_cstr(&url, "http://www.google.com.sg"); + param.timeout.sec = 0; + param.timeout.msec = 50; +#endif + + pj_http_headers_add_elmt2(¶m.headers, "Accept", + "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-ms-application, application/vnd.ms-xpsdocument, application/xaml+xml, application/x-ms-xbap, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*"); + pj_http_headers_add_elmt2(¶m.headers, "Accept-Language", "en-sg"); + pj_http_headers_add_elmt2(¶m.headers, "User-Agent", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.0.04506)"); + if (pj_http_req_create(pool, &url, timer_heap, ioqueue, + ¶m, &hcb, &http_req)) + return -43; + + if (pj_http_req_start(http_req)) + return -45; + + while (pj_http_req_is_running(http_req)) { + pj_time_val delay = {0, 50}; + pj_ioqueue_poll(ioqueue, &delay); + pj_timer_heap_poll(timer_heap, NULL); + } + +#ifdef USE_LOCAL_SERVER + g_server.action = ACTION_REPLY; +#endif + + timeout.sec = 0; timeout.msec = 10000; + pj_http_req_set_timeout(http_req, &timeout); + if (pj_http_req_start(http_req)) + return -47; + + while (pj_http_req_is_running(http_req)) { + pj_time_val delay = {0, 50}; + pj_ioqueue_poll(ioqueue, &delay); + pj_timer_heap_poll(timer_heap, NULL); + } + +#ifdef USE_LOCAL_SERVER + thread_quit = PJ_TRUE; + pj_sock_close(g_server.sock); +#endif + + pj_http_req_destroy(http_req); + pj_ioqueue_destroy(ioqueue); + pj_timer_heap_destroy(timer_heap); + pj_pool_release(pool); + + return PJ_SUCCESS; +} + +/* + * PUT request scenario 1: sending the whole data at once + */ +int http_client_test_put1() +{ + pj_str_t url; + pj_http_req_callback hcb; + pj_http_req_param param; + char *data; + int length = 3875; + + pj_bzero(&hcb, sizeof(hcb)); + hcb.on_complete = &on_complete; + hcb.on_data_read = &on_data_read; + hcb.on_response = &on_response; + + /* Create pool, timer, and ioqueue */ + pool = pj_pool_create(mem, NULL, 8192, 4096, NULL); + if (pj_timer_heap_create(pool, 16, &timer_heap)) + return -51; + if (pj_ioqueue_create(pool, 16, &ioqueue)) + return -52; + +#ifdef USE_LOCAL_SERVER + pj_cstr(&url, "http://127.0.0.1:380/test/test.txt"); + thread_quit = PJ_FALSE; + g_server.action = ACTION_REPLY; + g_server.send_content_length = PJ_TRUE; + g_server.data_size = 0; + g_server.port = 380; + g_server.buf_size = 4096; + + sstatus = pj_sock_socket(pj_AF_INET(), pj_SOCK_STREAM(), 0, + &g_server.sock); + if (sstatus != PJ_SUCCESS) + return -41; + + pj_sockaddr_in_init(&addr, NULL, (pj_uint16_t)g_server.port); + + sstatus = pj_sock_bind(g_server.sock, &addr, sizeof(addr)); + if (sstatus != PJ_SUCCESS) + return -43; + + sstatus = pj_sock_listen(g_server.sock, 8); + if (sstatus != PJ_SUCCESS) + return -45; + + sstatus = pj_thread_create(pool, NULL, &server_thread, &g_server, + 0, 0, &g_server.thread); + if (sstatus != PJ_SUCCESS) + return -47; + +#else + pj_cstr(&url, "http://127.0.0.1:280/test/test.txt"); + +#endif + + pj_http_req_param_default(¶m); + pj_strset2(¶m.method, "PUT"); + data = pj_pool_alloc(pool, length); + pj_create_random_string(data, length); + pj_ansi_sprintf(data, "PUT test\n"); + param.reqdata.data = data; + param.reqdata.size = length; + if (pj_http_req_create(pool, &url, timer_heap, ioqueue, + ¶m, &hcb, &http_req)) + return -53; + + if (pj_http_req_start(http_req)) + return -55; + + while (pj_http_req_is_running(http_req)) { + pj_time_val delay = {0, 50}; + pj_ioqueue_poll(ioqueue, &delay); + pj_timer_heap_poll(timer_heap, NULL); + } + +#ifdef USE_LOCAL_SERVER + thread_quit = PJ_TRUE; + pj_sock_close(g_server.sock); +#endif + + pj_http_req_destroy(http_req); + pj_ioqueue_destroy(ioqueue); + pj_timer_heap_destroy(timer_heap); + pj_pool_release(pool); + + return PJ_SUCCESS; +} + +/* + * PUT request scenario 2: using on_send_data() callback to + * sending the data in chunks + */ +int http_client_test_put2() +{ + pj_str_t url; + pj_http_req_callback hcb; + pj_http_req_param param; + + pj_bzero(&hcb, sizeof(hcb)); + hcb.on_complete = &on_complete; + hcb.on_send_data = &on_send_data; + hcb.on_data_read = &on_data_read; + hcb.on_response = &on_response; + + /* Create pool, timer, and ioqueue */ + pool = pj_pool_create(mem, NULL, 8192, 4096, NULL); + if (pj_timer_heap_create(pool, 16, &timer_heap)) + return -51; + if (pj_ioqueue_create(pool, 16, &ioqueue)) + return -52; + +#ifdef USE_LOCAL_SERVER + pj_cstr(&url, "http://127.0.0.1:380/test/test2.txt"); + thread_quit = PJ_FALSE; + g_server.action = ACTION_REPLY; + g_server.send_content_length = PJ_TRUE; + g_server.data_size = 0; + g_server.port = 380; + g_server.buf_size = 16384; + + sstatus = pj_sock_socket(pj_AF_INET(), pj_SOCK_STREAM(), 0, + &g_server.sock); + if (sstatus != PJ_SUCCESS) + return -41; + + pj_sockaddr_in_init(&addr, NULL, (pj_uint16_t)g_server.port); + + sstatus = pj_sock_bind(g_server.sock, &addr, sizeof(addr)); + if (sstatus != PJ_SUCCESS) + return -43; + + sstatus = pj_sock_listen(g_server.sock, 8); + if (sstatus != PJ_SUCCESS) + return -45; + + sstatus = pj_thread_create(pool, NULL, &server_thread, &g_server, + 0, 0, &g_server.thread); + if (sstatus != PJ_SUCCESS) + return -47; + +#else + pj_cstr(&url, "http://127.0.0.1:280/test/test2.txt"); + +#endif + + pj_http_req_param_default(¶m); + pj_strset2(¶m.method, "PUT"); + total_size = 15383; + send_size = 0; + param.reqdata.total_size = total_size; + if (pj_http_req_create(pool, &url, timer_heap, ioqueue, + ¶m, &hcb, &http_req)) + return -53; + + if (pj_http_req_start(http_req)) + return -55; + + while (pj_http_req_is_running(http_req)) { + pj_time_val delay = {0, 50}; + pj_ioqueue_poll(ioqueue, &delay); + pj_timer_heap_poll(timer_heap, NULL); + } + +#ifdef USE_LOCAL_SERVER + thread_quit = PJ_TRUE; + pj_sock_close(g_server.sock); +#endif + + pj_http_req_destroy(http_req); + pj_ioqueue_destroy(ioqueue); + pj_timer_heap_destroy(timer_heap); + pj_pool_release(pool); + + return PJ_SUCCESS; +} + +int http_client_test_delete() +{ + pj_str_t url; + pj_http_req_callback hcb; + pj_http_req_param param; + + pj_bzero(&hcb, sizeof(hcb)); + hcb.on_complete = &on_complete; + hcb.on_response = &on_response; + + /* Create pool, timer, and ioqueue */ + pool = pj_pool_create(mem, NULL, 8192, 4096, NULL); + if (pj_timer_heap_create(pool, 16, &timer_heap)) + return -61; + if (pj_ioqueue_create(pool, 16, &ioqueue)) + return -62; + +#ifdef USE_LOCAL_SERVER + pj_cstr(&url, "http://127.0.0.1:380/test/test2.txt"); + thread_quit = PJ_FALSE; + g_server.action = ACTION_REPLY; + g_server.send_content_length = PJ_TRUE; + g_server.data_size = 0; + g_server.port = 380; + g_server.buf_size = 1024; + + sstatus = pj_sock_socket(pj_AF_INET(), pj_SOCK_STREAM(), 0, + &g_server.sock); + if (sstatus != PJ_SUCCESS) + return -41; + + pj_sockaddr_in_init(&addr, NULL, (pj_uint16_t)g_server.port); + + sstatus = pj_sock_bind(g_server.sock, &addr, sizeof(addr)); + if (sstatus != PJ_SUCCESS) + return -43; + + sstatus = pj_sock_listen(g_server.sock, 8); + if (sstatus != PJ_SUCCESS) + return -45; + + sstatus = pj_thread_create(pool, NULL, &server_thread, &g_server, + 0, 0, &g_server.thread); + if (sstatus != PJ_SUCCESS) + return -47; + +#else + pj_cstr(&url, "http://127.0.0.1:280/test/test2.txt"); +#endif + + pj_http_req_param_default(¶m); + pj_strset2(¶m.method, "DELETE"); + if (pj_http_req_create(pool, &url, timer_heap, ioqueue, + ¶m, &hcb, &http_req)) + return -63; + + if (pj_http_req_start(http_req)) + return -65; + + while (pj_http_req_is_running(http_req)) { + pj_time_val delay = {0, 50}; + pj_ioqueue_poll(ioqueue, &delay); + pj_timer_heap_poll(timer_heap, NULL); + } + +#ifdef USE_LOCAL_SERVER + thread_quit = PJ_TRUE; + pj_sock_close(g_server.sock); +#endif + + pj_http_req_destroy(http_req); + pj_ioqueue_destroy(ioqueue); + pj_timer_heap_destroy(timer_heap); + pj_pool_release(pool); + + return PJ_SUCCESS; +} + +int http_client_test() +{ + int rc; + + PJ_LOG(3, (THIS_FILE, "..Testing URL parsing")); + rc = parse_url_test(); + if (rc) + return rc; + + PJ_LOG(3, (THIS_FILE, "..Testing GET request scenario 1")); + rc = http_client_test1(); + if (rc) + return rc; + + PJ_LOG(3, (THIS_FILE, "..Testing GET request scenario 2")); + rc = http_client_test2(); + if (rc) + return rc; + + PJ_LOG(3, (THIS_FILE, "..Testing PUT request scenario 1")); + rc = http_client_test_put1(); + if (rc) + return rc; + + PJ_LOG(3, (THIS_FILE, "..Testing PUT request scenario 2")); + rc = http_client_test_put2(); + if (rc) + return rc; + + PJ_LOG(3, (THIS_FILE, "..Testing DELETE request")); + rc = http_client_test_delete(); + if (rc) + return rc; + + return PJ_SUCCESS; +} + +#else +/* To prevent warning about "translation unit is empty" + * when this test is disabled. + */ +int dummy_http_client_test; +#endif /* INCLUDE_HTTP_CLIENT_TEST */ diff --git a/pjlib-util/src/pjlib-util-test/test.c b/pjlib-util/src/pjlib-util-test/test.c index 076ded19..d69028c0 100644 --- a/pjlib-util/src/pjlib-util-test/test.c +++ b/pjlib-util/src/pjlib-util-test/test.c @@ -85,6 +85,10 @@ static int test_inner(void) DO_TEST(resolver_test()); #endif +#if INCLUDE_HTTP_CLIENT_TEST + DO_TEST(http_client_test()); +#endif + on_return: return rc; } diff --git a/pjlib-util/src/pjlib-util-test/test.h b/pjlib-util/src/pjlib-util-test/test.h index 78a9aa15..69adb7db 100644 --- a/pjlib-util/src/pjlib-util-test/test.h +++ b/pjlib-util/src/pjlib-util-test/test.h @@ -23,6 +23,7 @@ #define INCLUDE_ENCRYPTION_TEST 1 #define INCLUDE_STUN_TEST 1 #define INCLUDE_RESOLVER_TEST 1 +#define INCLUDE_HTTP_CLIENT_TEST 1 extern int xml_test(void); extern int encryption_test(); @@ -30,6 +31,7 @@ extern int encryption_benchmark(); extern int stun_test(); extern int test_main(void); extern int resolver_test(void); +extern int http_client_test(); extern void app_perror(const char *title, pj_status_t rc); extern pj_pool_factory *mem; diff --git a/pjlib-util/src/pjlib-util/errno.c b/pjlib-util/src/pjlib-util/errno.c index 975f354f..9e726d12 100644 --- a/pjlib-util/src/pjlib-util/errno.c +++ b/pjlib-util/src/pjlib-util/errno.c @@ -88,6 +88,13 @@ static const struct PJ_BUILD_ERR( PJLIB_UTIL_ESTUNNOREALM, "Missing STUN REALM attribute"), PJ_BUILD_ERR( PJLIB_UTIL_ESTUNNONCE, "Missing/stale STUN NONCE attribute value"), PJ_BUILD_ERR( PJLIB_UTIL_ESTUNTSXFAILED, "STUN transaction terminates with failure"), + + /* HTTP Client */ + PJ_BUILD_ERR( PJLIB_UTIL_EHTTPINURL, "Invalid URL format"), + PJ_BUILD_ERR( PJLIB_UTIL_EHTTPINPORT, "Invalid URL port number"), + PJ_BUILD_ERR( PJLIB_UTIL_EHTTPINCHDR, "Incomplete response header received"), + PJ_BUILD_ERR( PJLIB_UTIL_EHTTPINSBUF, "Insufficient buffer"), + PJ_BUILD_ERR( PJLIB_UTIL_EHTTPLOST, "Connection lost"), }; #endif /* PJ_HAS_ERROR_STRING */ diff --git a/pjlib-util/src/pjlib-util/http_client.c b/pjlib-util/src/pjlib-util/http_client.c new file mode 100644 index 00000000..681799b6 --- /dev/null +++ b/pjlib-util/src/pjlib-util/http_client.c @@ -0,0 +1,1006 @@ +/* $Id$ */ +/* + * Copyright (C) 2008-2010 Teluu Inc. (http://www.teluu.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include <pjlib-util/http_client.h> +#include <pj/activesock.h> +#include <pj/assert.h> +#include <pj/errno.h> +#include <pj/except.h> +#include <pj/pool.h> +#include <pj/string.h> +#include <pj/timer.h> +#include <pjlib-util/errno.h> +#include <pjlib-util/scanner.h> + +#if 0 + /* Enable some tracing */ + #define THIS_FILE "http_client.c" + #define TRACE_(arg) PJ_LOG(3,arg) +#else + #define TRACE_(arg) +#endif + +#define NUM_PROTOCOL 2 +#define HTTP_1_0 "1.0" +#define HTTP_1_1 "1.1" +#define HTTP_SEPARATOR "://" +#define CONTENT_LENGTH "Content-Length" +/* Buffer size for sending/receiving messages. */ +#define BUF_SIZE 2048 +/* Initial data buffer size to store the data in case content- + * length is not specified in the server's response. + */ +#define INITIAL_DATA_BUF_SIZE 2048 +#define INITIAL_POOL_SIZE 1024 +#define POOL_INCREMENT_SIZE 512 + +enum http_protocol +{ + PROTOCOL_HTTP, + PROTOCOL_HTTPS +}; + +static char *http_protocol_names[NUM_PROTOCOL] = +{ + "HTTP", + "HTTPS" +}; + +static const unsigned int http_default_port[NUM_PROTOCOL] = +{ + 80, + 443 +}; + +enum http_method +{ + HTTP_GET, + HTTP_PUT, + HTTP_DELETE +}; + +static char *http_method_names[3] = +{ + "GET", + "PUT", + "DELETE" +}; + +enum http_state +{ + IDLE, + CONNECTING, + SENDING_REQUEST, + SENDING_REQUEST_BODY, + REQUEST_SENT, + READING_RESPONSE, + READING_DATA, + READING_COMPLETE, + ABORTING, +}; + +struct pj_http_req +{ + pj_str_t url; /* Request URL */ + pj_http_url hurl; /* Parsed request URL */ + pj_sockaddr addr; /* The host's socket address */ + pj_http_req_param param; /* HTTP request parameters */ + pj_pool_t *pool; /* Pool to allocate memory from */ + pj_timer_heap_t *timer; /* Timer for timeout management */ + pj_ioqueue_t *ioqueue; /* Ioqueue to use */ + pj_http_req_callback cb; /* Callbacks */ + pj_activesock_t *asock; /* Active socket */ + pj_status_t error; /* Error status */ + pj_str_t buffer; /* Buffer to send/receive msgs */ + enum http_state state; /* State of the HTTP request */ + pj_timer_entry timer_entry;/* Timer entry */ + pj_bool_t resolved; /* Whether URL's host is resolved */ + pj_http_resp response; /* HTTP response */ + pj_ioqueue_op_key_t op_key; + struct tcp_state + { + /* Total data sent so far if the data is sent in segments (i.e. + * if on_send_data() is not NULL and if param.reqdata.total_size > 0) + */ + pj_size_t tot_chunk_size; + /* Size of data to be sent (in a single activesock operation).*/ + pj_size_t send_size; + /* Data size sent so far. */ + pj_size_t current_send_size; + /* Total data received so far. */ + pj_size_t current_read_size; + } tcp_state; +}; + +/* Start sending the request */ +static pj_status_t http_req_start_sending(pj_http_req *hreq); +/* Start reading the response */ +static pj_status_t http_req_start_reading(pj_http_req *hreq); +/* End the request */ +static pj_status_t http_req_end_request(pj_http_req *hreq); +/* Parse the header data and populate the header fields with the result. */ +static pj_status_t http_headers_parse(char *hdata, pj_size_t size, + pj_http_headers *headers); +/* Parse the response */ +static pj_status_t http_response_parse(pj_pool_t *pool, + pj_http_resp *response, + void *data, pj_size_t size, + pj_size_t *remainder); + +static pj_uint16_t get_http_default_port(const pj_str_t *protocol) +{ + int i; + + for (i = 0; i < NUM_PROTOCOL; i++) { + if (!pj_stricmp2(protocol, http_protocol_names[i])) { + return (pj_uint16_t)http_default_port[i]; + } + } + return 0; +} + +static char * get_protocol(const pj_str_t *protocol) +{ + int i; + + for (i = 0; i < NUM_PROTOCOL; i++) { + if (!pj_stricmp2(protocol, http_protocol_names[i])) { + return http_protocol_names[i]; + } + } + + /* Should not happen */ + pj_assert(0); + return NULL; +} + + +/* Syntax error handler for parser. */ +static void on_syntax_error(pj_scanner *scanner) +{ + PJ_UNUSED_ARG(scanner); + PJ_THROW(PJ_EINVAL); // syntax error +} + +/* Callback when connection is established to the server */ +static pj_bool_t http_on_connect(pj_activesock_t *asock, + pj_status_t status) +{ + pj_http_req *hreq = (pj_http_req*) pj_activesock_get_user_data(asock); + + if (hreq->state == ABORTING) + return PJ_FALSE; + + if (status != PJ_SUCCESS) { + hreq->error = status; + pj_http_req_cancel(hreq, PJ_TRUE); + return PJ_FALSE; + } + + /* OK, we are connected. Start sending the request */ + hreq->state = SENDING_REQUEST; + http_req_start_sending(hreq); + return PJ_TRUE; +} + +static pj_bool_t http_on_data_sent(pj_activesock_t *asock, + pj_ioqueue_op_key_t *op_key, + pj_ssize_t sent) +{ + pj_http_req *hreq = (pj_http_req*) pj_activesock_get_user_data(asock); + + PJ_UNUSED_ARG(op_key); + + if (hreq->state == ABORTING) + return PJ_FALSE; + + if (sent <= 0) { + hreq->error = (sent < 0 ? -sent : PJLIB_UTIL_EHTTPLOST); + pj_http_req_cancel(hreq, PJ_TRUE); + return PJ_FALSE; + } + + hreq->tcp_state.current_send_size += sent; + TRACE_((THIS_FILE, "\nData sent: %d out of %d bytes\n", + hreq->tcp_state.current_send_size, hreq->tcp_state.send_size)); + if (hreq->tcp_state.current_send_size == hreq->tcp_state.send_size) { + /* Find out whether there is a request body to send. */ + if (hreq->param.reqdata.total_size > 0 || + hreq->param.reqdata.size > 0) + { + if (hreq->state == SENDING_REQUEST) { + /* Start sending the request body */ + hreq->state = SENDING_REQUEST_BODY; + hreq->tcp_state.tot_chunk_size = 0; + pj_assert(hreq->param.reqdata.total_size == 0 || + (hreq->param.reqdata.total_size > 0 && + hreq->param.reqdata.size == 0)); + } else { + /* Continue sending the next chunk of the request body */ + hreq->tcp_state.tot_chunk_size += hreq->tcp_state.send_size; + if (hreq->tcp_state.tot_chunk_size == + hreq->param.reqdata.total_size || + hreq->param.reqdata.total_size == 0) + { + /* Finish sending all the chunks, start reading + * the response. + */ + hreq->state = REQUEST_SENT; + http_req_start_reading(hreq); + return PJ_TRUE; + } + } + if (hreq->param.reqdata.total_size > 0 && + hreq->cb.on_send_data) + { + /* Call the callback for the application to provide + * the next chunk of data to be sent. + */ + (*hreq->cb.on_send_data)(hreq, &hreq->param.reqdata.data, + &hreq->param.reqdata.size); + /* Make sure the total data size given by the user does not + * exceed what the user originally said. + */ + pj_assert(hreq->tcp_state.tot_chunk_size + + hreq->param.reqdata.size <= + hreq->param.reqdata.total_size); + } + http_req_start_sending(hreq); + } else { + /* No request body, proceed to reading the server's response. */ + hreq->state = REQUEST_SENT; + http_req_start_reading(hreq); + } + } + return PJ_TRUE; +} + +static pj_bool_t http_on_data_read(pj_activesock_t *asock, + void *data, + pj_size_t size, + pj_status_t status, + pj_size_t *remainder) +{ + pj_http_req *hreq = (pj_http_req*) pj_activesock_get_user_data(asock); + + TRACE_((THIS_FILE, "\nData received: %d bytes\n", size)); + + if (hreq->state == ABORTING) + return PJ_FALSE; + + if (hreq->state == READING_RESPONSE) { + pj_status_t st; + pj_size_t rem; + + if (status != PJ_SUCCESS && status != PJ_EPENDING) { + hreq->error = status; + pj_http_req_cancel(hreq, PJ_TRUE); + return PJ_FALSE; + } + + /* Parse the response. */ + st = http_response_parse(hreq->pool, &hreq->response, + data, size, &rem); + if (st == PJLIB_UTIL_EHTTPINCHDR) { + /* If we already use up all our buffer and still + * hasn't received the whole header, return error + */ + if (size == BUF_SIZE) { + hreq->error = PJ_ETOOBIG; // response header size is too big + pj_http_req_cancel(hreq, PJ_TRUE); + return PJ_FALSE; + } + /* Keep the data if we do not get the whole response header */ + *remainder = size; + } else { + hreq->state = READING_DATA; + if (st != PJ_SUCCESS) { + /* Server replied with an invalid (or unknown) response + * format. We'll just pass the whole (unparsed) response + * to the user. + */ + hreq->response.data = data; + hreq->response.size = size - rem; + } + /* We already received the response header, call the + * appropriate callback. + */ + if (hreq->cb.on_response) + (*hreq->cb.on_response)(hreq, &hreq->response); + hreq->response.data = NULL; + hreq->response.size = 0; + if (rem > 0) { + /* There is some response data remaining after parsing the + * header, move it to the front of the buffer. + */ + pj_memmove((char *)data, (char *)data + size - rem, rem); + *remainder = rem; + } + /* Speed up the operation a bit rather than waiting for EOF */ + if (hreq->response.content_length == 0) { + return http_on_data_read(asock, NULL, 0, PJ_SUCCESS, NULL); + } + + } + + return PJ_TRUE; + } + + pj_assert(hreq->state == READING_DATA); + if (hreq->cb.on_data_read) { + /* If application wishes to receive the data once available, call + * its callback. + */ + if (size > 0) + (*hreq->cb.on_data_read)(hreq, data, size); + } else { + if (hreq->response.size == 0) { + /* If we know the content length, allocate the data based + * on that, otherwise we'll use initial buffer size and grow + * it later if necessary. + */ + hreq->response.size = (hreq->response.content_length == -1 ? + INITIAL_DATA_BUF_SIZE : + hreq->response.content_length); + hreq->response.data = pj_pool_alloc(hreq->pool, + hreq->response.size); + } + + /* If the size of data received exceeds its current size, + * grow the buffer by a factor of 2. + */ + if (hreq->tcp_state.current_read_size + size > + hreq->response.size) + { + void *olddata = hreq->response.data; + + hreq->response.data = pj_pool_alloc(hreq->pool, + hreq->response.size << 1); + pj_memcpy(hreq->response.data, olddata, hreq->response.size); + hreq->response.size <<= 1; + } + + /* Append the response data. */ + pj_memcpy((char *)hreq->response.data + + hreq->tcp_state.current_read_size, data, size); + } + hreq->tcp_state.current_read_size += size; + + /* If the total data received so far is equal to the content length + * or if it's already EOF. + */ + if ((pj_ssize_t)hreq->tcp_state.current_read_size >= + hreq->response.content_length || + (status == PJ_EEOF && hreq->response.content_length == -1)) + { + /* Finish reading */ + http_req_end_request(hreq); + hreq->response.size = hreq->tcp_state.current_read_size; + + /* HTTP request is completed, call the callback. */ + if (hreq->cb.on_complete) { + (*hreq->cb.on_complete)(hreq, PJ_SUCCESS, &hreq->response); + } + + return PJ_FALSE; + } + + /* Error status or premature EOF. */ + if ((status != PJ_SUCCESS && status != PJ_EPENDING && status != PJ_EEOF) + || (status == PJ_EEOF && hreq->response.content_length > -1)) + { + hreq->error = status; + pj_http_req_cancel(hreq, PJ_TRUE); + return PJ_FALSE; + } + + return PJ_TRUE; +} + +/* Callback to be called when query has timed out */ +static void on_timeout( pj_timer_heap_t *timer_heap, + struct pj_timer_entry *entry) +{ + pj_http_req *hreq = (pj_http_req *) entry->user_data; + + PJ_UNUSED_ARG(timer_heap); + + /* Recheck that the request is still not completed, since there is a + * slight possibility of race condition (timer elapsed while at the + * same time response arrives). + */ + if (hreq->state == READING_COMPLETE) { + /* Yeah, we finish on time */ + return; + } + + /* Invalidate id. */ + hreq->timer_entry.id = 0; + + /* Request timed out. */ + hreq->error = PJ_ETIMEDOUT; + pj_http_req_cancel(hreq, PJ_TRUE); +} + +/* The same as #pj_http_headers_add_elmt() with char * as + * its parameters. + */ +PJ_DEF(pj_status_t) pj_http_headers_add_elmt2(pj_http_headers *headers, + char *name, char *val) +{ + pj_str_t f, v; + pj_cstr(&f, name); + pj_cstr(&v, val); + return pj_http_headers_add_elmt(headers, &f, &v); +} + +PJ_DEF(pj_status_t) pj_http_headers_add_elmt(pj_http_headers *headers, + pj_str_t *name, + pj_str_t *val) +{ + PJ_ASSERT_RETURN(headers && name && val, PJ_FALSE); + if (headers->count >= PJ_HTTP_HEADER_SIZE) + return PJ_ETOOMANY; + pj_strassign(&headers->header[headers->count].name, name); + pj_strassign(&headers->header[headers->count++].value, val); + return PJ_SUCCESS; +} + +static pj_status_t http_response_parse(pj_pool_t *pool, + pj_http_resp *response, + void *data, pj_size_t size, + pj_size_t *remainder) +{ + pj_size_t i; + char *cptr; + void *newdata; + pj_scanner scanner; + pj_status_t status; + + PJ_USE_EXCEPTION; + + PJ_ASSERT_RETURN(response, PJ_EINVAL); + if (size < 2) + return PJLIB_UTIL_EHTTPINCHDR; + /* Detect whether we already receive the response's status-line + * and its headers. We're looking for a pair of CRLFs. A pair of + * LFs is also supported although it is not RFC standard. + */ + cptr = (char *)data; + for (i = 1, cptr++; i < size; i++, cptr++) { + if (*cptr == '\n') { + if (*(cptr - 1) == '\n') + break; + if (*(cptr - 1) == '\r') { + if (i >= 3 && *(cptr - 2) == '\n' && *(cptr - 3) == '\r') + break; + } + } + } + if (i == size) + return PJLIB_UTIL_EHTTPINCHDR; + *remainder = size - 1 - i; + + pj_bzero(response, sizeof(response)); + response->content_length = -1; + + newdata = pj_pool_alloc(pool, i); + pj_memcpy(newdata, data, i); + + /* Parse the status-line. */ + pj_scan_init(&scanner, newdata, i, 0, &on_syntax_error); + PJ_TRY { + pj_scan_get_until_ch(&scanner, ' ', &response->version); + pj_scan_advance_n(&scanner, 1, PJ_FALSE); + pj_scan_get_until_ch(&scanner, ' ', &response->status_code); + pj_scan_advance_n(&scanner, 1, PJ_FALSE); + pj_scan_get_until_ch(&scanner, '\n', &response->reason); + if (response->reason.ptr[response->reason.slen-1] == '\r') + response->reason.slen--; + } + PJ_CATCH_ANY { + pj_scan_fini(&scanner); + return PJ_GET_EXCEPTION(); + } + PJ_END; + + /* Parse the response headers. */ + size = i - 2 - (scanner.curptr - (char *)newdata); + if (size > 0) { + status = http_headers_parse(scanner.curptr + 1, size, + &response->headers); + } else { + status = PJ_SUCCESS; + } + + /* Find content-length header field. */ + for (i = 0; i < response->headers.count; i++) { + if (!pj_stricmp2(&response->headers.header[i].name, + CONTENT_LENGTH)) + { + response->content_length = + pj_strtoul(&response->headers.header[i].value); + /* If content length is zero, make sure that it is because the + * header value is really zero and not due to parsing error. + */ + if (response->content_length == 0) { + if (pj_strcmp2(&response->headers.header[i].value, "0")) { + response->content_length = -1; + } + } + break; + } + } + + pj_scan_fini(&scanner); + + return status; +} + +static pj_status_t http_headers_parse(char *hdata, pj_size_t size, + pj_http_headers *headers) +{ + pj_scanner scanner; + pj_str_t s, s2; + pj_status_t status; + PJ_USE_EXCEPTION; + + PJ_ASSERT_RETURN(headers, PJ_EINVAL); + + pj_scan_init(&scanner, hdata, size, 0, &on_syntax_error); + + /* Parse each line of header field consisting of header field name and + * value, separated by ":" and any number of white spaces. + */ + PJ_TRY { + do { + pj_scan_get_until_chr(&scanner, ":\n", &s); + if (*scanner.curptr == ':') { + pj_scan_advance_n(&scanner, 1, PJ_TRUE); + pj_scan_get_until_ch(&scanner, '\n', &s2); + if (s2.ptr[s2.slen-1] == '\r') + s2.slen--; + status = pj_http_headers_add_elmt(headers, &s, &s2); + if (status != PJ_SUCCESS) + PJ_THROW(status); + } + pj_scan_advance_n(&scanner, 1, PJ_TRUE); + /* Finish parsing */ + if (pj_scan_is_eof(&scanner)) + break; + } while (1); + } + PJ_CATCH_ANY { + pj_scan_fini(&scanner); + return PJ_GET_EXCEPTION(); + } + PJ_END; + + pj_scan_fini(&scanner); + + return PJ_SUCCESS; +} + +PJ_DEF(void) pj_http_req_param_default(pj_http_req_param *param) +{ + pj_assert(param); + pj_bzero(param, sizeof(*param)); + param->addr_family = pj_AF_INET(); + pj_strset2(¶m->method, http_method_names[HTTP_GET]); + pj_strset2(¶m->version, HTTP_1_0); + param->timeout.msec = PJ_HTTP_DEFAULT_TIMEOUT; + pj_time_val_normalize(¶m->timeout); +} + +PJ_DEF(pj_status_t) pj_http_req_parse_url(const pj_str_t *url, + pj_http_url *hurl) +{ + pj_scanner scanner; + int len = url->slen; + PJ_USE_EXCEPTION; + + if (!len) return -1; + + pj_scan_init(&scanner, url->ptr, url->slen, 0, &on_syntax_error); + + PJ_TRY { + pj_str_t s; + + /* Exhaust any whitespaces. */ + pj_scan_skip_whitespace(&scanner); + + /* Parse the protocol */ + pj_scan_get_until_ch(&scanner, ':', &s); + if (!pj_stricmp2(&s, http_protocol_names[PROTOCOL_HTTP])) { + pj_strset2(&hurl->protocol, http_protocol_names[PROTOCOL_HTTP]); + } else if (!pj_stricmp2(&s, http_protocol_names[PROTOCOL_HTTPS])) { + pj_strset2(&hurl->protocol, http_protocol_names[PROTOCOL_HTTPS]); + } else { + PJ_THROW(PJ_ENOTSUP); // unsupported protocol + } + + if (pj_scan_strcmp(&scanner, HTTP_SEPARATOR, + pj_ansi_strlen(HTTP_SEPARATOR))) + { + PJ_THROW(PJLIB_UTIL_EHTTPINURL); // no "://" after protocol name + } + pj_scan_advance_n(&scanner, pj_ansi_strlen(HTTP_SEPARATOR), PJ_FALSE); + + /* Parse the host and port number (if any) */ + pj_scan_get_until_chr(&scanner, ":/", &s); + pj_strassign(&hurl->host, &s); + if (pj_scan_is_eof(&scanner) || *scanner.curptr == '/') { + /* No port number specified */ + /* Assume default http/https port number */ + hurl->port = get_http_default_port(&hurl->protocol); + pj_assert(hurl->port > 0); + } else { + pj_scan_advance_n(&scanner, 1, PJ_FALSE); + pj_scan_get_until_ch(&scanner, '/', &s); + /* Parse the port number */ + hurl->port = (pj_uint16_t)pj_strtoul(&s); + if (!hurl->port) + PJ_THROW(PJLIB_UTIL_EHTTPINPORT); // invalid port number + } + + if (!pj_scan_is_eof(&scanner)) { + hurl->path.ptr = scanner.curptr; + hurl->path.slen = scanner.end - scanner.curptr; + } else { + /* no path, append '/' */ + pj_cstr(&hurl->path, "/"); + } + } + PJ_CATCH_ANY { + pj_scan_fini(&scanner); + return PJ_GET_EXCEPTION(); + } + PJ_END; + + pj_scan_fini(&scanner); + return PJ_SUCCESS; +} + +PJ_DEF(void) pj_http_req_set_timeout(pj_http_req *http_req, + const pj_time_val* timeout) +{ + pj_memcpy(&http_req->param.timeout, timeout, sizeof(*timeout)); +} + +PJ_DEF(pj_status_t) pj_http_req_create(pj_pool_t *pool, + const pj_str_t *url, + pj_timer_heap_t *timer, + pj_ioqueue_t *ioqueue, + const pj_http_req_param *param, + const pj_http_req_callback *hcb, + pj_http_req **http_req) +{ + pj_pool_t *own_pool; + pj_http_req *hreq; + pj_status_t status; + + PJ_ASSERT_RETURN(pool && url && timer && ioqueue && + hcb && http_req, PJ_EINVAL); + + *http_req = NULL; + own_pool = pj_pool_create(pool->factory, NULL, INITIAL_POOL_SIZE, + POOL_INCREMENT_SIZE, NULL); + hreq = PJ_POOL_ZALLOC_T(own_pool, struct pj_http_req); + if (!hreq) + return PJ_ENOMEM; + + /* Initialization */ + hreq->pool = own_pool; + hreq->ioqueue = ioqueue; + hreq->timer = timer; + hreq->asock = NULL; + pj_memcpy(&hreq->cb, hcb, sizeof(*hcb)); + hreq->state = IDLE; + hreq->resolved = PJ_FALSE; + hreq->buffer.ptr = NULL; + pj_timer_entry_init(&hreq->timer_entry, 0, hreq, &on_timeout); + + /* Initialize parameter */ + if (param) { + pj_memcpy(&hreq->param, param, sizeof(*param)); + /* TODO: validate the param here + * Should we validate the method as well? If yes, based on all HTTP + * methods or based on supported methods only? For the later, one + * drawback would be that you can't use this if the method is not + * officially supported + */ + PJ_ASSERT_RETURN(hreq->param.addr_family==PJ_AF_UNSPEC || + hreq->param.addr_family==PJ_AF_INET || + hreq->param.addr_family==PJ_AF_INET6, PJ_EAFNOTSUP); + PJ_ASSERT_RETURN(!pj_strcmp2(&hreq->param.version, HTTP_1_0) || + !pj_strcmp2(&hreq->param.version, HTTP_1_1), + PJ_ENOTSUP); + pj_time_val_normalize(&hreq->param.timeout); + } else { + pj_http_req_param_default(&hreq->param); + } + + /* Parse the URL */ + if (!pj_strdup(hreq->pool, &hreq->url, url)) + return PJ_ENOMEM; + status = pj_http_req_parse_url(&hreq->url, &hreq->hurl); + if (status != PJ_SUCCESS) + return status; // Invalid URL supplied + + *http_req = hreq; + return PJ_SUCCESS; +} + +PJ_DEF(pj_bool_t) pj_http_req_is_running(const pj_http_req *http_req) +{ + PJ_ASSERT_RETURN(http_req, PJ_FALSE); + return (http_req->state != IDLE); +} + +PJ_DEF(void*) pj_http_req_get_user_data(pj_http_req *http_req) +{ + PJ_ASSERT_RETURN(http_req, NULL); + return http_req->param.user_data; +} + +PJ_DEF(pj_status_t) pj_http_req_start(pj_http_req *http_req) +{ + pj_sock_t sock = PJ_INVALID_SOCKET; + pj_status_t status; + pj_activesock_cb asock_cb; + + PJ_ASSERT_RETURN(http_req, PJ_EINVAL); + /* Http request is not idle, a request was initiated before and + * is still in progress + */ + PJ_ASSERT_RETURN(http_req->state == IDLE, PJ_EBUSY); + + http_req->error = 0; + + if (!http_req->resolved) { + /* Resolve the Internet address of the host */ + status = pj_sockaddr_init(http_req->param.addr_family, + &http_req->addr, &http_req->hurl.host, + http_req->hurl.port); + if (status != PJ_SUCCESS || + !pj_sockaddr_has_addr(&http_req->addr) || + (http_req->param.addr_family==pj_AF_INET() && + http_req->addr.ipv4.sin_addr.s_addr==PJ_INADDR_NONE)) + { + return status; // cannot resolve host name + } + http_req->resolved = PJ_TRUE; + } + + status = pj_sock_socket(http_req->param.addr_family, pj_SOCK_STREAM(), + 0, &sock); + if (status != PJ_SUCCESS) + goto on_return; // error creating socket + + pj_bzero(&asock_cb, sizeof(asock_cb)); + asock_cb.on_data_read = &http_on_data_read; + asock_cb.on_data_sent = &http_on_data_sent; + asock_cb.on_connect_complete = &http_on_connect; + + // TODO: should we set whole data to 0 by default? + // or add it in the param? + status = pj_activesock_create(http_req->pool, sock, pj_SOCK_STREAM(), + NULL, http_req->ioqueue, + &asock_cb, http_req, &http_req->asock); + if (status != PJ_SUCCESS) { + if (sock != PJ_INVALID_SOCKET) + pj_sock_close(sock); + goto on_return; // error creating activesock + } + + /* Schedule timeout timer for the request */ + pj_assert(http_req->timer_entry.id == 0); + http_req->timer_entry.id = 1; + status = pj_timer_heap_schedule(http_req->timer, &http_req->timer_entry, + &http_req->param.timeout); + if (status != PJ_SUCCESS) { + http_req->timer_entry.id = 0; + goto on_return; // error scheduling timer + } + + /* Connect to host */ + http_req->state = CONNECTING; + status = pj_activesock_start_connect(http_req->asock, http_req->pool, + (pj_sock_t *)&(http_req->addr), + pj_sockaddr_get_len(&http_req->addr)); + if (status == PJ_SUCCESS) { + http_req->state = SENDING_REQUEST; + return http_req_start_sending(http_req); + } else if (status != PJ_EPENDING) { + goto on_return; // error connecting + } + + return PJ_SUCCESS; + +on_return: + http_req_end_request(http_req); + return status; +} + +#define STR_PREC(s) s.slen, s.ptr + +/* snprintf() to a pj_str_t struct with an option to append the + * result at the back of the string. + */ +void str_snprintf(pj_str_t *s, size_t size, + pj_bool_t append, const char *format, ...) +{ + va_list arg; + int retval; + + va_start(arg, format); + if (!append) + s->slen = 0; + size -= s->slen; + retval = pj_ansi_vsnprintf(s->ptr + s->slen, + size, format, arg); + s->slen += ((retval < (int)size) ? retval : size - 1); + va_end(arg); +} + +static pj_status_t http_req_start_sending(pj_http_req *hreq) +{ + pj_status_t status; + pj_str_t pkt; + pj_ssize_t len; + pj_size_t i; + + PJ_ASSERT_RETURN(hreq->state == SENDING_REQUEST || + hreq->state == SENDING_REQUEST_BODY, PJ_EBUG); + + if (hreq->state == SENDING_REQUEST) { + /* Prepare the request data */ + if (!hreq->buffer.ptr) + hreq->buffer.ptr = (char*)pj_pool_alloc(hreq->pool, BUF_SIZE); + pj_strassign(&pkt, &hreq->buffer); + pkt.slen = 0; + /* Start-line */ + str_snprintf(&pkt, BUF_SIZE, PJ_TRUE, "%.*s %.*s %s/%.*s\n", + STR_PREC(hreq->param.method), + STR_PREC(hreq->hurl.path), + get_protocol(&hreq->hurl.protocol), + STR_PREC(hreq->param.version)); + /* Header field "Host" */ + str_snprintf(&pkt, BUF_SIZE, PJ_TRUE, "Host: %.*s\n", + STR_PREC(hreq->hurl.host)); + if (!pj_strcmp2(&hreq->param.method, http_method_names[HTTP_PUT])) { + char buf[16]; + + /* Header field "Content-Length" */ + pj_utoa(hreq->param.reqdata.total_size ? + hreq->param.reqdata.total_size: + hreq->param.reqdata.size, buf); + str_snprintf(&pkt, BUF_SIZE, PJ_TRUE, "%s: %s\n", + CONTENT_LENGTH, buf); + } + + /* Append user-specified headers */ + for (i = 0; i < hreq->param.headers.count; i++) { + str_snprintf(&pkt, BUF_SIZE, PJ_TRUE, "%.*s: %.*s\n", + STR_PREC(hreq->param.headers.header[i].name), + STR_PREC(hreq->param.headers.header[i].value)); + } + if (pkt.slen >= BUF_SIZE - 1) { + status = PJLIB_UTIL_EHTTPINSBUF; + goto on_return; + } + + pj_strcat2(&pkt, "\n"); + pkt.ptr[pkt.slen] = 0; + TRACE_((THIS_FILE, "%s\n", pkt.ptr)); + } else { + pkt.ptr = hreq->param.reqdata.data; + pkt.slen = hreq->param.reqdata.size; + } + + /* Send the request */ + len = pj_strlen(&pkt); + pj_ioqueue_op_key_init(&hreq->op_key, sizeof(hreq->op_key)); + hreq->tcp_state.send_size = len; + hreq->tcp_state.current_send_size = 0; + status = pj_activesock_send(hreq->asock, &hreq->op_key, + pkt.ptr, &len, 0); + + if (status == PJ_SUCCESS) { + http_on_data_sent(hreq->asock, &hreq->op_key, len); + } else if (status != PJ_EPENDING) { + goto on_return; // error sending data + } + + return PJ_SUCCESS; + +on_return: + http_req_end_request(hreq); + return status; +} + +static pj_status_t http_req_start_reading(pj_http_req *hreq) +{ + pj_status_t status; + + PJ_ASSERT_RETURN(hreq->state == REQUEST_SENT, PJ_EBUG); + + /* Receive the response */ + hreq->state = READING_RESPONSE; + hreq->tcp_state.current_read_size = 0; + pj_assert(hreq->buffer.ptr); + status = pj_activesock_start_read2(hreq->asock, hreq->pool, BUF_SIZE, + &hreq->buffer.ptr, 0); + if (status != PJ_SUCCESS) { + /* Error reading */ + http_req_end_request(hreq); + return status; + } + + return PJ_SUCCESS; +} + +static pj_status_t http_req_end_request(pj_http_req *hreq) +{ + if (hreq->asock) { + pj_activesock_close(hreq->asock); + hreq->asock = NULL; + } + + /* Cancel query timeout timer. */ + if (hreq->timer_entry.id != 0) { + pj_timer_heap_cancel(hreq->timer, &hreq->timer_entry); + /* Invalidate id. */ + hreq->timer_entry.id = 0; + } + + hreq->state = IDLE; + + return PJ_SUCCESS; +} + +PJ_DEF(pj_status_t) pj_http_req_cancel(pj_http_req *http_req, + pj_bool_t notify) +{ + http_req->state = ABORTING; + + http_req_end_request(http_req); + + if (notify && http_req->cb.on_complete) { + (*http_req->cb.on_complete)(http_req, (!http_req->error? + PJ_ECANCELLED: http_req->error), NULL); + } + + return PJ_SUCCESS; +} + + +PJ_DEF(pj_status_t) pj_http_req_destroy(pj_http_req *http_req) +{ + PJ_ASSERT_RETURN(http_req, PJ_EINVAL); + + /* If there is any pending request, cancel it */ + if (http_req->state != IDLE) { + pj_http_req_cancel(http_req, PJ_FALSE); + } + + pj_pool_release(http_req->pool); + + return PJ_SUCCESS; +} |