contrib/hgsh/hgsh.c
changeset 2341 dbbe7f72d15a
child 2602 9cbeef33eaa3
equal deleted inserted replaced
2338:391c5d0f9ef3 2341:dbbe7f72d15a
       
     1 /*
       
     2  * hgsh.c - restricted login shell for mercurial
       
     3  *
       
     4  * Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
       
     5  *
       
     6  * This software may be used and distributed according to the terms of the
       
     7  * GNU General Public License, incorporated herein by reference.
       
     8  *
       
     9  * this program is login shell for dedicated mercurial user account. it
       
    10  * only allows few actions:
       
    11  *
       
    12  * 1. run hg in server mode on specific repository. no other hg commands
       
    13  * are allowed. we try to verify that repo to be accessed exists under
       
    14  * given top-level directory.
       
    15  *
       
    16  * 2. (optional) forward ssh connection from firewall/gateway machine to
       
    17  * "real" mercurial host, to let users outside intranet pull and push
       
    18  * changes through firewall.
       
    19  *
       
    20  * 3. (optional) run normal shell, to allow to "su" to mercurial user, use
       
    21  * "sudo" to run programs as that user, or run cron jobs as that user.
       
    22  *
       
    23  * only tested on linux yet. patches for non-linux systems welcome.
       
    24  */
       
    25 
       
    26 #ifndef _GNU_SOURCE
       
    27 #define _GNU_SOURCE /* for asprintf */
       
    28 #endif
       
    29 
       
    30 #include <stdio.h>
       
    31 #include <stdlib.h>
       
    32 #include <string.h>
       
    33 #include <sys/stat.h>
       
    34 #include <sys/types.h>
       
    35 #include <sysexits.h>
       
    36 #include <unistd.h>
       
    37 
       
    38 /*
       
    39  * user config.
       
    40  *
       
    41  * if you see a hostname below, just use first part of hostname. example,
       
    42  * if you have host named foo.bar.com, use "foo".
       
    43  */
       
    44 
       
    45 /*
       
    46  * HG_GATEWAY: hostname of gateway/firewall machine that people outside your
       
    47  * intranet ssh into if they need to ssh to other machines. if you do not
       
    48  * have such machine, set to NULL.
       
    49  */
       
    50 #ifndef HG_GATEWAY
       
    51 #define HG_GATEWAY     "gateway"
       
    52 #endif
       
    53 
       
    54 /*
       
    55  * HG_HOST: hostname of mercurial server. if any machine is allowed, set to
       
    56  * NULL.
       
    57  */
       
    58 #ifndef HG_HOST
       
    59 #define HG_HOST         "mercurial"
       
    60 #endif
       
    61 
       
    62 /*
       
    63  * HG_USER: username to log in from HG_GATEWAY to HG_HOST. if gateway and
       
    64  * host username are same, set to NULL.
       
    65  */
       
    66 #ifndef HG_USER
       
    67 #define HG_USER         "hg"
       
    68 #endif
       
    69 
       
    70 /*
       
    71  * HG_ROOT: root of tree full of mercurial repos. if you do not want to
       
    72  * validate location of repo when someone is try to access, set to NULL.
       
    73  */
       
    74 #ifndef HG_ROOT
       
    75 #define HG_ROOT         "/home/hg/repos"
       
    76 #endif
       
    77 
       
    78 /*
       
    79  * HG: path to the mercurial executable to run.
       
    80  */
       
    81 #ifndef HG
       
    82 #define HG              "/home/hg/bin/hg"
       
    83 #endif
       
    84 
       
    85 /*
       
    86  * HG_SHELL: shell to use for actions like "sudo" and "su" access to
       
    87  * mercurial user, and cron jobs. if you want to make these things
       
    88  * impossible, set to NULL.
       
    89  */
       
    90 #ifndef HG_SHELL
       
    91 #define HG_SHELL        NULL
       
    92 // #define HG_SHELL        "/bin/bash"
       
    93 #endif
       
    94 
       
    95 /*
       
    96  * HG_HELP: some way for users to get support if they have problem. if they
       
    97  * should not get helpful message, set to NULL.
       
    98  */
       
    99 #ifndef HG_HELP
       
   100 #define HG_HELP         "please contact support@example.com for help."
       
   101 #endif
       
   102 
       
   103 /*
       
   104  * SSH: path to ssh executable to run, if forwarding from HG_GATEWAY to
       
   105  * HG_HOST. if you want to use rsh instead (why?), you need to modify
       
   106  * arguments it is called with. see forward_through_gateway.
       
   107  */
       
   108 #ifndef SSH
       
   109 #define SSH             "/usr/bin/ssh"
       
   110 #endif
       
   111 
       
   112 /*
       
   113  * tell whether to print command that is to be executed. useful for
       
   114  * debugging. should not interfere with mercurial operation, since
       
   115  * mercurial only cares about stdin and stdout, and this prints to stderr.
       
   116  */
       
   117 static const int debug = 0;
       
   118 
       
   119 static void print_cmdline(int argc, char **argv)
       
   120 {
       
   121     FILE *fp = stderr;
       
   122     int i;
       
   123 
       
   124     fputs("command: ", fp);
       
   125 
       
   126     for (i = 0; i < argc; i++) {
       
   127         char *spc = strpbrk(argv[i], " \t\r\n");
       
   128         if (spc) {
       
   129             fputc('\'', fp);
       
   130         }
       
   131         fputs(argv[i], fp);
       
   132         if (spc) {
       
   133             fputc('\'', fp);
       
   134         }
       
   135         if (i < argc - 1) {
       
   136             fputc(' ', fp);
       
   137         }
       
   138     }
       
   139     fputc('\n', fp);
       
   140     fflush(fp);
       
   141 }
       
   142 
       
   143 static void usage(const char *reason, int exitcode)
       
   144 {
       
   145     char *hg_help = HG_HELP;
       
   146 
       
   147     if (reason) {
       
   148         fprintf(stderr, "*** Error: %s.\n", reason);
       
   149     }
       
   150     fprintf(stderr, "*** This program has been invoked incorrectly.\n");
       
   151     if (hg_help) {
       
   152         fprintf(stderr, "*** %s\n", hg_help);
       
   153     }
       
   154     exit(exitcode ? exitcode : EX_USAGE);
       
   155 }
       
   156 
       
   157 /*
       
   158  * run on gateway host to make another ssh connection, to "real" mercurial
       
   159  * server. it sends its command line unmodified to far end.
       
   160  *
       
   161  * never called if HG_GATEWAY is NULL.
       
   162  */
       
   163 static void forward_through_gateway(int argc, char **argv)
       
   164 {
       
   165     char *ssh = SSH;
       
   166     char *hg_host = HG_HOST;
       
   167     char *hg_user = HG_USER;
       
   168     char **nargv = alloca((10 + argc) * sizeof(char *));
       
   169     int i = 0, j;
       
   170 
       
   171     nargv[i++] = ssh;
       
   172     nargv[i++] = "-q";
       
   173     nargv[i++] = "-T";
       
   174     nargv[i++] = "-x";
       
   175     if (hg_user) {
       
   176         nargv[i++] = "-l";
       
   177         nargv[i++] = hg_user;
       
   178     }
       
   179     nargv[i++] = hg_host;
       
   180 
       
   181     /*
       
   182      * sshd called us with added "-c", because it thinks we are a shell.
       
   183      * drop it if we find it.
       
   184      */
       
   185     j = 1;
       
   186     if (j < argc && strcmp(argv[j], "-c") == 0) {
       
   187         j++;
       
   188     }
       
   189 
       
   190     for (; j < argc; i++, j++) {
       
   191         nargv[i] = argv[j];
       
   192     }
       
   193     nargv[i] = NULL;
       
   194 
       
   195     if (debug) {
       
   196         print_cmdline(i, nargv);
       
   197     }
       
   198 
       
   199     execv(ssh, nargv);
       
   200     perror(ssh);
       
   201     exit(EX_UNAVAILABLE);
       
   202 }
       
   203 
       
   204 /*
       
   205  * run shell. let administrator "su" to mercurial user's account to do
       
   206  * administrative works.
       
   207  *
       
   208  * never called if HG_SHELL is NULL.
       
   209  */
       
   210 static void run_shell(int argc, char **argv)
       
   211 {
       
   212     char *hg_shell = HG_SHELL;
       
   213     char **nargv;
       
   214     char *c;
       
   215     int i;
       
   216 
       
   217     nargv = alloca((argc + 3) * sizeof(char *));
       
   218     c = strrchr(hg_shell, '/');
       
   219 
       
   220     /* tell "real" shell it is login shell, if needed. */
       
   221 
       
   222     if (argv[0][0] == '-' && c) {
       
   223         nargv[0] = strdup(c);
       
   224         if (nargv[0] == NULL) {
       
   225             perror("malloc");
       
   226             exit(EX_OSERR);
       
   227         }
       
   228         nargv[0][0] = '-';
       
   229     } else {
       
   230         nargv[0] = hg_shell;
       
   231     }
       
   232 
       
   233     for (i = 1; i < argc; i++) {
       
   234         nargv[i] = argv[i];
       
   235     }
       
   236     nargv[i] = NULL;
       
   237 
       
   238     if (debug) {
       
   239         print_cmdline(i, nargv);
       
   240     }
       
   241 
       
   242     execv(hg_shell, nargv);
       
   243     perror(hg_shell);
       
   244     exit(EX_OSFILE);
       
   245 }
       
   246 
       
   247 /*
       
   248  * paranoid wrapper, runs hg executable in server mode.
       
   249  */
       
   250 static void serve_data(int argc, char **argv)
       
   251 {
       
   252     char *hg_root = HG_ROOT;
       
   253     char *repo, *abspath;
       
   254     char *nargv[6];
       
   255     struct stat st;
       
   256     size_t repolen;
       
   257     int i;
       
   258 
       
   259     /*
       
   260      * check argv for looking okay. we should be invoked with argv
       
   261      * resembling like this:
       
   262      *
       
   263      *   hgsh
       
   264      *   -c
       
   265      *   hg -R some/path serve --stdio
       
   266      *
       
   267      * the "-c" is added by sshd, because it thinks we are login shell.
       
   268      */
       
   269 
       
   270     if (argc != 3) {
       
   271         goto badargs;
       
   272     }
       
   273 
       
   274     if (strcmp(argv[1], "-c") != 0) {
       
   275         goto badargs;
       
   276     }
       
   277 
       
   278     if (sscanf(argv[2], "hg -R %as serve --stdio", &repo) != 1) {
       
   279         goto badargs;
       
   280     }
       
   281 
       
   282     repolen = repo ? strlen(repo) : 0;
       
   283 
       
   284     if (repolen == 0) {
       
   285         goto badargs;
       
   286     }
       
   287 
       
   288     if (hg_root) {
       
   289         if (asprintf(&abspath, "%s/%s/.hg/data", hg_root, repo) == -1) {
       
   290             goto badargs;
       
   291         }
       
   292 
       
   293         /*
       
   294          * attempt to stop break out from inside the repository tree. could
       
   295          * do something more clever here, because e.g. we could traverse a
       
   296          * symlink that looks safe, but really breaks us out of tree.
       
   297          */
       
   298 
       
   299         if (strstr(abspath, "/../") != NULL) {
       
   300             goto badargs;
       
   301         }
       
   302 
       
   303         /* verify that we really are looking at valid repo. */
       
   304 
       
   305         if (stat(abspath, &st) == -1) {
       
   306             perror(repo);
       
   307             exit(EX_DATAERR);
       
   308         }
       
   309 
       
   310         if (chdir(hg_root) == -1) {
       
   311             perror(hg_root);
       
   312             exit(EX_SOFTWARE);
       
   313         }
       
   314     }
       
   315 
       
   316     i = 0;
       
   317     nargv[i++] = HG;
       
   318     nargv[i++] = "-R";
       
   319     nargv[i++] = repo;
       
   320     nargv[i++] = "serve";
       
   321     nargv[i++] = "--stdio";
       
   322     nargv[i] = NULL;
       
   323 
       
   324     if (debug) {
       
   325         print_cmdline(i, nargv);
       
   326     }
       
   327 
       
   328     execv(HG, nargv);
       
   329     perror(HG);
       
   330     exit(EX_UNAVAILABLE);
       
   331 
       
   332 badargs:
       
   333     /* print useless error message. */
       
   334 
       
   335     usage("invalid arguments", EX_DATAERR);
       
   336 }
       
   337 
       
   338 int main(int argc, char **argv)
       
   339 {
       
   340     char host[1024];
       
   341     char *c;
       
   342 
       
   343     if (gethostname(host, sizeof(host)) == -1) {
       
   344         perror("gethostname");
       
   345         exit(EX_OSERR);
       
   346     }
       
   347 
       
   348     if ((c = strchr(host, '.')) != NULL) {
       
   349         *c = '\0';
       
   350     }
       
   351 
       
   352     if (getenv("SSH_CLIENT")) {
       
   353         char *hg_gateway = HG_GATEWAY;
       
   354         char *hg_host = HG_HOST;
       
   355 
       
   356         if (hg_gateway && strcmp(host, hg_gateway) == 0) {
       
   357             forward_through_gateway(argc, argv);
       
   358         }
       
   359 
       
   360         if (hg_host && strcmp(host, hg_host) != 0) {
       
   361             usage("invoked on unexpected host", EX_USAGE);
       
   362         }
       
   363 
       
   364         serve_data(argc, argv);
       
   365     } else if (HG_SHELL) {
       
   366         run_shell(argc, argv);
       
   367     } else {
       
   368         usage("invalid arguments", EX_DATAERR);
       
   369     }
       
   370 
       
   371     return 0;
       
   372 }