gitweb: Support for snapshot
[gitweb.git] / daemon.c
index 0c6182fb93fce6937ecb2634462f7f6b0a835662..012936f3bd76d9f9648ba9498b4126c7d6861e5c 100644 (file)
--- a/daemon.c
+++ b/daemon.c
@@ -1,5 +1,3 @@
-#include "cache.h"
-#include "pkt-line.h"
 #include <signal.h>
 #include <sys/wait.h>
 #include <sys/socket.h>
@@ -9,23 +7,39 @@
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <syslog.h>
+#include "pkt-line.h"
+#include "cache.h"
+#include "exec_cmd.h"
 
 static int log_syslog;
 static int verbose;
+static int reuseaddr;
 
 static const char daemon_usage[] =
 "git-daemon [--verbose] [--syslog] [--inetd | --port=n] [--export-all]\n"
-"           [--timeout=n] [--init-timeout=n] [directory...]";
+"           [--timeout=n] [--init-timeout=n] [--strict-paths]\n"
+"           [--base-path=path] [--user-path | --user-path=path]\n"
+"           [--reuseaddr] [--detach] [--pid-file=file] [directory...]";
 
 /* List of acceptable pathname prefixes */
-static char **ok_paths = NULL;
+static char **ok_paths;
+static int strict_paths;
 
 /* If this is set, git-daemon-export-ok is not required */
-static int export_all_trees = 0;
+static int export_all_trees;
+
+/* Take all paths relative to this one if non-NULL */
+static char *base_path;
+
+/* If defined, ~user notation is allowed and the string is inserted
+ * after ~user/.  E.g. a request to git://host/~alice/frotz would
+ * go to /home/alice/pub_git/frotz with --user-path=pub_git.
+ */
+static const char *user_path;
 
 /* Timeout, and initial timeout */
-static unsigned int timeout = 0;
-static unsigned int init_timeout = 0;
+static unsigned int timeout;
+static unsigned int init_timeout;
 
 static void logreport(int priority, const char *err, va_list params)
 {
@@ -81,69 +95,149 @@ static void loginfo(const char *err, ...)
        va_end(params);
 }
 
-static int path_ok(const char *dir)
+static void NORETURN daemon_die(const char *err, va_list params)
 {
-       const char *p = dir;
-       char **pp;
-       int sl, ndot;
-
-       /* The pathname here should be an absolute path. */
-       if ( *p++ != '/' )
-               return 0;
-
-       sl = 1;  ndot = 0;
+       logreport(LOG_ERR, err, params);
+       exit(1);
+}
 
-       for (;;) {
-               if ( *p == '.' ) {
-                       ndot++;
-               } else if ( *p == '\0' ) {
-                       /* Reject "." and ".." at the end of the path */
-                       if ( sl && ndot > 0 && ndot < 3 )
-                               return 0;
+static int avoid_alias(char *p)
+{
+       int sl, ndot;
 
-                       /* Otherwise OK */
-                       break;
-               } else if ( *p == '/' ) {
-                       /* Refuse "", "." or ".." */
-                       if ( sl && ndot < 3 )
+       /* 
+        * This resurrects the belts and suspenders paranoia check by HPA
+        * done in <435560F7.4080006@zytor.com> thread, now enter_repo()
+        * does not do getcwd() based path canonicalizations.
+        *
+        * sl becomes true immediately after seeing '/' and continues to
+        * be true as long as dots continue after that without intervening
+        * non-dot character.
+        */
+       if (!p || (*p != '/' && *p != '~'))
+               return -1;
+       sl = 1; ndot = 0;
+       p++;
+
+       while (1) {
+               char ch = *p++;
+               if (sl) {
+                       if (ch == '.')
+                               ndot++;
+                       else if (ch == '/') {
+                               if (ndot < 3)
+                                       /* reject //, /./ and /../ */
+                                       return -1;
+                               ndot = 0;
+                       }
+                       else if (ch == 0) {
+                               if (0 < ndot && ndot < 3)
+                                       /* reject /.$ and /..$ */
+                                       return -1;
                                return 0;
+                       }
+                       else
+                               sl = ndot = 0;
+               }
+               else if (ch == 0)
+                       return 0;
+               else if (ch == '/') {
                        sl = 1;
                        ndot = 0;
-               } else {
-                       sl = ndot = 0;
                }
-               p++;
        }
+}
 
-       if ( ok_paths && *ok_paths ) {
-               int ok = 0;
-               int dirlen = strlen(dir);
+static char *path_ok(char *dir)
+{
+       static char rpath[PATH_MAX];
+       char *path;
+
+       if (avoid_alias(dir)) {
+               logerror("'%s': aliased", dir);
+               return NULL;
+       }
+
+       if (*dir == '~') {
+               if (!user_path) {
+                       logerror("'%s': User-path not allowed", dir);
+                       return NULL;
+               }
+               if (*user_path) {
+                       /* Got either "~alice" or "~alice/foo";
+                        * rewrite them to "~alice/%s" or
+                        * "~alice/%s/foo".
+                        */
+                       int namlen, restlen = strlen(dir);
+                       char *slash = strchr(dir, '/');
+                       if (!slash)
+                               slash = dir + restlen;
+                       namlen = slash - dir;
+                       restlen -= namlen;
+                       loginfo("userpath <%s>, request <%s>, namlen %d, restlen %d, slash <%s>", user_path, dir, namlen, restlen, slash);
+                       snprintf(rpath, PATH_MAX, "%.*s/%s%.*s",
+                                namlen, dir, user_path, restlen, slash);
+                       dir = rpath;
+               }
+       }
+       else if (base_path) {
+               if (*dir != '/') {
+                       /* Allow only absolute */
+                       logerror("'%s': Non-absolute path denied (base-path active)", dir);
+                       return NULL;
+               }
+               else {
+                       snprintf(rpath, PATH_MAX, "%s%s", base_path, dir);
+                       dir = rpath;
+               }
+       }
 
+       path = enter_repo(dir, strict_paths);
+
+       if (!path) {
+               logerror("'%s': unable to chdir or not a git archive", dir);
+               return NULL;
+       }
+
+       if ( ok_paths && *ok_paths ) {
+               char **pp;
+               int pathlen = strlen(path);
+
+               /* The validation is done on the paths after enter_repo
+                * appends optional {.git,.git/.git} and friends, but 
+                * it does not use getcwd().  So if your /pub is
+                * a symlink to /mnt/pub, you can whitelist /pub and
+                * do not have to say /mnt/pub.
+                * Do not say /pub/.
+                */
                for ( pp = ok_paths ; *pp ; pp++ ) {
                        int len = strlen(*pp);
-                       if ( len <= dirlen &&
-                            !strncmp(*pp, dir, len) &&
-                            (dir[len] == '/' || dir[len] == '\0') ) {
-                               ok = 1;
-                               break;
-                       }
+                       if (len <= pathlen &&
+                           !memcmp(*pp, path, len) &&
+                           (path[len] == '\0' ||
+                            (!strict_paths && path[len] == '/')))
+                               return path;
                }
-
-               if ( !ok )
-                       return 0; /* Path not in whitelist */
+       }
+       else {
+               /* be backwards compatible */
+               if (!strict_paths)
+                       return path;
        }
 
-       return 1;               /* Path acceptable */
+       logerror("'%s': not in whitelist", path);
+       return NULL;            /* Fallthrough. Deny by default */
 }
 
-static int set_dir(const char *dir)
+static int upload(char *dir)
 {
-       if (!path_ok(dir)) {
-               errno = EACCES;
-               return -1;
-       }
+       /* Timeout as string */
+       char timeout_buf[64];
+       const char *path;
 
-       if ( chdir(dir) )
+       loginfo("Request for '%s'", dir);
+
+       if (!(path = path_ok(dir)))
                return -1;
 
        /*
@@ -152,45 +246,17 @@ static int set_dir(const char *dir)
         * We want a readable HEAD, usable "objects" directory, and
         * a "git-daemon-export-ok" flag that says that the other side
         * is ok with us doing this.
+        *
+        * path_ok() uses enter_repo() and does whitelist checking.
+        * We only need to make sure the repository is exported.
         */
+
        if (!export_all_trees && access("git-daemon-export-ok", F_OK)) {
+               logerror("'%s': repository not exported.", path);
                errno = EACCES;
                return -1;
        }
 
-       if (access("objects/", X_OK) || access("HEAD", R_OK)) {
-               errno = EINVAL;
-               return -1;
-       }
-
-       /* If all this passed, we're OK */
-       return 0;
-}
-
-static int upload(char *dir)
-{
-       /* Try paths in this order */
-       static const char *paths[] = { "%s", "%s/.git", "%s.git", "%s.git/.git", NULL };
-       const char **pp;
-       /* Enough for the longest path above including final null */
-       int buflen = strlen(dir)+10;
-       char *dirbuf = xmalloc(buflen);
-       /* Timeout as string */
-       char timeout_buf[64];
-
-       loginfo("Request for '%s'", dir);
-
-       for ( pp = paths ; *pp ; pp++ ) {
-               snprintf(dirbuf, buflen, *pp, dir);
-               if ( !set_dir(dirbuf) )
-                       break;
-       }
-
-       if ( !*pp ) {
-               logerror("Cannot set directory '%s': %s", dir, strerror(errno));
-               return -1;
-       }
-
        /*
         * We'll ignore SIGTERM from now on, we have a
         * good client.
@@ -200,23 +266,51 @@ static int upload(char *dir)
        snprintf(timeout_buf, sizeof timeout_buf, "--timeout=%u", timeout);
 
        /* git-upload-pack only ever reads stuff, so this is safe */
-       execlp("git-upload-pack", "git-upload-pack", "--strict", timeout_buf, ".", NULL);
+       execl_git_cmd("upload-pack", "--strict", timeout_buf, ".", NULL);
        return -1;
 }
 
-static int execute(void)
+static int execute(struct sockaddr *addr)
 {
        static char line[1000];
-       int len;
+       int pktlen, len;
+
+       if (addr) {
+               char addrbuf[256] = "";
+               int port = -1;
+
+               if (addr->sa_family == AF_INET) {
+                       struct sockaddr_in *sin_addr = (void *) addr;
+                       inet_ntop(addr->sa_family, &sin_addr->sin_addr, addrbuf, sizeof(addrbuf));
+                       port = sin_addr->sin_port;
+#ifndef NO_IPV6
+               } else if (addr && addr->sa_family == AF_INET6) {
+                       struct sockaddr_in6 *sin6_addr = (void *) addr;
+
+                       char *buf = addrbuf;
+                       *buf++ = '['; *buf = '\0'; /* stpcpy() is cool */
+                       inet_ntop(AF_INET6, &sin6_addr->sin6_addr, buf, sizeof(addrbuf) - 1);
+                       strcat(buf, "]");
+
+                       port = sin6_addr->sin6_port;
+#endif
+               }
+               loginfo("Connection from %s:%d", addrbuf, port);
+       }
 
        alarm(init_timeout ? init_timeout : timeout);
-       len = packet_read_line(0, line, sizeof(line));
+       pktlen = packet_read_line(0, line, sizeof(line));
        alarm(0);
 
+       len = strlen(line);
+       if (pktlen != len)
+               loginfo("Extended attributes (%d bytes) exist <%.*s>",
+                       (int) pktlen - len,
+                       (int) pktlen - len, line + len + 1);
        if (len && line[len-1] == '\n')
                line[--len] = 0;
 
-       if (!strncmp("git-upload-pack /", line, 17))
+       if (!strncmp("git-upload-pack ", line, 16))
                return upload(line+16);
 
        logerror("Protocol error: '%s'", line);
@@ -239,12 +333,12 @@ static int execute(void)
 static int max_connections = 25;
 
 /* These are updated by the signal handler */
-static volatile unsigned int children_reaped = 0;
+static volatile unsigned int children_reaped;
 static pid_t dead_child[MAX_CHILDREN];
 
 /* These are updated by the main loop */
-static unsigned int children_spawned = 0;
-static unsigned int children_deleted = 0;
+static unsigned int children_spawned;
+static unsigned int children_deleted;
 
 static struct child {
        pid_t pid;
@@ -349,8 +443,6 @@ static void check_max_connections(void)
 static void handle(int incoming, struct sockaddr *addr, int addrlen)
 {
        pid_t pid = fork();
-       char addrbuf[256] = "";
-       int port = -1;
 
        if (pid) {
                unsigned idx;
@@ -371,26 +463,7 @@ static void handle(int incoming, struct sockaddr *addr, int addrlen)
        dup2(incoming, 1);
        close(incoming);
 
-       if (addr->sa_family == AF_INET) {
-               struct sockaddr_in *sin_addr = (void *) addr;
-               inet_ntop(AF_INET, &sin_addr->sin_addr, addrbuf, sizeof(addrbuf));
-               port = sin_addr->sin_port;
-
-#ifndef NO_IPV6
-       } else if (addr->sa_family == AF_INET6) {
-               struct sockaddr_in6 *sin6_addr = (void *) addr;
-
-               char *buf = addrbuf;
-               *buf++ = '['; *buf = '\0'; /* stpcpy() is cool */
-               inet_ntop(AF_INET6, &sin6_addr->sin6_addr, buf, sizeof(addrbuf) - 1);
-               strcat(buf, "]");
-
-               port = sin6_addr->sin6_port;
-#endif
-       }
-       loginfo("Connection from %s:%d", addrbuf, port);
-
-       exit(execute());
+       exit(execute(addr));
 }
 
 static void child_handler(int signo)
@@ -405,7 +478,7 @@ static void child_handler(int signo)
                        children_reaped = reaped + 1;
                        /* XXX: Custom logging, since we don't wanna getpid() */
                        if (verbose) {
-                               char *dead = "";
+                               const char *dead = "";
                                if (!WIFEXITED(status) || WEXITSTATUS(status) > 0)
                                        dead = " (with error)";
                                if (log_syslog)
@@ -419,6 +492,16 @@ static void child_handler(int signo)
        }
 }
 
+static int set_reuse_addr(int sockfd)
+{
+       int on = 1;
+
+       if (!reuseaddr)
+               return 0;
+       return setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
+                         &on, sizeof(on));
+}
+
 #ifndef NO_IPV6
 
 static int socksetup(int port, int **socklist_p)
@@ -463,6 +546,11 @@ static int socksetup(int port, int **socklist_p)
                }
 #endif
 
+               if (set_reuse_addr(sockfd)) {
+                       close(sockfd);
+                       continue;
+               }
+
                if (bind(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) {
                        close(sockfd);
                        continue;       /* not fatal */
@@ -505,13 +593,24 @@ static int socksetup(int port, int **socklist_p)
        sin.sin_addr.s_addr = htonl(INADDR_ANY);
        sin.sin_port = htons(port);
 
+       if (set_reuse_addr(sockfd)) {
+               close(sockfd);
+               return 0;
+       }
+
        if ( bind(sockfd, (struct sockaddr *)&sin, sizeof sin) < 0 ) {
                close(sockfd);
                return 0;
        }
 
+       if (listen(sockfd, 5) < 0) {
+               close(sockfd);
+               return 0;
+       }
+
        *socklist_p = xmalloc(sizeof(int));
        **socklist_p = sockfd;
+       return 1;
 }
 
 #endif
@@ -563,6 +662,45 @@ static int service_loop(int socknum, int *socklist)
        }
 }
 
+/* if any standard file descriptor is missing open it to /dev/null */
+static void sanitize_stdfds(void)
+{
+       int fd = open("/dev/null", O_RDWR, 0);
+       while (fd != -1 && fd < 2)
+               fd = dup(fd);
+       if (fd == -1)
+               die("open /dev/null or dup failed: %s", strerror(errno));
+       if (fd > 2)
+               close(fd);
+}
+
+static void daemonize(void)
+{
+       switch (fork()) {
+               case 0:
+                       break;
+               case -1:
+                       die("fork failed: %s", strerror(errno));
+               default:
+                       exit(0);
+       }
+       if (setsid() == -1)
+               die("setsid failed: %s", strerror(errno));
+       close(0);
+       close(1);
+       close(2);
+       sanitize_stdfds();
+}
+
+static void store_pid(const char *path)
+{
+       FILE *f = fopen(path, "w");
+       if (!f)
+               die("cannot open pid file %s: %s", path, strerror(errno));
+       fprintf(f, "%d\n", getpid());
+       fclose(f);
+}
+
 static int serve(int port)
 {
        int socknum, *socklist;
@@ -578,8 +716,15 @@ int main(int argc, char **argv)
 {
        int port = DEFAULT_GIT_PORT;
        int inetd_mode = 0;
+       const char *pid_file = NULL;
+       int detach = 0;
        int i;
 
+       /* Without this we cannot rely on waitpid() to tell
+        * what happened to our children.
+        */
+       signal(SIGCHLD, SIG_DFL);
+
        for (i = 1; i < argc; i++) {
                char *arg = argv[i];
 
@@ -594,6 +739,7 @@ int main(int argc, char **argv)
                }
                if (!strcmp(arg, "--inetd")) {
                        inetd_mode = 1;
+                       log_syslog = 1;
                        continue;
                }
                if (!strcmp(arg, "--verbose")) {
@@ -602,7 +748,6 @@ int main(int argc, char **argv)
                }
                if (!strcmp(arg, "--syslog")) {
                        log_syslog = 1;
-                       openlog("git-daemon", 0, LOG_DAEMON);
                        continue;
                }
                if (!strcmp(arg, "--export-all")) {
@@ -611,9 +756,40 @@ int main(int argc, char **argv)
                }
                if (!strncmp(arg, "--timeout=", 10)) {
                        timeout = atoi(arg+10);
+                       continue;
                }
                if (!strncmp(arg, "--init-timeout=", 15)) {
                        init_timeout = atoi(arg+15);
+                       continue;
+               }
+               if (!strcmp(arg, "--strict-paths")) {
+                       strict_paths = 1;
+                       continue;
+               }
+               if (!strncmp(arg, "--base-path=", 12)) {
+                       base_path = arg+12;
+                       continue;
+               }
+               if (!strcmp(arg, "--reuseaddr")) {
+                       reuseaddr = 1;
+                       continue;
+               }
+               if (!strcmp(arg, "--user-path")) {
+                       user_path = "";
+                       continue;
+               }
+               if (!strncmp(arg, "--user-path=", 12)) {
+                       user_path = arg + 12;
+                       continue;
+               }
+               if (!strncmp(arg, "--pid-file=", 11)) {
+                       pid_file = arg + 11;
+                       continue;
+               }
+               if (!strcmp(arg, "--detach")) {
+                       detach = 1;
+                       log_syslog = 1;
+                       continue;
                }
                if (!strcmp(arg, "--")) {
                        ok_paths = &argv[i+1];
@@ -626,10 +802,34 @@ int main(int argc, char **argv)
                usage(daemon_usage);
        }
 
+       if (log_syslog) {
+               openlog("git-daemon", 0, LOG_DAEMON);
+               set_die_routine(daemon_die);
+       }
+
+       if (strict_paths && (!ok_paths || !*ok_paths))
+               die("option --strict-paths requires a whitelist");
+
        if (inetd_mode) {
-               fclose(stderr); //FIXME: workaround
-               return execute();
-       } else {
-               return serve(port);
+               struct sockaddr_storage ss;
+               struct sockaddr *peer = (struct sockaddr *)&ss;
+               socklen_t slen = sizeof(ss);
+
+               freopen("/dev/null", "w", stderr);
+
+               if (getpeername(0, peer, &slen))
+                       peer = NULL;
+
+               return execute(peer);
        }
+
+       if (detach)
+               daemonize();
+       else
+               sanitize_stdfds();
+
+       if (pid_file)
+               store_pid(pid_file);
+
+       return serve(port);
 }