From: Felix Fietkau Date: Tue, 1 Jan 2013 22:28:19 +0000 (+0100) Subject: add preliminary cgi support, needs fixing for close handling X-Git-Url: http://git.lede-project.org./?a=commitdiff_plain;h=58c5fd1f9a72db878e29958a4c4e1b65db5b2e07;p=project%2Fuhttpd.git add preliminary cgi support, needs fixing for close handling --- diff --git a/CMakeLists.txt b/CMakeLists.txt index c542df6..48addac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,12 @@ cmake_minimum_required(VERSION 2.6) PROJECT(uhttpd C) -ADD_DEFINITIONS(-Os -Wall -Werror -Wmissing-declarations --std=gnu99 -g3) +ADD_DEFINITIONS(-O2 -Wall -Werror -Wmissing-declarations --std=gnu99 -g3) IF(APPLE) INCLUDE_DIRECTORIES(/opt/local/include) LINK_DIRECTORIES(/opt/local/lib) ENDIF() -ADD_EXECUTABLE(uhttpd main.c listen.c client.c utils.c file.c auth.c) +ADD_EXECUTABLE(uhttpd main.c listen.c client.c utils.c file.c auth.c cgi.c relay.c proc.c) TARGET_LINK_LIBRARIES(uhttpd ubox ubus) diff --git a/cgi.c b/cgi.c new file mode 100644 index 0000000..cfd71fb --- /dev/null +++ b/cgi.c @@ -0,0 +1,115 @@ +/* + * uhttpd - Tiny single-threaded httpd + * + * Copyright (C) 2010-2012 Jo-Philipp Wich + * Copyright (C) 2012 Felix Fietkau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "uhttpd.h" + +static LIST_HEAD(interpreters); + +void uh_interpreter_add(const char *ext, const char *path) +{ + struct interpreter *in; + char *new_ext, *new_path; + + in = calloc_a(sizeof(*in), + &new_ext, strlen(ext) + 1, + &new_path, strlen(path) + 1); + + in->ext = strcpy(new_ext, ext); + in->path = strcpy(new_path, path); + list_add_tail(&in->list, &interpreters); +} + +static void cgi_main(struct client *cl, struct path_info *pi, int fd) +{ + struct interpreter *ip = pi->ip; + struct env_var *var; + + dup2(fd, 0); + dup2(fd, 1); + close(fd); + clearenv(); + setenv("PATH", conf.cgi_path, 1); + + for (var = uh_get_process_vars(cl, pi); var->name; var++) { + if (!var->value) + continue; + + setenv(var->name, var->value, 1); + } + + chdir(pi->root); + + if (ip) + execl(ip->path, ip->path, pi->phys, NULL); + else + execl(pi->phys, pi->phys, NULL); + + printf("Status: 500 Internal Server Error\r\n\r\n" + "Unable to launch the requested CGI program:\n" + " %s: %s\n", ip ? ip->path : pi->phys, strerror(errno)); +} + +static void cgi_handle_request(struct client *cl, const char *url, struct path_info *pi) +{ + unsigned int mode = S_IFREG | S_IXOTH; + + if (!pi->ip && !((pi->stat.st_mode & mode) == mode)) { + uh_client_error(cl, 403, "Forbidden", + "You don't have permission to access %s on this server.", + url); + return; + } + + if (!uh_create_process(cl, pi, cgi_main)) { + uh_client_error(cl, 500, "Internal Server Error", + "Failed to create CGI process: %s", strerror(errno)); + return; + } + + return; +} + +static bool check_cgi_path(struct path_info *pi, const char *url) +{ + struct interpreter *ip; + const char *path = pi->phys; + int path_len = strlen(path); + + list_for_each_entry(ip, &interpreters, list) { + int len = strlen(ip->ext); + + if (len >= path_len) + continue; + + if (strcmp(path + path_len - len, ip->ext) != 0) + continue; + + pi->ip = ip; + return true; + } + + pi->ip = NULL; + return uh_path_match(conf.cgi_prefix, url); +} + +struct dispatch_handler cgi_dispatch = { + .check_path = check_cgi_path, + .handle_request = cgi_handle_request, +}; diff --git a/client.c b/client.c index 6e75c5b..483a1a4 100644 --- a/client.c +++ b/client.c @@ -174,7 +174,7 @@ static bool client_init_cb(struct client *cl, char *buf, int len) static void client_header_complete(struct client *cl) { - uh_handle_file_request(cl); + uh_handle_request(cl); } static int client_parse_header(struct client *cl, char *data) diff --git a/file.c b/file.c index 0fb17d6..4a10eaa 100644 --- a/file.c +++ b/file.c @@ -32,6 +32,7 @@ static char _tag[128]; static LIST_HEAD(index_files); +static LIST_HEAD(dispatch_handlers); struct index_file { struct list_head list; @@ -577,7 +578,7 @@ static void uh_file_data(struct client *cl, struct path_info *pi, int fd) file_write_cb(cl); } -static void uh_file_request(struct client *cl, struct path_info *pi, const char *url) +static void uh_file_request(struct client *cl, const char *url, struct path_info *pi) { static const struct blobmsg_policy hdr_policy[__HDR_MAX] = { [HDR_IF_MODIFIED_SINCE] = { "if-modified-since", BLOBMSG_TYPE_STRING }, @@ -611,33 +612,78 @@ static void uh_file_request(struct client *cl, struct path_info *pi, const char goto error; } + cl->dispatch.file.hdr = NULL; return; error: uh_client_error(cl, 403, "Forbidden", "You don't have permission to access %s on this server.", url); + cl->dispatch.file.hdr = NULL; +} + +void uh_dispatch_add(struct dispatch_handler *d) +{ + list_add_tail(&d->list, &dispatch_handlers); +} + +static struct dispatch_handler * +dispatch_find(const char *url, struct path_info *pi) +{ + struct dispatch_handler *d; + + list_for_each_entry(d, &dispatch_handlers, list) { + if (pi) { + if (d->check_url) + continue; + + if (d->check_path(pi, url)) + return d; + } else { + if (d->check_path) + continue; + + if (d->check_url(url)) + return d; + } + } + + return NULL; } static bool __handle_file_request(struct client *cl, const char *url) { + struct dispatch_handler *d; struct path_info *pi; pi = uh_path_lookup(cl, url); if (!pi) return false; - if (!pi->redirected) { - uh_file_request(cl, pi, url); - cl->dispatch.file.hdr = NULL; - } + if (pi->redirected) + return true; + + d = dispatch_find(url, pi); + if (d) + d->handle_request(cl, url, pi); + else + uh_file_request(cl, url, pi); return true; } -void uh_handle_file_request(struct client *cl) +void uh_handle_request(struct client *cl) { - if (__handle_file_request(cl, cl->request.url) || + struct dispatch_handler *d; + const char *url = cl->request.url; + + d = dispatch_find(url, NULL); + if (d) { + d->handle_request(cl, url, NULL); + return; + } + + if (__handle_file_request(cl, url) || __handle_file_request(cl, conf.error_handler)) return; diff --git a/main.c b/main.c index bc6893a..f61574f 100644 --- a/main.c +++ b/main.c @@ -142,13 +142,9 @@ static int usage(const char *name) " -u string URL prefix for HTTP/JSON handler\n" " -U file Override ubus socket path\n" #endif -#ifdef HAVE_CGI " -x string URL prefix for CGI handler, default is '/cgi-bin'\n" " -i .ext=path Use interpreter at path for files with the given extension\n" -#endif -#if defined(HAVE_CGI) || defined(HAVE_LUA) || defined(HAVE_UBUS) " -t seconds CGI, Lua and UBUS script timeout in seconds, default is 60\n" -#endif " -T seconds Network timeout in seconds, default is 30\n" " -d string URL decode given string\n" " -r string Specify basic auth realm\n" @@ -174,6 +170,21 @@ static void init_defaults(void) uh_index_add("default.htm"); } +static void fixup_prefix(char *str) +{ + int len; + + if (!str || !str[0]) + return; + + len = strlen(str) - 1; + + while (len > 0 && str[len] == '/') + len--; + + str[len + 1] = 0; +} + int main(int argc, char **argv) { bool nofork = false; @@ -184,6 +195,7 @@ int main(int argc, char **argv) BUILD_BUG_ON(sizeof(uh_buf) < PATH_MAX); + uh_dispatch_add(&cgi_dispatch); init_defaults(); signal(SIGPIPE, SIG_IGN); @@ -241,6 +253,23 @@ int main(int argc, char **argv) conf.max_requests = atoi(optarg); break; + case 'x': + fixup_prefix(optarg); + conf.cgi_prefix = optarg; + break; + + case 'i': + port = strchr(optarg, '='); + if (optarg[0] != '.' || !port) { + fprintf(stderr, "Error: Invalid interpreter: %s\n", + optarg); + exit(1); + } + + *port++ = 0; + uh_interpreter_add(optarg, port); + break; + case 't': conf.script_timeout = atoi(optarg); break; diff --git a/proc.c b/proc.c new file mode 100644 index 0000000..9d662ad --- /dev/null +++ b/proc.c @@ -0,0 +1,241 @@ +/* + * uhttpd - Tiny single-threaded httpd + * + * Copyright (C) 2010-2012 Jo-Philipp Wich + * Copyright (C) 2012 Felix Fietkau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "uhttpd.h" + +#define __headers \ + __header(accept) \ + __header(accept_charset) \ + __header(accept_encoding) \ + __header(accept_language) \ + __header(authorization) \ + __header(connection) \ + __header(cookie) \ + __header(host) \ + __header(referer) \ + __header(user_agent) \ + __header(content_type) \ + __header(content_length) + +#undef __header +#define __header __enum_header +enum client_hdr { + __headers + __HDR_MAX, +}; + +#undef __header +#define __header __blobmsg_header +static const struct blobmsg_policy hdr_policy[__HDR_MAX] = { + __headers +}; + +static const struct { + const char *name; + int idx; +} proc_header_env[] = { + { "HTTP_ACCEPT", HDR_accept }, + { "HTTP_ACCEPT_CHARSET", HDR_accept_charset }, + { "HTTP_ACCEPT_ENCODING", HDR_accept_encoding }, + { "HTTP_ACCEPT_LANGUAGE", HDR_accept_language }, + { "HTTP_AUTHORIZATION", HDR_authorization }, + { "HTTP_CONNECTION", HDR_connection }, + { "HTTP_COOKIE", HDR_cookie }, + { "HTTP_HOST", HDR_host }, + { "HTTP_REFERER", HDR_referer }, + { "HTTP_USER_AGENT", HDR_user_agent }, + { "CONTENT_TYPE", HDR_content_type }, + { "CONTENT_LENGTH", HDR_content_length }, +}; + +enum extra_vars { + /* no update needed */ + _VAR_GW, + _VAR_SOFTWARE, + + /* updated by uh_get_process_vars */ + VAR_SCRIPT_NAME, + VAR_SCRIPT_FILE, + VAR_DOCROOT, + VAR_QUERY, + VAR_REQUEST, + VAR_PROTO, + VAR_METHOD, + VAR_PATH_INFO, + VAR_USER, + VAR_REDIRECT, + + __VAR_MAX, +}; + +static struct env_var extra_vars[] = { + [_VAR_GW] = { "GATEWAY_INTERFACE", "CGI/1.1" }, + [_VAR_SOFTWARE] = { "SERVER_SOFTWARE", "uhttpd" }, + [VAR_SCRIPT_NAME] = { "SCRIPT_NAME" }, + [VAR_SCRIPT_FILE] = { "SCRIPT_FILENAME" }, + [VAR_DOCROOT] = { "DOCUMENT_ROOT" }, + [VAR_QUERY] = { "QUERY_STRING" }, + [VAR_REQUEST] = { "REQUEST_URI" }, + [VAR_PROTO] = { "SERVER_PROTOCOL" }, + [VAR_METHOD] = { "REQUEST_METHOD" }, + [VAR_PATH_INFO] = { "PATH_INFO" }, + [VAR_USER] = { "REMOTE_USER" }, + [VAR_REDIRECT] = { "REDIRECT_STATUS" }, +}; + +struct env_var *uh_get_process_vars(struct client *cl, struct path_info *pi) +{ + struct http_request *req = &cl->request; + struct blob_attr *data = cl->hdr.head; + struct env_var *vars = (void *) uh_buf; + struct blob_attr *tb[__HDR_MAX]; + static char buf[4]; + int len; + int i; + + len = ARRAY_SIZE(proc_header_env); + len += ARRAY_SIZE(extra_vars); + len *= sizeof(struct env_var); + + BUILD_BUG_ON(sizeof(uh_buf) < len); + + extra_vars[VAR_SCRIPT_NAME].value = pi->name; + extra_vars[VAR_SCRIPT_FILE].value = pi->phys; + extra_vars[VAR_DOCROOT].value = pi->root; + extra_vars[VAR_QUERY].value = pi->query ? pi->query : ""; + extra_vars[VAR_REQUEST].value = req->url; + extra_vars[VAR_PROTO].value = http_versions[req->version]; + extra_vars[VAR_METHOD].value = http_methods[req->method]; + extra_vars[VAR_PATH_INFO].value = pi->info; + extra_vars[VAR_USER].value = req->realm ? req->realm->user : NULL; + + snprintf(buf, sizeof(buf), "%d", req->redirect_status); + extra_vars[VAR_REDIRECT].value = buf; + + blobmsg_parse(hdr_policy, __HDR_MAX, tb, blob_data(data), blob_len(data)); + for (i = 0; i < ARRAY_SIZE(proc_header_env); i++) { + struct blob_attr *cur; + + cur = tb[proc_header_env[i].idx]; + vars[i].name = proc_header_env[i].name; + vars[i].value = cur ? blobmsg_data(cur) : ""; + } + + memcpy(&vars[i], extra_vars, sizeof(extra_vars)); + i += ARRAY_SIZE(extra_vars); + vars[i].name = NULL; + vars[i].value = NULL; + + return vars; +} + +static void proc_close_fds(struct client *cl) +{ + close(cl->dispatch.proc.r.sfd.fd.fd); +} + +static void proc_handle_close(struct relay *r, int ret) +{ + if (r->header_cb) { + uh_client_error(r->cl, 502, "Bad Gateway", + "The process did not produce any response"); + return; + } + + uh_request_done(r->cl); +} + +static void proc_handle_header(struct relay *r, const char *name, const char *val) +{ + static char status_buf[64]; + struct client *cl = r->cl; + char *sep; + char buf[4]; + + if (strcmp(name, "Status")) { + sep = strchr(val, ' '); + if (sep != val + 3) + return; + + memcpy(buf, val, 3); + buf[3] = 0; + snprintf(status_buf, sizeof(status_buf), "%s", sep + 1); + cl->dispatch.proc.status_msg = status_buf; + return; + } + + blobmsg_add_string(&cl->dispatch.proc.hdr, name, val); +} + +static void proc_handle_header_end(struct relay *r) +{ + struct client *cl = r->cl; + struct blob_attr *cur; + int rem; + + uh_http_header(cl, cl->dispatch.proc.status_code, cl->dispatch.proc.status_msg); + blob_for_each_attr(cur, cl->dispatch.proc.hdr.head, rem) + ustream_printf(cl->us, "%s: %s\r\n", blobmsg_name(cur), blobmsg_data(cur)); + + ustream_printf(cl->us, "\r\n"); +} + +static void proc_free(struct client *cl) +{ + uh_relay_free(&cl->dispatch.proc.r); +} + +bool uh_create_process(struct client *cl, struct path_info *pi, + void (*cb)(struct client *cl, struct path_info *pi, int fd)) +{ + int fds[2]; + int pid; + + blob_buf_init(&cl->dispatch.proc.hdr, 0); + cl->dispatch.proc.status_code = 200; + cl->dispatch.proc.status_msg = "OK"; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds)) + return false; + + pid = fork(); + if (pid < 0) { + close(fds[0]); + close(fds[1]); + return false; + } + + if (!pid) { + close(fds[0]); + uh_close_fds(); + cb(cl, pi, fds[1]); + exit(0); + } + + close(fds[1]); + uh_relay_open(cl, &cl->dispatch.proc.r, fds[0], pid); + cl->dispatch.free = proc_free; + cl->dispatch.close_fds = proc_close_fds; + cl->dispatch.proc.r.header_cb = proc_handle_header; + cl->dispatch.proc.r.header_end = proc_handle_header_end; + cl->dispatch.proc.r.close = proc_handle_close; + + return true; +} diff --git a/relay.c b/relay.c new file mode 100644 index 0000000..30be3ec --- /dev/null +++ b/relay.c @@ -0,0 +1,178 @@ +/* + * uhttpd - Tiny single-threaded httpd + * + * Copyright (C) 2010-2012 Jo-Philipp Wich + * Copyright (C) 2012 Felix Fietkau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "uhttpd.h" + +void uh_relay_free(struct relay *r) +{ + if (!r->cl) + return; + + if (r->proc.pending) + kill(r->proc.pid, SIGKILL); + + uloop_process_delete(&r->proc); + ustream_free(&r->sfd.stream); + close(r->sfd.fd.fd); + + r->cl = NULL; +} + +void uh_relay_close(struct relay *r, int ret) +{ + struct ustream *us = &r->sfd.stream; + + if (!us->notify_read) + return; + + us->notify_read = NULL; + us->notify_write = NULL; + us->notify_state = NULL; + + if (r->close) + r->close(r, ret); +} + +static void relay_error(struct relay *r) +{ + struct ustream *s = &r->sfd.stream; + int len; + + s->eof = true; + ustream_get_read_buf(s, &len); + if (len) + ustream_consume(s, len); + ustream_state_change(s); +} + +static void relay_process_headers(struct relay *r) +{ + struct ustream *s = &r->sfd.stream; + char *buf, *newline; + int len; + + if (!r->header_cb) + return; + + while (r->header_cb) { + int line_len; + char *val; + + buf = ustream_get_read_buf(s, &len); + newline = strchr(buf, '\n'); + if (!newline) + break; + + line_len = newline + 1 - buf; + if (newline > buf && newline[-1] == '\r') { + newline--; + line_len++; + } + + *newline = 0; + if (newline == buf) { + r->header_cb = NULL; + if (r->header_end) + r->header_end(r); + break; + } + + val = uh_split_header(buf); + if (!val) { + relay_error(r); + return; + } + + r->header_cb(r, buf, val); + ustream_consume(s, line_len); + } +} + +static void relay_read_cb(struct ustream *s, int bytes) +{ + struct relay *r = container_of(s, struct relay, sfd.stream); + struct client *cl = r->cl; + struct ustream *us = cl->us; + char *buf; + int len; + + relay_process_headers(r); + + if (r->header_cb) { + /* + * if eof, ensure that remaining data is discarded, so the + * state change cb will tear down the stream + */ + if (s->eof) + relay_error(r); + return; + } + + if (!s->eof && ustream_pending_data(us, true)) { + ustream_set_read_blocked(s, true); + return; + } + + buf = ustream_get_read_buf(s, &len); + uh_chunk_write(cl, buf, len); + ustream_consume(s, len); +} + +static void relay_close_if_done(struct relay *r) +{ + struct ustream *s = &r->sfd.stream; + + if (!s->eof || ustream_pending_data(s, false)) + return; + + uh_relay_close(r, r->ret); +} + +static void relay_state_cb(struct ustream *s) +{ + struct relay *r = container_of(s, struct relay, sfd.stream); + + if (r->process_done) + relay_close_if_done(r); +} + +static void relay_proc_cb(struct uloop_process *proc, int ret) +{ + struct relay *r = container_of(proc, struct relay, proc); + + r->process_done = true; + r->ret = ret; + relay_close_if_done(r); +} + +void uh_relay_open(struct client *cl, struct relay *r, int fd, int pid) +{ + struct ustream *us = &r->sfd.stream; + + r->cl = cl; + ustream_fd_init(&r->sfd, fd); + us->notify_read = relay_read_cb; + us->notify_state = relay_state_cb; + us->string_data = true; + + r->proc.pid = pid; + r->proc.cb = relay_proc_cb; + uloop_process_add(&r->proc); +} diff --git a/uhttpd.h b/uhttpd.h index c1b52f8..24ad83d 100644 --- a/uhttpd.h +++ b/uhttpd.h @@ -35,6 +35,11 @@ #define UH_LIMIT_CLIENTS 64 #define UH_LIMIT_HEADERS 64 +#define __enum_header(_name) HDR_##_name, +#define __blobmsg_header(_name) [HDR_##_name] = { .name = #_name, .type = BLOBMSG_TYPE_STRING }, + +struct client; + struct config { const char *docroot; const char *realm; @@ -52,16 +57,6 @@ struct config { int script_timeout; }; -struct path_info { - const char *root; - const char *phys; - const char *name; - const char *info; - const char *query; - int redirected; - struct stat stat; -}; - struct auth_realm { struct list_head list; char *path; @@ -89,12 +84,6 @@ struct http_request { const struct auth_realm *realm; }; -struct http_response { - int statuscode; - char *statusmsg; - char *headers[UH_LIMIT_HEADERS]; -}; - enum client_state { CLIENT_STATE_INIT, CLIENT_STATE_HEADER, @@ -103,6 +92,50 @@ enum client_state { CLIENT_STATE_CLOSE, }; +struct interpreter { + struct list_head list; + char *path; + char *ext; +}; + +struct path_info { + const char *root; + const char *phys; + const char *name; + const char *info; + const char *query; + int redirected; + struct stat stat; + struct interpreter *ip; +}; + +struct env_var { + const char *name; + const char *value; +}; + +struct relay { + struct ustream_fd sfd; + struct uloop_process proc; + struct client *cl; + + bool process_done; + int ret; + int header_ofs; + + void (*header_cb)(struct relay *r, const char *name, const char *value); + void (*header_end)(struct relay *r); + void (*close)(struct relay *r, int ret); +}; + +struct dispatch_handler { + struct list_head list; + + bool (*check_url)(const char *url); + bool (*check_path)(struct path_info *pi, const char *url); + void (*handle_request)(struct client *cl, const char *url, struct path_info *pi); +}; + struct client { struct list_head list; int id; @@ -117,7 +150,6 @@ struct client { enum client_state state; struct http_request request; - struct http_response response; struct sockaddr_in6 servaddr; struct sockaddr_in6 peeraddr; @@ -132,6 +164,12 @@ struct client { struct blob_attr **hdr; int fd; } file; + struct { + struct blob_buf hdr; + struct relay r; + int status_code; + char *status_msg; + } proc; }; } dispatch; }; @@ -141,6 +179,7 @@ extern int n_clients; extern struct config conf; extern const char * const http_versions[]; extern const char * const http_methods[]; +extern struct dispatch_handler cgi_dispatch; void uh_index_add(const char *filename); @@ -164,11 +203,22 @@ void uh_http_header(struct client *cl, int code, const char *summary); void __printf(4, 5) uh_client_error(struct client *cl, int code, const char *summary, const char *fmt, ...); -void uh_handle_file_request(struct client *cl); +void uh_handle_request(struct client *cl); void uh_auth_add(const char *path, const char *user, const char *pass); void uh_close_listen_fds(void); void uh_close_fds(void); +void uh_interpreter_add(const char *ext, const char *path); +void uh_dispatch_add(struct dispatch_handler *d); + +void uh_relay_open(struct client *cl, struct relay *r, int fd, int pid); +void uh_relay_close(struct relay *r, int ret); +void uh_relay_free(struct relay *r); + +struct env_var *uh_get_process_vars(struct client *cl, struct path_info *pi); +bool uh_create_process(struct client *cl, struct path_info *pi, + void (*cb)(struct client *cl, struct path_info *pi, int fd)); + #endif