/* * blogc: A blog compiler. * Copyright (C) 2015-2016 Rafael G. Martins * * This program can be distributed under the terms of the BSD License. * See the file LICENSE. */ #ifdef HAVE_CONFIG_H #include #endif /* HAVE_CONFIG_H */ #include #include #include #include #include #include #include #include #include #include #include #include #include "utils.h" #ifndef BUFFER_SIZE #define BUFFER_SIZE 4096 #endif static unsigned int cpu_count(void) { #ifdef _SC_NPROCESSORS_ONLN long num = sysconf(_SC_NPROCESSORS_ONLN); if (num >= 1) return (unsigned int) num; #endif return 1; } static void rmdir_recursive(const char *dir) { struct stat buf; if (0 != stat(dir, &buf)) { fprintf(stderr, "warning: failed to remove directory (%s): %s\n", dir, strerror(errno)); return; } if (!S_ISDIR(buf.st_mode)) { fprintf(stderr, "error: trying to remove invalid directory: %s\n", dir); exit(2); } DIR *d = opendir(dir); if (d == NULL) { fprintf(stderr, "error: failed to open directory: %s\n", strerror(errno)); exit(2); } struct dirent *e; while (NULL != (e = readdir(d))) { if ((0 == strcmp(e->d_name, ".")) || (0 == strcmp(e->d_name, ".."))) continue; char *f = sb_strdup_printf("%s/%s", dir, e->d_name); if (0 != stat(f, &buf)) { fprintf(stderr, "error: failed to stat directory entry (%s): %s\n", e->d_name, strerror(errno)); free(f); exit(2); } if (S_ISDIR(buf.st_mode)) { rmdir_recursive(f); } else if (0 != unlink(f)) { fprintf(stderr, "error: failed to remove file (%s): %s\n", f, strerror(errno)); free(f); exit(2); } free(f); } if (0 != closedir(d)) { fprintf(stderr, "error: failed to close directory: %s\n", strerror(errno)); exit(2); } if (0 != rmdir(dir)) { fprintf(stderr, "error: failed to remove directory: %s\n", strerror(errno)); exit(2); } } static int git_shell(int argc, char *argv[]) { int rv = 0; char *repo = NULL; char *command_orig = NULL; char *command_name = NULL; char command_new[BUFFER_SIZE]; bool exec_git = false; // validate git command size_t len = strlen(argv[2]); if (!((len > 17 && (0 == strncmp(argv[2], "git-receive-pack ", 17))) || (len > 16 && (0 == strncmp(argv[2], "git-upload-pack ", 16))) || (len > 19 && (0 == strncmp(argv[2], "git-upload-archive ", 19))))) { fprintf(stderr, "error: unsupported git command: %s\n", argv[2]); rv = 1; goto cleanup; } // get shell path char *self = getenv("SHELL"); if (self == NULL) { fprintf(stderr, "error: failed to find blogc-git-receiver path\n"); rv = 1; goto cleanup; } // get home path char *home = getenv("HOME"); if (home == NULL) { fprintf(stderr, "error: failed to find user home path\n"); rv = 1; goto cleanup; } // get git repository command_orig = sb_strdup(argv[2]); char *p, *r; for (p = command_orig; *p != ' ' && *p != '\0'; p++); if (*p == ' ') p++; if (*p == '\'' || *p == '"') p++; if (*p == '/') p++; for (r = p; *p != '\'' && *p != '"' && *p != '\0'; p++); if (*p == '\'' || *p == '"') *p = '\0'; if (*--p == '/') *p = '\0'; repo = sb_strdup_printf("repos/%s", r); // check if repository is sane if (0 == strlen(repo)) { fprintf(stderr, "error: invalid repository\n"); rv = 1; goto cleanup; } if (0 == strncmp(argv[2], "git-upload-", 11)) // no need to check len here goto git_exec; if (0 != chdir(home)) { fprintf(stderr, "error: failed to chdir (%s): %s\n", home, strerror(errno)); rv = 1; goto cleanup; } if (0 != access(repo, F_OK)) { char *git_init_cmd = sb_strdup_printf( "git init --bare \"%s\" > /dev/null", repo); if (0 != system(git_init_cmd)) { fprintf(stderr, "error: failed to create git repository: %s\n", repo); rv = 1; free(git_init_cmd); goto cleanup; } free(git_init_cmd); } if (0 != chdir(repo)) { fprintf(stderr, "error: failed to chdir (%s/%s): %s\n", home, repo, strerror(errno)); rv = 1; goto cleanup; } if (0 != access("hooks", F_OK)) { // openwrt git package won't install git templates, then the git // repositories created with it won't have the hooks/ directory. if (0 != mkdir("hooks", 0777)) { // mkdir honors umask for us. fprintf(stderr, "error: failed to create directory (%s/%s/hooks): " "%s\n", home, repo, strerror(errno)); rv = 1; goto cleanup; } } if (0 != chdir("hooks")) { fprintf(stderr, "error: failed to chdir (%s/%s/hooks): %s\n", home, repo, strerror(errno)); rv = 1; goto cleanup; } if (0 == access("pre-receive", F_OK)) { if (0 != unlink("pre-receive")) { fprintf(stderr, "error: failed to remove old symlink " "(%s/%s/hooks/pre-receive): %s\n", home, repo, strerror(errno)); rv = 1; goto cleanup; } } if (0 != symlink(self, "pre-receive")) { fprintf(stderr, "error: failed to create symlink " "(%s/%s/hooks/pre-receive): %s\n", home, repo, strerror(errno)); rv = 1; goto cleanup; } if (0 == access("post-receive", F_OK)) { if (0 != unlink("post-receive")) { fprintf(stderr, "error: failed to remove old symlink " "(%s/%s/hooks/post-receive): %s\n", home, repo, strerror(errno)); rv = 1; goto cleanup; } } if (0 != symlink(self, "post-receive")) { fprintf(stderr, "error: failed to create symlink " "(%s/%s/hooks/post-receive): %s\n", home, repo, strerror(errno)); rv = 1; goto cleanup; } if (0 != chdir(home)) { fprintf(stderr, "error: failed to chdir (%s): %s\n", home, strerror(errno)); rv = 1; goto cleanup; } git_exec: command_name = sb_strdup(argv[2]); for (p = command_name; *p != ' ' && *p != '\0'; p++); if (*p == ' ') *p = '\0'; if (BUFFER_SIZE < (strlen(command_name) + strlen(repo) + 4)) { fprintf(stderr, "error: git-shell command is too big\n"); rv = 1; goto cleanup; } if (snprintf(command_new, BUFFER_SIZE, "%s '%s'", command_name, repo)) exec_git = true; cleanup: free(repo); free(command_orig); free(command_name); if (exec_git) { execlp("git-shell", "git-shell", "-c", command_new, NULL); // execlp only returns on error, then something bad happened fprintf(stderr, "error: failed to execute git-shell\n"); rv = 1; } return rv; } static int git_post_receive_hook(int argc, char *argv[]) { if (0 != system("git remote get-url --push mirror &> /dev/null")) return 0; // at this point we know that we have a remote called mirror, we can just // push to it. if (0 != system("git push --mirror mirror")) fprintf(stderr, "warning: failed push to git mirror\n"); return 0; } typedef enum { START_OLD = 1, OLD, START_NEW, NEW, START_REF, REF } input_state_t; static int git_pre_receive_hook(int argc, char *argv[]) { int c; char buffer[BUFFER_SIZE]; input_state_t state = START_OLD; size_t i = 0; size_t start = 0; int rv = 0; char *new = NULL; char *master = NULL; while (EOF != (c = getc(stdin))) { buffer[i] = (char) c; switch (state) { case START_OLD: start = i; state = OLD; break; case OLD: if (c != ' ') break; // no need to store old state = START_NEW; break; case START_NEW: start = i; state = NEW; break; case NEW: if (c != ' ') break; state = START_REF; new = strndup(buffer + start, i - start); break; case START_REF: start = i; state = REF; break; case REF: if (c != '\n') break; state = START_OLD; // we just care about a ref (refs/heads/master), everything // else is disposable :) if (!((i - start == 17) && (0 == strncmp("refs/heads/master", buffer + start, 17)))) { free(new); new = NULL; break; } master = new; break; } if (++i >= BUFFER_SIZE) { fprintf(stderr, "error: pre-receive hook payload is too big.\n"); rv = 1; goto cleanup2; } } if (master == NULL) { fprintf(stderr, "warning: no reference to master branch found. " "nothing to deploy.\n"); goto cleanup2; } char *repo_dir = NULL; char *output_dir = NULL; if (NULL == getcwd(buffer, BUFFER_SIZE)) { fprintf(stderr, "error: failed to get repository remote path: %s\n", strerror(errno)); rv = 1; goto cleanup; } repo_dir = sb_strdup(buffer); char dir[] = "/tmp/blogc_XXXXXX"; if (NULL == mkdtemp(dir)) { rv = 1; goto cleanup; } char *git_archive_cmd = sb_strdup_printf( "git archive \"%s\" | tar -x -C \"%s\" -f -", master, dir); if (0 != system(git_archive_cmd)) { fprintf(stderr, "error: failed to extract git content to temporary " "directory: %s\n", dir); rv = 1; free(git_archive_cmd); goto cleanup; } free(git_archive_cmd); if (0 != chdir(dir)) { fprintf(stderr, "error: failed to chdir (%s): %s\n", dir, strerror(errno)); rv = 1; goto cleanup; } if ((0 != access("Makefile", F_OK)) && (0 != access("GNUMakefile", F_OK))) { fprintf(stderr, "warning: no makefile found. skipping ...\n"); goto cleanup; } char *home = getenv("HOME"); if (home == NULL) { fprintf(stderr, "error: failed to find user home path\n"); rv = 1; goto cleanup; } unsigned long epoch = time(NULL); output_dir = sb_strdup_printf("%s/builds/%s-%lu", home, master, epoch); char *gmake_cmd = sb_strdup_printf( "gmake -j%d OUTPUT_DIR=\"%s\" BLOGC_GIT_RECEIVER=1", cpu_count(), output_dir); fprintf(stdout, "running command: %s\n\n", gmake_cmd); fflush(stdout); if (0 != system(gmake_cmd)) { fprintf(stderr, "error: failed to build website ...\n"); rmdir_recursive(output_dir); free(gmake_cmd); rv = 1; goto cleanup; } free(gmake_cmd); if (0 != chdir(repo_dir)) { fprintf(stderr, "error: failed to chdir (%s): %s\n", repo_dir, strerror(errno)); rmdir_recursive(output_dir); rv = 1; goto cleanup; } char *htdocs_sym = NULL; ssize_t htdocs_sym_len = readlink("htdocs", buffer, BUFFER_SIZE); if (0 < htdocs_sym_len) { if (0 != unlink("htdocs")) { fprintf(stderr, "error: failed to remove symlink (%s/htdocs): %s\n", repo_dir, strerror(errno)); rmdir_recursive(output_dir); rv = 1; goto cleanup; } buffer[htdocs_sym_len] = '\0'; htdocs_sym = buffer; } if (0 != symlink(output_dir, "htdocs")) { fprintf(stderr, "error: failed to create symlink (%s/htdocs): %s\n", repo_dir, strerror(errno)); rmdir_recursive(output_dir); rv = 1; goto cleanup; } if (htdocs_sym != NULL) rmdir_recursive(htdocs_sym); cleanup: free(output_dir); rmdir_recursive(dir); free(repo_dir); cleanup2: free(new); return rv; } int main(int argc, char *argv[]) { if (argc > 0) { if (0 == strcmp(basename(argv[0]), "pre-receive")) return git_pre_receive_hook(argc, argv); if (0 == strcmp(basename(argv[0]), "post-receive")) return git_post_receive_hook(argc, argv); } if (argc == 3 && (0 == strcmp(argv[1], "-c"))) return git_shell(argc, argv); fprintf(stderr, "error: this is a special shell, go away!\n"); return 1; }