commit b9b8fcd563d661e96c75efb20e5c09bb5226e3f9 Author: Stefan Bühler Date: Sun Jul 20 21:33:42 2008 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d67d6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.lock-wscript +.waf-* +build +*~ +*.o diff --git a/spawn-fcgi.1 b/spawn-fcgi.1 new file mode 100644 index 0000000..bb96b0a --- /dev/null +++ b/spawn-fcgi.1 @@ -0,0 +1,10 @@ +.TH SPAWN-FCGI 1 "July 20, 2008" +.SH NAME +spawn-fcgi \- spawns fastcgi processes +.SH OPTIONS +Use --help to find out ;-) +.SH AUTHOR +spawn-fcgi was part of the lighttpd project, written by Jan Kneschke. +.PP +This manual page was written by Stefan B\"uhler , +for the Debian project (but may be used by others). diff --git a/spawn-fcgi.c b/spawn-fcgi.c new file mode 100644 index 0000000..f67f71a --- /dev/null +++ b/spawn-fcgi.c @@ -0,0 +1,534 @@ + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include + +#include "config.h" + +#define UNUSED(x) ((void)(x)) + +#define FCGI_LISTENSOCK_FILENO 0 + +#ifndef __GNUC__ +# define __attribute__(x) /*NOTHING*/ +#endif + + +/* +spawn-fcgi - spawns fastcgi processes + +The basic code was extracted from lighttpd (http://www.lighttpd.net/) and modified +by Stefan Buehler in 2008 (use glib2, keep fds open and change socket ownership). + +COPYING from lighttpd: + Copyright (c) 2004, Jan Kneschke, incremental + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + - Neither the name of the 'incremental' nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +typedef struct { + gchar *fcgiapp, **args; + in_addr_t addr; + gint port; + gchar *unixsocket; + gint php_childs; /* set env var */ + gint fork_childs; + gchar *pid_file; + gboolean no_fork; + gboolean show_version; + gboolean keep_fds, close_fds; /* keep/close STDIN/STDOUT */ + + gchar *chroot; + gchar *uid, *gid; + gchar *socketuid, *socketgid; + gint socketmode; +} options; + +struct data { + socklen_t socklen; + struct sockaddr *sockaddr; + int socket; + + gboolean i_am_root; + uid_t uid, socketuid; + gid_t gid, socketgid; + gchar *username; + + int pid_fd; +}; + +static options opts = { + NULL, NULL, + 0, + 0, + NULL, + 5, + 1, + NULL, + FALSE, + FALSE, + FALSE, FALSE, + + NULL, + NULL, NULL, + NULL, NULL, + 0600 +}; + +static struct data data; + +/* only require an entry for name != NULL, otherwise a id as key is ok */ +int readpwdent(gchar *key, uid_t *uid, gid_t *gid, gchar** name) { + struct passwd *pwd; + errno = 0; + *gid = (gid_t) -1; + if (NULL == (pwd = getpwnam(key)) && NULL == (pwd = getpwuid(atoi(key)))) { + if (name == NULL && 0 < (*uid = (uid_t) atoi(key))) return 0; + g_printerr("Couldn't find passwd entry for '%s': %s\n", key, g_strerror(errno)); + return -1; + } + *uid = pwd->pw_uid; + *gid = pwd->pw_gid; + if (name) *name = g_strdup(pwd->pw_name); + return 0; +} + +int readgrpent(gchar *key, gid_t *gid) { + struct group *grp; + errno = 0; + if (NULL == (grp = getgrnam(key)) && NULL == (grp = getgrgid(atoi(key)))) { + if (0 < (*gid = (gid_t) atoi(key))) return 0; + g_printerr("Couldn't find group entry for '%s': %s\n", key, g_strerror(errno)); + return -1; + } + *gid = grp->gr_gid; + return 0; +} + +int create_sockaddr() { + if (opts.addr != 0 && opts.port == 0) { + g_printerr("Specified address without port\n"); + return -1; + } + + if (opts.port != 0 && opts.unixsocket != NULL) { + g_printerr("Either tcp:port or unix domain socket, not both\n"); + return -1; + } + + if (opts.port == 0 && opts.unixsocket == NULL) { + g_printerr("Need either tcp:port or unix domain socket\n"); + return -1; + } + + if (opts.port != 0) { + struct sockaddr_in *sin; + sin = g_malloc0(sizeof(struct sockaddr_in)); + sin->sin_family = AF_INET; + if (opts.addr) + sin->sin_addr.s_addr = opts.addr; + else + sin->sin_addr.s_addr = htonl(INADDR_ANY); + sin->sin_port = htons(opts.port); + data.sockaddr = (struct sockaddr*) sin; + data.socklen = sizeof(struct sockaddr_in); + } else { + struct sockaddr_un *sun; + gsize slen = strlen(opts.unixsocket), len = 1 + slen + (gsize) (((struct sockaddr_un *) 0)->sun_path); + sun = (struct sockaddr_un*) g_malloc0(len); + sun->sun_family = AF_UNIX; + strcpy(sun->sun_path, opts.unixsocket); + data.sockaddr = (struct sockaddr*) sun; + data.socklen = len - 1; + } + return 0; +} + +int bind_socket() { + int s, val; + + /* Check if socket is already open */ + /* TODO: should this be skippable? */ + if (-1 == (s = socket(data.sockaddr->sa_family, SOCK_STREAM, 0))) { + g_printerr("Couldn't open socket: %s\n", g_strerror(errno)); + return -1; + } + if (0 == connect(s, data.sockaddr, data.socklen)) { + close(s); + g_printerr("Socket already in use, can't spawn\n"); + return -1; + } + close(s); + + if (opts.unixsocket) unlink(opts.unixsocket); + if (-1 == (data.socket = socket(data.sockaddr->sa_family, SOCK_STREAM, 0))) { + g_printerr("Couldn't open socket: %s\n", g_strerror(errno)); + return -1; + } + + val = 1; + if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) { + close(s); + g_printerr("Couldn't set SO_REUSEADDR: %s\n", g_strerror(errno)); + return -1; + } + if (-1 == bind(s, data.sockaddr, data.socklen)) { + close(s); + g_printerr("Couldn't bind socket: %s\n", g_strerror(errno)); + return -1; + } + + if (-1 == listen(s, 1024)) { + close(s); + g_printerr("Couldn't listen on socket: %s\n", g_strerror(errno)); + return -1; + } + + if (opts.unixsocket) { + data.socketuid = (uid_t) -1; + data.socketgid = (gid_t) -1; + if (opts.socketuid + && 0 != (readpwdent(opts.socketuid, &data.socketuid, &data.socketgid, NULL))) { + return -1; + } + if (opts.socketgid + && 0 != (readgrpent(opts.socketgid, &data.socketgid))) { + return -1; + } + if (-1 == chown(opts.unixsocket, data.socketuid, data.socketgid)) { + close(s); + g_printerr("Couldn't chown socket: %s\n", g_strerror(errno)); + return -1; + } + + if (-1 == chmod(opts.unixsocket, opts.socketmode)) { + close(s); + g_printerr("Couldn't chmod socket: %s\n", g_strerror(errno)); + return -1; + } + } + + data.socket = s; + return 0; +} + +int open_pidfile() { + if (opts.pid_file) { + struct stat st; + if (0 == (data.pid_fd = open(opts.pid_file, O_WRONLY | O_CREAT | O_EXCL | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))) { + return 0; + } + + if (errno != EEXIST) { + g_printerr("Opening pid-file '%s' failed: %s\n", opts.pid_file, g_strerror(errno)); + return -1; + } + + if (0 != stat(opts.pid_file, &st)) { + g_printerr("Stating pid-file '%s' failed: %s\n", opts.pid_file, g_strerror(errno)); + return -1; + } + + if (!S_ISREG(st.st_mode)) { + g_printerr("pid-file exists and isn't regular file: '%s'\n", opts.pid_file); + return -1; + } + + if (-1 == (data.pid_fd = open(opts.pid_file, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))) { + g_printerr("Opening pid-file '%s' failed: %s\n", opts.pid_file, g_strerror(errno)); + return -1; + } + } + return 0; +} + +void prepare_env() { + GString *tmp; + if (-1 != opts.php_childs) { + tmp = g_string_sized_new(0); + g_string_printf(tmp, "PHP_FCGI_CHILDREN=%d", opts.php_childs); + putenv(tmp->str); + g_string_free(tmp, FALSE); + } +} + +int drop_priv() { + if (data.i_am_root) { + /* set user and group */ + + data.uid = (uid_t) -1; + data.gid = (gid_t) -1; + data.username = NULL; + + if (opts.uid + && 0 != (readpwdent(opts.uid, &data.uid, &data.gid, &data.username))) { + return -1; + } + if (opts.gid + && 0 != (readgrpent(opts.gid, &data.gid))) { + return -1; + } + + /* do the change before we do the chroot() */ + if (data.gid != (gid_t) -1) { + if (0 != setgid(data.gid)) { + g_printerr("setgid failed: %s\n", strerror(errno)); + return -1; + } + if (0 != setgroups(0, NULL)) { + g_printerr("setgroups failed: %s\n", strerror(errno)); + return -1; + } + + if (data.username + && 0 != initgroups(data.username, data.gid)) { + g_printerr("initgroups failed: %s\n", strerror(errno)); + return -1; + } + } + + if (opts.chroot) { + if (-1 == chroot(opts.chroot)) { + g_printerr("chroot failed: %s\n", strerror(errno)); + return -1; + } + if (-1 == chdir("/")) { + g_printerr("chdir failed: %s\n", strerror(errno)); + return -1; + } + } + + /* drop root privs */ + if (data.uid != (uid_t) -1 + && 0 != setuid(data.uid)) { + g_printerr("setuid failed: %s\n", strerror(errno)); + return -1; + } + } + return 0; +} + +void move2fd(int srcfd, int dstfd) { + if (srcfd != dstfd) { + close(dstfd); + dup2(srcfd, dstfd); + close(srcfd); + } +} + +void move2devnull(int fd) { + move2fd(open("/dev/null", O_RDWR), fd); +} + +pid_t daemonize() { + pid_t child; + int status; + struct timeval tv = { 0, 100 * 1000 }; + + switch (child = fork()) { + case 0: + /* loose control terminal */ + setsid(); + + return 0; + case -1: + g_printerr("Fork failed: %s\n", g_strerror(errno)); + return (pid_t) -1; + default: + /* wait */ + select(0, NULL, NULL, NULL, &tv); + +waitforchild: + switch (waitpid(child, &status, WNOHANG)) { + case 0: + g_printerr("Child spawned successfully: PID: %d\n", child); + return child; + case -1: + if (EINTR == errno) goto waitforchild; + g_printerr("Unknown error: %s\n", g_strerror(errno)); + return (pid_t) -1; + default: + if (WIFEXITED(status)) { + g_printerr("Child exited with: %d, %s\n", WEXITSTATUS(status), strerror(WEXITSTATUS(status))); + } else if (WIFSIGNALED(status)) { + g_printerr("Child signaled: %d\n", WTERMSIG(status)); + } else { + g_printerr("Child died somehow: %d\n", status); + } + return (pid_t) -1; + } + } +} + +void start() __attribute__((noreturn)); +void start() { + int save_err_fileno = -1; + move2fd(data.socket, FCGI_LISTENSOCK_FILENO); + + if (opts.close_fds) { + move2devnull(STDOUT_FILENO); + + save_err_fileno = dup(STDERR_FILENO); + fcntl(save_err_fileno, F_SETFD, FD_CLOEXEC); + + close(STDERR_FILENO); + dup2(STDOUT_FILENO, STDERR_FILENO); + } + + if (opts.args) { + execv(opts.args[0], opts.args); + } else { + GString *tmp = g_string_sized_new(0); + g_string_printf(tmp, "exec %s", opts.fcgiapp); + execl("/bin/sh", "sh", "-c", tmp->str, (char *)NULL); + } + + if (opts.close_fds) { + dup2(save_err_fileno, STDERR_FILENO); + } + g_printerr("Exec failed: %s\n", g_strerror(errno)); + exit(errno); +} + +gboolean option_parse_address(const gchar *option_name, const gchar *value, gpointer d, GError **error) { + UNUSED(option_name); + UNUSED(d); + UNUSED(error); + + opts.addr = inet_addr(value); + return TRUE; +} + +static const GOptionEntry entries[] = { + { "application", 'f', 0, G_OPTION_ARG_FILENAME, &opts.fcgiapp, "Filename of the fcgi-application", "fcgiapp" }, + { "address", 'a', 0, G_OPTION_ARG_CALLBACK, (gpointer) (intptr_t) &option_parse_address, "Bind to ip address", "addr" }, + { "port", 'p', 0, G_OPTION_ARG_INT, &opts.port, "Bind to tcp-port", "port" }, + { "socket", 's', 0, G_OPTION_ARG_FILENAME, &opts.unixsocket, "Bind to unix-domain socket", "path" }, + { "socket-uid", 'U', 0, G_OPTION_ARG_STRING, &opts.socketuid, "change unix-domain socket owner to user-id", "user" }, + { "socket-gid", 'G', 0, G_OPTION_ARG_STRING, &opts.socketgid, "change unix-domain socket group to group-id", "group" }, + { "socket-mode", 'M', 0, G_OPTION_ARG_INT, &opts.socketmode, "change unix-domain socket mode", "mode" }, + { "phpchilds", 'C', 0, G_OPTION_ARG_INT, &opts.php_childs, "(PHP only) Number of childs to spawn (default 5)", "childs" }, + { "childs", 'F', 0, G_OPTION_ARG_INT, &opts.fork_childs, "Number of childs to fork (default 1)", "childs" }, + { "pid", 'P', 0, G_OPTION_ARG_FILENAME, &opts.pid_file, "Name of PID-file for spawned process", "path" }, + { "no-daemon", 'n', 0, G_OPTION_ARG_NONE, &opts.no_fork, "Don't fork (for daemontools)", NULL }, + { "keep-fds", 0, 0, G_OPTION_ARG_NONE, &opts.keep_fds, "Keep stdout/stderr open (default for --no-daemon)", NULL }, + { "close-fds", 0, 0, G_OPTION_ARG_NONE, &opts.close_fds, "Close stdout/stderr (default it not --no-daemon)", NULL }, + { "version", 'v', 0, G_OPTION_ARG_NONE, &opts.show_version, "Show version", NULL }, + { "chroot", 'c', 0, G_OPTION_ARG_FILENAME, &opts.chroot, "(root only) chroot to directory", "dir" }, + { "uid", 'u', 0, G_OPTION_ARG_STRING, &opts.uid, "(root only) change to user-id", "user" }, + { "gid", 'g', 0, G_OPTION_ARG_STRING, &opts.gid, "(root only) change to group-id", "group" }, + { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_STRING_ARRAY, &opts.args, " [fcgi app arguments]", NULL }, + { NULL, 0, 0, 0, NULL, NULL, NULL } +}; + +int main(int argc, char **argv) { + GOptionContext *context; + GError *error = NULL; + int res; + GString *tmp = g_string_sized_new(0); + + /* init */ + data.socket = -1; + data.pid_fd = -1; + + data.i_am_root = (getuid() == 0); + /* UID handling */ + if (!data.i_am_root && (geteuid() == 0 || getegid() == 0)) { + /* we are setuid-root */ + g_printerr("Are you nuts ? Don't apply a SUID bit to this binary\n"); + return -1; + } + + context = g_option_context_new("-- [fcgi app arguments]"); + g_option_context_add_main_entries(context, entries, NULL); + g_option_context_set_summary(context, PACKAGE_NAME "-" PACKAGE_VERSION " - spawns fastcgi processes"); + + opts.close_fds = opts.no_fork ? opts.close_fds : !opts.keep_fds; + + if (!g_option_context_parse (context, &argc, &argv, &error)) { + g_printerr("Option parsing failed: %s\n", error->message); + return -1; + } + + if (opts.show_version) { + g_printerr(PACKAGE_NAME "-" PACKAGE_VERSION " - spawns fastcgi processes\n"); + g_printerr("Build: " PACKAGE_BUILD_DATE "\n"); + return 0; + } + + /* Check options */ + if ((opts.fcgiapp && opts.args) || (!opts.fcgiapp && !opts.args)) { + g_printerr("Specify either a fcgi application with -f or the application with arguments after '--'\n"); + return -1; + } + + if (0 != (res = create_sockaddr())) return res; + if (0 != (res = bind_socket())) return res; + if (0 != (res = open_pidfile())) return res; + if (0 != (res = drop_priv())) return res; + + prepare_env(); + + if (opts.no_fork) { + start(); + } else { + gint i; + for (i = 0; i < opts.fork_childs; i++) { + pid_t child = daemonize(); + if (child == (pid_t) -1) return -1; + if (child == 0) start(); + /* write pid file */ + if (data.pid_fd != -1) { + g_string_printf(tmp, "%d\n", child); + if (i == opts.fork_childs-1) { + /* avoid eol for the last one */ + write(data.pid_fd, tmp->str, tmp->len-1); + } else { + write(data.pid_fd, tmp->str, tmp->len); + } + } + } + } + + g_string_free(tmp, TRUE); + return 0; +} diff --git a/waf b/waf new file mode 100755 index 0000000..e62eb21 Binary files /dev/null and b/waf differ diff --git a/wscript b/wscript new file mode 100644 index 0000000..5d143f0 --- /dev/null +++ b/wscript @@ -0,0 +1,65 @@ +#! /usr/bin/env python +# encoding: utf-8 + +import Options, types, sys, Runner +from time import gmtime, strftime, timezone + +# the following two variables are used by the target "waf dist" +VERSION='1.0' +APPNAME='spawn-fcgi' + +# these variables are mandatory ('/' are converted automatically) +srcdir = '.' +blddir = 'build' + +def set_options(opt): + opt.tool_options('compiler_cc') + +def tolist(x): + if type(x) is types.ListType: + return x + return [x] + +def PKGCONFIG(conf, name, uselib = None, define = '', version = '', mandatory = 0): + if not uselib: uselib = name + hconf = conf.create_pkgconfig_configurator() + hconf.name = name + hconf.version = version + hconf.uselib_store = uselib + hconf.define = define + hconf.mandatory = mandatory + res = hconf.run() + return res + +def configure(conf): + opts = Options.options + + conf.check_tool('compiler_cc') + + conf.define("PACKAGE_NAME", APPNAME) + conf.define("PACKAGE_VERSION", VERSION) + conf.define("PACKAGE_BUILD_DATE", strftime("%b %d %Y %H:%M:%S UTC", gmtime())); + + common_ccflags = [ + '-std=gnu99', '-Wall', '-g', '-Wshadow', '-W', '-pedantic', + '-fPIC', + '-DHAVE_CONFIG_H', '-D_GNU_SOURCE', + ] + conf.env['CCFLAGS_spawnfcgi'] += common_ccflags + + PKGCONFIG(conf, "glib-2.0", uselib = 'glib', mandatory = 1) + incdir = conf.env['CPPPATH_glib'][0] + conf.env['CPPPATH_glib'] += [ incdir+'/glib-2.0/', incdir + '/glib-2.0/include/' ] +# CHECK_INCLUDE_FILES(conf, "glib.h", "HAVE_GLIB_H", uselib = 'glib', use = ['glib'], mandatory = 1) + + conf.write_config_header('config.h') + +def build(bld): + spawnfcgi = bld.new_task_gen('cc', 'program') + spawnfcgi.name = 'spawn-fcgi' + spawnfcgi.source = ''' + spawn-fcgi.c + ''' + spawnfcgi.target = 'spawn-fcgi' + spawnfcgi.uselib += 'glib spawnfcgi' + spawnfcgi.includes = '.'