summaryrefslogtreecommitdiff
path: root/pjlib-util
diff options
context:
space:
mode:
authorBenny Prijono <bennylp@teluu.com>2010-09-27 08:35:08 +0000
committerBenny Prijono <bennylp@teluu.com>2010-09-27 08:35:08 +0000
commitc850d430469b470fc4b2d029fcb4f34d90cbb0f9 (patch)
treefdcb666c1c514951fc1ccef366b1e05968b047bb /pjlib-util
parentdfb5e40e66d2c0169adb3e0d22f2e83b9e842994 (diff)
Implemented and closed #1136: added HTTP authentication support
git-svn-id: http://svn.pjsip.org/repos/pjproject/trunk@3321 74dad513-b988-da41-8d7b-12977e46ad98
Diffstat (limited to 'pjlib-util')
-rw-r--r--pjlib-util/include/pjlib-util/http_client.h101
-rw-r--r--pjlib-util/src/pjlib-util-test/http_client.c132
-rw-r--r--pjlib-util/src/pjlib-util/http_client.c605
3 files changed, 782 insertions, 56 deletions
diff --git a/pjlib-util/include/pjlib-util/http_client.h b/pjlib-util/include/pjlib-util/http_client.h
index 2259b3f5..337c8e94 100644
--- a/pjlib-util/include/pjlib-util/http_client.h
+++ b/pjlib-util/include/pjlib-util/http_client.h
@@ -49,21 +49,78 @@ typedef struct pj_http_req pj_http_req;
#define PJ_HTTP_HEADER_SIZE 32
/**
+ * HTTP header representation.
+ */
+typedef struct pj_http_header_elmt
+{
+ pj_str_t name; /**< Header name */
+ pj_str_t value; /**< Header value */
+} pj_http_header_elmt;
+
+/**
* 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 */
+ /**< Number of header fields */
+ unsigned count;
+
+ /** Header elements/fields */
+ pj_http_header_elmt header[PJ_HTTP_HEADER_SIZE];
} pj_http_headers;
/**
+ * Structure to save HTTP authentication credential.
+ */
+typedef struct pj_http_auth_cred
+{
+ /**
+ * Specify specific authentication schemes to be responded. Valid values
+ * are "basic" and "digest". If this field is not set, any authentication
+ * schemes will be responded.
+ *
+ * Default is empty.
+ */
+ pj_str_t scheme;
+
+ /**
+ * Specify specific authentication realm to be responded. If this field
+ * is set, only 401/407 response with matching realm will be responded.
+ * If this field is not set, any realms will be responded.
+ *
+ * Default is empty.
+ */
+ pj_str_t realm;
+
+ /**
+ * Specify authentication username.
+ *
+ * Default is empty.
+ */
+ pj_str_t username;
+
+ /**
+ * The type of password in \a data field. Currently only 0 is
+ * supported, meaning the \a data contains plain-text password.
+ *
+ * Default is 0.
+ */
+ unsigned data_type;
+
+ /**
+ * Specify authentication password. The encoding of the password depends
+ * on the value of \a data_type field above.
+ *
+ * Default is empty.
+ */
+ pj_str_t data;
+
+} pj_http_auth_cred;
+
+
+/**
* Parameters that can be given during http request creation. Application
* must initialize this structure with #pj_http_req_param_default().
*/
@@ -125,9 +182,30 @@ typedef struct pj_http_req_param
pj_size_t total_size; /**< If total_size > 0, data */
/**< will be provided later */
} reqdata;
+
+ /**
+ * Authentication credential needed to respond to 401/407 response.
+ */
+ pj_http_auth_cred auth_cred;
+
} pj_http_req_param;
/**
+ * HTTP authentication challenge, parsed from WWW-Authenticate header.
+ */
+typedef struct pj_http_auth_chal
+{
+ pj_str_t scheme; /**< Auth scheme. */
+ pj_str_t realm; /**< Realm for the challenge. */
+ pj_str_t domain; /**< Domain. */
+ pj_str_t nonce; /**< Nonce challenge. */
+ pj_str_t opaque; /**< Opaque value. */
+ int stale; /**< Stale parameter. */
+ pj_str_t algorithm; /**< Algorithm parameter. */
+ pj_str_t qop; /**< Quality of protection. */
+} pj_http_auth_chal;
+
+/**
* This structure describes HTTP response.
*/
typedef struct pj_http_resp
@@ -136,11 +214,10 @@ typedef struct pj_http_resp
pj_uint16_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;
+ pj_http_auth_chal auth_chal; /**< Parsed WWW-Authenticate header, if
+ any. */
+ pj_int32_t content_length; /**< The value of content-length header
+ field. -1 if not specified. */
void *data; /**< Data received */
pj_size_t size; /**< Data size */
} pj_http_resp;
@@ -150,6 +227,8 @@ typedef struct pj_http_resp
*/
typedef struct pj_http_url
{
+ pj_str_t username; /**< Username part */
+ pj_str_t passwd; /**< Password part */
pj_str_t protocol; /**< Protocol used */
pj_str_t host; /**< Host name */
pj_uint16_t port; /**< Port number */
diff --git a/pjlib-util/src/pjlib-util-test/http_client.c b/pjlib-util/src/pjlib-util-test/http_client.c
index b942a4d6..d8445092 100644
--- a/pjlib-util/src/pjlib-util-test/http_client.c
+++ b/pjlib-util/src/pjlib-util-test/http_client.c
@@ -237,49 +237,125 @@ static void on_response(pj_http_req *hreq, const pj_http_resp *resp)
}
-pj_status_t parse_url(const char *url)
+pj_status_t parse_url(const char *url, pj_http_url *hurl)
{
pj_str_t surl;
- pj_http_url hurl;
pj_status_t status;
pj_cstr(&surl, url);
- status = pj_http_req_parse_url(&surl, &hurl);
+ 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));
+ url, STR_PREC(hurl->protocol), STR_PREC(hurl->host),
+ hurl->port, STR_PREC(hurl->path));
} else {
}
#endif
return status;
}
-int parse_url_test()
+static 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;
+ struct test_data
+ {
+ char *url;
+ pj_status_t result;
+ const char *username;
+ const char *passwd;
+ const char *host;
+ int port;
+ const char *path;
+ } test_data[] =
+ {
+ /* Simple URL without '/' in the end */
+ {"http://www.pjsip.org", PJ_SUCCESS, "", "", "www.pjsip.org", 80, "/"},
+
+ /* Simple URL with port number but without '/' in the end */
+ {"http://pjsip.org:8080", PJ_SUCCESS, "", "", "pjsip.org", 8080, "/"},
+
+ /* URL with path */
+ {"http://127.0.0.1:280/Joomla/index.php?option=com_content&task=view&id=5&Itemid=6",
+ PJ_SUCCESS, "", "", "127.0.0.1", 280,
+ "/Joomla/index.php?option=com_content&task=view&id=5&Itemid=6"},
+
+ /* URL with port and path */
+ {"http://pjsip.org:81/about-us/", PJ_SUCCESS, "", "", "pjsip.org", 81, "/about-us/"},
+
+ /* unsupported protocol */
+ {"ftp://www.pjsip.org", PJ_ENOTSUP, "", "", "", 80, ""},
+
+ /* invalid format */
+ {"http:/pjsip.org/about-us/", PJLIB_UTIL_EHTTPINURL, "", "", "", 80, ""},
+
+ /* invalid port number */
+ {"http://pjsip.org:xyz/", PJLIB_UTIL_EHTTPINPORT, "", "", "", 80, ""},
+
+ /* with username and password */
+ {"http://user:pass@pjsip.org", PJ_SUCCESS, "user", "pass", "pjsip.org", 80, "/"},
+
+ /* password only*/
+ {"http://:pass@pjsip.org", PJ_SUCCESS, "", "pass", "pjsip.org", 80, "/"},
+
+ /* user only*/
+ {"http://user:@pjsip.org", PJ_SUCCESS, "user", "", "pjsip.org", 80, "/"},
+
+ /* empty username and passwd*/
+ {"http://:@pjsip.org", PJ_SUCCESS, "", "", "pjsip.org", 80, "/"},
+
+ /* Invalid URL */
+ {"http://:", PJ_EINVAL, "", "", "", 0, ""},
+
+ /* Invalid URL */
+ {"http://@", PJ_EINVAL, "", "", "", 0, ""},
+
+ /* Invalid URL */
+ {"http", PJ_EINVAL, "", "", "", 0, ""},
+
+ /* Invalid URL */
+ {"http:/", PJ_EINVAL, "", "", "", 0, ""},
+
+ /* Invalid URL */
+ {"http://", PJ_EINVAL, "", "", "", 0, ""},
+
+ /* Invalid URL */
+ {"http:///", PJ_EINVAL, "", "", "", 0, ""},
+
+ /* Invalid URL */
+ {"http://@/", PJ_EINVAL, "", "", "", 0, ""},
+
+ /* Invalid URL */
+ {"http://:::", PJ_EINVAL, "", "", "", 0, ""},
+ };
+ unsigned i;
+
+ for (i=0; i<PJ_ARRAY_SIZE(test_data); ++i) {
+ struct test_data *ptd;
+ pj_http_url hurl;
+ pj_status_t status;
+
+ ptd = &test_data[i];
+
+ PJ_LOG(3, (THIS_FILE, ".. %s", ptd->url));
+ status = parse_url(ptd->url, &hurl);
+
+ if (status != ptd->result) {
+ PJ_LOG(3,(THIS_FILE, "%d", status));
+ return -11;
+ }
+ if (status != PJ_SUCCESS)
+ continue;
+ if (pj_strcmp2(&hurl.username, ptd->username))
+ return -12;
+ if (pj_strcmp2(&hurl.passwd, ptd->passwd))
+ return -13;
+ if (pj_strcmp2(&hurl.host, ptd->host))
+ return -14;
+ if (hurl.port != ptd->port)
+ return -15;
+ if (pj_strcmp2(&hurl.path, ptd->path))
+ return -16;
+ }
return 0;
}
diff --git a/pjlib-util/src/pjlib-util/http_client.c b/pjlib-util/src/pjlib-util/http_client.c
index 885c6f7d..31c46b45 100644
--- a/pjlib-util/src/pjlib-util/http_client.c
+++ b/pjlib-util/src/pjlib-util/http_client.c
@@ -20,13 +20,17 @@
#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 <pjlib-util/base64.h>
#include <pjlib-util/errno.h>
+#include <pjlib-util/md5.h>
#include <pjlib-util/scanner.h>
+#include <pjlib-util/string.h>
#if 0
/* Enable some tracing */
@@ -39,7 +43,6 @@
#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
@@ -95,6 +98,13 @@ enum http_state
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 */
@@ -109,6 +119,7 @@ struct pj_http_req
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 */
@@ -142,6 +153,11 @@ 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)
{
@@ -318,8 +334,59 @@ static pj_bool_t http_on_data_read(pj_activesock_t *asock,
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.
+ * appropriate callback.
*/
if (hreq->cb.on_response)
(*hreq->cb.on_response)(hreq, &hreq->response);
@@ -384,7 +451,7 @@ static pj_bool_t http_on_data_read(pj_activesock_t *asock,
hreq->response.content_length) ||
(status == PJ_EEOF && hreq->response.content_length == -1))
{
- /* Finish reading */
+ /* Finish reading */
http_req_end_request(hreq);
hreq->response.size = hreq->tcp_state.current_read_size;
@@ -433,6 +500,98 @@ static void on_timeout( pj_timer_heap_t *timer_heap,
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.
*/
@@ -464,9 +623,10 @@ static pj_status_t http_response_parse(pj_pool_t *pool,
{
pj_size_t i;
char *cptr;
- char *newdata;
+ 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;
@@ -517,10 +677,13 @@ static pj_status_t http_response_parse(pj_pool_t *pool,
}
PJ_END;
+ end_status = scanner.curptr;
+ pj_scan_fini(&scanner);
+
/* Parse the response headers. */
- size = i - 2 - (scanner.curptr - newdata);
+ size = i - 2 - (end_status - newdata);
if (size > 0) {
- status = http_headers_parse(scanner.curptr + 1, size,
+ status = http_headers_parse(end_status + 1, size,
&response->headers);
} else {
status = PJ_SUCCESS;
@@ -528,8 +691,8 @@ static pj_status_t http_response_parse(pj_pool_t *pool,
/* Find content-length header field. */
for (i = 0; i < response->headers.count; i++) {
- if (!pj_stricmp2(&response->headers.header[i].name,
- CONTENT_LENGTH))
+ if (!pj_stricmp(&response->headers.header[i].name,
+ &STR_CONTENT_LENGTH))
{
response->content_length =
pj_strtoul(&response->headers.header[i].value);
@@ -545,8 +708,6 @@ static pj_status_t http_response_parse(pj_pool_t *pool,
}
}
- pj_scan_fini(&scanner);
-
return status;
}
@@ -614,6 +775,7 @@ PJ_DEF(pj_status_t) pj_http_req_parse_url(const pj_str_t *url,
if (!len) return -1;
+ pj_bzero(hurl, sizeof(*hurl));
pj_scan_init(&scanner, url->ptr, url->slen, 0, &on_syntax_error);
PJ_TRY {
@@ -634,16 +796,28 @@ PJ_DEF(pj_status_t) pj_http_req_parse_url(const pj_str_t *url,
PJ_THROW(PJ_ENOTSUP); // unsupported protocol
}
- if (pj_scan_strcmp(&scanner, HTTP_SEPARATOR,
- pj_ansi_strlen(HTTP_SEPARATOR)))
- {
+ if (pj_scan_strcmp(&scanner, "://", 3)) {
PJ_THROW(PJLIB_UTIL_EHTTPINURL); // no "://" after protocol name
}
- pj_scan_advance_n(&scanner, pj_ansi_strlen(HTTP_SEPARATOR), PJ_FALSE);
+ pj_scan_advance_n(&scanner, 3, PJ_FALSE);
+
+ if (pj_memchr(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 */
@@ -692,6 +866,7 @@ PJ_DEF(pj_status_t) pj_http_req_create(pj_pool_t *pool,
{
pj_pool_t *own_pool;
pj_http_req *hreq;
+ char *at_pos;
pj_status_t status;
PJ_ASSERT_RETURN(pool && url && timer && ioqueue &&
@@ -736,11 +911,54 @@ PJ_DEF(pj_status_t) pj_http_req_create(pj_pool_t *pool,
}
/* Parse the URL */
- if (!pj_strdup(hreq->pool, &hreq->url, 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)
+ 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=pj_strchr(&hreq->url, '@')) != 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 = 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;
@@ -770,7 +988,10 @@ PJ_DEF(pj_status_t) pj_http_req_start(pj_http_req *http_req)
*/
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 */
@@ -837,7 +1058,357 @@ on_return:
return status;
}
-#define STR_PREC(s) s.slen, s.ptr
+/* 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 = 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 -= 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, 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 = 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= */
+ 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 };
+
+ /* 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);
+
+ len = pj_ansi_snprintf(
+ phdr->value.ptr, 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)
+ 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("1");
+ const pj_str_t cnonce = pj_str("b39971");
+
+ auth_create_digest_response(&digest_response, cred,
+ &chal->nonce, &nc, &cnonce, &qop,
+ &hreq->hurl.path, &chal->realm,
+ &hreq->param.method);
+ len = pj_ansi_snprintf(
+ phdr->value.ptr, 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)
+ return PJ_ETOOSMALL;
+ phdr->value.slen = len;
+
+ } 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 = pj_http_req_start(hreq);
+ 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.