[MEDIUM] added the new 'stats' keyword with user authentication subsystem.

Right now it only validates the user/passwd according to a specified list,
and lets the user pass through the proxy if the authentication is OK, and
it refuses any invalid access with a 401 Unauthorized response.
This commit is contained in:
willy tarreau 2006-05-14 23:06:28 +02:00
parent 4404b7ebcc
commit 9e1388671a
6 changed files with 438 additions and 3 deletions

View File

@ -109,7 +109,7 @@ LDFLAGS = -g
all: haproxy
haproxy: src/list.o src/chtbl.o src/hashpjw.o haproxy.o
haproxy: src/list.o src/chtbl.o src/hashpjw.o haproxy.o src/base64.o src/uri_auth.o
$(LD) $(LDFLAGS) -o $@ $^ $(LIBS)
%.o: %.c

146
haproxy.c
View File

@ -85,6 +85,8 @@
#include <assert.h>
#endif
#include <include/base64.h>
#include <include/uri_auth.h>
#include "include/appsession.h"
#include "include/mini-clist.h"
@ -493,6 +495,12 @@ int strlcpy2(char *dst, const char *src, int size) {
/*********************************************************************/
/* describes a chunk of string */
struct chunk {
char *str; /* beginning of the string itself. Might not be 0-terminated */
int len; /* size of the string from first to last char. <0 = uninit. */
};
struct cap_hdr {
struct cap_hdr *next;
char *name; /* header name, case insensitive */
@ -584,6 +592,8 @@ struct session {
struct pendconn *pend_pos; /* if not NULL, points to the position in the pending queue */
char **req_cap; /* array of captured request headers (may be NULL) */
char **rsp_cap; /* array of captured response headers (may be NULL) */
struct chunk req_line; /* points to first line */
struct chunk auth_hdr; /* points to 'Authorization:' header */
struct {
int logwait; /* log fields waiting to be collected : LW_* */
struct timeval tv_accept; /* date of the accept() (beginning of the session) */
@ -630,6 +640,7 @@ struct proxy {
char *capture_name; /* beginning of the name of the cookie to capture */
int capture_namelen; /* length of the cookie name to match */
int capture_len; /* length of the string to be captured */
struct uri_auth *uri_auth; /* if non-NULL, the (list of) per-URI authentications */
int clitimeout; /* client I/O timeout (in milliseconds) */
int srvtimeout; /* server I/O timeout (in milliseconds) */
int contimeout; /* connect timeout (in milliseconds) */
@ -839,6 +850,15 @@ const char *HTTP_400 =
"\r\n"
"<html><body><h1>400 Bad request</h1>\nYour browser sent an invalid request.\n</body></html>\n";
/* Warning: this one is an sprintf() fmt string, with <realm> as its only argument */
const char *HTTP_401_fmt =
"HTTP/1.0 401 Unauthorized\r\n"
"Cache-Control: no-cache\r\n"
"Connection: close\r\n"
"WWW-Authenticate: Basic realm=\"%s\"\r\n"
"\r\n"
"<html><body><h1>401 Unauthorized</h1>\nYou need a valid user and password to access this content.\n</body></html>\n";
const char *HTTP_403 =
"HTTP/1.0 403 Forbidden\r\n"
"Cache-Control: no-cache\r\n"
@ -3139,6 +3159,8 @@ int event_accept(int fd) {
s->res_cr = s->res_cw = s->res_sr = s->res_sw = RES_SILENT;
s->cli_fd = cfd;
s->srv_fd = -1;
s->req_line.len = -1;
s->auth_hdr.len = -1;
s->srv = NULL;
s->pend_pos = NULL;
s->conn_retries = p->conn_retries;
@ -3683,6 +3705,71 @@ int process_cli(struct session *t) {
return 1;
}
/* Right now, we know that we have processed the entire headers
* and that unwanted requests have been filtered out. We can do
* whatever we want.
*/
/* FIXME debugging code !!! */
if (t->req_line.len >= 0) {
write(2, t->req_line.str, t->req_line.len);
}
if (t->proxy->uri_auth != NULL
&& t->req_line.len >= t->proxy->uri_auth->uri_len + 4) { /* +4 for "GET /" */
if (!memcmp(t->req_line.str + 4,
t->proxy->uri_auth->uri_prefix, t->proxy->uri_auth->uri_len)
&& !memcmp(t->req_line.str, "GET ", 4)) {
struct user_auth *user;
int authenticated;
/* we are in front of a interceptable URI. Let's check
* if there's an authentication and if it's valid.
*/
user = t->proxy->uri_auth->users;
if (!user) {
/* no user auth required, it's OK */
authenticated = 1;
} else {
authenticated = 0;
/* a user list is defined, we have to check.
* skip 21 chars for "Authorization: Basic ".
*/
if (t->auth_hdr.len < 21 || memcmp(t->auth_hdr.str + 14, " Basic ", 7))
user = NULL;
while (user) {
if ((t->auth_hdr.len == user->user_len + 21)
&& !memcmp(t->auth_hdr.str+21, user->user_pwd, user->user_len)) {
authenticated = 1;
break;
}
user = user->next;
write(2, t->auth_hdr.str, t->auth_hdr.len);
}
}
if (!authenticated) {
int msglen;
/* no need to go further */
msglen = sprintf(trash, HTTP_401_fmt, t->proxy->uri_auth->auth_realm);
t->logs.status = 401;
client_retnclose(t, msglen, trash);
if (!(t->flags & SN_ERR_MASK))
t->flags |= SN_ERR_PRXCOND;
if (!(t->flags & SN_FINST_MASK))
t->flags |= SN_FINST_R;
return 1;
}
/* Hey, we have passed the authentication ! */
}
}
for (line = 0; line < t->proxy->nb_reqadd; line++) {
len = sprintf(trash, "%s\r\n", t->proxy->req_add[line]);
buffer_replace2(req, req->h, req->h, trash, len);
@ -4228,8 +4315,8 @@ int process_cli(struct session *t) {
/* WARNING! <ptr> becomes invalid for now. If some code
* below needs to rely on it before the end of the global
* header loop, we need to correct it with this code :
* ptr = del_colon;
*/
ptr = del_colon;
}
else
delete_header = 1;
@ -4239,8 +4326,24 @@ int process_cli(struct session *t) {
/* let's look if we have to delete this header */
if (delete_header && !(t->flags & SN_CLDENY)) {
buffer_replace2(req, req->h, req->lr, NULL, 0);
/* WARNING: ptr is not valid anymore, since the header may have
* been deleted or truncated ! */
} else {
/* try to catch the first line as the request */
if (t->req_line.len < 0) {
t->req_line.str = req->h;
t->req_line.len = ptr - req->h;
}
/* We might also need the 'Authorization: ' header */
if (t->auth_hdr.len < 0 &&
t->proxy->uri_auth != NULL &&
ptr > req->h + 15 &&
!strncasecmp("Authorization: ", req->h, 15)) {
t->auth_hdr.str = req->h;
t->auth_hdr.len = ptr - req->h;
}
}
/* WARNING: ptr is not valid anymore, since the header may have been deleted or truncated ! */
req->h = req->lr;
} /* while (req->lr < req->r) */
@ -7545,6 +7648,45 @@ int cfg_parse_listen(char *file, int linenum, char **args) {
}
curproxy->conn_retries = atol(args[1]);
}
else if (!strcmp(args[0], "stats")) {
if (*(args[1]) == 0) {
Alert("parsing [%s:%d] : '%s' expects 'uri', 'realm', 'auth' or 'enable'.\n", file, linenum, args[0]);
return -1;
} else if (!strcmp(args[1], "uri")) {
if (*(args[2]) == 0) {
Alert("parsing [%s:%d] : 'uri' needs an URI prefix.\n", file, linenum);
return -1;
} else if (!stats_set_uri(&curproxy->uri_auth, args[2])) {
Alert("parsing [%s:%d] : out of memory.\n", file, linenum);
return -1;
}
} else if (!strcmp(args[1], "realm")) {
if (*(args[2]) == 0) {
Alert("parsing [%s:%d] : 'realm' needs an realm name.\n", file, linenum);
return -1;
} else if (!stats_set_realm(&curproxy->uri_auth, args[2])) {
Alert("parsing [%s:%d] : out of memory.\n", file, linenum);
return -1;
}
} else if (!strcmp(args[1], "auth")) {
if (*(args[2]) == 0) {
Alert("parsing [%s:%d] : 'auth' needs a user:password account.\n", file, linenum);
return -1;
} else if (!stats_add_auth(&curproxy->uri_auth, args[2])) {
Alert("parsing [%s:%d] : out of memory.\n", file, linenum);
return -1;
}
} else if (!strcmp(args[1], "enable")) {
if (!stats_check_init_uri_auth(&curproxy->uri_auth)) {
Alert("parsing [%s:%d] : out of memory.\n", file, linenum);
return -1;
}
} else {
Alert("parsing [%s:%d] : unknown stats parameter '%s' (expects 'uri', 'realm', 'auth' or 'enable').\n",
file, linenum, args[0]);
return -1;
}
}
else if (!strcmp(args[0], "option")) {
if (*(args[1]) == 0) {
Alert("parsing [%s:%d] : '%s' expects an option name.\n", file, linenum, args[0]);

17
include/base64.h Normal file
View File

@ -0,0 +1,17 @@
/*
* Ascii to Base64 conversion as described in RFC1421.
* Copyright 2006 Willy Tarreau <willy@w.ods.org>
*
* 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
* 2 of the License, or (at your option) any later version.
*
*/
#ifndef BASE64_H
#define BASE64_H
int a2base64(char *in, int ilen, char *out, int olen);
#endif /* BASE64_H */

57
include/uri_auth.h Normal file
View File

@ -0,0 +1,57 @@
/*
* URI-based user authentication using the HTTP basic method.
*
* Copyright 2006 Willy Tarreau <willy@w.ods.org>
*
* 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
* 2 of the License, or (at your option) any later version.
*
*/
#ifndef URI_AUTH_H
#define URI_AUTH_H
/* here we find a very basic list of base64-encoded 'user:passwd' strings */
struct user_auth {
struct user_auth *next; /* next entry, NULL if none */
int user_len; /* user:passwd length */
char *user_pwd; /* auth as base64("user":"passwd") (see RFC2617) */
};
/* later we may link them to support multiple URI matching */
struct uri_auth {
int uri_len; /* the prefix length */
char *uri_prefix; /* the prefix we want to match */
char *auth_realm; /* the realm reported to the client */
struct user_auth *users; /* linked list of valid user:passwd couples */
};
/* This is the default statistics URI */
#ifdef CONFIG_STATS_DEFAULT_URI
#define STATS_DEFAULT_URI CONFIG_STATS_DEFAULT_URI
#else
#define STATS_DEFAULT_URI "/haproxy?stats"
#endif
/* This is the default statistics realm */
#ifdef CONFIG_STATS_DEFAULT_REALM
#define STATS_DEFAULT_REALM CONFIG_STATS_DEFAULT_REALM
#else
#define STATS_DEFAULT_REALM "HAProxy Statistics"
#endif
/* Various functions used to set the fields during the configuration parsing.
* Please that all those function can initialize the root entry in order not to
* force the user to respect a certain order in the configuration file.
*
* Default values are used during initialization. Check STATS_DEFAULT_* for
* more information.
*/
struct uri_auth *stats_check_init_uri_auth(struct uri_auth **root);
struct uri_auth *stats_set_uri(struct uri_auth **root, char *uri);
struct uri_auth *stats_set_realm(struct uri_auth **root, char *realm);
struct uri_auth *stats_add_auth(struct uri_auth **root, char *user);
#endif /* URI_AUTH_H */

57
src/base64.c Normal file
View File

@ -0,0 +1,57 @@
/*
* Ascii to Base64 conversion as described in RFC1421.
* Copyright 2006 Willy Tarreau <willy@w.ods.org>
*
* 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
* 2 of the License, or (at your option) any later version.
*
*/
#include <include/base64.h>
/* Encodes <ilen> bytes from <in> to <out> for at most <olen> chars (including
* the trailing zero). Returns the number of bytes written. No check is made
* for <in> or <out> to be NULL. Returns negative value if <olen> is too short
* to accept <ilen>. 4 output bytes are produced for 1 to 3 input bytes.
*/
int a2base64(char *in, int ilen, char *out, int olen)
{
char base64[64]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
int convlen;
convlen = ((ilen + 2) / 3) * 4;
if (convlen >= olen)
return -1;
/* we don't need to check olen anymore */
while (ilen >= 3) {
out[0] = base64[(((unsigned char)in[0]) >> 2)];
out[1] = base64[(((unsigned char)in[0] & 0x03) << 4) | (((unsigned char)in[1]) >> 4)];
out[2] = base64[(((unsigned char)in[1] & 0x0F) << 2) | (((unsigned char)in[2]) >> 6)];
out[3] = base64[(((unsigned char)in[2] & 0x3F))];
out += 4;
in += 3; ilen -= 3;
}
if (!ilen) {
out[0] = '\0';
} else {
out[0] = base64[((unsigned char)in[0]) >> 2];
if (ilen == 1) {
out[1] = base64[((unsigned char)in[0] & 0x03) << 4];
out[2] = '=';
} else {
out[1] = base64[(((unsigned char)in[0] & 0x03) << 4) |
(((unsigned char)in[1]) >> 4)];
out[2] = base64[((unsigned char)in[1] & 0x0F) << 2];
}
out[3] = '=';
out[4] = '\0';
}
return convlen;
}

162
src/uri_auth.c Normal file
View File

@ -0,0 +1,162 @@
/*
* URI-based user authentication using the HTTP basic method.
*
* Copyright 2006 Willy Tarreau <willy@w.ods.org>
*
* 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
* 2 of the License, or (at your option) any later version.
*
*/
#include <stdlib.h>
#include <string.h>
#include <include/uri_auth.h>
#include <include/base64.h>
/*
* Initializes a basic uri_auth structure header and returns a pointer to it.
* Uses the pointer provided if not NULL and not initialized.
*/
struct uri_auth *stats_check_init_uri_auth(struct uri_auth **root)
{
struct uri_auth *u;
if (!root || !*root) {
if ((u = (struct uri_auth *)calloc(1, sizeof (*u))) == NULL)
goto out_u;
} else
u = *root;
if (!u->uri_prefix) {
u->uri_len = strlen(STATS_DEFAULT_URI);
if ((u->uri_prefix = strdup(STATS_DEFAULT_URI)) == NULL)
goto out_uri;
}
if (!u->auth_realm)
if ((u->auth_realm = strdup(STATS_DEFAULT_REALM)) == NULL)
goto out_realm;
if (root && !*root)
*root = u;
return u;
out_realm:
free(u->uri_prefix);
out_uri:
if (!root || !*root)
free(u);
out_u:
return NULL;
}
/*
* Returns a default uri_auth with <uri> set as the uri_prefix.
* Uses the pointer provided if not NULL and not initialized.
*/
struct uri_auth *stats_set_uri(struct uri_auth **root, char *uri)
{
struct uri_auth *u;
char *uri_copy;
int uri_len;
uri_len = strlen(uri);
if ((uri_copy = strdup(uri)) == NULL)
goto out_uri;
if ((u = stats_check_init_uri_auth(root)) == NULL)
goto out_u;
if (u->uri_prefix)
free(u->uri_prefix);
u->uri_len = uri_len;
u->uri_prefix = uri_copy;
return u;
out_u:
free(uri_copy);
out_uri:
return NULL;
}
/*
* Returns a default uri_auth with <realm> set as the realm.
* Uses the pointer provided if not NULL and not initialized.
*/
struct uri_auth *stats_set_realm(struct uri_auth **root, char *realm)
{
struct uri_auth *u;
char *realm_copy;
if ((realm_copy = strdup(realm)) == NULL)
goto out_realm;
if ((u = stats_check_init_uri_auth(root)) == NULL)
goto out_u;
if (u->auth_realm)
free(u->auth_realm);
u->auth_realm = realm_copy;
return u;
out_u:
free(realm_copy);
out_realm:
return NULL;
}
/*
* Returns a default uri_auth with a <user:passwd> entry added to the list of
* authorized users. If a matching entry is found, no update will be performed.
* Uses the pointer provided if not NULL and not initialized.
*/
struct uri_auth *stats_add_auth(struct uri_auth **root, char *auth)
{
struct uri_auth *u;
char *auth_base64;
int alen, blen;
struct user_auth *users, **ulist;
alen = strlen(auth);
blen = ((alen + 2) / 3) * 4;
if ((auth_base64 = (char *)calloc(1, blen + 1)) == NULL)
goto out_ubase64;
/* convert user:passwd to base64. It should return exactly blen */
if (a2base64(auth, alen, auth_base64, blen + 1) != blen)
goto out_base64;
if ((u = stats_check_init_uri_auth(root)) == NULL)
goto out_base64;
ulist = &u->users;
while ((users = *ulist)) {
if (!strcmp(users->user_pwd, auth_base64))
break;
ulist = &users->next;
}
if (!users) {
if ((users = (struct user_auth *)calloc(1, sizeof(*users))) == NULL)
goto out_u;
*ulist = users;
users->user_pwd = auth_base64;
users->user_len = blen;
}
return u;
out_u:
free(u);
out_base64:
free(auth_base64);
out_ubase64:
return NULL;
}