ctx.cfg.commit_filter = cgit_new_filter(value, COMMIT);
else if (!strcmp(name, "email-filter"))
ctx.cfg.email_filter = cgit_new_filter(value, EMAIL);
+ else if (!strcmp(name, "auth-filter"))
+ ctx.cfg.auth_filter = cgit_new_filter(value, AUTH);
else if (!strcmp(name, "embedded"))
ctx.cfg.embedded = atoi(value);
else if (!strcmp(name, "max-atom-items"))
ctx->env.script_name = getenv("SCRIPT_NAME");
ctx->env.server_name = getenv("SERVER_NAME");
ctx->env.server_port = getenv("SERVER_PORT");
+ ctx->env.http_cookie = getenv("HTTP_COOKIE");
+ ctx->env.http_referer = getenv("HTTP_REFERER");
+ ctx->env.content_length = getenv("CONTENT_LENGTH") ? strtoul(getenv("CONTENT_LENGTH"), NULL, 10) : 0;
+ ctx->env.authenticated = 0;
ctx->page.mimetype = "text/html";
ctx->page.charset = PAGE_ENCODING;
ctx->page.filename = NULL;
return 0;
}
+static inline void open_auth_filter(struct cgit_context *ctx, const char *function)
+{
+ cgit_open_filter(ctx->cfg.auth_filter, function,
+ ctx->env.http_cookie ? ctx->env.http_cookie : "",
+ ctx->env.request_method ? ctx->env.request_method : "",
+ ctx->env.query_string ? ctx->env.query_string : "",
+ ctx->env.http_referer ? ctx->env.http_referer : "",
+ ctx->env.path_info ? ctx->env.path_info : "",
+ ctx->env.http_host ? ctx->env.http_host : "",
+ ctx->env.https ? ctx->env.https : "",
+ ctx->qry.repo ? ctx->qry.repo : "",
+ ctx->qry.page ? ctx->qry.page : "",
+ ctx->qry.url ? ctx->qry.url : "");
+}
+
+#define MAX_AUTHENTICATION_POST_BYTES 4096
+static inline void authenticate_post(struct cgit_context *ctx)
+{
+ if (ctx->env.http_referer && strlen(ctx->env.http_referer) > 0) {
+ html("Status: 302 Redirect\n");
+ html("Cache-Control: no-cache, no-store\n");
+ htmlf("Location: %s\n", ctx->env.http_referer);
+ } else {
+ html("Status: 501 Missing Referer\n");
+ html("Cache-Control: no-cache, no-store\n\n");
+ exit(0);
+ }
+
+ open_auth_filter(ctx, "authenticate-post");
+ char buffer[MAX_AUTHENTICATION_POST_BYTES];
+ int len;
+ len = ctx->env.content_length;
+ if (len > MAX_AUTHENTICATION_POST_BYTES)
+ len = MAX_AUTHENTICATION_POST_BYTES;
+ if (read(STDIN_FILENO, buffer, len) < 0)
+ die_errno("Could not read POST from stdin");
+ if (write(STDOUT_FILENO, buffer, len) < 0)
+ die_errno("Could not write POST to stdout");
+ /* The filter may now spit out a Set-Cookie: ... */
+ cgit_close_filter(ctx->cfg.auth_filter);
+
+ html("\n");
+ exit(0);
+}
+
+static inline void authenticate_cookie(struct cgit_context *ctx)
+{
+ /* If we don't have an auth_filter, consider all cookies valid, and thus return early. */
+ if (!ctx->cfg.auth_filter) {
+ ctx->env.authenticated = 1;
+ return;
+ }
+
+ /* If we're having something POST'd to /login, we're authenticating POST,
+ * instead of the cookie, so call authenticate_post and bail out early.
+ * This pattern here should match /?p=login with POST. */
+ if (ctx->env.request_method && ctx->qry.page && !ctx->repo && \
+ !strcmp(ctx->env.request_method, "POST") && !strcmp(ctx->qry.page, "login")) {
+ authenticate_post(ctx);
+ return;
+ }
+
+ /* If we've made it this far, we're authenticating the cookie for real, so do that. */
+ open_auth_filter(ctx, "authenticate-cookie");
+ ctx->env.authenticated = cgit_close_filter(ctx->cfg.auth_filter);
+}
+
static void process_request(void *cbdata)
{
struct cgit_context *ctx = cbdata;
struct cgit_cmd *cmd;
+ /* If we're not yet authenticated, no matter what page we're on,
+ * display the authentication body from the auth_filter. This should
+ * never be cached. */
+ if (!ctx->env.authenticated) {
+ ctx->page.title = "Authentication Required";
+ cgit_print_http_headers(ctx);
+ cgit_print_docstart(ctx);
+ cgit_print_pageheader(ctx);
+ open_auth_filter(ctx, "body");
+ cgit_close_filter(ctx->cfg.auth_filter);
+ cgit_print_docend();
+ return;
+ }
+
cmd = cgit_get_cmd(ctx);
if (!cmd) {
ctx->page.title = "cgit error";
int err, ttl;
cgit_init_filters();
+ atexit(cgit_cleanup_filters);
prepare_context(&ctx);
cgit_repolist.length = 0;
cgit_parse_url(ctx.qry.url);
}
+ /* Before we go any further, we set ctx.env.authenticated by checking to see
+ * if the supplied cookie is valid. All cookies are valid if there is no
+ * auth_filter. If there is an auth_filter, the filter decides. */
+ authenticate_cookie(&ctx);
+
ttl = calc_ttl();
if (ttl < 0)
ctx.page.expires += 10 * 365 * 24 * 60 * 60; /* 10 years */
else
ctx.page.expires += ttl * 60;
- if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD"))
+ if (!ctx.env.authenticated || (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD")))
ctx.cfg.nocache = 1;
if (ctx.cfg.nocache)
ctx.cfg.cache_size = 0;
err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root,
ctx.qry.raw, ttl, process_request, &ctx);
- cgit_cleanup_filters();
if (err)
cgit_print_error("Error processing page: %s (%d)",
strerror(err), err);
typedef void (*linediff_fn)(char *line, int len);
typedef enum {
- ABOUT, COMMIT, SOURCE, EMAIL
+ ABOUT, COMMIT, SOURCE, EMAIL, AUTH
} filter_type;
struct cgit_filter {
struct cgit_filter *commit_filter;
struct cgit_filter *source_filter;
struct cgit_filter *email_filter;
+ struct cgit_filter *auth_filter;
};
struct cgit_page {
const char *script_name;
const char *server_name;
const char *server_port;
+ const char *http_cookie;
+ const char *http_referer;
+ unsigned int content_length;
+ int authenticated;
};
struct cgit_context {
hh:mm:ss". You may want to generate this file from a post-receive
hook. Default value: "info/web/last-modified".
+auth-filter::
+ Specifies a command that will be invoked for authenticating repository
+ access. Receives quite a few arguments, and data on both stdin and
+ stdout for authentication processing. Details follow later in this
+ document. If no auth-filter is specified, no authentication is
+ performed. Default value: none. See also: "FILTER API".
+
branch-sort::
Flag which, when set to "age", enables date ordering in the branch ref
list, and when set to "name" enables ordering by branch name. Default
URL escapes for a path and writes 'str' to the webpage.
'html_url_arg(str)'::
URL escapes for an argument and writes 'str' to the webpage.
+ 'html_include(file)'::
+ Includes 'file' in webpage.
Parameters are provided to filters as follows.
file that is to be filtered is available on standard input and the
filtered contents is expected on standard output.
-Also, all filters are handed the following environment variables:
+auth filter::
+ The authentication filter receives 11 parameters:
+ - filter action, explained below, which specifies which action the
+ filter is called for
+ - http cookie
+ - http method
+ - http referer
+ - http path
+ - http https flag
+ - cgit repo
+ - cgit page
+ - cgit url
+ When the filter action is "body", this filter must write to output the
+ HTML for displaying the login form, which POSTs to "/?p=login". When
+ the filter action is "authenticate-cookie", this filter must validate
+ the http cookie and return a 0 if it is invalid or 1 if it is invalid,
+ in the exit code / close function. If the filter action is
+ "authenticate-post", this filter receives POST'd parameters on
+ standard input, and should write to output one or more "Set-Cookie"
+ HTTP headers, each followed by a newline.
+
+ Please see `filters/simple-authentication.lua` for a clear example
+ script that may be modified.
+
+
+All filters are handed the following environment variables:
- CGIT_REPO_URL (from repo.url)
- CGIT_REPO_NAME (from repo.name)
return hook_lua_filter(lua_state, html_url_arg);
}
+static int html_include_lua_filter(lua_State *lua_state)
+{
+ return hook_lua_filter(lua_state, (void (*)(const char *))html_include);
+}
+
static void cleanup_lua_filter(struct cgit_filter *base)
{
struct lua_filter *filter = (struct lua_filter *)base;
lua_setglobal(filter->lua_state, "html_url_path");
lua_pushcfunction(filter->lua_state, html_url_arg_lua_filter);
lua_setglobal(filter->lua_state, "html_url_arg");
+ lua_pushcfunction(filter->lua_state, html_include_lua_filter);
+ lua_setglobal(filter->lua_state, "html_include");
if (luaL_dofile(filter->lua_state, filter->script_file)) {
error_lua_filter(filter);
colon = NULL;
switch (filtertype) {
+ case AUTH:
+ argument_count = 11;
+ break;
+
case EMAIL:
argument_count = 2;
break;
--- /dev/null
+-- This script may be used with the auth-filter. Be sure to configure it as you wish.
+--
+-- Requirements:
+-- luacrypto >= 0.3
+-- <http://mkottman.github.io/luacrypto/>
+--
+
+
+--
+--
+-- Configure these variables for your settings.
+--
+--
+
+local protected_repos = {
+ glouglou = { laurent = true, jason = true },
+ qt = { jason = true, bob = true }
+}
+
+local users = {
+ jason = "secretpassword",
+ laurent = "s3cr3t",
+ bob = "ilikelua"
+}
+
+local secret = "BE SURE TO CUSTOMIZE THIS STRING TO SOMETHING BIG AND RANDOM"
+
+
+
+--
+--
+-- Authentication functions follow below. Swap these out if you want different authentication semantics.
+--
+--
+
+-- Sets HTTP cookie headers based on post
+function authenticate_post()
+ local password = users[post["username"]]
+ -- TODO: Implement time invariant string comparison function to mitigate against timing attack.
+ if password == nil or password ~= post["password"] then
+ construct_cookie("", "cgitauth")
+ else
+ construct_cookie(post["username"], "cgitauth")
+ end
+ return 0
+end
+
+
+-- Returns 1 if the cookie is valid and 0 if it is not.
+function authenticate_cookie()
+ accepted_users = protected_repos[cgit["repo"]]
+ if accepted_users == nil then
+ -- We return as valid if the repo is not protected.
+ return 1
+ end
+
+ local username = validate_cookie(get_cookie(http["cookie"], "cgitauth"))
+ if username == nil or not accepted_users[username] then
+ return 0
+ else
+ return 1
+ end
+end
+
+-- Prints the html for the login form.
+function body()
+ html("<h2>Authentication Required</h2>")
+ html("<form method='post' action='")
+ html_attr(cgit["login"])
+ html("'>")
+ html("<table>")
+ html("<tr><td><label for='username'>Username:</label></td><td><input id='username' name='username' autofocus /></td></tr>")
+ html("<tr><td><label for='password'>Password:</label></td><td><input id='password' name='password' type='password' /></td></tr>")
+ html("<tr><td colspan='2'><input value='Login' type='submit' /></td></tr>")
+ html("</table></form>")
+
+ return 0
+end
+
+
+--
+--
+-- Cookie construction and validation helpers.
+--
+--
+
+local crypto = require("crypto")
+
+-- Returns username of cookie if cookie is valid. Otherwise returns nil.
+function validate_cookie(cookie)
+ local i = 0
+ local username = ""
+ local expiration = 0
+ local salt = ""
+ local hmac = ""
+
+ if cookie:len() < 3 or cookie:sub(1, 1) == "|" then
+ return nil
+ end
+
+ for component in string.gmatch(cookie, "[^|]+") do
+ if i == 0 then
+ username = component
+ elseif i == 1 then
+ expiration = tonumber(component)
+ if expiration == nil then
+ expiration = 0
+ end
+ elseif i == 2 then
+ salt = component
+ elseif i == 3 then
+ hmac = component
+ else
+ break
+ end
+ i = i + 1
+ end
+
+ if hmac == nil or hmac:len() == 0 then
+ return nil
+ end
+
+ -- TODO: implement time invariant comparison to prevent against timing attack.
+ if hmac ~= crypto.hmac.digest("sha1", username .. "|" .. tostring(expiration) .. "|" .. salt, secret) then
+ return nil
+ end
+
+ if expiration <= os.time() then
+ return nil
+ end
+
+ return username:lower()
+end
+
+function construct_cookie(username, cookie)
+ local authstr = ""
+ if username:len() > 0 then
+ -- One week expiration time
+ local expiration = os.time() + 604800
+ local salt = crypto.hex(crypto.rand.bytes(16))
+
+ authstr = username .. "|" .. tostring(expiration) .. "|" .. salt
+ authstr = authstr .. "|" .. crypto.hmac.digest("sha1", authstr, secret)
+ end
+
+ html("Set-Cookie: " .. cookie .. "=" .. authstr .. "; HttpOnly")
+ if http["https"] == "yes" or http["https"] == "on" or http["https"] == "1" then
+ html("; secure")
+ end
+ html("\n")
+end
+
+--
+--
+-- Wrapper around filter API follows below, exposing the http table, the cgit table, and the post table to the above functions.
+--
+--
+
+local actions = {}
+actions["authenticate-post"] = authenticate_post
+actions["authenticate-cookie"] = authenticate_cookie
+actions["body"] = body
+
+function filter_open(...)
+ action = actions[select(1, ...)]
+
+ http = {}
+ http["cookie"] = select(2, ...)
+ http["method"] = select(3, ...)
+ http["query"] = select(4, ...)
+ http["referer"] = select(5, ...)
+ http["path"] = select(6, ...)
+ http["host"] = select(7, ...)
+ http["https"] = select(8, ...)
+
+ cgit = {}
+ cgit["repo"] = select(9, ...)
+ cgit["page"] = select(10, ...)
+ cgit["url"] = select(11, ...)
+
+ cgit["login"] = ""
+ for _ in cgit["url"]:gfind("/") do
+ cgit["login"] = cgit["login"] .. "../"
+ end
+ cgit["login"] = cgit["login"] .. "?p=login"
+
+end
+
+function filter_close()
+ return action()
+end
+
+function filter_write(str)
+ post = parse_qs(str)
+end
+
+
+--
+--
+-- Utility functions follow below, based on keplerproject/wsapi.
+--
+--
+
+function url_decode(str)
+ if not str then
+ return ""
+ end
+ str = string.gsub(str, "+", " ")
+ str = string.gsub(str, "%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end)
+ str = string.gsub(str, "\r\n", "\n")
+ return str
+end
+
+function parse_qs(qs)
+ local tab = {}
+ for key, val in string.gmatch(qs, "([^&=]+)=([^&=]*)&?") do
+ tab[url_decode(key)] = url_decode(val)
+ end
+ return tab
+end
+
+function get_cookie(cookies, name)
+ cookies = string.gsub(";" .. cookies .. ";", "%s*;%s*", ";")
+ return url_decode(string.match(cookies, ";" .. name .. "=(.-);"))
+end
if (ctx->page.filename)
htmlf("Content-Disposition: inline; filename=\"%s\"\n",
ctx->page.filename);
+ if (!ctx->env.authenticated)
+ html("Cache-Control: no-cache, no-store\n");
htmlf("Last-Modified: %s\n", http_date(ctx->page.modified));
htmlf("Expires: %s\n", http_date(ctx->page.expires));
if (ctx->page.etag)
cgit_index_link("index", NULL, NULL, NULL, NULL, 0);
html(" : ");
cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL);
- html("</td><td class='form'>");
- html("<form method='get' action=''>\n");
- cgit_add_hidden_formfields(0, 1, ctx->qry.page);
- html("<select name='h' onchange='this.form.submit();'>\n");
- for_each_branch_ref(print_branch_option, ctx->qry.head);
- html("</select> ");
- html("<input type='submit' name='' value='switch'/>");
- html("</form>");
+ if (ctx->env.authenticated) {
+ html("</td><td class='form'>");
+ html("<form method='get' action=''>\n");
+ cgit_add_hidden_formfields(0, 1, ctx->qry.page);
+ html("<select name='h' onchange='this.form.submit();'>\n");
+ for_each_branch_ref(print_branch_option, ctx->qry.head);
+ html("</select> ");
+ html("<input type='submit' name='' value='switch'/>");
+ html("</form>");
+ }
} else
html_txt(ctx->cfg.root_title);
html("</td></tr>\n");
void cgit_print_pageheader(struct cgit_context *ctx)
{
html("<div id='cgit'>");
- if (!ctx->cfg.noheader)
+ if (!ctx->env.authenticated || !ctx->cfg.noheader)
print_header(ctx);
html("<table class='tabs'><tr><td>\n");
- if (ctx->repo) {
+ if (ctx->env.authenticated && ctx->repo) {
cgit_summary_link("summary", NULL, hc(ctx, "summary"),
ctx->qry.head);
cgit_refs_link("refs", NULL, hc(ctx, "refs"), ctx->qry.head,
html("'/>\n");
html("<input type='submit' value='search'/>\n");
html("</form>\n");
- } else {
+ } else if (ctx->env.authenticated) {
site_link(NULL, "index", NULL, hc(ctx, "repolist"), NULL, NULL, 0);
if (ctx->cfg.root_readme)
site_link("about", "about", NULL, hc(ctx, "about"),
html("</form>");
}
html("</td></tr></table>\n");
- if (ctx->qry.vpath) {
+ if (ctx->env.authenticated && ctx->qry.vpath) {
html("<div class='path'>");
html("path: ");
cgit_print_path_crumbs(ctx, ctx->qry.vpath);