blob: fdb6699ba661e3c3e9687cd4f32f65a2519211f8 [file] [log] [blame]
/* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* mod_digest: MD5 digest authentication
*
* by Alexei Kosut <akosut@nueva.pvt.k12.ca.us>
* based on mod_auth, by Rob McCool and Robert S. Thau
*
*/
#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_log.h"
#include "http_protocol.h"
#include "util_md5.h"
typedef struct digest_config_struct {
char *pwfile;
} digest_config_rec;
typedef struct digest_header_struct {
char *username;
char *realm;
char *nonce;
char *requested_uri;
char *digest;
} digest_header_rec;
static void *create_digest_dir_config(pool *p, char *d)
{
return ap_pcalloc(p, sizeof(digest_config_rec));
}
static const char *set_digest_slot(cmd_parms *cmd, void *offset, char *f, char *t)
{
if (t && strcmp(t, "standard"))
return ap_pstrcat(cmd->pool, "Invalid auth file type: ", t, NULL);
return ap_set_string_slot(cmd, offset, f);
}
static const command_rec digest_cmds[] =
{
{"AuthDigestFile", set_digest_slot,
(void *) XtOffsetOf(digest_config_rec, pwfile), OR_AUTHCFG, TAKE12, NULL},
{NULL}
};
module MODULE_VAR_EXPORT digest_module;
static char *get_hash(request_rec *r, char *user, char *auth_pwfile)
{
configfile_t *f;
char l[MAX_STRING_LEN];
const char *rpw;
char *w, *x;
if (!(f = ap_pcfg_openfile(r->pool, auth_pwfile))) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, r,
"Could not open password file: %s", auth_pwfile);
return NULL;
}
while (!(ap_cfg_getline(l, MAX_STRING_LEN, f))) {
if ((l[0] == '#') || (!l[0]))
continue;
rpw = l;
w = ap_getword(r->pool, &rpw, ':');
x = ap_getword(r->pool, &rpw, ':');
if (x && w && !strcmp(user, w) && !strcmp(ap_auth_name(r), x)) {
ap_cfg_closefile(f);
return ap_pstrdup(r->pool, rpw);
}
}
ap_cfg_closefile(f);
return NULL;
}
/* Parse the Authorization header, if it exists */
static int get_digest_rec(request_rec *r, digest_header_rec * response)
{
const char *auth_line;
int l;
int s, vk = 0, vv = 0;
const char *t;
char *key, *value;
const char *scheme;
if (!(t = ap_auth_type(r)) || strcasecmp(t, "Digest"))
return DECLINED;
if (!ap_auth_name(r)) {
ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
"need AuthName: %s", r->uri);
return SERVER_ERROR;
}
auth_line = ap_table_get(r->headers_in,
r->proxyreq == STD_PROXY ? "Proxy-Authorization"
: "Authorization");
if (!auth_line) {
ap_note_digest_auth_failure(r);
return AUTH_REQUIRED;
}
if (strcasecmp(scheme = ap_getword_white(r->pool, &auth_line), "Digest")) {
/* Client tried to authenticate using wrong auth scheme */
ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r->server,
"client used wrong authentication scheme: %s for %s",
scheme, r->uri);
ap_note_digest_auth_failure(r);
return AUTH_REQUIRED;
}
l = strlen(auth_line);
/* Note we don't allocate l + 1 bytes for these deliberately, because
* there has to be at least one '=' character for either of these two
* new strings to be terminated. That takes care of the need for +1.
*/
key = ap_palloc(r->pool, l);
value = ap_palloc(r->pool, l);
/* There's probably a better way to do this, but for the time being...
*
* Right now the parsing is very 'slack'. Actual rules from RFC 2617 are:
*
* Authorization = "Digest" digest-response
* digest-response = 1#( username | realm | nonce | digest-uri |
* response | [ cnonce ] | [ algorithm ] |
* [opaque] | [message-qop] | [nonce-count] |
* [auth-param] ) (see note 4)
* username = "username" "=" username-value
* username-value = quoted-string
* digest-uri = "uri" "=" digest-uri-value
* digest-uri-value = request-uri
* message-qop = "qop" "=" qop-value
* qop-options = "qop" "=" <"> 1#qop-value <"> (see note 3)
* qop-value = "auth" | "auth-int" | token
* cnonce = "cnonce" "=" cnonce-value
* cnonce-value = nonce-value
* nonce-count = "nc" "=" nc-value
* nc-value = 8LHEX
* response = "response" "=" response-digest
* response-digest = <"> *LHEX <">
* LHEX = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" |
* "8" | "9" | "a" | "b" | "c" | "d" | "e" | "f"
*
* Current Discrepancies:
* quoted-string section 2.2 of RFC 2068
* --> We also acccept unquoted strings or strings
* like foo" bar". And take a space, comma or EOL as
* the terminator in that case.
*
* request-uri section 5.1 of RFC 2068
* --> We currently also accept any quoted string - and
* ignore those quotes.
*
* response/entity-digest
* --> We ignore the presense of the " if any.
*
* Note: There is an inherent problem with the request URI; as it should
* be used unquoted - yet may contain a ',' - which is used as
* a terminator:
* Authorization: Digest username="dirkx", realm="DAV", nonce="1031662894",
* uri=/mary,+dirkx,+peter+and+mary.ics, response="99a6275793be28c31a5b6e4467fa4c79",
* algorithm=MD5
*
* Note3: Taken from section 3.2.1 - as this is not actually defined in section 3.2.2
* which deals with the Authorization Request Header.
*
* Note4: The 'comma separated' list concept is refered to in the RFC
* but whitespace eating and other such things are assumed to be
* as per MIME/RFC2068 spec.
*/
#define D_KEY 0
#define D_VALUE 1
#define D_STRING 2
#define D_EXIT -1
s = D_KEY;
while (s != D_EXIT) {
switch (s) {
case D_STRING:
if (auth_line[0] == '\"') {
s = D_VALUE;
}
else {
value[vv] = auth_line[0];
vv++;
}
auth_line++;
break;
case D_VALUE:
/* A request URI may be unquoted and yet
* contain non alpha/num chars. (Though gets terminated by
* a ',' - which in fact may be in the URI - so I guess
* 2069 should be updated to suggest strongly to quote).
*/
if (auth_line[0] == '\"') {
s = D_STRING;
}
else if ((auth_line[0] != ',') && (auth_line[0] != ' ') && (auth_line[0] != '\0')) {
value[vv] = auth_line[0];
vv++;
}
else {
value[vv] = '\0';
if (!strcasecmp(key, "username"))
response->username = ap_pstrdup(r->pool, value);
else if (!strcasecmp(key, "realm"))
response->realm = ap_pstrdup(r->pool, value);
else if (!strcasecmp(key, "nonce"))
response->nonce = ap_pstrdup(r->pool, value);
else if (!strcasecmp(key, "uri"))
response->requested_uri = ap_pstrdup(r->pool, value);
else if (!strcasecmp(key, "response"))
response->digest = ap_pstrdup(r->pool, value);
vv = 0;
s = D_KEY;
}
auth_line++;
break;
case D_KEY:
if (ap_isalnum(auth_line[0])) {
key[vk] = auth_line[0];
vk++;
}
else if (auth_line[0] == '=') {
key[vk] = '\0';
vk = 0;
s = D_VALUE;
}
auth_line++;
break;
}
if (auth_line[-1] == '\0')
s = D_EXIT;
}
if (!response->username || !response->realm || !response->nonce ||
!response->requested_uri || !response->digest) {
ap_note_digest_auth_failure(r);
return AUTH_REQUIRED;
}
r->connection->user = response->username;
r->connection->ap_auth_type = "Digest";
return OK;
}
/* The actual MD5 code... whee */
/* Check that a given nonce is actually one which was
* issued by this server in the right context.
*/
static int check_nonce(pool *p, const char *prefix, const char *nonce) {
char *timestamp = (char *)nonce + 2 * MD5_DIGESTSIZE;
char *md5;
if (strlen(nonce) < 2 * MD5_DIGESTSIZE)
return AUTH_REQUIRED;
md5 = ap_md5(p, (unsigned char *)ap_pstrcat(p, prefix, timestamp, NULL));
return strncmp(md5, nonce, 2 * MD5_DIGESTSIZE);
}
/* Check the digest itself.
*/
static char *find_digest(request_rec *r, digest_header_rec * h, char *a1)
{
return ap_md5(r->pool,
(unsigned char *)ap_pstrcat(r->pool, a1, ":", h->nonce, ":",
ap_md5(r->pool,
(unsigned char *)ap_pstrcat(r->pool, r->method, ":",
h->requested_uri, NULL)),
NULL));
}
/* These functions return 0 if client is OK, and proper error status
* if not... either AUTH_REQUIRED, if we made a check, and it failed, or
* SERVER_ERROR, if things are so totally confused that we couldn't
* figure out how to tell if the client is authorized or not.
*
* If they return DECLINED, and all other modules also decline, that's
* treated by the server core as a configuration error, logged and
* reported as such.
*/
/* Determine user ID, and check if it really is that user, for HTTP
* basic authentication...
*/
static int authenticate_digest_user(request_rec *r)
{
digest_config_rec *sec =
(digest_config_rec *) ap_get_module_config(r->per_dir_config,
&digest_module);
digest_header_rec *response = ap_pcalloc(r->pool, sizeof(digest_header_rec));
conn_rec *c = r->connection;
char *a1;
int res;
if ((res = get_digest_rec(r, response)))
return res;
if (!sec->pwfile)
return DECLINED;
/* Check that the nonce was one we actually issued. */
if (check_nonce(r->pool, ap_auth_nonce(r), response->nonce)) {
ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
"Client is using a nonce which was not issued by "
"this server for this context: %s", r->uri);
ap_note_digest_auth_failure(r);
return AUTH_REQUIRED;
}
if (!(a1 = get_hash(r, c->user, sec->pwfile))) {
ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
"user %s not found: %s", c->user, r->uri);
ap_note_digest_auth_failure(r);
return AUTH_REQUIRED;
}
if (strcmp(response->digest, find_digest(r, response, a1))) {
ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
"user %s: password mismatch: %s", c->user, r->uri);
ap_note_digest_auth_failure(r);
return AUTH_REQUIRED;
}
return OK;
}
/* Checking ID */
static int digest_check_auth(request_rec *r)
{
char *user = r->connection->user;
int m = r->method_number;
int method_restricted = 0;
register int x;
const char *t;
char *w;
const array_header *reqs_arr;
require_line *reqs;
if (!(t = ap_auth_type(r)) || strcasecmp(t, "Digest"))
return DECLINED;
reqs_arr = ap_requires(r);
/* If there is no "requires" directive,
* then any user will do.
*/
if (!reqs_arr)
return OK;
reqs = (require_line *) reqs_arr->elts;
for (x = 0; x < reqs_arr->nelts; x++) {
if (!(reqs[x].method_mask & (1 << m)))
continue;
method_restricted = 1;
t = reqs[x].requirement;
w = ap_getword_white(r->pool, &t);
if (!strcmp(w, "valid-user"))
return OK;
else if (!strcmp(w, "user")) {
while (t[0]) {
w = ap_getword_conf(r->pool, &t);
if (!strcmp(user, w))
return OK;
}
}
else
return DECLINED;
}
if (!method_restricted)
return OK;
ap_note_digest_auth_failure(r);
return AUTH_REQUIRED;
}
module MODULE_VAR_EXPORT digest_module =
{
STANDARD_MODULE_STUFF,
NULL, /* initializer */
create_digest_dir_config, /* dir config creater */
NULL, /* dir merger --- default is to override */
NULL, /* server config */
NULL, /* merge server config */
digest_cmds, /* command table */
NULL, /* handlers */
NULL, /* filename translation */
authenticate_digest_user, /* check_user_id */
digest_check_auth, /* check auth */
NULL, /* check access */
NULL, /* type_checker */
NULL, /* fixups */
NULL, /* logger */
NULL, /* header parser */
NULL, /* child_init */
NULL, /* child_exit */
NULL /* post read-request */
};