blob: 74884d63d1b42ce29a8866f333e45df36c7f45fa [file] [log] [blame]
/* $Id$ */
/*
* Copyright (C) 2008-2011 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/ctype.h>
#include <pj/errno.h>
#include <pj/except.h>
#include <pj/pool.h>
#include <pj/string.h>
#include <pj/timer.h>
#include <pj/rand.h>
#include <pjlib-util/base64.h>
#include <pjlib-util/errno.h>
#include <pjlib-util/md5.h>
#include <pjlib-util/scanner.h>
#include <pjlib-util/string.h>
#define THIS_FILE "http_client.c"
#if 0
/* Enable some tracing */
#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 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 const 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 const 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,
};
enum auth_state
{
AUTH_NONE, /* Not authenticating */
AUTH_RETRYING, /* New request with auth has been submitted */
AUTH_DONE /* Done retrying the request with auth. */
};
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 */
enum auth_state auth_state; /* Authentication state */
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);
/* Restart the request with authentication */
static void restart_req_with_auth(pj_http_req *hreq);
/* Parse authentication challenge */
static pj_status_t parse_auth_chal(pj_pool_t *pool, pj_str_t *input,
pj_http_auth_chal *chal);
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 const 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 || hreq->state == IDLE)
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 || hreq->state == IDLE)
return PJ_FALSE;
if (sent <= 0) {
hreq->error = (sent < 0 ? (pj_status_t)-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",
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", size));
if (hreq->state == ABORTING || hreq->state == IDLE)
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;
}
/* If code is 401 or 407, find and parse WWW-Authenticate or
* Proxy-Authenticate header
*/
if (hreq->response.status_code == 401 ||
hreq->response.status_code == 407)
{
const pj_str_t STR_WWW_AUTH = { "WWW-Authenticate", 16 };
const pj_str_t STR_PROXY_AUTH = { "Proxy-Authenticate", 18 };
pj_http_resp *response = &hreq->response;
pj_http_headers *hdrs = &response->headers;
unsigned i;
status = PJ_ENOTFOUND;
for (i = 0; i < hdrs->count; i++) {
if (!pj_stricmp(&hdrs->header[i].name, &STR_WWW_AUTH) ||
!pj_stricmp(&hdrs->header[i].name, &STR_PROXY_AUTH))
{
status = parse_auth_chal(hreq->pool,
&hdrs->header[i].value,
&response->auth_chal);
break;
}
}
/* Check if we should perform authentication */
if (status == PJ_SUCCESS &&
hreq->auth_state == AUTH_NONE &&
hreq->response.auth_chal.scheme.slen &&
hreq->param.auth_cred.username.slen &&
(hreq->param.auth_cred.scheme.slen == 0 ||
!pj_stricmp(&hreq->response.auth_chal.scheme,
&hreq->param.auth_cred.scheme)) &&
(hreq->param.auth_cred.realm.slen == 0 ||
!pj_stricmp(&hreq->response.auth_chal.realm,
&hreq->param.auth_cred.realm))
)
{
/* Yes, authentication is required and we have been
* configured with credential.
*/
restart_req_with_auth(hreq);
if (hreq->auth_state == AUTH_RETRYING) {
/* We'll be resending the request with auth. This
* connection has been closed.
*/
return PJ_FALSE;
}
}
}
/* 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 || hreq->response.content_length == 0)
return http_on_data_read(asock, (rem == 0 ? NULL:
(char *)data + size - rem),
rem, PJ_SUCCESS, NULL);
}
return PJ_TRUE;
}
if (hreq->state != READING_DATA)
return PJ_FALSE;
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 ((hreq->response.content_length >=0 &&
(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);
}
/* Parse authentication challenge */
static pj_status_t parse_auth_chal(pj_pool_t *pool, pj_str_t *input,
pj_http_auth_chal *chal)
{
pj_scanner scanner;
const pj_str_t REALM_STR = { "realm", 5},
NONCE_STR = { "nonce", 5},
ALGORITHM_STR = { "algorithm", 9 },
STALE_STR = { "stale", 5},
QOP_STR = { "qop", 3},
OPAQUE_STR = { "opaque", 6};
pj_status_t status = PJ_SUCCESS;
PJ_USE_EXCEPTION ;
pj_scan_init(&scanner, input->ptr, input->slen, PJ_SCAN_AUTOSKIP_WS,
&on_syntax_error);
PJ_TRY {
/* Get auth scheme */
if (*scanner.curptr == '"') {
pj_scan_get_quote(&scanner, '"', '"', &chal->scheme);
chal->scheme.ptr++;
chal->scheme.slen -= 2;
} else {
pj_scan_get_until_chr(&scanner, " \t\r\n", &chal->scheme);
}
/* Loop parsing all parameters */
for (;;) {
const char *end_param = ", \t\r\n;";
pj_str_t name, value;
/* Get pair of parameter name and value */
value.ptr = NULL;
value.slen = 0;
pj_scan_get_until_chr(&scanner, "=, \t\r\n", &name);
if (*scanner.curptr == '=') {
pj_scan_get_char(&scanner);
if (!pj_scan_is_eof(&scanner)) {
if (*scanner.curptr == '"' || *scanner.curptr == '\'') {
int quote_char = *scanner.curptr;
pj_scan_get_quote(&scanner, quote_char, quote_char,
&value);
value.ptr++;
value.slen -= 2;
} else if (!strchr(end_param, *scanner.curptr)) {
pj_scan_get_until_chr(&scanner, end_param, &value);
}
}
value = pj_str_unescape(pool, &value);
}
if (!pj_stricmp(&name, &REALM_STR)) {
chal->realm = value;
} else if (!pj_stricmp(&name, &NONCE_STR)) {
chal->nonce = value;
} else if (!pj_stricmp(&name, &ALGORITHM_STR)) {
chal->algorithm = value;
} else if (!pj_stricmp(&name, &OPAQUE_STR)) {
chal->opaque = value;
} else if (!pj_stricmp(&name, &QOP_STR)) {
chal->qop = value;
} else if (!pj_stricmp(&name, &STALE_STR)) {
chal->stale = value.slen &&
(*value.ptr != '0') &&
(*value.ptr != 'f') &&
(*value.ptr != 'F');
}
/* Eat comma */
if (!pj_scan_is_eof(&scanner) && *scanner.curptr == ',')
pj_scan_get_char(&scanner);
else
break;
}
}
PJ_CATCH_ANY {
status = PJ_GET_EXCEPTION();
pj_bzero(chal, sizeof(*chal));
TRACE_((THIS_FILE, "Error: parsing of auth header failed"));
}
PJ_END;
pj_scan_fini(&scanner);
return status;
}
/* 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;
char *end_status, *newdata;
pj_scanner scanner;
pj_str_t s;
const pj_str_t STR_CONTENT_LENGTH = { CONTENT_LENGTH, 14 };
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 = (char*) 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, ' ', &s);
response->status_code = (pj_uint16_t)pj_strtoul(&s);
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;
end_status = scanner.curptr;
pj_scan_fini(&scanner);
/* Parse the response headers. */
size = i - 2 - (end_status - newdata);
if (size > 0) {
status = http_headers_parse(end_status + 1, size,
&response->headers);
} else {
status = PJ_SUCCESS;
}
/* Find content-length header field. */
for (i = 0; i < response->headers.count; i++) {
if (!pj_stricmp(&response->headers.header[i].name,
&STR_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;
}
}
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(&param->method, (char*)http_method_names[HTTP_GET]);
pj_strset2(&param->version, (char*)HTTP_1_0);
param->timeout.msec = PJ_HTTP_DEFAULT_TIMEOUT;
pj_time_val_normalize(&param->timeout);
param->max_retries = 3;
}
/* Get the location of '@' character to indicate the end of
* user:passwd part of an URI. If user:passwd part is not
* present, NULL will be returned.
*/
static char *get_url_at_pos(const char *str, pj_size_t len)
{
const char *end = str + len;
const char *p = str;
/* skip scheme: */
while (p!=end && *p!='/') ++p;
if (p!=end && *p=='/') ++p;
if (p!=end && *p=='/') ++p;
if (p==end) return NULL;
for (; p!=end; ++p) {
switch (*p) {
case '/':
return NULL;
case '@':
return (char*)p;
}
}
return NULL;
}
PJ_DEF(pj_status_t) pj_http_req_parse_url(const pj_str_t *url,
pj_http_url *hurl)
{
pj_scanner scanner;
pj_size_t len = url->slen;
PJ_USE_EXCEPTION;
if (!len) return -1;
pj_bzero(hurl, sizeof(*hurl));
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,
(char*)http_protocol_names[PROTOCOL_HTTP]);
} else if (!pj_stricmp2(&s, http_protocol_names[PROTOCOL_HTTPS])) {
pj_strset2(&hurl->protocol,
(char*)http_protocol_names[PROTOCOL_HTTPS]);
} else {
PJ_THROW(PJ_ENOTSUP); // unsupported protocol
}
if (pj_scan_strcmp(&scanner, "://", 3)) {
PJ_THROW(PJLIB_UTIL_EHTTPINURL); // no "://" after protocol name
}
pj_scan_advance_n(&scanner, 3, PJ_FALSE);
if (get_url_at_pos(url->ptr, url->slen)) {
/* Parse username and password */
pj_scan_get_until_chr(&scanner, ":@", &hurl->username);
if (*scanner.curptr == ':') {
pj_scan_get_char(&scanner);
pj_scan_get_until_chr(&scanner, "@", &hurl->passwd);
} else {
hurl->passwd.slen = 0;
}
pj_scan_get_char(&scanner);
}
/* Parse the host and port number (if any) */
pj_scan_get_until_chr(&scanner, ":/", &s);
pj_strassign(&hurl->host, &s);
if (hurl->host.slen==0)
PJ_THROW(PJ_EINVAL);
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;
char *at_pos;
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_with_null(hreq->pool, &hreq->url, url)) {
pj_pool_release(hreq->pool);
return PJ_ENOMEM;
}
status = pj_http_req_parse_url(&hreq->url, &hreq->hurl);
if (status != PJ_SUCCESS) {
pj_pool_release(hreq->pool);
return status; // Invalid URL supplied
}
/* If URL contains username/password, move them to credential and
* remove them from the URL.
*/
if ((at_pos=get_url_at_pos(hreq->url.ptr, hreq->url.slen)) != NULL) {
pj_str_t tmp;
char *user_pos = pj_strchr(&hreq->url, '/');
int removed_len;
/* Save credential first, unescape the string */
tmp = pj_str_unescape(hreq->pool, &hreq->hurl.username);;
pj_strdup(hreq->pool, &hreq->param.auth_cred.username, &tmp);
tmp = pj_str_unescape(hreq->pool, &hreq->hurl.passwd);
pj_strdup(hreq->pool, &hreq->param.auth_cred.data, &tmp);
hreq->hurl.username.ptr = hreq->hurl.passwd.ptr = NULL;
hreq->hurl.username.slen = hreq->hurl.passwd.slen = 0;
/* Remove "username:password@" from the URL */
pj_assert(user_pos != 0 && user_pos < at_pos);
user_pos += 2;
removed_len = (int)(at_pos + 1 - user_pos);
pj_memmove(user_pos, at_pos+1, hreq->url.ptr+hreq->url.slen-at_pos-1);
hreq->url.slen -= removed_len;
/* Need to adjust hostname and path pointers due to memmove*/
if (hreq->hurl.host.ptr > user_pos &&
hreq->hurl.host.ptr < user_pos + hreq->url.slen)
{
hreq->hurl.host.ptr -= removed_len;
}
/* path may come from a string constant, don't shift it if so */
if (hreq->hurl.path.ptr > user_pos &&
hreq->hurl.path.ptr < user_pos + hreq->url.slen)
{
hreq->hurl.path.ptr -= removed_len;
}
}
*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;
}
static pj_status_t start_http_req(pj_http_req *http_req,
pj_bool_t notify_on_fail)
{
pj_sock_t sock = PJ_INVALID_SOCKET;
pj_status_t status;
pj_activesock_cb asock_cb;
int retry = 0;
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);
/* Reset few things to make sure restarting works */
http_req->error = 0;
http_req->response.headers.count = 0;
pj_bzero(&http_req->tcp_state, sizeof(http_req->tcp_state));
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))
{
goto on_return;
}
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;
do
{
pj_sockaddr_in bound_addr;
pj_uint16_t port = 0;
/* If we are using port restriction.
* Get a random port within the range
*/
if (http_req->param.source_port_range_start != 0) {
port = (pj_uint16_t)
(http_req->param.source_port_range_start +
(pj_rand() % http_req->param.source_port_range_size));
}
pj_sockaddr_in_init(&bound_addr, NULL, port);
status = pj_sock_bind(sock, &bound_addr, sizeof(bound_addr));
} while (status != PJ_SUCCESS && (retry++ < http_req->param.max_retries));
if (status != PJ_SUCCESS) {
PJ_PERROR(1,(THIS_FILE, status,
"Unable to bind to the requested port"));
pj_sock_close(sock);
goto on_return;
}
// 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) {
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;
status = http_req_start_sending(http_req);
if (status != PJ_SUCCESS)
goto on_return;
} else if (status != PJ_EPENDING) {
goto on_return; // error connecting
}
return PJ_SUCCESS;
on_return:
http_req->error = status;
if (notify_on_fail)
pj_http_req_cancel(http_req, PJ_TRUE);
else
http_req_end_request(http_req);
return status;
}
/* Starts an asynchronous HTTP request to the URL specified. */
PJ_DEF(pj_status_t) pj_http_req_start(pj_http_req *http_req)
{
return start_http_req(http_req, PJ_FALSE);
}
/* Respond to basic authentication challenge */
static pj_status_t auth_respond_basic(pj_http_req *hreq)
{
/* Basic authentication:
* credentials = "Basic" basic-credentials
* basic-credentials = base64-user-pass
* base64-user-pass = <base64 [4] encoding of user-pass>
* user-pass = userid ":" password
*
* Sample:
* Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
*/
pj_str_t user_pass;
pj_http_header_elmt *phdr;
int len;
/* Use send buffer to store userid ":" password */
user_pass.ptr = hreq->buffer.ptr;
pj_strcpy(&user_pass, &hreq->param.auth_cred.username);
pj_strcat2(&user_pass, ":");
pj_strcat(&user_pass, &hreq->param.auth_cred.data);
/* Create Authorization header */
phdr = &hreq->param.headers.header[hreq->param.headers.count++];
pj_bzero(phdr, sizeof(*phdr));
if (hreq->response.status_code == 401)
phdr->name = pj_str("Authorization");
else
phdr->name = pj_str("Proxy-Authorization");
len = (int)(PJ_BASE256_TO_BASE64_LEN(user_pass.slen) + 10);
phdr->value.ptr = (char*)pj_pool_alloc(hreq->pool, len);
phdr->value.slen = 0;
pj_strcpy2(&phdr->value, "Basic ");
len -= (int)phdr->value.slen;
pj_base64_encode((pj_uint8_t*)user_pass.ptr, (int)user_pass.slen,
phdr->value.ptr + phdr->value.slen, &len);
phdr->value.slen += len;
return PJ_SUCCESS;
}
/** Length of digest string. */
#define MD5_STRLEN 32
/* A macro just to get rid of type mismatch between char and unsigned char */
#define MD5_APPEND(pms,buf,len) pj_md5_update(pms, (const pj_uint8_t*)buf, \
(unsigned)len)
/* Transform digest to string.
* output must be at least PJSIP_MD5STRLEN+1 bytes.
*
* NOTE: THE OUTPUT STRING IS NOT NULL TERMINATED!
*/
static void digest2str(const unsigned char digest[], char *output)
{
int i;
for (i = 0; i<16; ++i) {
pj_val_to_hex_digit(digest[i], output);
output += 2;
}
}
static void auth_create_digest_response(pj_str_t *result,
const pj_http_auth_cred *cred,
const pj_str_t *nonce,
const pj_str_t *nc,
const pj_str_t *cnonce,
const pj_str_t *qop,
const pj_str_t *uri,
const pj_str_t *realm,
const pj_str_t *method)
{
char ha1[MD5_STRLEN];
char ha2[MD5_STRLEN];
unsigned char digest[16];
pj_md5_context pms;
pj_assert(result->slen >= MD5_STRLEN);
TRACE_((THIS_FILE, "Begin creating digest"));
if (cred->data_type == 0) {
/***
*** ha1 = MD5(username ":" realm ":" password)
***/
pj_md5_init(&pms);
MD5_APPEND( &pms, cred->username.ptr, cred->username.slen);
MD5_APPEND( &pms, ":", 1);
MD5_APPEND( &pms, realm->ptr, realm->slen);
MD5_APPEND( &pms, ":", 1);
MD5_APPEND( &pms, cred->data.ptr, cred->data.slen);
pj_md5_final(&pms, digest);
digest2str(digest, ha1);
} else if (cred->data_type == 1) {
pj_assert(cred->data.slen == 32);
pj_memcpy( ha1, cred->data.ptr, cred->data.slen );
} else {
pj_assert(!"Invalid data_type");
}
TRACE_((THIS_FILE, " ha1=%.32s", ha1));
/***
*** ha2 = MD5(method ":" req_uri)
***/
pj_md5_init(&pms);
MD5_APPEND( &pms, method->ptr, method->slen);
MD5_APPEND( &pms, ":", 1);
MD5_APPEND( &pms, uri->ptr, uri->slen);
pj_md5_final(&pms, digest);
digest2str(digest, ha2);
TRACE_((THIS_FILE, " ha2=%.32s", ha2));
/***
*** When qop is not used:
*** response = MD5(ha1 ":" nonce ":" ha2)
***
*** When qop=auth is used:
*** response = MD5(ha1 ":" nonce ":" nc ":" cnonce ":" qop ":" ha2)
***/
pj_md5_init(&pms);
MD5_APPEND( &pms, ha1, MD5_STRLEN);
MD5_APPEND( &pms, ":", 1);
MD5_APPEND( &pms, nonce->ptr, nonce->slen);
if (qop && qop->slen != 0) {
MD5_APPEND( &pms, ":", 1);
MD5_APPEND( &pms, nc->ptr, nc->slen);
MD5_APPEND( &pms, ":", 1);
MD5_APPEND( &pms, cnonce->ptr, cnonce->slen);
MD5_APPEND( &pms, ":", 1);
MD5_APPEND( &pms, qop->ptr, qop->slen);
}
MD5_APPEND( &pms, ":", 1);
MD5_APPEND( &pms, ha2, MD5_STRLEN);
/* This is the final response digest. */
pj_md5_final(&pms, digest);
/* Convert digest to string and store in chal->response. */
result->slen = MD5_STRLEN;
digest2str(digest, result->ptr);
TRACE_((THIS_FILE, " digest=%.32s", result->ptr));
TRACE_((THIS_FILE, "Digest created"));
}
/* Find out if qop offer contains "auth" token */
static pj_bool_t auth_has_qop( pj_pool_t *pool, const pj_str_t *qop_offer)
{
pj_str_t qop;
char *p;
pj_strdup_with_null( pool, &qop, qop_offer);
p = qop.ptr;
while (*p) {
*p = (char)pj_tolower(*p);
++p;
}
p = qop.ptr;
while (*p) {
if (*p=='a' && *(p+1)=='u' && *(p+2)=='t' && *(p+3)=='h') {
int e = *(p+4);
if (e=='"' || e==',' || e==0)
return PJ_TRUE;
else
p += 4;
} else {
++p;
}
}
return PJ_FALSE;
}
#define STR_PREC(s) (int)(s).slen, (s).ptr
/* Respond to digest authentication */
static pj_status_t auth_respond_digest(pj_http_req *hreq)
{
const pj_http_auth_chal *chal = &hreq->response.auth_chal;
const pj_http_auth_cred *cred = &hreq->param.auth_cred;
pj_http_header_elmt *phdr;
char digest_response_buf[MD5_STRLEN];
int len;
pj_str_t digest_response;
/* Check algorithm is supported. We only support MD5 */
if (chal->algorithm.slen!=0 &&
pj_stricmp2(&chal->algorithm, "MD5"))
{
TRACE_((THIS_FILE, "Error: Unsupported digest algorithm \"%.*s\"",
chal->algorithm.slen, chal->algorithm.ptr));
return PJ_ENOTSUP;
}
/* Add Authorization header */
phdr = &hreq->param.headers.header[hreq->param.headers.count++];
pj_bzero(phdr, sizeof(*phdr));
if (hreq->response.status_code == 401)
phdr->name = pj_str("Authorization");
else
phdr->name = pj_str("Proxy-Authorization");
/* Allocate space for the header */
len = (int)(8 + /* Digest */
16 + hreq->param.auth_cred.username.slen + /* username= */
12 + chal->realm.slen + /* realm= */
12 + chal->nonce.slen + /* nonce= */
8 + hreq->hurl.path.slen + /* uri= */
16 + /* algorithm=MD5 */
16 + MD5_STRLEN + /* response= */
12 + /* qop=auth */
8 + /* nc=.. */
30 + /* cnonce= */
12 + chal->opaque.slen + /* opaque=".." */
0);
phdr->value.ptr = (char*)pj_pool_alloc(hreq->pool, len);
/* Configure buffer to temporarily store the digest */
digest_response.ptr = digest_response_buf;
digest_response.slen = MD5_STRLEN;
if (chal->qop.slen == 0) {
const pj_str_t STR_MD5 = { "MD5", 3 };
int max_len;
/* Server doesn't require quality of protection. */
auth_create_digest_response(&digest_response, cred,
&chal->nonce, NULL, NULL, NULL,
&hreq->hurl.path, &chal->realm,
&hreq->param.method);
max_len = len;
len = pj_ansi_snprintf(
phdr->value.ptr, max_len,
"Digest username=\"%.*s\", "
"realm=\"%.*s\", "
"nonce=\"%.*s\", "
"uri=\"%.*s\", "
"algorithm=%.*s, "
"response=\"%.*s\"",
STR_PREC(cred->username),
STR_PREC(chal->realm),
STR_PREC(chal->nonce),
STR_PREC(hreq->hurl.path),
STR_PREC(STR_MD5),
STR_PREC(digest_response));
if (len < 0 || len >= max_len)
return PJ_ETOOSMALL;
phdr->value.slen = len;
} else if (auth_has_qop(hreq->pool, &chal->qop)) {
/* Server requires quality of protection.
* We respond with selecting "qop=auth" protection.
*/
const pj_str_t STR_MD5 = { "MD5", 3 };
const pj_str_t qop = pj_str("auth");
const pj_str_t nc = pj_str("00000001");
const pj_str_t cnonce = pj_str("b39971");
int max_len;
auth_create_digest_response(&digest_response, cred,
&chal->nonce, &nc, &cnonce, &qop,
&hreq->hurl.path, &chal->realm,
&hreq->param.method);
max_len = len;
len = pj_ansi_snprintf(
phdr->value.ptr, max_len,
"Digest username=\"%.*s\", "
"realm=\"%.*s\", "
"nonce=\"%.*s\", "
"uri=\"%.*s\", "
"algorithm=%.*s, "
"response=\"%.*s\", "
"qop=%.*s, "
"nc=%.*s, "
"cnonce=\"%.*s\"",
STR_PREC(cred->username),
STR_PREC(chal->realm),
STR_PREC(chal->nonce),
STR_PREC(hreq->hurl.path),
STR_PREC(STR_MD5),
STR_PREC(digest_response),
STR_PREC(qop),
STR_PREC(nc),
STR_PREC(cnonce));
if (len < 0 || len >= max_len)
return PJ_ETOOSMALL;
phdr->value.slen = len;
if (chal->opaque.slen) {
pj_strcat2(&phdr->value, ", opaque=\"");
pj_strcat(&phdr->value, &chal->opaque);
pj_strcat2(&phdr->value, "\"");
}
} else {
/* Server requires quality protection that we don't support. */
TRACE_((THIS_FILE, "Error: Unsupported qop offer %.*s",
chal->qop.slen, chal->qop.ptr));
return PJ_ENOTSUP;
}
return PJ_SUCCESS;
}
static void restart_req_with_auth(pj_http_req *hreq)
{
pj_http_auth_chal *chal = &hreq->response.auth_chal;
pj_http_auth_cred *cred = &hreq->param.auth_cred;
pj_status_t status;
if (hreq->param.headers.count >= PJ_HTTP_HEADER_SIZE) {
TRACE_((THIS_FILE, "Error: no place to put Authorization header"));
hreq->auth_state = AUTH_DONE;
return;
}
/* If credential specifies specific scheme, make sure they match */
if (cred->scheme.slen && pj_stricmp(&chal->scheme, &cred->scheme)) {
status = PJ_ENOTSUP;
TRACE_((THIS_FILE, "Error: auth schemes mismatch"));
goto on_error;
}
/* If credential specifies specific realm, make sure they match */
if (cred->realm.slen && pj_stricmp(&chal->realm, &cred->realm)) {
status = PJ_ENOTSUP;
TRACE_((THIS_FILE, "Error: auth realms mismatch"));
goto on_error;
}
if (!pj_stricmp2(&chal->scheme, "basic")) {
status = auth_respond_basic(hreq);
} else if (!pj_stricmp2(&chal->scheme, "digest")) {
status = auth_respond_digest(hreq);
} else {
TRACE_((THIS_FILE, "Error: unsupported HTTP auth scheme"));
status = PJ_ENOTSUP;
}
if (status != PJ_SUCCESS)
goto on_error;
http_req_end_request(hreq);
status = start_http_req(hreq, PJ_TRUE);
if (status != PJ_SUCCESS)
goto on_error;
hreq->auth_state = AUTH_RETRYING;
return;
on_error:
hreq->auth_state = AUTH_DONE;
}
/* 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\r\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:%d\r\n",
STR_PREC(hreq->hurl.host), hreq->hurl.port);
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 ?
(unsigned long)hreq->param.reqdata.total_size:
(unsigned long)hreq->param.reqdata.size, buf);
str_snprintf(&pkt, BUF_SIZE, PJ_TRUE, "%s: %s\r\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\r\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, "\r\n");
pkt.ptr[pkt.slen] = 0;
TRACE_((THIS_FILE, "%s", pkt.ptr));
} else {
pkt.ptr = (char*)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,
(void**)&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;
}