]> gitweb.ps.run Git - ps-cgit/commitdiff
auth: add basic authentication filter framework
authorJason A. Donenfeld <Jason@zx2c4.com>
Tue, 14 Jan 2014 20:49:31 +0000 (21:49 +0100)
committerJason A. Donenfeld <Jason@zx2c4.com>
Thu, 16 Jan 2014 01:28:12 +0000 (02:28 +0100)
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>
cgit.c
cgit.h
cgitrc.5.txt
filter.c
filters/simple-authentication.lua [new file with mode: 0644]
ui-shared.c

diff --git a/cgit.c b/cgit.c
index f3fe56bb2748bb976061c819f24f4f7ddcd2caf8..c52ef331b76995419aff93935784f1174150a8bc 100644 (file)
--- a/cgit.c
+++ b/cgit.c
@@ -192,6 +192,8 @@ static void config_cb(const char *name, const char *value)
                ctx.cfg.commit_filter = cgit_new_filter(value, COMMIT);
        else if (!strcmp(name, "email-filter"))
                ctx.cfg.email_filter = cgit_new_filter(value, EMAIL);
                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"))
        else if (!strcmp(name, "embedded"))
                ctx.cfg.embedded = atoi(value);
        else if (!strcmp(name, "max-atom-items"))
@@ -378,6 +380,10 @@ static void prepare_context(struct cgit_context *ctx)
        ctx->env.script_name = getenv("SCRIPT_NAME");
        ctx->env.server_name = getenv("SERVER_NAME");
        ctx->env.server_port = getenv("SERVER_PORT");
        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;
        ctx->page.mimetype = "text/html";
        ctx->page.charset = PAGE_ENCODING;
        ctx->page.filename = NULL;
@@ -593,11 +599,92 @@ static int prepare_repo_cmd(struct cgit_context *ctx)
        return 0;
 }
 
        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;
 
 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";
        cmd = cgit_get_cmd(ctx);
        if (!cmd) {
                ctx->page.title = "cgit error";
@@ -911,6 +998,7 @@ int main(int argc, const char **argv)
        int err, ttl;
 
        cgit_init_filters();
        int err, ttl;
 
        cgit_init_filters();
+       atexit(cgit_cleanup_filters);
 
        prepare_context(&ctx);
        cgit_repolist.length = 0;
 
        prepare_context(&ctx);
        cgit_repolist.length = 0;
@@ -948,18 +1036,22 @@ int main(int argc, const char **argv)
                cgit_parse_url(ctx.qry.url);
        }
 
                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;
        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);
                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);
        if (err)
                cgit_print_error("Error processing page: %s (%d)",
                                 strerror(err), err);
diff --git a/cgit.h b/cgit.h
index e200a06994c178468f670f63dcb13cc36de19d2a..496d0f685075f66865d7c4d12d3ccd155e7b68dc 100644 (file)
--- a/cgit.h
+++ b/cgit.h
@@ -53,7 +53,7 @@ typedef void (*filepair_fn)(struct diff_filepair *pair);
 typedef void (*linediff_fn)(char *line, int len);
 
 typedef enum {
 typedef void (*linediff_fn)(char *line, int len);
 
 typedef enum {
-       ABOUT, COMMIT, SOURCE, EMAIL
+       ABOUT, COMMIT, SOURCE, EMAIL, AUTH
 } filter_type;
 
 struct cgit_filter {
 } filter_type;
 
 struct cgit_filter {
@@ -252,6 +252,7 @@ struct cgit_config {
        struct cgit_filter *commit_filter;
        struct cgit_filter *source_filter;
        struct cgit_filter *email_filter;
        struct cgit_filter *commit_filter;
        struct cgit_filter *source_filter;
        struct cgit_filter *email_filter;
+       struct cgit_filter *auth_filter;
 };
 
 struct cgit_page {
 };
 
 struct cgit_page {
@@ -278,6 +279,10 @@ struct cgit_environment {
        const char *script_name;
        const char *server_name;
        const char *server_port;
        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 {
 };
 
 struct cgit_context {
index 170e825b414d4daebf719bb6567f5cc70e447a80..c45dbd3681de8f7a258d8f84669d1bc08ca7d6d5 100644 (file)
@@ -42,6 +42,13 @@ agefile::
        hh:mm:ss". You may want to generate this file from a post-receive
        hook. Default value: "info/web/last-modified".
 
        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
 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
@@ -605,6 +612,8 @@ specification with the relevant string; available values are:
                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.
                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.
 
 
 Parameters are provided to filters as follows.
@@ -635,7 +644,32 @@ source filter::
        file that is to be filtered is available on standard input and the
        filtered contents is expected on standard output.
 
        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)
 
 - CGIT_REPO_URL (from repo.url)
 - CGIT_REPO_NAME (from repo.name)
index 0cce7bb6a4d6e42ec018c009df1abfc55e652a31..a5e5e4b93ef6b25718bf18e7b1c2fe8d51a03098 100644 (file)
--- a/filter.c
+++ b/filter.c
@@ -244,6 +244,11 @@ static int html_url_arg_lua_filter(lua_State *lua_state)
        return hook_lua_filter(lua_state, html_url_arg);
 }
 
        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;
 static void cleanup_lua_filter(struct cgit_filter *base)
 {
        struct lua_filter *filter = (struct lua_filter *)base;
@@ -279,6 +284,8 @@ static int init_lua_filter(struct lua_filter *filter)
        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_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);
 
        if (luaL_dofile(filter->lua_state, filter->script_file)) {
                error_lua_filter(filter);
@@ -409,6 +416,10 @@ struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype)
                colon = NULL;
 
        switch (filtertype) {
                colon = NULL;
 
        switch (filtertype) {
+               case AUTH:
+                       argument_count = 11;
+                       break;
+
                case EMAIL:
                        argument_count = 2;
                        break;
                case EMAIL:
                        argument_count = 2;
                        break;
diff --git a/filters/simple-authentication.lua b/filters/simple-authentication.lua
new file mode 100644 (file)
index 0000000..4cd4983
--- /dev/null
@@ -0,0 +1,225 @@
+-- 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
index abe15cdaee8eb3e1986a0a740caf1ad0b7eb83ad..4f47c507054b1d3c7ca854c0481fccd9cf9f5847 100644 (file)
@@ -641,6 +641,8 @@ void cgit_print_http_headers(struct cgit_context *ctx)
        if (ctx->page.filename)
                htmlf("Content-Disposition: inline; filename=\"%s\"\n",
                      ctx->page.filename);
        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)
        htmlf("Last-Modified: %s\n", http_date(ctx->page.modified));
        htmlf("Expires: %s\n", http_date(ctx->page.expires));
        if (ctx->page.etag)
@@ -814,14 +816,16 @@ static void print_header(struct cgit_context *ctx)
                cgit_index_link("index", NULL, NULL, NULL, NULL, 0);
                html(" : ");
                cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL);
                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");
        } else
                html_txt(ctx->cfg.root_title);
        html("</td></tr>\n");
@@ -843,11 +847,11 @@ static void print_header(struct cgit_context *ctx)
 void cgit_print_pageheader(struct cgit_context *ctx)
 {
        html("<div id='cgit'>");
 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");
                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,
                cgit_summary_link("summary", NULL, hc(ctx, "summary"),
                                  ctx->qry.head);
                cgit_refs_link("refs", NULL, hc(ctx, "refs"), ctx->qry.head,
@@ -886,7 +890,7 @@ void cgit_print_pageheader(struct cgit_context *ctx)
                html("'/>\n");
                html("<input type='submit' value='search'/>\n");
                html("</form>\n");
                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"),
                site_link(NULL, "index", NULL, hc(ctx, "repolist"), NULL, NULL, 0);
                if (ctx->cfg.root_readme)
                        site_link("about", "about", NULL, hc(ctx, "about"),
@@ -902,7 +906,7 @@ void cgit_print_pageheader(struct cgit_context *ctx)
                html("</form>");
        }
        html("</td></tr></table>\n");
                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);
                html("<div class='path'>");
                html("path: ");
                cgit_print_path_crumbs(ctx, ctx->qry.vpath);