diff --git a/configure.ac b/configure.ac index 6ddbcc0..51e1118 100644 --- a/configure.ac +++ b/configure.ac @@ -134,7 +134,7 @@ AC_FUNC_LSTAT_FOLLOWS_SLASHED_SYMLINK AC_FUNC_MALLOC AC_FUNC_REALLOC -AC_CHECK_FUNCS([inet_ntoa strdup]) +AC_CHECK_FUNCS([inet_ntoa strdup fdopen]) AC_CHECK_FUNCS([strlcpy strlcat setgroups]) dnl Enable extra warnings diff --git a/src/Makefile.am b/src/Makefile.am index af2f621..c306f46 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -46,6 +46,7 @@ tinyproxy_SOURCES = \ utils.c utils.h \ vector.c vector.h \ upstream.c upstream.h \ + upstream-command.c upstream-command.h \ basicauth.c basicauth.h \ base64.c base64.h \ connect-ports.c connect-ports.h diff --git a/src/conf.c b/src/conf.c index 5ebf179..01e91ba 100644 --- a/src/conf.c +++ b/src/conf.c @@ -165,6 +165,7 @@ static HANDLE_FUNC (handle_xtinyproxy); #ifdef UPSTREAM_SUPPORT static HANDLE_FUNC (handle_upstream); static HANDLE_FUNC (handle_upstream_no); +static HANDLE_FUNC (handle_upstream_command); #endif static void config_free_regex (void); @@ -264,6 +265,12 @@ struct { ":" INT "(" WS STR ")?" END, handle_upstream, NULL }, + { + BEGIN "(upstream)" WS "(command)" WS + STR /* path to script */ + "(" WS STR ")?" /* host filter */ + END, handle_upstream_command, NULL + }, #endif /* loglevel */ STDCONF ("loglevel", "(critical|error|warning|notice|connect|info)", @@ -1135,7 +1142,7 @@ static HANDLE_FUNC (handle_upstream) if (match[mi].rm_so != -1) domain = get_string_arg (line, &match[mi]); - upstream_add (ip, port, domain, user, pass, pt, &conf->upstream_list); + upstream_add (ip, port, domain, user, pass, NULL, pt, &conf->upstream_list); safefree (user); safefree (pass); @@ -1153,9 +1160,31 @@ static HANDLE_FUNC (handle_upstream_no) if (!domain) return -1; - upstream_add (NULL, 0, domain, 0, 0, PT_NONE, &conf->upstream_list); + upstream_add (NULL, 0, domain, 0, 0, NULL, PT_NONE, &conf->upstream_list); safefree (domain); return 0; } -#endif + +static HANDLE_FUNC (handle_upstream_command) +{ + int mi = 3; + char *command = 0, *domain = 0; + + command = get_string_arg (line, &match[mi]); + mi++; + + if (match[mi].rm_so != -1) + domain = get_string_arg (line, &match[mi]); + mi++; + + upstream_add (NULL, 0, domain, 0, 0, command, PT_NONE, &conf->upstream_list); + + safefree (command); + safefree (domain); + + return 0; +} + + +#endif /* UPSTREAM_SUPPORT */ diff --git a/src/upstream-command.c b/src/upstream-command.c new file mode 100644 index 0000000..6a9fbfd --- /dev/null +++ b/src/upstream-command.c @@ -0,0 +1,140 @@ +/* TODO(tkluck): this is needed for fdopen. Does it have portability + * downsides? */ +#define _POSIX_C_SOURCE 1 +#include + +#include "upstream-command.h" +#include "upstream.h" + +#define MAX_RESULT_MESSAGE_SIZE 1024 + +/* + * ensure that st->in is writable (according to select() ). Use select() + * to wait at most UPSTREAM_COMMAND_WRITABLE_TIMOUT_MS milliseconds, + * and respawn otherwise. + * Returns a negative error code if we do not succeed in respawning. + */ +static int ensure_upstream_command_writable(const char *cmd, struct upstream_command_state *st) +{ + int err; + int parent2child[2]; + int child2parent[2]; + + assert(st != NULL); + + if (0 == st->pid) { + err = pipe(parent2child); + if (0 != err) + return -1; + err = pipe(child2parent); + if (0 != err) + return -1; + + st->pid = fork(); + if (0 > std->pid) + return -1; + if (0 == st->pid) { + /* I'm the child; connect stdin/stdout to the pipes */ + if (0 > close(parent2child[1])) + exit(1); + if (0 > close(child2parent[0])) + exit(1); + if (0 > dup2(parent2child[0], 0)) + exit(1); + if (0 > dup2(child2parent[1], 1)) + exit(1); + + /* TODO(tkluck): do we need to worry about installed + * signal handlers (e.g. for SIGCHLD)? + */ + + err = execl("/bin/sh", "sh", "-c", cmd, (char *) 0); + + /* unreachable unless something is wrong with the command */ + exit(1); + } else { + /* I'm the parent */ + sleep(1); + close(parent2child[0]); + close(child2parent[1]); + st->in = fdopen(parent2child[1], "w"); + st->out = fdopen(child2parent[0], "r"); + } + } + + /* TODO(tkluck): use select(2) to ensure that writing to + * the process stdin doesn't block (for sufficiently small + * message). Otherwise, fork a new copy and kill the old one. */ + + return 0; +} + +/* + * Communicate with the configured command to obtain a proxy + * specification, and _replace_ the fields in *up with the appropriate + * spec. + * + * In case of success, returns one of the following positive constants: + * UPSTREAM_DIRECT - no proxy + * UPSTREAM_FALLTHROUGH - use subsequent rules + * UPSTREAM_PROXY - use upstream as configured in *up + * + * Returns a negative error code if we do not succeed in communicating + * with the subprocess. + */ +int upstream_get_from_command(const char *host, struct upstream *up) +{ + int err; + char msg[MAX_RESULT_MESSAGE_SIZE]; + char *msgres; + + assert(up != NULL); + assert(up->command != NULL); + if (NULL != strchr(host, '\n')) + return -1; + + err = ensure_upstream_command_writable(up->command, &up->cmdstate); + if (err < 0) + return -2; + + /* TODO(tkluck): replace by version with timeout. */ + err = fprintf(up->cmdstate.in, "HOST %s\n", host); + if (err < 0) + return -3; + err = fflush(up->cmdstate.in); + if (err != 0) + return -4; + + /* TODO(tkluck): replace by version with timeout */ + msgres = fgets(msg, MAX_RESULT_MESSAGE_SIZE, up->cmdstate.out); + if (NULL == msgres) + return -5; + + if (strncmp(msg, "DIRECT", 6)) { + return UPSTREAM_DIRECT; + } else if + (strncmp(msg, "FALLTHROUGH", 11)) { + return UPSTREAM_FALLTHROUGH; + } else if + (strncmp(msg, "HTTP ", 5)) { + /* TODO(tkluck): parse and fill the struct here. + * use same/similar format as the configuration file */ + return UPSTREAM_PROXY; + } else if + (strncmp(msg, "SOCKS4 ", 7)) { + /* TODO(tkluck): parse and fill the struct here. + * use same/similar format as the configuration file */ + return UPSTREAM_PROXY; + } else if + (strncmp(msg, "SOCKS5 ", 7)) { + /* TODO(tkluck): parse and fill the struct here. + * use same/similar format as the configuration file */ + return UPSTREAM_PROXY; + } else { + goto parsefail; + } + +parsefail: + return -6; + +} diff --git a/src/upstream-command.h b/src/upstream-command.h new file mode 100644 index 0000000..2104166 --- /dev/null +++ b/src/upstream-command.h @@ -0,0 +1,19 @@ +#ifndef _TINYPROXY_UPSTREAM_COMMAND_H_ +#define _TINYPROXY_UPSTREAM_COMMAND_H_ + +#include + +typedef enum upstream_command_result { + UPSTREAM_DIRECT = 1, + UPSTREAM_FALLTHROUGH, + UPSTREAM_PROXY, +} upstream_command_result; + +struct upstream_command_state { + int pid; + FILE *in; + FILE *out; +}; + + +#endif /* _TINYPROXY_UPSTREAM_COMMAND_H_ */ diff --git a/src/upstream.c b/src/upstream.c index 327b727..a1d664a 100644 --- a/src/upstream.c +++ b/src/upstream.c @@ -47,7 +47,7 @@ proxy_type_name(proxy_type type) * Construct an upstream struct from input data. */ static struct upstream *upstream_build (const char *host, int port, const char *domain, - const char *user, const char *pass, + const char *user, const char *pass, const char *command, proxy_type type) { char *ptr; @@ -63,6 +63,9 @@ static struct upstream *upstream_build (const char *host, int port, const char * up->type = type; up->host = up->domain = up->ua.user = up->pass = NULL; up->ip = up->mask = 0; + up->command = NULL; + up->cmdstate.in = up->cmdstate.out = NULL; + up->cmdstate.pid = 0; if (user) { if (type == PT_HTTP) { char b[BASE64ENC_BYTES((256+2)-1) + 1]; @@ -80,6 +83,21 @@ static struct upstream *upstream_build (const char *host, int port, const char * } } + if (command) { + up->command = safestrdup(command); + if (domain && domain[0] != '\0') + up->domain = safestrdup (domain); + + if (domain && domain[0] != '\0') { + log_message (LOG_INFO, "Added upstream command %s for %s", + up->command, up->domain); + } else { + log_message (LOG_INFO, "Added upstream command %s", + up->command); + } + return up; + } + if (domain == NULL) { if (!host || host[0] == '\0' || port < 1) { log_message (LOG_WARNING, @@ -154,12 +172,12 @@ fail: * Add an entry to the upstream list */ void upstream_add (const char *host, int port, const char *domain, - const char *user, const char *pass, + const char *user, const char *pass, const char *command, proxy_type type, struct upstream **upstream_list) { struct upstream *up; - up = upstream_build (host, port, domain, user, pass, type); + up = upstream_build (host, port, domain, user, pass, command, type); if (up == NULL) { return; } @@ -203,32 +221,58 @@ upstream_cleanup: struct upstream *upstream_get (char *host, struct upstream *up) { in_addr_t my_ip = INADDR_NONE; + int maskfail; + int res; while (up) { + maskfail = (up->domain || up->ip) ? 1 : 0; if (up->domain) { if (strcasecmp (host, up->domain) == 0) - break; /* exact match */ + maskfail = 0; /* exact match */ if (up->domain[0] == '.') { char *dot = strchr (host, '.'); if (!dot && !up->domain[1]) - break; /* local host matches "." */ + maskfail = 0; /* local host matches "." */ while (dot && strcasecmp (dot, up->domain)) dot = strchr (dot + 1, '.'); if (dot) - break; /* subdomain match */ + maskfail = 0; /* subdomain match */ } } else if (up->ip) { if (my_ip == INADDR_NONE) my_ip = ntohl (inet_addr (host)); if ((my_ip & up->mask) == up->ip) + maskfail = 0; + } + + if (!maskfail) { + if (!up->command) + break; + + log_message (LOG_INFO, + "Running command %s for host %s", + up->command, host); + res = upstream_get_from_command(host, up); + if (UPSTREAM_DIRECT == res || UPSTREAM_PROXY == res) { break; - } else { - break; /* No domain or IP, default upstream */ + } else if (UPSTREAM_FALLTHROUGH == res) { + /* continue */ + } else { + /* command failure + * TODO(tkluck): we should propagate this failure + * and reject the request rather than forwarding + * the request to a 'random' location. E.g. if + * the user configures a proxy for privacy reasons, + * command failure should not compromise their + * privacy. + */ + return NULL; + } } up = up->next; @@ -251,6 +295,7 @@ void free_upstream_list (struct upstream *up) while (up) { struct upstream *tmp = up; up = up->next; + safefree (tmp->command); safefree (tmp->domain); safefree (tmp->host); safefree (tmp); diff --git a/src/upstream.h b/src/upstream.h index c112784..13756ff 100644 --- a/src/upstream.h +++ b/src/upstream.h @@ -26,6 +26,7 @@ #define _TINYPROXY_UPSTREAM_H_ #include "common.h" +#include "upstream-command.h" /* * Even if upstream support is not compiled into tinyproxy, this @@ -50,15 +51,19 @@ struct upstream { int port; in_addr_t ip, mask; proxy_type type; + char *command; + struct upstream_command_state cmdstate; }; #ifdef UPSTREAM_SUPPORT const char *proxy_type_name(proxy_type type); extern void upstream_add (const char *host, int port, const char *domain, const char *user, const char *pass, + const char *command, proxy_type type, struct upstream **upstream_list); extern struct upstream *upstream_get (char *host, struct upstream *up); extern void free_upstream_list (struct upstream *up); +int upstream_get_from_command(const char *host, struct upstream *up); #endif /* UPSTREAM_SUPPORT */ #endif /* _TINYPROXY_UPSTREAM_H_ */