/* * File: auth.c * * Copyright 2008 Jeremy Henty * * 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 /* for parsing */ #include #include #include #include #include #include #include "auth.h" #include "msg.h" #include "misc.h" #include "dialog.hh" #include "IO/Url.h" #include "../dlib/dlib.h" typedef struct { int ok; const char *realm; } AuthParse_t; typedef struct { char *name; Dlist *paths; /* stripped of any trailing '/', so the root path is "" */ char *authorization; /* the authorization request header */ } AuthRealm_t; typedef struct { char *scheme; char *authority; Dlist *realms; } AuthHost_t; typedef struct { const char *realm_name; 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->realm = NULL; return auth_parse; } static void Auth_parse_free(AuthParse_t *auth_parse) { if (auth_parse) { dFree((void *)auth_parse->realm); } } 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(c)) ? 0 : 1; } /* * Unquote the content of a quoted string. * Return: newly allocated unquoted content. * * Arguments: * quoted: pointer to the first char *after* the initial double quote. * size: the number of chars in the result, *not* including a final '\0'. * * Preconditions: * quoted points to a correctly quoted and escaped string. * size is the number of characters in the quoted string, *after* * removing escape characters. * */ static const char *Auth_unquote_value(const char *quoted, int size) { char c, *value, *value_ptr; value_ptr = value = dNew(char, size + 1); while ((c = *quoted++) != '"') *value_ptr++ = (c == '\\') ? *quoted++ : c; *value_ptr = '\0'; return value; } /* * Parse a quoted string. Save the result as the auth realm if required. * Return: 1 if the parse succeeds, 0 otherwise. */ static int Auth_parse_quoted_string(AuthParse_t *auth_parse, int set_realm, char **auth) { char *value; int size; /* parse the '"' */ switch (*(*auth)++) { case '"': break; case '\0': case ',': MSG("auth.c: missing Basic auth token value after '='\n"); return 0; break; default: MSG("auth.c: garbage in Basic auth after '='\n"); return 0; break; } /* parse the rest */ value = *auth; size = 0; while (1) { switch (*(*auth)++) { case '"': if (set_realm) { dFree((void *)auth_parse->realm); auth_parse->realm = Auth_unquote_value(value, size); auth_parse->ok = 1; } return 1; break; case '\0': MSG("auth.c: auth string ended inside quoted string value\n"); return 0; break; case '\\': /* end of string? */ if (!*(*auth)++) { MSG("auth.c: " "auth string ended inside quoted string value " "immediately after \\\n"); return 0; } /* fall through to the next case */ default: size++; break; } } } /* * Parse a token-value pair. * Return: 1 if the parse succeeds, 0 otherwise. */ static int Auth_parse_token_value(AuthParse_t *auth_parse, char **auth) { char *token; int token_size, set_realm; static const char realm_token[] = "realm"; /* parse a token */ token = *auth; token_size = 0; while (Auth_is_token_char(**auth)) { (*auth)++; token_size++; } if (token_size == 0) { MSG("auth.c: Auth_parse_token_value: " "missing Basic auth token\n"); return 0; } /* skip space characters */ while (**auth == ' ') (*auth)++; /* parse the '=' */ switch (*(*auth)++) { case '=': break; case '\0': case ',': MSG("auth.c: Auth_parse_token_value: " "missing Basic auth token value\n"); return 0; break; default: MSG("auth.c: Auth_parse_token_value: " "garbage after Basic auth token\n"); return 0; break; } /* skip space characters */ while (**auth == ' ') (*auth)++; /* is this value the realm? */ set_realm = auth_parse->realm == NULL && strncasecmp(realm_token,token,token_size) == 0 && strlen(realm_token) == token_size; return Auth_parse_quoted_string(auth_parse, set_realm, auth); } static void Auth_parse_auth_basic(AuthParse_t *auth_parse, char **auth) { int token_value_pairs_found; /* parse comma-separated token-value pairs */ token_value_pairs_found = 0; while (1) { /* skip space and comma characters */ while (**auth == ' ' || **auth == ',') (*auth)++; /* end of string? */ if (!**auth) break; /* parse token-value pair */ if (!Auth_parse_token_value(auth_parse, auth)) break; token_value_pairs_found = 1; } if (!token_value_pairs_found) { MSG("auth.c: Auth_parse_auth_basic: " "missing Basic auth token-value pairs\n"); return; } if (!auth_parse->realm) { MSG("auth.c: Auth_parse_auth_basic: " "missing Basic auth realm\n"); return; } } static void Auth_parse_auth(AuthParse_t *auth_parse, char *auth) { _MSG("auth.c: Auth_parse_auth: auth = '%s'\n", auth); if (strncasecmp(auth, "Basic ", 6) == 0) { auth += 6; Auth_parse_auth_basic(auth_parse, &auth); } else { MSG("auth.c: Auth_parse_auth: " "unknown authorization scheme: auth = {%s}\n", auth); } } /* * 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; 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 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 = strdup(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. */ const char *a_Auth_get_auth_str(const DilloUrl *url) { AuthHost_t *host; AuthRealm_t *realm; return ((host = Auth_host_by_url(url)) && (realm = Auth_realm_by_path(host, URL_PATH(url)))) ? realm->authorization : NULL; } /* * Determine whether the user needs to authenticate. */ static int Auth_do_auth_required(const char *realm_name, 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, realm_name)) && (!Auth_realm_includes_path(realm, URL_PATH(url)))) { _MSG("Auth_do_auth_required: updating realm '%s' with URL '%s'\n", realm_name, URL_STR(url)); Auth_realm_add_path(realm, URL_PATH(url)); 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; char *user_password, *response, *authorization, *authorization_old; 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->realm_name))) { /* create a new realm */ realm = dNew(AuthRealm_t, 1); realm->name = dStrdup(data->realm_name); realm->paths = dList_new(1); realm->authorization = NULL; dList_append(host->realms, realm); } Auth_realm_add_path(realm, URL_PATH(data->url)); /* create and set the authorization */ user_password = dStrconcat(user, ":", password, NULL); response = a_Misc_encode_base64(user_password); authorization = dStrconcat("Authorization: Basic ", response, "\r\n", NULL); authorization_old = realm->authorization; realm->authorization = authorization; dFree(authorization_old); dFree(user_password); dFree(response); } static int Auth_do_auth_dialog(const char *realm, const DilloUrl *url) { int ret; char *message; AuthDialogData_t *data; _MSG("auth.c: Auth_do_auth_dialog: realm = '%s'\n", realm); message = dStrconcat("Enter a user and password for \"", realm, "\".", NULL); data = dNew(AuthDialogData_t, 1); data->realm_name = dStrdup(realm); data->url = a_Url_dup(url); ret = a_Dialog_user_password(message, Auth_do_auth_dialog_cb, data); dFree(message); dFree((void*)data->realm_name); a_Url_free((void*)data->url); dFree(data); return ret; } /* * Do authorization for an auth string. */ static int Auth_do_auth(char *auth, const DilloUrl *url) { int reload; AuthParse_t *auth_parse; _MSG("auth.c: Auth_do_auth: auth={%s}\n", auth); reload = 0; auth_parse = Auth_parse_new(); Auth_parse_auth(auth_parse, auth); if (auth_parse->ok) reload = Auth_do_auth_required(auth_parse->realm, url) ? Auth_do_auth_dialog(auth_parse->realm, url) : 1; Auth_parse_free(auth_parse); return reload; } /* * Do authorization for a set of auth strings. */ int a_Auth_do_auth(Dlist *auths, const DilloUrl *url) { int reload, i; char *auth; reload = 0; for (i = 0; (auth = dList_nth_data(auths, i)); ++i) if (Auth_do_auth(auth, url)) reload = 1; return reload; }