1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-10 09:58:01 +00:00
putty-source/proxy/http.c
Simon Tatham 3c21fa54c5 HTTP proxy: implement Digest authentication.
In http.c, this drops in reasonably neatly alongside the existing
support for Basic, now that we're waiting for an initial 407 response
from the proxy to tell us which auth mechanism it would prefer to use.

The rest of this patch is mostly contriving to add testcrypt support
for the function in cproxy.c that generates the complicated output
header to go in the HTTP request: you need about a dozen assorted
parameters, the actual response hash has two more hashes in its
preimage, and there's even an option to hash the username as well if
necessary. Much more complicated than CHAP (which is just plain
HMAC-MD5), so it needs testing!

Happily, RFC 7616 comes with some reasonably useful test cases, and
I've managed to transcribe them directly into cryptsuite.py and
demonstrate that my response-generator agrees with them.

End-to-end testing of the whole system was done against Squid 4.13
(specifically, the squid package in Debian bullseye, version 4.13-10).
2021-11-20 15:08:19 +00:00

597 lines
21 KiB
C

/*
* HTTP CONNECT proxy negotiation.
*/
#include "putty.h"
#include "network.h"
#include "proxy.h"
#include "sshcr.h"
static bool read_line(bufchain *input, strbuf *output, bool is_header)
{
char c;
while (bufchain_try_fetch(input, &c, 1)) {
if (is_header && output->len > 0 &&
output->s[output->len - 1] == '\n') {
/*
* A newline terminates the header, provided we're sure it
* is _not_ followed by a space or a tab.
*/
if (c != ' ' && c != '\t')
goto done; /* we have a complete header line */
} else {
put_byte(output, c);
bufchain_consume(input, 1);
if (!is_header && output->len > 0 &&
output->s[output->len - 1] == '\n') {
/* If we're looking for just a line, not an HTTP
* header, then any newline terminates it. */
goto done;
}
}
}
return false;
done:
strbuf_chomp(output, '\n');
strbuf_chomp(output, '\r');
return true;
}
typedef enum HttpAuthType { AUTH_NONE, AUTH_BASIC, AUTH_DIGEST } HttpAuthType;
typedef struct HttpProxyNegotiator {
int crLine;
strbuf *response, *header, *token;
int http_status_pos;
size_t header_pos;
strbuf *username, *password;
int http_status;
bool connection_close;
HttpAuthType next_auth_type;
bool try_auth_from_conf;
strbuf *realm, *nonce, *opaque, *uri;
bool got_opaque;
uint32_t nonce_count;
bool digest_nonce_was_stale;
HttpDigestHash digest_hash;
bool hash_username;
prompts_t *prompts;
int username_prompt_index, password_prompt_index;
size_t content_length;
ProxyNegotiator pn;
} HttpProxyNegotiator;
static ProxyNegotiator *proxy_http_new(const ProxyNegotiatorVT *vt)
{
HttpProxyNegotiator *s = snew(HttpProxyNegotiator);
memset(s, 0, sizeof(*s));
s->pn.vt = vt;
s->response = strbuf_new();
s->header = strbuf_new();
s->token = strbuf_new();
s->username = strbuf_new();
s->password = strbuf_new_nm();
s->realm = strbuf_new();
s->nonce = strbuf_new();
s->opaque = strbuf_new();
s->uri = strbuf_new();
s->nonce_count = 0;
/*
* Always start with a CONNECT request containing no auth. If the
* proxy rejects that, it will tell us what kind of auth it would
* prefer.
*/
s->next_auth_type = AUTH_NONE;
return &s->pn;
}
static void proxy_http_free(ProxyNegotiator *pn)
{
HttpProxyNegotiator *s = container_of(pn, HttpProxyNegotiator, pn);
strbuf_free(s->response);
strbuf_free(s->header);
strbuf_free(s->token);
strbuf_free(s->username);
strbuf_free(s->password);
strbuf_free(s->realm);
strbuf_free(s->nonce);
strbuf_free(s->opaque);
strbuf_free(s->uri);
if (s->prompts)
free_prompts(s->prompts);
sfree(s);
}
#define HTTP_HEADER_LIST(X) \
X(HDR_CONNECTION, "Connection") \
X(HDR_CONTENT_LENGTH, "Content-Length") \
X(HDR_PROXY_AUTHENTICATE, "Proxy-Authenticate") \
/* end of list */
typedef enum HttpHeader {
#define ENUM_DEF(id, string) id,
HTTP_HEADER_LIST(ENUM_DEF)
#undef ENUM_DEF
HDR_UNKNOWN
} HttpHeader;
static inline bool is_whitespace(char c)
{
return (c == ' ' || c == '\t' || c == '\n');
}
static inline bool is_separator(char c)
{
return (c == '(' || c == ')' || c == '<' || c == '>' || c == '@' ||
c == ',' || c == ';' || c == ':' || c == '\\' || c == '"' ||
c == '/' || c == '[' || c == ']' || c == '?' || c == '=' ||
c == '{' || c == '}');
}
#define HTTP_SEPARATORS
static bool get_end_of_header(HttpProxyNegotiator *s)
{
size_t pos = s->header_pos;
while (pos < s->header->len && is_whitespace(s->header->s[pos]))
pos++;
if (pos == s->header->len) {
s->header_pos = pos;
return true;
}
return false;
}
static bool get_token(HttpProxyNegotiator *s)
{
size_t pos = s->header_pos;
while (pos < s->header->len && is_whitespace(s->header->s[pos]))
pos++;
if (pos == s->header->len)
return false; /* end of string */
if (is_separator(s->header->s[pos]))
return false;
strbuf_clear(s->token);
while (pos < s->header->len &&
!is_whitespace(s->header->s[pos]) &&
!is_separator(s->header->s[pos]))
put_byte(s->token, s->header->s[pos++]);
s->header_pos = pos;
return true;
}
static bool get_separator(HttpProxyNegotiator *s, char sep)
{
size_t pos = s->header_pos;
while (pos < s->header->len && is_whitespace(s->header->s[pos]))
pos++;
if (pos == s->header->len)
return false; /* end of string */
if (s->header->s[pos] != sep)
return false;
s->header_pos = ++pos;
return true;
}
static bool get_quoted_string(HttpProxyNegotiator *s)
{
size_t pos = s->header_pos;
while (pos < s->header->len && is_whitespace(s->header->s[pos]))
pos++;
if (pos == s->header->len)
return false; /* end of string */
if (s->header->s[pos] != '"')
return false;
pos++;
strbuf_clear(s->token);
while (pos < s->header->len && s->header->s[pos] != '"') {
if (s->header->s[pos] == '\\') {
/* Backslash makes the next char literal, even if it's " or \ */
pos++;
if (pos == s->header->len)
return false; /* unexpected end of string */
}
put_byte(s->token, s->header->s[pos++]);
}
if (pos == s->header->len)
return false; /* no closing quote */
pos++;
s->header_pos = pos;
return true;
}
static void proxy_http_process_queue(ProxyNegotiator *pn)
{
HttpProxyNegotiator *s = container_of(pn, HttpProxyNegotiator, pn);
crBegin(s->crLine);
/*
* Initialise our username and password strbufs from the Conf.
*/
put_dataz(s->username, conf_get_str(pn->ps->conf, CONF_proxy_username));
put_dataz(s->password, conf_get_str(pn->ps->conf, CONF_proxy_password));
if (s->username->len || s->password->len)
s->try_auth_from_conf = true;
/*
* Set up the host:port string we're trying to connect to, also
* used as the URI string in HTTP Digest auth.
*/
{
char dest[512];
sk_getaddr(pn->ps->remote_addr, dest, lenof(dest));
put_fmt(s->uri, "%s:%d", dest, pn->ps->remote_port);
}
while (true) {
/*
* Standard prefix for the HTTP CONNECT request.
*/
put_fmt(pn->output,
"CONNECT %s HTTP/1.1\r\n"
"Host: %s\r\n", s->uri->s, s->uri->s);
/*
* Add an auth header, if we're planning to this time round.
*/
if (s->next_auth_type == AUTH_BASIC) {
put_datalit(pn->output, "Proxy-Authorization: Basic ");
strbuf *base64_input = strbuf_new_nm();
put_datapl(base64_input, ptrlen_from_strbuf(s->username));
put_byte(base64_input, ':');
put_datapl(base64_input, ptrlen_from_strbuf(s->password));
char base64_output[4];
for (size_t i = 0, e = base64_input->len; i < e; i += 3) {
base64_encode_atom(base64_input->u + i,
e-i > 3 ? 3 : e-i, base64_output);
put_data(pn->output, base64_output, 4);
}
strbuf_free(base64_input);
smemclr(base64_output, sizeof(base64_output));
put_datalit(pn->output, "\r\n");
} else if (s->next_auth_type == AUTH_DIGEST) {
put_datalit(pn->output, "Proxy-Authorization: Digest ");
http_digest_response(BinarySink_UPCAST(pn->output),
ptrlen_from_strbuf(s->username),
ptrlen_from_strbuf(s->password),
ptrlen_from_strbuf(s->realm),
PTRLEN_LITERAL("CONNECT"),
ptrlen_from_strbuf(s->uri),
PTRLEN_LITERAL("auth"),
ptrlen_from_strbuf(s->nonce),
(s->got_opaque ?
ptrlen_from_strbuf(s->opaque) :
make_ptrlen(NULL, 0)),
++s->nonce_count, s->digest_hash,
s->hash_username);
put_datalit(pn->output, "\r\n");
}
/*
* Blank line to terminate the HTTP request.
*/
put_datalit(pn->output, "\r\n");
crReturnV;
s->content_length = 0;
s->connection_close = false;
/*
* Read and parse the HTTP status line, and check if it's a 2xx
* for success.
*/
strbuf_clear(s->response);
crMaybeWaitUntilV(read_line(pn->input, s->response, false));
{
int maj_ver, min_ver, n_scanned;
n_scanned = sscanf(
s->response->s, "HTTP/%d.%d %n%d",
&maj_ver, &min_ver, &s->http_status_pos, &s->http_status);
if (n_scanned < 3) {
pn->error = dupstr("HTTP response was absent or malformed");
crStopV;
}
if (maj_ver < 1 && (maj_ver == 1 && min_ver < 1)) {
/* Before HTTP/1.1, connections close by default */
s->connection_close = true;
}
}
/*
* Read the HTTP response header section.
*/
do {
strbuf_clear(s->header);
crMaybeWaitUntilV(read_line(pn->input, s->header, true));
s->header_pos = 0;
if (!get_token(s)) {
/* Possibly we ought to panic if we see an HTTP header
* we can't make any sense of at all? But whatever,
* ignore it and hope the next one makes more sense */
continue;
}
/* Parse the header name */
HttpHeader hdr = HDR_UNKNOWN;
{
#define CHECK_HEADER(id, string) \
if (!stricmp(s->token->s, string)) hdr = id;
HTTP_HEADER_LIST(CHECK_HEADER);
#undef CHECK_HEADER
}
if (!get_separator(s, ':'))
continue;
if (hdr == HDR_CONTENT_LENGTH) {
if (!get_token(s))
continue;
s->content_length = strtoumax(s->token->s, NULL, 10);
} else if (hdr == HDR_CONNECTION) {
if (!get_token(s))
continue;
if (!stricmp(s->token->s, "close"))
s->connection_close = true;
else if (!stricmp(s->token->s, "keep-alive"))
s->connection_close = false;
} else if (hdr == HDR_PROXY_AUTHENTICATE) {
if (!get_token(s))
continue;
if (!stricmp(s->token->s, "Basic")) {
s->next_auth_type = AUTH_BASIC;
} else if (!stricmp(s->token->s, "Digest")) {
if (!http_digest_available) {
pn->error = dupprintf(
"HTTP proxy requested Digest authentication "
"which we do not support");
crStopV;
}
/* Parse the rest of the Digest header */
s->digest_nonce_was_stale = false;
s->digest_hash = HTTP_DIGEST_MD5;
strbuf_clear(s->realm);
strbuf_clear(s->nonce);
strbuf_clear(s->opaque);
s->got_opaque = false;
s->hash_username = false;
while (true) {
if (!get_token(s))
goto bad_digest;
if (!stricmp(s->token->s, "realm")) {
if (!get_separator(s, '=') ||
!get_quoted_string(s))
goto bad_digest;
put_datapl(s->realm, ptrlen_from_strbuf(s->token));
} else if (!stricmp(s->token->s, "nonce")) {
if (!get_separator(s, '=') ||
!get_quoted_string(s))
goto bad_digest;
/* If we have a fresh nonce, reset the
* nonce count. Otherwise, keep incrementing it. */
if (!ptrlen_eq_ptrlen(
ptrlen_from_strbuf(s->token),
ptrlen_from_strbuf(s->nonce)))
s->nonce_count = 0;
put_datapl(s->nonce, ptrlen_from_strbuf(s->token));
} else if (!stricmp(s->token->s, "opaque")) {
if (!get_separator(s, '=') ||
!get_quoted_string(s))
goto bad_digest;
put_datapl(s->opaque,
ptrlen_from_strbuf(s->token));
s->got_opaque = true;
} else if (!stricmp(s->token->s, "stale")) {
if (!get_separator(s, '=') ||
!get_token(s))
goto bad_digest;
s->digest_nonce_was_stale = !stricmp(
s->token->s, "true");
} else if (!stricmp(s->token->s, "userhash")) {
if (!get_separator(s, '=') ||
!get_token(s))
goto bad_digest;
s->hash_username = !stricmp(s->token->s, "true");
} else if (!stricmp(s->token->s, "algorithm")) {
if (!get_separator(s, '=') ||
!get_token(s))
goto bad_digest;
bool found = false;
for (size_t i = 0; i < N_HTTP_DIGEST_HASHES; i++) {
if (!stricmp(s->token->s, httphashnames[i])) {
s->digest_hash = i;
found = true;
break;
}
}
if (!found) {
pn->error = dupprintf(
"HTTP proxy requested Digest hash "
"algorithm '%s' which we do not support",
s->token->s);
crStopV;
}
} else if (!stricmp(s->token->s, "qop")) {
if (!get_separator(s, '=') ||
!get_quoted_string(s))
goto bad_digest;
if (stricmp(s->token->s, "auth")) {
pn->error = dupprintf(
"HTTP proxy requested Digest quality-of-"
"protection type '%s' which we do not "
"support", s->token->s);
crStopV;
}
} else {
/* Ignore any other auth-param */
if (!get_separator(s, '=') ||
(!get_quoted_string(s) && !get_token(s)))
goto bad_digest;
}
if (get_end_of_header(s))
break;
if (!get_separator(s, ','))
goto bad_digest;
}
s->next_auth_type = AUTH_DIGEST;
continue;
bad_digest:
pn->error = dupprintf("HTTP proxy sent Digest auth "
"request we could not parse");
crStopV;
} else {
pn->error = dupprintf("HTTP proxy asked for unsupported "
"authentication type '%s'",
s->token->s);
crStopV;
}
}
} while (s->header->len > 0);
/* Read and ignore the entire response document */
crMaybeWaitUntilV(bufchain_try_consume(
pn->input, s->content_length));
if (200 <= s->http_status && s->http_status < 300) {
/* Any 2xx HTTP response means we're done */
goto authenticated;
} else if (s->http_status == 407) {
/* 407 is Proxy Authentication Required, which we may be
* able to do something about. */
if (s->connection_close) {
pn->error = dupprintf("HTTP proxy closed connection after "
"asking for authentication");
crStopV;
}
/* If we have auth details from the Conf and haven't tried
* them yet, that's our first step. */
if (s->try_auth_from_conf) {
s->try_auth_from_conf = false;
continue;
}
/* If the server sent us stale="true" in a Digest auth
* header, that means we _don't_ need to request a new
* password yet; just try again with the existing details
* and the fresh nonce it sent us. */
if (s->digest_nonce_was_stale)
continue;
/* Either we never had a password in the first place, or
* the one we already presented was rejected. We can only
* proceed from here if we have a way to ask the user
* questions. */
if (!pn->itr) {
pn->error = dupprintf("HTTP proxy requested authentication "
"which we do not have");
crStopV;
}
/*
* Send some prompts to the user. We'll assume the
* password is always required (since it's just been
* rejected, even if we did send one before), and we'll
* prompt for the username only if we don't have one from
* the Conf.
*/
s->prompts = proxy_new_prompts(pn->ps);
s->prompts->to_server = true;
s->prompts->from_server = false;
s->prompts->name = dupstr("HTTP proxy authentication");
if (!s->username->len) {
s->username_prompt_index = s->prompts->n_prompts;
add_prompt(s->prompts, dupstr("Proxy username: "), true);
} else {
s->username_prompt_index = -1;
}
s->password_prompt_index = s->prompts->n_prompts;
add_prompt(s->prompts, dupstr("Proxy password: "), false);
while (true) {
int prompt_result = seat_get_userpass_input(
interactor_announce(pn->itr), s->prompts);
if (prompt_result > 0) {
break;
} else if (prompt_result == 0) {
pn->aborted = true;
crStopV;
}
crReturnV;
}
if (s->username_prompt_index != -1) {
strbuf_clear(s->username);
put_dataz(s->username,
prompt_get_result_ref(
s->prompts->prompts[s->username_prompt_index]));
}
strbuf_clear(s->password);
put_dataz(s->password,
prompt_get_result_ref(
s->prompts->prompts[s->password_prompt_index]));
free_prompts(s->prompts);
s->prompts = NULL;
} else {
/* Any other HTTP response is treated as permanent failure */
pn->error = dupprintf("HTTP response %s",
s->response->s + s->http_status_pos);
crStopV;
}
}
authenticated:
/*
* Success! Hand over to the main connection.
*/
pn->done = true;
crFinishV;
}
const struct ProxyNegotiatorVT http_proxy_negotiator_vt = {
.new = proxy_http_new,
.free = proxy_http_free,
.process_queue = proxy_http_process_queue,
.type = "HTTP",
};