From b813b2c567747c86ebe2c3de24ed6af26119ccf2 Mon Sep 17 00:00:00 2001 From: Liong Sauw Ming Date: Thu, 4 Feb 2010 18:29:16 +0000 Subject: Implemented ticket #1018: Simple HTTP client implementation * pjlib-util: * implement http_client * pjlib-util-test: * unit test for http_client * pjsip-apps/samples: * sample http client implementation * build: * added http_client support on VS6, VS2005, MMP, and Makefile git-svn-id: http://svn.pjsip.org/repos/pjproject/trunk@3087 74dad513-b988-da41-8d7b-12977e46ad98 --- build.symbian/pjlib_util.mmp | 2 + pjlib-util/build/Makefile | 5 +- pjlib-util/build/pjlib_util.dsp | 8 + pjlib-util/build/pjlib_util.vcproj | 628 ++++++++-------- pjlib-util/build/pjlib_util_test.dsp | 4 + pjlib-util/build/pjlib_util_test.vcproj | 456 ++++++------ pjlib-util/include/pjlib-util.h | 3 + pjlib-util/include/pjlib-util/config.h | 11 + pjlib-util/include/pjlib-util/errno.h | 30 +- pjlib-util/include/pjlib-util/http_client.h | 381 ++++++++++ pjlib-util/src/pjlib-util-test/http_client.c | 765 ++++++++++++++++++++ pjlib-util/src/pjlib-util-test/test.c | 4 + pjlib-util/src/pjlib-util-test/test.h | 2 + pjlib-util/src/pjlib-util/errno.c | 7 + pjlib-util/src/pjlib-util/http_client.c | 1006 ++++++++++++++++++++++++++ pjsip-apps/build/Samples-vc.mak | 1 + pjsip-apps/build/Samples.mak | 1 + pjsip-apps/build/samples.dsp | 4 + pjsip-apps/build/samples.vcproj | 354 ++++----- pjsip-apps/src/samples/httpdemo.c | 151 ++++ 20 files changed, 3108 insertions(+), 715 deletions(-) create mode 100644 pjlib-util/include/pjlib-util/http_client.h create mode 100644 pjlib-util/src/pjlib-util-test/http_client.c create mode 100644 pjlib-util/src/pjlib-util/http_client.c create mode 100644 pjsip-apps/src/samples/httpdemo.c diff --git a/build.symbian/pjlib_util.mmp b/build.symbian/pjlib_util.mmp index 3eb3701a..fbf4b426 100644 --- a/build.symbian/pjlib_util.mmp +++ b/build.symbian/pjlib_util.mmp @@ -40,6 +40,7 @@ SOURCE errno.c SOURCE getopt.c SOURCE hmac_md5.c SOURCE hmac_sha1.c +SOURCE http_client.c SOURCE md5.c SOURCE pcap.c SOURCE resolver_wrap.cpp @@ -61,6 +62,7 @@ SOURCE xml_wrap.cpp //DOCUMENT pjlib-util\\getopt.h //DOCUMENT pjlib-util\\hmac_md5.h //DOCUMENT pjlib-util\hmac_sha1.h +//DOCUMENT pjlib-util\http_client.h //DOCUMENT pjlib-util\md5.h //DOCUMENT pjlib-util\resolver.h //DOCUMENT pjlib-util\scanner.h diff --git a/pjlib-util/build/Makefile b/pjlib-util/build/Makefile index ef0939af..6acea05b 100644 --- a/pjlib-util/build/Makefile +++ b/pjlib-util/build/Makefile @@ -28,7 +28,7 @@ export _LDFLAGS := $(subst /,$(HOST_PSEP),$(PJLIB_UTIL_LIB)) \ export PJLIB_UTIL_SRCDIR = ../src/pjlib-util export PJLIB_UTIL_OBJS += $(OS_OBJS) $(M_OBJS) $(CC_OBJS) $(HOST_OBJS) \ base64.o crc32.o errno.o dns.o dns_dump.o dns_server.o \ - getopt.o hmac_md5.o hmac_sha1.o md5.o pcap.o resolver.o \ + getopt.o hmac_md5.o hmac_sha1.o http_client.o md5.o pcap.o resolver.o \ scanner.o sha1.o srv_resolver.o string.o stun_simple.o \ stun_simple_client.o xml.o export PJLIB_UTIL_CFLAGS += $(_CFLAGS) @@ -37,7 +37,8 @@ export PJLIB_UTIL_CFLAGS += $(_CFLAGS) # Defines for building test application # export UTIL_TEST_SRCDIR = ../src/pjlib-util-test -export UTIL_TEST_OBJS += xml.o encryption.o stun.o resolver_test.o test.o +export UTIL_TEST_OBJS += xml.o encryption.o stun.o resolver_test.o test.o \ + http_client.o export UTIL_TEST_CFLAGS += $(_CFLAGS) export UTIL_TEST_LDFLAGS += $(_LDFLAGS) export UTIL_TEST_EXE:=../bin/pjlib-util-test-$(TARGET_NAME)$(HOST_EXE) diff --git a/pjlib-util/build/pjlib_util.dsp b/pjlib-util/build/pjlib_util.dsp index 15863579..dfbb3318 100644 --- a/pjlib-util/build/pjlib_util.dsp +++ b/pjlib-util/build/pjlib_util.dsp @@ -125,6 +125,10 @@ SOURCE="..\src\pjlib-util\hmac_sha1.c" # End Source File # Begin Source File +SOURCE="..\src\pjlib-util\http_client.c" +# End Source File +# Begin Source File + SOURCE="..\src\pjlib-util\md5.c" # End Source File # Begin Source File @@ -219,6 +223,10 @@ SOURCE="..\include\pjlib-util\hmac_sha1.h" # End Source File # Begin Source File +SOURCE="..\include\pjlib-util\http_client.h" +# End Source File +# Begin Source File + SOURCE="..\include\pjlib-util\md5.h" # End Source File # Begin Source File diff --git a/pjlib-util/build/pjlib_util.vcproj b/pjlib-util/build/pjlib_util.vcproj index 25a70bde..4eb8cadf 100644 --- a/pjlib-util/build/pjlib_util.vcproj +++ b/pjlib-util/build/pjlib_util.vcproj @@ -11,16 +11,16 @@ Name="Win32" /> + + @@ -3370,7 +3374,7 @@ /> + + diff --git a/pjlib-util/build/pjlib_util_test.dsp b/pjlib-util/build/pjlib_util_test.dsp index 3ea7e71d..0742ecdd 100644 --- a/pjlib-util/build/pjlib_util_test.dsp +++ b/pjlib-util/build/pjlib_util_test.dsp @@ -91,6 +91,10 @@ SOURCE="..\src\pjlib-util-test\encryption.c" # End Source File # Begin Source File +SOURCE="..\src\pjlib-util-test\http_client.c" +# End Source File +# Begin Source File + SOURCE="..\src\pjlib-util-test\main.c" # End Source File # Begin Source File diff --git a/pjlib-util/build/pjlib_util_test.vcproj b/pjlib-util/build/pjlib_util_test.vcproj index faef4255..675ecc18 100644 --- a/pjlib-util/build/pjlib_util_test.vcproj +++ b/pjlib-util/build/pjlib_util_test.vcproj @@ -11,16 +11,16 @@ Name="Win32" /> + + @@ -2841,7 +2845,7 @@ /> +/* HTTP */ +#include + #endif /* __PJLIB_UTIL_H__ */ diff --git a/pjlib-util/include/pjlib-util/config.h b/pjlib-util/include/pjlib-util/config.h index da2fb8c5..0723ab40 100644 --- a/pjlib-util/include/pjlib-util/config.h +++ b/pjlib-util/include/pjlib-util/config.h @@ -255,6 +255,17 @@ #endif +/* ************************************************************************** + * HTTP Client configuration + */ +/** + * Timeout value for HTTP request operation. The value is in ms. + * Default: 60000ms + */ +#ifndef PJ_HTTP_DEFAULT_TIMEOUT +# define PJ_HTTP_DEFAULT_TIMEOUT (60000) +#endif + /** * @} */ diff --git a/pjlib-util/include/pjlib-util/errno.h b/pjlib-util/include/pjlib-util/errno.h index 26786a4c..5fa3d8fe 100644 --- a/pjlib-util/include/pjlib-util/errno.h +++ b/pjlib-util/include/pjlib-util/errno.h @@ -357,8 +357,34 @@ //#define PJ_STATUS_FROM_STUN_CODE(code) (PJLIB_UTIL_ERRNO_START+code) - - +/************************************************************ + * HTTP Client ERROR + ***********************************************************/ +/** + * @hideinitializer + * Invalid URL format + */ +#define PJLIB_UTIL_EHTTPINURL (PJLIB_UTIL_ERRNO_START+151)/* 320151 */ +/** + * @hideinitializer + * Invalid port number + */ +#define PJLIB_UTIL_EHTTPINPORT (PJLIB_UTIL_ERRNO_START+152)/* 320152 */ +/** + * @hideinitializer + * Incomplete headers received + */ +#define PJLIB_UTIL_EHTTPINCHDR (PJLIB_UTIL_ERRNO_START+153)/* 320153 */ +/** + * @hideinitializer + * Insufficient buffer + */ +#define PJLIB_UTIL_EHTTPINSBUF (PJLIB_UTIL_ERRNO_START+154)/* 320154 */ +/** + * @hideinitializer + * Connection lost + */ +#define PJLIB_UTIL_EHTTPLOST (PJLIB_UTIL_ERRNO_START+155)/* 320155 */ /** * @} diff --git a/pjlib-util/include/pjlib-util/http_client.h b/pjlib-util/include/pjlib-util/http_client.h new file mode 100644 index 00000000..9c4e6da9 --- /dev/null +++ b/pjlib-util/include/pjlib-util/http_client.h @@ -0,0 +1,381 @@ +/* $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 + */ +#ifndef __PJLIB_UTIL_HTTP_CLIENT_H__ +#define __PJLIB_UTIL_HTTP_CLIENT_H__ + +/** + * @file http_client.h + * @brief Simple HTTP Client + */ +#include +#include + +PJ_BEGIN_DECL + +/** + * @defgroup PJ_HTTP_CLIENT Simple HTTP Client + * @ingroup PJ_PROTOCOLS + * @{ + * This contains a simple HTTP client implementation. + * Some known limitations: + * - Does not support chunked Transfer-Encoding. + */ + +/** + * This opaque structure describes the http request. + */ +typedef struct pj_http_req pj_http_req; + +/** + * Defines the maximum number of elements in a pj_http_headers + * structure. + */ +#define PJ_HTTP_HEADER_SIZE 32 + +/** + * This structure describes http request/response headers. + * Application should call #pj_http_headers_add_elmt() to + * add a header field. + */ +typedef struct pj_http_headers +{ + unsigned count; /**< Number of header fields */ + struct pj_http_header_elmt + { + pj_str_t name; + pj_str_t value; + } header[PJ_HTTP_HEADER_SIZE]; /**< Header elements/fields */ +} pj_http_headers; + +/** + * Parameters that can be given during http request creation. Application + * must initialize this structure with #pj_http_req_param_default(). + */ +typedef struct pj_http_req_param +{ + /** + * The address family of the URL. + * Default is pj_AF_INET(). + */ + int addr_family; + + /** + * The HTTP request method. + * Default is GET. + */ + pj_str_t method; + + /** + * The HTTP protocol version ("1.0" or "1.1"). + * Default is "1.0". + */ + pj_str_t version; + + /** + * HTTP request operation timeout. + * Default is PJ_HTTP_DEFAULT_TIMEOUT. + */ + pj_time_val timeout; + + /** + * User-defined data. + * Default is NULL. + */ + void *user_data; + + /** + * HTTP request headers. + * Default is empty. + */ + pj_http_headers headers; + + /** + * This structure describes the http request body. If application + * specifies the data to send, the data must remain valid until + * the HTTP request is sent. Alternatively, application can choose + * to specify total_size as the total data size to send instead + * while leaving the data NULL (and its size 0). In this case, + * HTTP request will then call on_send_data() callback once it is + * ready to send the request body. This will be useful if + * application does not wish to load the data into the buffer at + * once. + * + * Default is empty. + */ + struct pj_http_reqdata + { + void *data; /**< Request body data */ + pj_size_t size; /**< Request body size */ + pj_size_t total_size; /**< If total_size > 0, data */ + /**< will be provided later */ + } reqdata; +} pj_http_req_param; + +/** + * This structure describes HTTP response. + */ +typedef struct pj_http_resp +{ + pj_str_t version; /**< HTTP version of the server */ + pj_str_t status_code; /**< Status code of the request */ + pj_str_t reason; /**< Reason phrase */ + pj_http_headers headers; /**< Response headers */ + /** + * The value of content-length header field. -1 if not + * specified. + */ + pj_int32_t content_length; + void *data; /**< Data received */ + pj_size_t size; /**< Data size */ +} pj_http_resp; + +/** + * This structure describes HTTP URL. + */ +typedef struct pj_http_url +{ + pj_str_t protocol; /**< Protocol used */ + pj_str_t host; /**< Host name */ + pj_uint16_t port; /**< Port number */ + pj_str_t path; /**< Path */ +} pj_http_url; + +/** + * This structure describes the callbacks to be called by the HTTP request. + */ +typedef struct pj_http_req_callback +{ + /** + * This callback is called when a complete HTTP response header + * is received. + * + * @param http_req The http request. + * @param resp The response of the request. + */ + void (*on_response)(pj_http_req *http_req, const pj_http_resp *resp); + + /** + * This callback is called when the HTTP request is ready to send + * its request body. Application may wish to use this callback if + * it wishes to load the data at a later time or if it does not + * wish to load the whole data into memory. In order for this + * callback to be called, application MUST set http_req_param.total_size + * to a value greater than 0. + * + * @param http_req The http request. + * @param data Pointer to the data that will be sent. Application + * must set the pointer to the current data chunk/segment + * to be sent. Data must remain valid until the next + * on_send_data() callback or for the last segment, + * until it is sent. + * @param size Pointer to the data size that will be sent. + */ + void (*on_send_data)(pj_http_req *http_req, + void **data, pj_size_t *size); + + /** + * This callback is called when a segment of response body data + * arrives. If this callback is specified (i.e. not NULL), the + * on_complete() callback will be called with zero-length data + * (within the response parameter), hence the application must + * store and manage its own data buffer, otherwise the + * on_complete() callback will be called with the response + * parameter containing the complete data. + * + * @param http_req The http request. + * @param data The buffer containing the data. + * @param size The length of data in the buffer. + */ + void (*on_data_read)(pj_http_req *http_req, + void *data, pj_size_t size); + + /** + * This callback is called when the HTTP request is completed. + * If the callback on_data_read() is specified, the variable + * response->data will be set to NULL, otherwise it will + * contain the complete data. Response data is allocated from + * pj_http_req's internal memory pool so the data remain valid + * as long as pj_http_req is not destroyed and application does + * not start a new request. + * + * @param http_req The http request. + * @param status The status of the request operation. PJ_SUCCESS + * if the operation completed successfully + * (connection-wise). To check the server's + * status-code response to the HTTP request, + * application should check resp->status_code instead. + * @param resp The response of the corresponding request. If + * the status argument is non-PJ_SUCCESS, this + * argument will be set to NULL. + */ + void (*on_complete)(pj_http_req *http_req, + pj_status_t status, + const pj_http_resp *resp); + +} pj_http_req_callback; + + +/** + * Initialize the http request parameters with the default values. + * + * @param param The parameter to be initialized. + */ +PJ_DECL(void) pj_http_req_param_default(pj_http_req_param *param); + +/** + * Add a header element/field. Application MUST make sure that + * name and val pointer remains valid until the HTTP request is sent. + * + * @param headers The headers. + * @param name The header field name. + * @param value The header field value. + * + * @return PJ_SUCCESS if the operation has been successful, + * or the appropriate error code on failure. + */ +PJ_DECL(pj_status_t) pj_http_headers_add_elmt(pj_http_headers *headers, + pj_str_t *name, + pj_str_t *val); + +/** + * The same as #pj_http_headers_add_elmt() with char * as + * its parameters. Application MUST make sure that name and val pointer + * remains valid until the HTTP request is sent. + * + * @param headers The headers. + * @param name The header field name. + * @param value The header field value. + * + * @return PJ_SUCCESS if the operation has been successful, + * or the appropriate error code on failure. + */ +PJ_DECL(pj_status_t) pj_http_headers_add_elmt2(pj_http_headers *headers, + char *name, char *val); + +/** + * Parse a http URL into its components. + * + * @param url The URL to be parsed. + * @param hurl Pointer to receive the parsed result. + * + * @return PJ_SUCCESS if the operation has been successful, + * or the appropriate error code on failure. + */ +PJ_DECL(pj_status_t) pj_http_req_parse_url(const pj_str_t *url, + pj_http_url *hurl); + +/** + * Create the HTTP request. + * + * @param pool Pool to use. HTTP request will use the pool's factory + * to allocate its own memory pool. + * @param url HTTP URL request. + * @param timer The timer to use. + * @param ioqueue The ioqueue to use. + * @param param Optional parameters. When this parameter is not + * specifed (NULL), the default values will be used. + * @param hcb Pointer to structure containing application + * callbacks. + * @param http_req Pointer to receive the http request instance. + * + * @return PJ_SUCCESS if the operation has been successful, + * or the appropriate error code on failure. + */ +PJ_DECL(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); + +/** + * Set the timeout of the HTTP request operation. Note that if the + * HTTP request is currently running, the timeout will only affect + * subsequent request operations. + * + * @param http_req The http request. + * @param timeout Timeout value for HTTP request operation. + */ +PJ_DECL(void) pj_http_req_set_timeout(pj_http_req *http_req, + const pj_time_val* timeout); + +/** + * Starts an asynchronous HTTP request to the URL specified. + * + * @param http_req The http request. + * + * @return + * - PJ_SUCCESS if success + * - non-zero which indicates the appropriate error code. + */ +PJ_DECL(pj_status_t) pj_http_req_start(pj_http_req *http_req); + +/** + * Cancel the asynchronous HTTP request. + * + * @param http_req The http request. + * @param notify If non-zero, the on_complete() callback will be + * called with status PJ_ECANCELLED to notify that + * the query has been cancelled. + * + * @return + * - PJ_SUCCESS if success + * - non-zero which indicates the appropriate error code. + */ +PJ_DECL(pj_status_t) pj_http_req_cancel(pj_http_req *http_req, + pj_bool_t notify); + +/** + * Destroy the http request. + * + * @param http_req The http request to be destroyed. + * + * @return PJ_SUCCESS if success. + */ +PJ_DECL(pj_status_t) pj_http_req_destroy(pj_http_req *http_req); + +/** + * Find out whether the http request is running. + * + * @param http_req The http request. + * + * @return PJ_TRUE if a request is pending, or + * PJ_FALSE if idle + */ +PJ_DECL(pj_bool_t) pj_http_req_is_running(const pj_http_req *http_req); + +/** + * Retrieve the user data previously associated with this http + * request. + * + * @param http_req The http request. + * + * @return The user data. + */ +PJ_DECL(void *) pj_http_req_get_user_data(pj_http_req *http_req); + +/** + * @} + */ + +PJ_END_DECL + + +#endif /* __PJLIB_UTIL_HTTP_CLIENT_H__ */ 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 +#include + +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/pjsip-apps/build/Samples-vc.mak b/pjsip-apps/build/Samples-vc.mak index 89b9204d..3b03cf72 100644 --- a/pjsip-apps/build/Samples-vc.mak +++ b/pjsip-apps/build/Samples-vc.mak @@ -62,6 +62,7 @@ SAMPLES = $(BINDIR)\auddemo.exe \ $(BINDIR)\confsample.exe \ $(BINDIR)\confbench.exe \ $(BINDIR)\encdec.exe \ + $(BINDIR)\httpdemo.exe \ $(BINDIR)\icedemo.exe \ $(BINDIR)\jbsim.exe \ $(BINDIR)\latency.exe \ diff --git a/pjsip-apps/build/Samples.mak b/pjsip-apps/build/Samples.mak index 18b13bca..0247b888 100644 --- a/pjsip-apps/build/Samples.mak +++ b/pjsip-apps/build/Samples.mak @@ -16,6 +16,7 @@ BINDIR := ../bin/samples/$(TARGET_NAME) SAMPLES := auddemo \ confsample \ encdec \ + httpdemo \ icedemo \ jbsim \ latency \ diff --git a/pjsip-apps/build/samples.dsp b/pjsip-apps/build/samples.dsp index 758e6b6f..dd950370 100644 --- a/pjsip-apps/build/samples.dsp +++ b/pjsip-apps/build/samples.dsp @@ -110,6 +110,10 @@ SOURCE=..\src\samples\footprint.c # End Source File # Begin Source File +SOURCE=..\src\samples\httpdemo.c +# End Source File +# Begin Source File + SOURCE=..\src\samples\icedemo.c # End Source File # Begin Source File diff --git a/pjsip-apps/build/samples.vcproj b/pjsip-apps/build/samples.vcproj index ba73d151..09767799 100644 --- a/pjsip-apps/build/samples.vcproj +++ b/pjsip-apps/build/samples.vcproj @@ -12,16 +12,16 @@ Name="Win32" /> - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/pjsip-apps/src/samples/httpdemo.c b/pjsip-apps/src/samples/httpdemo.c new file mode 100644 index 00000000..9dc3c0eb --- /dev/null +++ b/pjsip-apps/src/samples/httpdemo.c @@ -0,0 +1,151 @@ +/* $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 + */ + +/** + * \page page_httpdemo_c Samples: HTTP Client demo + * + * This file is pjsip-apps/src/samples/httpdemo.c + * + * \includelineno httpdemo.c + */ + +#include +#include +#include +#include +#include +#include +#include + +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_pool_factory *mem; +static FILE *f = NULL; + +//#define VERBOSE +#define THIS_FILE "http_demo" + +static void on_data_read(pj_http_req *hreq, void *data, pj_size_t size) +{ + PJ_UNUSED_ARG(hreq); + + if (size > 0) { + fwrite(data, 1, size, f); + fflush(f); +#ifdef VERBOSE + PJ_LOG(3, (THIS_FILE, "\nData received: %d bytes\n", size)); + printf("%.*s\n", (int)size, (char *)data); +#endif + } +} + +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(3, (THIS_FILE, "Request cancelled\n")); + return; + } else if (status == PJ_ETIMEDOUT) { + PJ_LOG(3, (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(3, (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 + } +} + +pj_status_t getURL(const char *curl) +{ + pj_str_t url; + pj_http_req_callback hcb; + pj_status_t status; + + pj_bzero(&hcb, sizeof(hcb)); + hcb.on_complete = &on_complete; + hcb.on_data_read = &on_data_read; + + /* 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; + + pj_strdup2(pool, &url, curl); + + if ((status = pj_http_req_create(pool, &url, timer_heap, ioqueue, + NULL, &hcb, &http_req)) != PJ_SUCCESS) + return status; + + if ((status = pj_http_req_start(http_req)) != PJ_SUCCESS) + return status; + + 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); + } + + pj_http_req_destroy(http_req); + pj_ioqueue_destroy(ioqueue); + pj_timer_heap_destroy(timer_heap); + pj_pool_release(pool); + + return PJ_SUCCESS; +} +/* + * main() + */ +int main(int argc, char *argv[]) +{ + pj_caching_pool cp; + pj_status_t status; + + if (argc != 3) { + puts("Usage: httpdemo URL filename"); + return 1; + } + + pj_log_set_level(3); + + pj_init(); + pj_caching_pool_init(&cp, NULL, 0); + mem = &cp.factory; + pjlib_util_init(); + + f = fopen(argv[2], "wb"); + status = getURL(argv[1]); + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (THIS_FILE, status, "Error")); + } + fclose(f); + + pj_shutdown(); + return 0; +} -- cgit v1.2.3