/* * File: auth.c * * Copyright 2008 Jeremy Henty * Copyright 2009 Justus Winter <4winter@informatik.uni-hamburg.de> * * 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 3 of the License, or * (at your option) any later version. */ /* Handling of HTTP AUTH takes place here. * This implementation aims to follow RFC 2617: * http://www.ietf.org/rfc/rfc2617.txt */ #include /* iscntrl */ #include "auth.h" #include "msg.h" #include "misc.h" #include "dialog.hh" #include "digest.h" #include "../dlib/dlib.h" typedef struct { int ok; enum AuthParseHTTPAuthType_t type; const char *realm; const char *nonce; const char *opaque; int stale; enum AuthParseDigestAlgorithm_t algorithm; const char *domain; enum AuthParseDigestQOP_t qop; } AuthParse_t; typedef struct { char *scheme; char *authority; Dlist *realms; } AuthHost_t; typedef struct { const AuthParse_t *auth_parse; const DilloUrl *url; } AuthDialogData_t; /* * Local data */ static Dlist *auth_hosts; /* * Initialize the auth module. */ void a_Auth_init(void) { auth_hosts = dList_new(1); } static AuthParse_t *Auth_parse_new() { AuthParse_t *auth_parse = dNew(AuthParse_t, 1); auth_parse->ok = 0; auth_parse->type = TYPENOTSET; auth_parse->realm = NULL; auth_parse->nonce = NULL; auth_parse->opaque = NULL; auth_parse->stale = 0; auth_parse->algorithm = ALGORITHMNOTSET; auth_parse->domain = NULL; auth_parse->qop = QOPNOTSET; return auth_parse; } static void Auth_parse_free(AuthParse_t *auth_parse) { if (auth_parse) { dFree((void *)auth_parse->realm); dFree((void *)auth_parse->nonce); dFree((void *)auth_parse->opaque); dFree((void *)auth_parse->domain); dFree(auth_parse); } } static int Auth_path_is_inside(const char *path1, const char *path2, int len) { /* * path2 is effectively truncated to length len. Typically len will be * strlen(path2), or 1 less when we want to ignore a trailing '/'. */ return strncmp(path1, path2, len) == 0 && (path1[len] == '\0' || path1[len] == '/'); } /* * Check valid chars. * Return: 0 if invalid, 1 otherwise. */ static int Auth_is_token_char(char c) { const char *invalid = "\"()<>@,;:\\[]?=/{} \t"; return (strchr(invalid, c) || iscntrl((uchar_t)c)) ? 0 : 1; } /* * Unquote the content of a (potentially) quoted string. * Return: newly allocated unquoted content. * * Arguments: * valuep: pointer to a pointer to the first char. * * Preconditions: * *valuep points to a correctly quoted and escaped string. * * Postconditions: * *valuep points to the first not processed char. * */ static Dstr *Auth_unquote_value(char **valuep) { char c, quoted; char *value = *valuep; Dstr *result; while (*value == ' ' || *value == '\t') value++; if ((quoted = *value == '"')) value++; result = dStr_new(NULL); while ((c = *value) && (( quoted && c != '"') || (!quoted && Auth_is_token_char(c)))) { dStr_append_c(result, (c == '\\' && value[1]) ? *++value : c); value++; } if (quoted && *value == '\"') value++; *valuep = value; return result; } typedef int (Auth_parse_token_value_callback_t)(AuthParse_t *auth_parse, char *token, const char *value); /* * Parse authentication challenge into token-value pairs * and feed them into the callback function. * * The parsing is aborted should the callback function return 0. * * Return: 1 if the parse succeeds, 0 otherwise. */ static int Auth_parse_token_value(AuthParse_t *auth_parse, char **auth, Auth_parse_token_value_callback_t *callback) { char keep_going, expect_quoted; char *token, *beyond_token; Dstr *value; size_t *token_size; while (**auth) { _MSG("Auth_parse_token_value: remaining: %s\n", *auth); /* parse a token */ token = *auth; token_size = 0; while (Auth_is_token_char(**auth)) { (*auth)++; token_size++; } if (token_size == 0) { MSG("Auth_parse_token_value: missing auth token\n"); return 0; } beyond_token = *auth; /* skip linear whitespace characters */ while (**auth == ' ' || **auth == '\t') (*auth)++; /* parse the '=' */ switch (*(*auth)++) { case '=': *beyond_token = '\0'; break; case '\0': case ',': MSG("Auth_parse_token_value: missing auth token value\n"); return 0; break; default: MSG("Auth_parse_token_value: garbage after auth token\n"); return 0; break; } value = Auth_unquote_value(auth); expect_quoted = !(strcmp(token, "stale") == 0 || strcmp(token, "algorithm") == 0); if (((*auth)[-1] == '"') != expect_quoted) MSG_WARN("Auth_parse_token_value: " "Values for key %s should%s be quoted.\n", token, expect_quoted ? "" : " not"); keep_going = callback(auth_parse, token, value->str); dStr_free(value, 1); if (!keep_going) break; /* skip ' ' and ',' */ while ((**auth == ' ') || (**auth == ',')) (*auth)++; } return 1; } static int Auth_parse_basic_challenge_cb(AuthParse_t *auth_parse, char *token, const char *value) { if (dStrcasecmp("realm", token) == 0) { if (!auth_parse->realm) auth_parse->realm = strdup(value); return 0; /* end parsing */ } else MSG("Auth_parse_basic_challenge_cb: Ignoring unknown parameter: %s = " "'%s'\n", token, value); return 1; } static int Auth_parse_digest_challenge_cb(AuthParse_t *auth_parse, char *token, const char *value) { const char *const fn = "Auth_parse_digest_challenge_cb"; if (!dStrcasecmp("realm", token) && !auth_parse->realm) auth_parse->realm = strdup(value); else if (!strcmp("domain", token) && !auth_parse->domain) auth_parse->domain = strdup(value); else if (!strcmp("nonce", token) && !auth_parse->nonce) auth_parse->nonce = strdup(value); else if (!strcmp("opaque", token) && !auth_parse->opaque) auth_parse->opaque = strdup(value); else if (strcmp("stale", token) == 0) { if (dStrcasecmp("true", value) == 0) auth_parse->stale = 1; else if (dStrcasecmp("false", value) == 0) auth_parse->stale = 0; else { MSG("%s: Invalid stale value: %s\n", fn, value); return 0; } } else if (strcmp("algorithm", token) == 0) { if (strcmp("MD5", value) == 0) auth_parse->algorithm = MD5; else if (strcmp("MD5-sess", value) == 0) { /* auth_parse->algorithm = MD5SESS; */ MSG("%s: MD5-sess algorithm disabled (not tested because 'not " "correctly implemented yet' in Apache 2.2)\n", fn); return 0; } else { MSG("%s: Unknown algorithm: %s\n", fn, value); return 0; } } else if (strcmp("qop", token) == 0) { while (*value) { int len = strcspn(value, ", \t"); if (len == 4 && strncmp("auth", value, 4) == 0) { auth_parse->qop = AUTH; break; } if (len == 8 && strncmp("auth-int", value, 8) == 0) { /* auth_parse->qop = AUTHINT; */ /* Keep searching; maybe we'll find an "auth" yet. */ MSG("%s: auth-int qop disabled (not tested because 'not " "implemented yet' in Apache 2.2)\n", fn); } else { MSG("%s: Unknown qop value in %s\n", fn, value); } value += len; while (*value == ' ' || *value == '\t') value++; if (*value == ',') value++; while (*value == ' ' || *value == '\t') value++; } } else { MSG("%s: Ignoring unknown parameter: %s = '%s'\n", fn, token, value); } return 1; } static void Auth_parse_challenge_args(AuthParse_t *auth_parse, char **challenge, Auth_parse_token_value_callback_t *cb) { /* parse comma-separated token-value pairs */ while (1) { /* skip space and comma characters */ while (**challenge == ' ' || **challenge == ',') (*challenge)++; /* end of string? */ if (!**challenge) break; /* parse token-value pair */ if (!Auth_parse_token_value(auth_parse, challenge, cb)) break; } if (auth_parse->type == BASIC) { if (auth_parse->realm) { auth_parse->ok = 1; } else { MSG("Auth_parse_challenge_args: missing Basic auth realm\n"); return; } } else if (auth_parse->type == DIGEST) { if (auth_parse->realm && auth_parse->nonce) { auth_parse->ok = 1; } else { MSG("Auth_parse_challenge_args: Digest challenge incomplete\n"); return; } } } static void Auth_parse_challenge(AuthParse_t *auth_parse, char *challenge) { Auth_parse_token_value_callback_t *cb; MSG("auth.c: Auth_parse_challenge: challenge = '%s'\n", challenge); if (auth_parse->type == DIGEST) { challenge += 7; cb = Auth_parse_digest_challenge_cb; } else { challenge += 6; cb = Auth_parse_basic_challenge_cb; } Auth_parse_challenge_args(auth_parse, &challenge, cb); } /* * Return the host that contains a URL, or NULL if there is no such host. */ static AuthHost_t *Auth_host_by_url(const DilloUrl *url) { AuthHost_t *host; int i; for (i = 0; (host = dList_nth_data(auth_hosts, i)); i++) if (((dStrcasecmp(URL_SCHEME(url), host->scheme) == 0) && (dStrcasecmp(URL_AUTHORITY(url), host->authority) == 0))) return host; return NULL; } /* * Search all realms for the one with the given name. */ static AuthRealm_t *Auth_realm_by_name(const AuthHost_t *host, const char *name) { AuthRealm_t *realm; int i; for (i = 0; (realm = dList_nth_data(host->realms, i)); i++) if (strcmp(realm->name, name) == 0) return realm; return NULL; } /* * Search all realms for the one with the best-matching path. */ static AuthRealm_t *Auth_realm_by_path(const AuthHost_t *host, const char *path) { AuthRealm_t *realm_best, *realm; int i, j; int match_length = 0; realm_best = NULL; for (i = 0; (realm = dList_nth_data(host->realms, i)); i++) { char *realm_path; for (j = 0; (realm_path = dList_nth_data(realm->paths, j)); j++) { int realm_path_length = strlen(realm_path); if (Auth_path_is_inside(path, realm_path, realm_path_length) && !(realm_best && match_length >= realm_path_length)) { realm_best = realm; match_length = realm_path_length; } } } return realm_best; } static void Auth_realm_delete(AuthRealm_t *realm) { int i; MSG("Auth_realm_delete: \"%s\"\n", realm->name); for (i = dList_length(realm->paths) - 1; i >= 0; i--) dFree(dList_nth_data(realm->paths, i)); dList_free(realm->paths); dFree(realm->name); dFree(realm->username); dFree(realm->authorization); dFree(realm->cnonce); dFree(realm->nonce); dFree(realm->opaque); dFree(realm->domain); dFree(realm); } static int Auth_realm_includes_path(const AuthRealm_t *realm, const char *path) { int i; char *realm_path; for (i = 0; (realm_path = dList_nth_data(realm->paths, i)); i++) if (Auth_path_is_inside(path, realm_path, strlen(realm_path))) return 1; return 0; } static void Auth_realm_add_path(AuthRealm_t *realm, const char *path) { int len, i; char *realm_path, *n_path; n_path = dStrdup(path); len = strlen(n_path); /* remove trailing '/' */ if (len && n_path[len - 1] == '/') n_path[--len] = 0; /* delete existing paths that are inside the new one */ for (i = 0; (realm_path = dList_nth_data(realm->paths, i)); i++) { if (Auth_path_is_inside(realm_path, path, len)) { dList_remove_fast(realm->paths, realm_path); dFree(realm_path); i--; /* reconsider this slot */ } } dList_append(realm->paths, n_path); } /* * Return the authorization header for an HTTP query. * request_uri is a separate argument because we want it precisely as * formatted in the request. */ char *a_Auth_get_auth_str(const DilloUrl *url, const char *request_uri) { char *ret = NULL; AuthHost_t *host; AuthRealm_t *realm; if ((host = Auth_host_by_url(url)) && (realm = Auth_realm_by_path(host, URL_PATH(url)))) { if (realm->type == BASIC) ret = dStrdup(realm->authorization); else if (realm->type == DIGEST) ret = a_Digest_authorization_hdr(realm, url, request_uri); else MSG("a_Auth_get_auth_str() got an unknown realm type: %i.\n", realm->type); } return ret; } /* * Determine whether the user needs to authenticate. */ static int Auth_do_auth_required(const AuthParse_t *auth_parse, const DilloUrl *url) { /* * TO DO: I dislike the way that this code must decide whether we * sent authentication during the request and trust us to resend it * after the reload. Could it be more robust if every DilloUrl * recorded its authentication, and whether it was accepted? (JCH) */ AuthHost_t *host; AuthRealm_t *realm; /* * The size of the following comments reflects the concerns in the * TO DO at the top of this function. It should not be so hard to * explain why code is correct! (JCH) */ /* * If we have authentication but did not send it (because we did * not know this path was in the realm) then we update the realm. * We do not re-authenticate because our authentication is probably * OK. Thanks to the updated realm the forthcoming reload will * make us send the authentication. If our authentication is not * OK the server will challenge us again after the reload and then * we will re-authenticate. */ if ((host = Auth_host_by_url(url)) && (realm = Auth_realm_by_name(host, auth_parse->realm))) { if (!Auth_realm_includes_path(realm, URL_PATH(url))) { _MSG("Auth_do_auth_required: updating realm '%s' with URL '%s'\n", auth_parse->realm, URL_STR(url)); Auth_realm_add_path(realm, URL_PATH(url)); return 0; } if (auth_parse->type == DIGEST && auth_parse->stale) { /* we do have valid credentials but our nonce is old */ dFree((void *)realm->nonce); realm->nonce = dStrdup(auth_parse->nonce); return 0; } } /* * Either we had no authentication or we sent it and the server * rejected it, so we must re-authenticate. */ return 1; } static void Auth_do_auth_dialog_cb(const char *user, const char *password, void *vData) { AuthDialogData_t *data; AuthHost_t *host; AuthRealm_t *realm; data = (AuthDialogData_t *)vData; /* find or create the host */ if (!(host = Auth_host_by_url(data->url))) { /* create a new host */ host = dNew(AuthHost_t, 1); host->scheme = dStrdup(URL_SCHEME(data->url)); host->authority = dStrdup(URL_AUTHORITY(data->url)); host->realms = dList_new(1); dList_append(auth_hosts, host); } /* find or create the realm */ if (!(realm = Auth_realm_by_name(host, data->auth_parse->realm))) { realm = dNew0(AuthRealm_t, 1); realm->name = dStrdup(data->auth_parse->realm); realm->paths = dList_new(1); dList_append(host->realms, realm); } realm->type = data->auth_parse->type; dFree(realm->authorization); realm->authorization = NULL; Auth_realm_add_path(realm, URL_PATH(data->url)); if (realm->type == BASIC) { char *user_password = dStrconcat(user, ":", password, NULL); char *response = a_Misc_encode_base64(user_password); char *authorization = dStrconcat("Authorization: Basic ", response, "\r\n", NULL); dFree(realm->authorization); realm->authorization = authorization; dFree(response); dStrshred(user_password); dFree(user_password); } else if (realm->type == DIGEST) { dFree(realm->username); realm->username = dStrdup(user); realm->nonce_count = 0; dFree(realm->nonce); realm->nonce = dStrdup(data->auth_parse->nonce); dFree(realm->opaque); realm->opaque = dStrdup(data->auth_parse->opaque); realm->algorithm = data->auth_parse->algorithm; dFree(realm->domain); realm->domain = dStrdup(data->auth_parse->domain); realm->qop = data->auth_parse->qop; dFree(realm->cnonce); if (realm->qop != QOPNOTSET) realm->cnonce = a_Digest_create_cnonce(); if (!a_Digest_compute_digest(realm, user, password)) { MSG("Auth_do_auth_dialog_cb: a_Digest_compute_digest failed.\n"); dList_remove_fast(host->realms, realm); Auth_realm_delete(realm); } } else { MSG("Auth_do_auth_dialog_cb: Unknown auth type: %i\n", realm->type); } dStrshred((char *)password); } /* * Return: Nonzero if we got new credentials from the user and everything * seems fine. */ static int Auth_do_auth_dialog(const AuthParse_t *auth_parse, const DilloUrl *url) { int ret; char *message; AuthDialogData_t *data; const char *typestr = auth_parse->type == DIGEST ? "Digest" : "Basic"; _MSG("auth.c: Auth_do_auth_dialog: realm = '%s'\n", auth_parse->realm); message = dStrconcat("The server at ", URL_HOST(url), " requires a username" " and password for \"", auth_parse->realm, "\".\n\n" "Authentication scheme: ", typestr, NULL); data = dNew(AuthDialogData_t, 1); data->auth_parse = auth_parse; data->url = a_Url_dup(url); ret = a_Dialog_user_password(message, Auth_do_auth_dialog_cb, data); dFree(message); a_Url_free((void *)data->url); dFree(data); return ret; } /* * Do authorization for an auth string. */ static int Auth_do_auth(char *challenge, enum AuthParseHTTPAuthType_t type, const DilloUrl *url) { AuthParse_t *auth_parse; int reload = 0; _MSG("auth.c: Auth_do_auth: challenge={%s}\n", challenge); auth_parse = Auth_parse_new(); auth_parse->type = type; Auth_parse_challenge(auth_parse, challenge); if (auth_parse->ok) reload = Auth_do_auth_required(auth_parse, url) ? Auth_do_auth_dialog(auth_parse, url) : 1; Auth_parse_free(auth_parse); return reload; } /* * Given authentication challenge(s), prepare authorization. * Return: 0 on failure * nonzero on success. A new query will be sent to the server. */ int a_Auth_do_auth(Dlist *challenges, const DilloUrl *url) { int i; char *chal; for (i = 0; (chal = dList_nth_data(challenges, i)); ++i) if (!dStrncasecmp(chal, "Digest ", 7)) if (Auth_do_auth(chal, DIGEST, url)) return 1; for (i = 0; (chal = dList_nth_data(challenges, i)); ++i) if (!dStrncasecmp(chal, "Basic ", 6)) if (Auth_do_auth(chal, BASIC, url)) return 1; return 0; }