[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.
diff --git a/Makefile b/Makefile
index 63e2316..6607ce0 100644
--- a/Makefile
+++ b/Makefile
@@ -109,7 +109,7 @@
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
diff --git a/haproxy.c b/haproxy.c
index b09ae3c..0832cda 100644
--- a/haproxy.c
+++ b/haproxy.c
@@ -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 @@
/*********************************************************************/
+/* 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 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 @@
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 @@
"\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 @@
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 @@
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 @@
/* 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 @@
/* 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 @@
}
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]);
diff --git a/include/base64.h b/include/base64.h
new file mode 100644
index 0000000..6408acf
--- /dev/null
+++ b/include/base64.h
@@ -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 */
diff --git a/include/uri_auth.h b/include/uri_auth.h
new file mode 100644
index 0000000..f7ff6c8
--- /dev/null
+++ b/include/uri_auth.h
@@ -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 */
diff --git a/src/base64.c b/src/base64.c
new file mode 100644
index 0000000..79e86c2
--- /dev/null
+++ b/src/base64.c
@@ -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;
+}
diff --git a/src/uri_auth.c b/src/uri_auth.c
new file mode 100644
index 0000000..3c4965e
--- /dev/null
+++ b/src/uri_auth.c
@@ -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;
+}
+