aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Jason A. Donenfeld <Jason@zx2c4.com>2014-01-15 05:49:31 (JST)
committerGravatar Jason A. Donenfeld <Jason@zx2c4.com>2014-01-16 10:28:12 (JST)
commitd6e9200cc35411f3f27426b608bcfdef9348e6d3 (patch)
tree9cdd921b03465458d10b99ff4357f79a810501c0
parent3741254a6989b2837cd8d20480f152f0096bcb9a (diff)
downloadcgit-d6e9200cc35411f3f27426b608bcfdef9348e6d3.zip
cgit-d6e9200cc35411f3f27426b608bcfdef9348e6d3.tar.gz
auth: add basic authentication filter framework
This leverages the new lua support. See filters/simple-authentication.lua for explaination of how this works. There is also additional documentation in cgitrc.5.txt. Though this is a cookie-based approach, cgit's caching mechanism is preserved for authenticated pages. Very plugable and extendable depending on user needs. The sample script uses an HMAC-SHA1 based cookie to store the currently logged in user, with an expiration date. Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
-rw-r--r--cgit.c96
-rw-r--r--cgit.h7
-rw-r--r--cgitrc.5.txt36
-rw-r--r--filter.c11
-rw-r--r--filters/simple-authentication.lua225
-rw-r--r--ui-shared.c28
6 files changed, 387 insertions, 16 deletions
diff --git a/cgit.c b/cgit.c
index f3fe56b..c52ef33 100644
--- a/cgit.c
+++ b/cgit.c
@@ -192,6 +192,8 @@ static void config_cb(const char *name, const char *value)
192 ctx.cfg.commit_filter = cgit_new_filter(value, COMMIT); 192 ctx.cfg.commit_filter = cgit_new_filter(value, COMMIT);
193 else if (!strcmp(name, "email-filter")) 193 else if (!strcmp(name, "email-filter"))
194 ctx.cfg.email_filter = cgit_new_filter(value, EMAIL); 194 ctx.cfg.email_filter = cgit_new_filter(value, EMAIL);
195 else if (!strcmp(name, "auth-filter"))
196 ctx.cfg.auth_filter = cgit_new_filter(value, AUTH);
195 else if (!strcmp(name, "embedded")) 197 else if (!strcmp(name, "embedded"))
196 ctx.cfg.embedded = atoi(value); 198 ctx.cfg.embedded = atoi(value);
197 else if (!strcmp(name, "max-atom-items")) 199 else if (!strcmp(name, "max-atom-items"))
@@ -378,6 +380,10 @@ static void prepare_context(struct cgit_context *ctx)
378 ctx->env.script_name = getenv("SCRIPT_NAME"); 380 ctx->env.script_name = getenv("SCRIPT_NAME");
379 ctx->env.server_name = getenv("SERVER_NAME"); 381 ctx->env.server_name = getenv("SERVER_NAME");
380 ctx->env.server_port = getenv("SERVER_PORT"); 382 ctx->env.server_port = getenv("SERVER_PORT");
383 ctx->env.http_cookie = getenv("HTTP_COOKIE");
384 ctx->env.http_referer = getenv("HTTP_REFERER");
385 ctx->env.content_length = getenv("CONTENT_LENGTH") ? strtoul(getenv("CONTENT_LENGTH"), NULL, 10) : 0;
386 ctx->env.authenticated = 0;
381 ctx->page.mimetype = "text/html"; 387 ctx->page.mimetype = "text/html";
382 ctx->page.charset = PAGE_ENCODING; 388 ctx->page.charset = PAGE_ENCODING;
383 ctx->page.filename = NULL; 389 ctx->page.filename = NULL;
@@ -593,11 +599,92 @@ static int prepare_repo_cmd(struct cgit_context *ctx)
593 return 0; 599 return 0;
594} 600}
595 601
602static inline void open_auth_filter(struct cgit_context *ctx, const char *function)
603{
604 cgit_open_filter(ctx->cfg.auth_filter, function,
605 ctx->env.http_cookie ? ctx->env.http_cookie : "",
606 ctx->env.request_method ? ctx->env.request_method : "",
607 ctx->env.query_string ? ctx->env.query_string : "",
608 ctx->env.http_referer ? ctx->env.http_referer : "",
609 ctx->env.path_info ? ctx->env.path_info : "",
610 ctx->env.http_host ? ctx->env.http_host : "",
611 ctx->env.https ? ctx->env.https : "",
612 ctx->qry.repo ? ctx->qry.repo : "",
613 ctx->qry.page ? ctx->qry.page : "",
614 ctx->qry.url ? ctx->qry.url : "");
615}
616
617#define MAX_AUTHENTICATION_POST_BYTES 4096
618static inline void authenticate_post(struct cgit_context *ctx)
619{
620 if (ctx->env.http_referer && strlen(ctx->env.http_referer) > 0) {
621 html("Status: 302 Redirect\n");
622 html("Cache-Control: no-cache, no-store\n");
623 htmlf("Location: %s\n", ctx->env.http_referer);
624 } else {
625 html("Status: 501 Missing Referer\n");
626 html("Cache-Control: no-cache, no-store\n\n");
627 exit(0);
628 }
629
630 open_auth_filter(ctx, "authenticate-post");
631 char buffer[MAX_AUTHENTICATION_POST_BYTES];
632 int len;
633 len = ctx->env.content_length;
634 if (len > MAX_AUTHENTICATION_POST_BYTES)
635 len = MAX_AUTHENTICATION_POST_BYTES;
636 if (read(STDIN_FILENO, buffer, len) < 0)
637 die_errno("Could not read POST from stdin");
638 if (write(STDOUT_FILENO, buffer, len) < 0)
639 die_errno("Could not write POST to stdout");
640 /* The filter may now spit out a Set-Cookie: ... */
641 cgit_close_filter(ctx->cfg.auth_filter);
642
643 html("\n");
644 exit(0);
645}
646
647static inline void authenticate_cookie(struct cgit_context *ctx)
648{
649 /* If we don't have an auth_filter, consider all cookies valid, and thus return early. */
650 if (!ctx->cfg.auth_filter) {
651 ctx->env.authenticated = 1;
652 return;
653 }
654
655 /* If we're having something POST'd to /login, we're authenticating POST,
656 * instead of the cookie, so call authenticate_post and bail out early.
657 * This pattern here should match /?p=login with POST. */
658 if (ctx->env.request_method && ctx->qry.page && !ctx->repo && \
659 !strcmp(ctx->env.request_method, "POST") && !strcmp(ctx->qry.page, "login")) {
660 authenticate_post(ctx);
661 return;
662 }
663
664 /* If we've made it this far, we're authenticating the cookie for real, so do that. */
665 open_auth_filter(ctx, "authenticate-cookie");
666 ctx->env.authenticated = cgit_close_filter(ctx->cfg.auth_filter);
667}
668
596static void process_request(void *cbdata) 669static void process_request(void *cbdata)
597{ 670{
598 struct cgit_context *ctx = cbdata; 671 struct cgit_context *ctx = cbdata;
599 struct cgit_cmd *cmd; 672 struct cgit_cmd *cmd;
600 673
674 /* If we're not yet authenticated, no matter what page we're on,
675 * display the authentication body from the auth_filter. This should
676 * never be cached. */
677 if (!ctx->env.authenticated) {
678 ctx->page.title = "Authentication Required";
679 cgit_print_http_headers(ctx);
680 cgit_print_docstart(ctx);
681 cgit_print_pageheader(ctx);
682 open_auth_filter(ctx, "body");
683 cgit_close_filter(ctx->cfg.auth_filter);
684 cgit_print_docend();
685 return;
686 }
687
601 cmd = cgit_get_cmd(ctx); 688 cmd = cgit_get_cmd(ctx);
602 if (!cmd) { 689 if (!cmd) {
603 ctx->page.title = "cgit error"; 690 ctx->page.title = "cgit error";
@@ -911,6 +998,7 @@ int main(int argc, const char **argv)
911 int err, ttl; 998 int err, ttl;
912 999
913 cgit_init_filters(); 1000 cgit_init_filters();
1001 atexit(cgit_cleanup_filters);
914 1002
915 prepare_context(&ctx); 1003 prepare_context(&ctx);
916 cgit_repolist.length = 0; 1004 cgit_repolist.length = 0;
@@ -948,18 +1036,22 @@ int main(int argc, const char **argv)
948 cgit_parse_url(ctx.qry.url); 1036 cgit_parse_url(ctx.qry.url);
949 } 1037 }
950 1038
1039 /* Before we go any further, we set ctx.env.authenticated by checking to see
1040 * if the supplied cookie is valid. All cookies are valid if there is no
1041 * auth_filter. If there is an auth_filter, the filter decides. */
1042 authenticate_cookie(&ctx);
1043
951 ttl = calc_ttl(); 1044 ttl = calc_ttl();
952 if (ttl < 0) 1045 if (ttl < 0)
953 ctx.page.expires += 10 * 365 * 24 * 60 * 60; /* 10 years */ 1046 ctx.page.expires += 10 * 365 * 24 * 60 * 60; /* 10 years */
954 else 1047 else
955 ctx.page.expires += ttl * 60; 1048 ctx.page.expires += ttl * 60;
956 if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD")) 1049 if (!ctx.env.authenticated || (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD")))
957 ctx.cfg.nocache = 1; 1050 ctx.cfg.nocache = 1;
958 if (ctx.cfg.nocache) 1051 if (ctx.cfg.nocache)
959 ctx.cfg.cache_size = 0; 1052 ctx.cfg.cache_size = 0;
960 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root, 1053 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root,
961 ctx.qry.raw, ttl, process_request, &ctx); 1054 ctx.qry.raw, ttl, process_request, &ctx);
962 cgit_cleanup_filters();
963 if (err) 1055 if (err)
964 cgit_print_error("Error processing page: %s (%d)", 1056 cgit_print_error("Error processing page: %s (%d)",
965 strerror(err), err); 1057 strerror(err), err);
diff --git a/cgit.h b/cgit.h
index e200a06..496d0f6 100644
--- a/cgit.h
+++ b/cgit.h
@@ -53,7 +53,7 @@ typedef void (*filepair_fn)(struct diff_filepair *pair);
53typedef void (*linediff_fn)(char *line, int len); 53typedef void (*linediff_fn)(char *line, int len);
54 54
55typedef enum { 55typedef enum {
56 ABOUT, COMMIT, SOURCE, EMAIL 56 ABOUT, COMMIT, SOURCE, EMAIL, AUTH
57} filter_type; 57} filter_type;
58 58
59struct cgit_filter { 59struct cgit_filter {
@@ -252,6 +252,7 @@ struct cgit_config {
252 struct cgit_filter *commit_filter; 252 struct cgit_filter *commit_filter;
253 struct cgit_filter *source_filter; 253 struct cgit_filter *source_filter;
254 struct cgit_filter *email_filter; 254 struct cgit_filter *email_filter;
255 struct cgit_filter *auth_filter;
255}; 256};
256 257
257struct cgit_page { 258struct cgit_page {
@@ -278,6 +279,10 @@ struct cgit_environment {
278 const char *script_name; 279 const char *script_name;
279 const char *server_name; 280 const char *server_name;
280 const char *server_port; 281 const char *server_port;
282 const char *http_cookie;
283 const char *http_referer;
284 unsigned int content_length;
285 int authenticated;
281}; 286};
282 287
283struct cgit_context { 288struct cgit_context {
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index 170e825..c45dbd3 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -42,6 +42,13 @@ agefile::
42 hh:mm:ss". You may want to generate this file from a post-receive 42 hh:mm:ss". You may want to generate this file from a post-receive
43 hook. Default value: "info/web/last-modified". 43 hook. Default value: "info/web/last-modified".
44 44
45auth-filter::
46 Specifies a command that will be invoked for authenticating repository
47 access. Receives quite a few arguments, and data on both stdin and
48 stdout for authentication processing. Details follow later in this
49 document. If no auth-filter is specified, no authentication is
50 performed. Default value: none. See also: "FILTER API".
51
45branch-sort:: 52branch-sort::
46 Flag which, when set to "age", enables date ordering in the branch ref 53 Flag which, when set to "age", enables date ordering in the branch ref
47 list, and when set to "name" enables ordering by branch name. Default 54 list, and when set to "name" enables ordering by branch name. Default
@@ -605,6 +612,8 @@ specification with the relevant string; available values are:
605 URL escapes for a path and writes 'str' to the webpage. 612 URL escapes for a path and writes 'str' to the webpage.
606 'html_url_arg(str)':: 613 'html_url_arg(str)'::
607 URL escapes for an argument and writes 'str' to the webpage. 614 URL escapes for an argument and writes 'str' to the webpage.
615 'html_include(file)'::
616 Includes 'file' in webpage.
608 617
609 618
610Parameters are provided to filters as follows. 619Parameters are provided to filters as follows.
@@ -635,7 +644,32 @@ source filter::
635 file that is to be filtered is available on standard input and the 644 file that is to be filtered is available on standard input and the
636 filtered contents is expected on standard output. 645 filtered contents is expected on standard output.
637 646
638Also, all filters are handed the following environment variables: 647auth filter::
648 The authentication filter receives 11 parameters:
649 - filter action, explained below, which specifies which action the
650 filter is called for
651 - http cookie
652 - http method
653 - http referer
654 - http path
655 - http https flag
656 - cgit repo
657 - cgit page
658 - cgit url
659 When the filter action is "body", this filter must write to output the
660 HTML for displaying the login form, which POSTs to "/?p=login". When
661 the filter action is "authenticate-cookie", this filter must validate
662 the http cookie and return a 0 if it is invalid or 1 if it is invalid,
663 in the exit code / close function. If the filter action is
664 "authenticate-post", this filter receives POST'd parameters on
665 standard input, and should write to output one or more "Set-Cookie"
666 HTTP headers, each followed by a newline.
667
668 Please see `filters/simple-authentication.lua` for a clear example
669 script that may be modified.
670
671
672All filters are handed the following environment variables:
639 673
640- CGIT_REPO_URL (from repo.url) 674- CGIT_REPO_URL (from repo.url)
641- CGIT_REPO_NAME (from repo.name) 675- CGIT_REPO_NAME (from repo.name)
diff --git a/filter.c b/filter.c
index 0cce7bb..a5e5e4b 100644
--- a/filter.c
+++ b/filter.c
@@ -244,6 +244,11 @@ static int html_url_arg_lua_filter(lua_State *lua_state)
244 return hook_lua_filter(lua_state, html_url_arg); 244 return hook_lua_filter(lua_state, html_url_arg);
245} 245}
246 246
247static int html_include_lua_filter(lua_State *lua_state)
248{
249 return hook_lua_filter(lua_state, (void (*)(const char *))html_include);
250}
251
247static void cleanup_lua_filter(struct cgit_filter *base) 252static void cleanup_lua_filter(struct cgit_filter *base)
248{ 253{
249 struct lua_filter *filter = (struct lua_filter *)base; 254 struct lua_filter *filter = (struct lua_filter *)base;
@@ -279,6 +284,8 @@ static int init_lua_filter(struct lua_filter *filter)
279 lua_setglobal(filter->lua_state, "html_url_path"); 284 lua_setglobal(filter->lua_state, "html_url_path");
280 lua_pushcfunction(filter->lua_state, html_url_arg_lua_filter); 285 lua_pushcfunction(filter->lua_state, html_url_arg_lua_filter);
281 lua_setglobal(filter->lua_state, "html_url_arg"); 286 lua_setglobal(filter->lua_state, "html_url_arg");
287 lua_pushcfunction(filter->lua_state, html_include_lua_filter);
288 lua_setglobal(filter->lua_state, "html_include");
282 289
283 if (luaL_dofile(filter->lua_state, filter->script_file)) { 290 if (luaL_dofile(filter->lua_state, filter->script_file)) {
284 error_lua_filter(filter); 291 error_lua_filter(filter);
@@ -409,6 +416,10 @@ struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype)
409 colon = NULL; 416 colon = NULL;
410 417
411 switch (filtertype) { 418 switch (filtertype) {
419 case AUTH:
420 argument_count = 11;
421 break;
422
412 case EMAIL: 423 case EMAIL:
413 argument_count = 2; 424 argument_count = 2;
414 break; 425 break;
diff --git a/filters/simple-authentication.lua b/filters/simple-authentication.lua
new file mode 100644
index 0000000..4cd4983
--- /dev/null
+++ b/filters/simple-authentication.lua
@@ -0,0 +1,225 @@
1-- This script may be used with the auth-filter. Be sure to configure it as you wish.
2--
3-- Requirements:
4-- luacrypto >= 0.3
5-- <http://mkottman.github.io/luacrypto/>
6--
7
8
9--
10--
11-- Configure these variables for your settings.
12--
13--
14
15local protected_repos = {
16 glouglou = { laurent = true, jason = true },
17 qt = { jason = true, bob = true }
18}
19
20local users = {
21 jason = "secretpassword",
22 laurent = "s3cr3t",
23 bob = "ilikelua"
24}
25
26local secret = "BE SURE TO CUSTOMIZE THIS STRING TO SOMETHING BIG AND RANDOM"
27
28
29
30--
31--
32-- Authentication functions follow below. Swap these out if you want different authentication semantics.
33--
34--
35
36-- Sets HTTP cookie headers based on post
37function authenticate_post()
38 local password = users[post["username"]]
39 -- TODO: Implement time invariant string comparison function to mitigate against timing attack.
40 if password == nil or password ~= post["password"] then
41 construct_cookie("", "cgitauth")
42 else
43 construct_cookie(post["username"], "cgitauth")
44 end
45 return 0
46end
47
48
49-- Returns 1 if the cookie is valid and 0 if it is not.
50function authenticate_cookie()
51 accepted_users = protected_repos[cgit["repo"]]
52 if accepted_users == nil then
53 -- We return as valid if the repo is not protected.
54 return 1
55 end
56
57 local username = validate_cookie(get_cookie(http["cookie"], "cgitauth"))
58 if username == nil or not accepted_users[username] then
59 return 0
60 else
61 return 1
62 end
63end
64
65-- Prints the html for the login form.
66function body()
67 html("<h2>Authentication Required</h2>")
68 html("<form method='post' action='")
69 html_attr(cgit["login"])
70 html("'>")
71 html("<table>")
72 html("<tr><td><label for='username'>Username:</label></td><td><input id='username' name='username' autofocus /></td></tr>")
73 html("<tr><td><label for='password'>Password:</label></td><td><input id='password' name='password' type='password' /></td></tr>")
74 html("<tr><td colspan='2'><input value='Login' type='submit' /></td></tr>")
75 html("</table></form>")
76
77 return 0
78end
79
80
81--
82--
83-- Cookie construction and validation helpers.
84--
85--
86
87local crypto = require("crypto")
88