/* * The two backends behind a cgit filter and the plumbing that hands stdout to * whichever one is open. An exec filter forks a program and feeds it the page * through a pipe, while a Lua filter runs a script inside cgit and feeds it * the page through a write that has been interposed in front of libc's. A * cgitrc command picks its backend with an exec or lua prefix, and a command * with no prefix is taken as a program to run. */ #include "cgit.h" #include "filter.h" #include "html.h" #include "shared.h" #ifndef NO_LUA #include #include #include #include #endif static int open_exec_filter(struct cgit_filter *base, va_list ap) { struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base; int pipefd[2]; int i; for (i = 0; i < filter->base.argument_count; i++) filter->argv[i + 1] = va_arg(ap, char *); filter->old_stdout = cgit_die_unless_positive(dup(STDOUT_FILENO), "Unable to duplicate STDOUT"); cgit_die_unless_zero(pipe(pipefd), "Unable to create pipe to subprocess"); filter->pid = cgit_die_unless_non_negative(fork(), "Unable to create subprocess"); if (filter->pid == 0) { close(pipefd[1]); cgit_die_unless_non_negative(dup2(pipefd[0], STDIN_FILENO), "Unable to use pipe as STDIN"); execvp(filter->cmd, filter->argv); die_errno("Unable to exec subprocess %s", filter->cmd); } close(pipefd[0]); cgit_die_unless_non_negative(dup2(pipefd[1], STDOUT_FILENO), "Unable to use pipe as STDOUT"); close(pipefd[1]); return 0; } static int close_exec_filter(struct cgit_filter *base) { struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base; int i, exit_status = 0; cgit_die_unless_non_negative(dup2(filter->old_stdout, STDOUT_FILENO), "Unable to restore STDOUT"); close(filter->old_stdout); if (filter->pid < 0) goto done; waitpid(filter->pid, &exit_status, 0); if (WIFEXITED(exit_status)) goto done; die("Subprocess %s exited abnormally", filter->cmd); done: for (i = 0; i < filter->base.argument_count; i++) filter->argv[i + 1] = NULL; return WEXITSTATUS(exit_status); } static void fprintf_exec_filter(struct cgit_filter *base, FILE *f, const char *prefix) { struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base; fprintf(f, "%sexec:%s\n", prefix, filter->cmd); } static void cleanup_exec_filter(struct cgit_filter *base) { struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base; free(filter->argv); filter->argv = NULL; free(filter->cmd); filter->cmd = NULL; } static struct cgit_filter *new_exec_filter(const char *cmd, int argument_count) { struct cgit_exec_filter *filter; int argv_size; filter = xmalloc(sizeof(*filter)); cgit_exec_filter_init(filter, cgit_strdup_first_line(cmd), NULL); filter->base.argument_count = argument_count; // argv is the command, then a slot per argument, then the NULL that // execvp needs to find the end. argv_size = (2 + argument_count) * sizeof(char *); filter->argv = xmalloc(argv_size); memset(filter->argv, 0, argv_size); filter->argv[0] = filter->cmd; return &filter->base; } void cgit_exec_filter_init(struct cgit_exec_filter *filter, char *cmd, char **argv) { memset(filter, 0, sizeof(*filter)); filter->base.open = open_exec_filter; filter->base.close = close_exec_filter; filter->base.fprintfp = fprintf_exec_filter; filter->base.cleanup = cleanup_exec_filter; filter->cmd = cmd; filter->argv = argv; filter->base.argument_count = 0; } #ifdef NO_LUA void cgit_init_filters(void) { } #endif #ifndef NO_LUA struct lua_filter { struct cgit_filter base; char *script_file; lua_State *lua_state; }; typedef ssize_t (*filter_write_fn)(struct cgit_filter *base, const void *buf, size_t count); static ssize_t (*libc_write)(int fd, const void *buf, size_t count); static filter_write_fn filter_write = NULL; static struct cgit_filter *current_write_filter = NULL; void cgit_init_filters(void) { libc_write = dlsym(RTLD_NEXT, "write"); if (!libc_write) die("Could not locate libc's write function"); } /* * Interposing on write is what lets a Lua filter see output produced by code * that has no idea a filter is running. */ ssize_t write(int fd, const void *buf, size_t count) { if (fd != STDOUT_FILENO || !filter_write) return libc_write(fd, buf, count); return filter_write(current_write_filter, buf, count); } static inline void hook_write(struct cgit_filter *filter, filter_write_fn write_fn) { // Filters cannot nest, because there is one stdout and one hook, so a // second one would strand the first. assert(filter_write == NULL); assert(current_write_filter == NULL); current_write_filter = filter; filter_write = write_fn; } static inline void unhook_write(void) { assert(filter_write != NULL); assert(current_write_filter != NULL); filter_write = NULL; current_write_filter = NULL; } static NORETURN void die_lua_error(struct lua_filter *filter) { die("Lua error in %s: %s", filter->script_file, lua_tostring(filter->lua_state, -1)); } static ssize_t write_lua_filter(struct cgit_filter *base, const void *buf, size_t count) { struct lua_filter *filter = (struct lua_filter *)base; lua_getglobal(filter->lua_state, "filter_write"); lua_pushlstring(filter->lua_state, buf, count); if (lua_pcall(filter->lua_state, 1, 0, 0)) die_lua_error(filter); return count; } /* * Output a script asks for belongs on the page, not back in its own filter, * so the hook comes off around the call. The zero returned is Lua's count * of values pushed, not a success code. */ static inline int emit_unfiltered(lua_State *lua_state, void (*emit)(const char *text)) { const char *text; filter_write_fn saved_write; struct cgit_filter *saved_filter; text = lua_tostring(lua_state, 1); if (!text) return 0; saved_write = filter_write; saved_filter = current_write_filter; unhook_write(); emit(text); // emit leaves bytes in the html buffer, so empty it while the hook is // still off. Re-hooking first would send the page's own bytes back into // the filter. html_flush(); hook_write(saved_filter, saved_write); return 0; } static int script_html(lua_State *lua_state) { return emit_unfiltered(lua_state, html); } static int script_html_txt(lua_State *lua_state) { return emit_unfiltered(lua_state, html_txt); } static int script_html_attr(lua_State *lua_state) { return emit_unfiltered(lua_state, html_attr); } static int script_html_url_path(lua_State *lua_state) { return emit_unfiltered(lua_state, html_url_path); } static int script_html_url_arg(lua_State *lua_state) { return emit_unfiltered(lua_state, html_url_arg); } /* * html_include returns whether it found the file, and calling it through a * pointer that claims it returns nothing would be undefined, so the result is * dropped in a wrapper of the right shape. */ static void include_and_discard_result(const char *filename) { html_include(filename); } static int script_html_include(lua_State *lua_state) { return emit_unfiltered(lua_state, include_and_discard_result); } static const struct { const char *name; lua_CFunction fn; } script_globals[] = { { "html", script_html }, { "html_txt", script_html_txt }, { "html_attr", script_html_attr }, { "html_url_path", script_html_url_path }, { "html_url_arg", script_html_url_arg }, { "html_include", script_html_include }, }; static int init_lua_filter(struct lua_filter *filter) { size_t i; if (filter->lua_state) return 0; if (!(filter->lua_state = luaL_newstate())) return 1; luaL_openlibs(filter->lua_state); for (i = 0; i < ARRAY_SIZE(script_globals); i++) { lua_pushcfunction(filter->lua_state, script_globals[i].fn); lua_setglobal(filter->lua_state, script_globals[i].name); } if (luaL_dofile(filter->lua_state, filter->script_file)) die_lua_error(filter); return 0; } static int open_lua_filter(struct cgit_filter *base, va_list ap) { struct lua_filter *filter = (struct lua_filter *)base; int i; if (init_lua_filter(filter)) return 1; hook_write(base, write_lua_filter); lua_getglobal(filter->lua_state, "filter_open"); for (i = 0; i < filter->base.argument_count; ++i) lua_pushstring(filter->lua_state, va_arg(ap, char *)); if (lua_pcall(filter->lua_state, filter->base.argument_count, 0, 0)) die_lua_error(filter); return 0; } static int close_lua_filter(struct cgit_filter *base) { struct lua_filter *filter = (struct lua_filter *)base; int ret; lua_getglobal(filter->lua_state, "filter_close"); if (lua_pcall(filter->lua_state, 0, 1, 0)) die_lua_error(filter); ret = lua_tonumber(filter->lua_state, -1); lua_pop(filter->lua_state, 1); unhook_write(); return ret; } static void fprintf_lua_filter(struct cgit_filter *base, FILE *f, const char *prefix) { struct lua_filter *filter = (struct lua_filter *)base; fprintf(f, "%slua:%s\n", prefix, filter->script_file); } static void cleanup_lua_filter(struct cgit_filter *base) { struct lua_filter *filter = (struct lua_filter *)base; if (!filter->lua_state) return; lua_close(filter->lua_state); filter->lua_state = NULL; free(filter->script_file); filter->script_file = NULL; } static struct cgit_filter *new_lua_filter(const char *cmd, int argument_count) { struct lua_filter *filter; filter = xmalloc(sizeof(*filter)); memset(filter, 0, sizeof(*filter)); filter->base.open = open_lua_filter; filter->base.close = close_lua_filter; filter->base.fprintfp = fprintf_lua_filter; filter->base.cleanup = cleanup_lua_filter; filter->base.argument_count = argument_count; filter->script_file = cgit_strdup_first_line(cmd); return &filter->base; } #endif int cgit_open_filter(struct cgit_filter *filter, ...) { int result; va_list ap; if (!filter) return 0; // Whatever is still buffered belongs to the page, not to the filter // that is about to take over stdout. html_flush(); va_start(ap, filter); result = filter->open(filter, ap); va_end(ap); return result; } int cgit_close_filter(struct cgit_filter *filter) { if (!filter) return 0; // Anything written while the filter was open is meant for it, so this // has to go out before the filter hands stdout back. html_flush(); return filter->close(filter); } void cgit_fprintf_filter(struct cgit_filter *filter, FILE *f, const char *prefix) { filter->fprintfp(filter, f, prefix); } static inline void cleanup_filter(struct cgit_filter *filter) { if (filter && filter->cleanup) filter->cleanup(filter); } void cgit_cleanup_filters(void) { int i; cleanup_filter(ctx.cfg.about_filter); cleanup_filter(ctx.cfg.commit_filter); cleanup_filter(ctx.cfg.source_filter); cleanup_filter(ctx.cfg.email_filter); cleanup_filter(ctx.cfg.auth_filter); for (i = 0; i < cgit_repolist.count; ++i) { cleanup_filter(cgit_repolist.repos[i].about_filter); cleanup_filter(cgit_repolist.repos[i].commit_filter); cleanup_filter(cgit_repolist.repos[i].source_filter); cleanup_filter(cgit_repolist.repos[i].email_filter); } } static const struct { const char *prefix; struct cgit_filter *(*create)(const char *cmd, int argument_count); } filter_specs[] = { { "exec", new_exec_filter }, #ifndef NO_LUA { "lua", new_lua_filter }, #endif }; struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype) { const char *colon; size_t i; size_t len; int argument_count; if (!cmd || !cmd[0]) return NULL; colon = strchr(cmd, ':'); // Measured only once there is a colon to measure against, because // subtracting from a null pointer is not something C defines. len = colon ? (size_t)(colon - cmd) : 0; // A single letter before the colon is a Windows drive letter, not a // filter prefix. if (len == 1) colon = NULL; switch (filtertype) { case AUTH: argument_count = 12; break; case EMAIL: argument_count = 2; break; case SOURCE: case ABOUT: argument_count = 1; break; case COMMIT: default: argument_count = 0; break; } if (!colon) return new_exec_filter(cmd, argument_count); for (i = 0; i < ARRAY_SIZE(filter_specs); i++) { if (len == strlen(filter_specs[i].prefix) && !strncmp(filter_specs[i].prefix, cmd, len)) return filter_specs[i].create(colon + 1, argument_count); } die("Invalid filter type: %.*s", (int) len, cmd); }