/* $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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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(¶m->method, (char*)http_method_names[HTTP_GET]); pj_strset2(¶m->version, (char*)HTTP_1_0); param->timeout.msec = PJ_HTTP_DEFAULT_TIMEOUT; pj_time_val_normalize(¶m->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 = * 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. */ static 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; }