summaryrefslogtreecommitdiffstats
path: root/modules/proxy/mod_proxy_scgi.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:01:30 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:01:30 +0000
commit6beeb1b708550be0d4a53b272283e17e5e35fe17 (patch)
tree1ce8673d4aaa948e5554000101f46536a1e4cc29 /modules/proxy/mod_proxy_scgi.c
parentInitial commit. (diff)
downloadapache2-6beeb1b708550be0d4a53b272283e17e5e35fe17.tar.xz
apache2-6beeb1b708550be0d4a53b272283e17e5e35fe17.zip
Adding upstream version 2.4.57.upstream/2.4.57
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'modules/proxy/mod_proxy_scgi.c')
-rw-r--r--modules/proxy/mod_proxy_scgi.c676
1 files changed, 676 insertions, 0 deletions
diff --git a/modules/proxy/mod_proxy_scgi.c b/modules/proxy/mod_proxy_scgi.c
new file mode 100644
index 0000000..5444a5c
--- /dev/null
+++ b/modules/proxy/mod_proxy_scgi.c
@@ -0,0 +1,676 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/*
+ * mod_proxy_scgi.c
+ * Proxy backend module for the SCGI protocol
+ * (http://python.ca/scgi/protocol.txt)
+ *
+ * Andr� Malo (nd/perlig.de), August 2007
+ */
+
+#define APR_WANT_MEMFUNC
+#define APR_WANT_STRFUNC
+#include "apr_strings.h"
+#include "ap_hooks.h"
+#include "apr_optional_hooks.h"
+#include "apr_buckets.h"
+
+#include "httpd.h"
+#include "http_config.h"
+#include "http_log.h"
+#include "http_protocol.h"
+#include "http_request.h"
+#include "util_script.h"
+
+#include "mod_proxy.h"
+#include "scgi.h"
+
+
+#define SCHEME "scgi"
+#define PROXY_FUNCTION "SCGI"
+#define SCGI_MAGIC "SCGI"
+#define SCGI_PROTOCOL_VERSION "1"
+
+/* just protect from typos */
+#define CONTENT_LENGTH "CONTENT_LENGTH"
+#define GATEWAY_INTERFACE "GATEWAY_INTERFACE"
+
+module AP_MODULE_DECLARE_DATA proxy_scgi_module;
+
+
+typedef enum {
+ scgi_internal_redirect,
+ scgi_sendfile
+} scgi_request_type;
+
+typedef struct {
+ const char *location; /* target URL */
+ scgi_request_type type; /* type of request */
+} scgi_request_config;
+
+const char *scgi_sendfile_off = "off";
+const char *scgi_sendfile_on = "X-Sendfile";
+const char *scgi_internal_redirect_off = "off";
+const char *scgi_internal_redirect_on = "Location";
+
+typedef struct {
+ const char *sendfile;
+ const char *internal_redirect;
+} scgi_config;
+
+
+/*
+ * We create our own bucket type, which is actually derived (c&p) from the
+ * socket bucket.
+ * Maybe some time this should be made more abstract (like passing an
+ * interception function to read or something) and go into the ap_ or
+ * even apr_ namespace.
+ */
+
+typedef struct {
+ apr_socket_t *sock;
+ apr_off_t *counter;
+} socket_ex_data;
+
+static apr_bucket *bucket_socket_ex_create(socket_ex_data *data,
+ apr_bucket_alloc_t *list);
+
+
+static apr_status_t bucket_socket_ex_read(apr_bucket *a, const char **str,
+ apr_size_t *len,
+ apr_read_type_e block)
+{
+ socket_ex_data *data = a->data;
+ apr_socket_t *p = data->sock;
+ char *buf;
+ apr_status_t rv;
+ apr_interval_time_t timeout;
+
+ if (block == APR_NONBLOCK_READ) {
+ apr_socket_timeout_get(p, &timeout);
+ apr_socket_timeout_set(p, 0);
+ }
+
+ *str = NULL;
+ *len = APR_BUCKET_BUFF_SIZE;
+ buf = apr_bucket_alloc(*len, a->list);
+
+ rv = apr_socket_recv(p, buf, len);
+
+ if (block == APR_NONBLOCK_READ) {
+ apr_socket_timeout_set(p, timeout);
+ }
+
+ if (rv != APR_SUCCESS && rv != APR_EOF) {
+ apr_bucket_free(buf);
+ return rv;
+ }
+
+ if (*len > 0) {
+ apr_bucket_heap *h;
+
+ /* count for stats */
+ *data->counter += *len;
+
+ /* Change the current bucket to refer to what we read */
+ a = apr_bucket_heap_make(a, buf, *len, apr_bucket_free);
+ h = a->data;
+ h->alloc_len = APR_BUCKET_BUFF_SIZE; /* note the real buffer size */
+ *str = buf;
+ APR_BUCKET_INSERT_AFTER(a, bucket_socket_ex_create(data, a->list));
+ }
+ else {
+ apr_bucket_free(buf);
+ a = apr_bucket_immortal_make(a, "", 0);
+ *str = a->data;
+ }
+ return APR_SUCCESS;
+}
+
+static const apr_bucket_type_t bucket_type_socket_ex = {
+ "SOCKET_EX", 5, APR_BUCKET_DATA,
+ apr_bucket_destroy_noop,
+ bucket_socket_ex_read,
+ apr_bucket_setaside_notimpl,
+ apr_bucket_split_notimpl,
+ apr_bucket_copy_notimpl
+};
+
+static apr_bucket *bucket_socket_ex_make(apr_bucket *b, socket_ex_data *data)
+{
+ b->type = &bucket_type_socket_ex;
+ b->length = (apr_size_t)(-1);
+ b->start = -1;
+ b->data = data;
+ return b;
+}
+
+static apr_bucket *bucket_socket_ex_create(socket_ex_data *data,
+ apr_bucket_alloc_t *list)
+{
+ apr_bucket *b = apr_bucket_alloc(sizeof(*b), list);
+
+ APR_BUCKET_INIT(b);
+ b->free = apr_bucket_free;
+ b->list = list;
+ return bucket_socket_ex_make(b, data);
+}
+
+
+/*
+ * Canonicalize scgi-like URLs.
+ */
+static int scgi_canon(request_rec *r, char *url)
+{
+ char *host, sport[sizeof(":65535")];
+ const char *err, *path;
+ apr_port_t port, def_port;
+ core_dir_config *d = ap_get_core_module_config(r->per_dir_config);
+ int flags = d->allow_encoded_slashes && !d->decode_encoded_slashes ? PROXY_CANONENC_NOENCODEDSLASHENCODING : 0;
+
+ if (ap_cstr_casecmpn(url, SCHEME "://", sizeof(SCHEME) + 2)) {
+ return DECLINED;
+ }
+ url += sizeof(SCHEME); /* Keep slashes */
+
+ port = def_port = SCGI_DEF_PORT;
+
+ err = ap_proxy_canon_netloc(r->pool, &url, NULL, NULL, &host, &port);
+ if (err) {
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00857)
+ "error parsing URL %s: %s", url, err);
+ return HTTP_BAD_REQUEST;
+ }
+
+ if (port != def_port) {
+ apr_snprintf(sport, sizeof(sport), ":%u", port);
+ }
+ else {
+ sport[0] = '\0';
+ }
+
+ if (ap_strchr(host, ':')) { /* if literal IPv6 address */
+ host = apr_pstrcat(r->pool, "[", host, "]", NULL);
+ }
+
+ path = ap_proxy_canonenc_ex(r->pool, url, strlen(url), enc_path, flags,
+ r->proxyreq);
+ if (!path) {
+ return HTTP_BAD_REQUEST;
+ }
+
+ r->filename = apr_pstrcat(r->pool, "proxy:" SCHEME "://", host, sport, "/",
+ path, NULL);
+
+ if (apr_table_get(r->subprocess_env, "proxy-scgi-pathinfo")) {
+ r->path_info = apr_pstrcat(r->pool, "/", path, NULL);
+ }
+
+ return OK;
+}
+
+
+/*
+ * Send a block of data, ensure, everything is sent
+ */
+static int sendall(proxy_conn_rec *conn, const char *buf, apr_size_t length,
+ request_rec *r)
+{
+ apr_status_t rv;
+ apr_size_t written;
+
+ while (length > 0) {
+ written = length;
+ if ((rv = apr_socket_send(conn->sock, buf, &written)) != APR_SUCCESS) {
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(00858)
+ "sending data to %s:%u failed",
+ conn->hostname, conn->port);
+ return HTTP_SERVICE_UNAVAILABLE;
+ }
+
+ /* count for stats */
+ conn->worker->s->transferred += written;
+ buf += written;
+ length -= written;
+ }
+
+ return OK;
+}
+
+
+/*
+ * Send SCGI header block
+ */
+static int send_headers(request_rec *r, proxy_conn_rec *conn)
+{
+ char *buf, *cp, *bodylen;
+ const char *ns_len;
+ const apr_array_header_t *env_table;
+ const apr_table_entry_t *env;
+ int j;
+ apr_size_t len, bodylen_size;
+ apr_size_t headerlen = sizeof(CONTENT_LENGTH)
+ + sizeof(SCGI_MAGIC)
+ + sizeof(SCGI_PROTOCOL_VERSION);
+
+ ap_add_common_vars(r);
+ ap_add_cgi_vars(r);
+
+ /*
+ * The header blob basically takes the environment and concatenates
+ * keys and values using 0 bytes. There are special treatments here:
+ * - GATEWAY_INTERFACE and SCGI_MAGIC are dropped
+ * - CONTENT_LENGTH is always set and must be sent as the very first
+ * variable
+ *
+ * Additionally it's wrapped into a so-called netstring (see SCGI spec)
+ */
+ env_table = apr_table_elts(r->subprocess_env);
+ env = (apr_table_entry_t *)env_table->elts;
+ for (j = 0; j < env_table->nelts; ++j) {
+ if ( (!strcmp(env[j].key, GATEWAY_INTERFACE))
+ || (!strcmp(env[j].key, CONTENT_LENGTH))
+ || (!strcmp(env[j].key, SCGI_MAGIC))) {
+ continue;
+ }
+ headerlen += strlen(env[j].key) + strlen(env[j].val) + 2;
+ }
+ bodylen = apr_psprintf(r->pool, "%" APR_OFF_T_FMT, r->remaining);
+ bodylen_size = strlen(bodylen) + 1;
+ headerlen += bodylen_size;
+
+ ns_len = apr_psprintf(r->pool, "%" APR_SIZE_T_FMT ":", headerlen);
+ len = strlen(ns_len);
+ headerlen += len + 1; /* 1 == , */
+ cp = buf = apr_palloc(r->pool, headerlen);
+ memcpy(cp, ns_len, len);
+ cp += len;
+
+ memcpy(cp, CONTENT_LENGTH, sizeof(CONTENT_LENGTH));
+ cp += sizeof(CONTENT_LENGTH);
+ memcpy(cp, bodylen, bodylen_size);
+ cp += bodylen_size;
+ memcpy(cp, SCGI_MAGIC, sizeof(SCGI_MAGIC));
+ cp += sizeof(SCGI_MAGIC);
+ memcpy(cp, SCGI_PROTOCOL_VERSION, sizeof(SCGI_PROTOCOL_VERSION));
+ cp += sizeof(SCGI_PROTOCOL_VERSION);
+
+ for (j = 0; j < env_table->nelts; ++j) {
+ if ( (!strcmp(env[j].key, GATEWAY_INTERFACE))
+ || (!strcmp(env[j].key, CONTENT_LENGTH))
+ || (!strcmp(env[j].key, SCGI_MAGIC))) {
+ continue;
+ }
+ len = strlen(env[j].key) + 1;
+ memcpy(cp, env[j].key, len);
+ cp += len;
+ len = strlen(env[j].val) + 1;
+ memcpy(cp, env[j].val, len);
+ cp += len;
+ }
+ *cp++ = ',';
+
+ return sendall(conn, buf, headerlen, r);
+}
+
+
+/*
+ * Send request body (if any)
+ */
+static int send_request_body(request_rec *r, proxy_conn_rec *conn)
+{
+ if (ap_should_client_block(r)) {
+ char *buf = apr_palloc(r->pool, AP_IOBUFSIZE);
+ int status;
+ long readlen;
+
+ readlen = ap_get_client_block(r, buf, AP_IOBUFSIZE);
+ while (readlen > 0) {
+ status = sendall(conn, buf, (apr_size_t)readlen, r);
+ if (status != OK) {
+ return HTTP_SERVICE_UNAVAILABLE;
+ }
+ readlen = ap_get_client_block(r, buf, AP_IOBUFSIZE);
+ }
+ if (readlen == -1) {
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00859)
+ "receiving request body failed");
+ return HTTP_INTERNAL_SERVER_ERROR;
+ }
+ }
+
+ return OK;
+}
+
+
+/*
+ * Fetch response from backend and pass back to the front
+ */
+static int pass_response(request_rec *r, proxy_conn_rec *conn)
+{
+ apr_bucket_brigade *bb;
+ apr_bucket *b;
+ const char *location;
+ scgi_config *conf;
+ socket_ex_data *sock_data;
+ int status;
+
+ sock_data = apr_palloc(r->pool, sizeof(*sock_data));
+ sock_data->sock = conn->sock;
+ sock_data->counter = &conn->worker->s->read;
+
+ bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
+ b = bucket_socket_ex_create(sock_data, r->connection->bucket_alloc);
+ APR_BRIGADE_INSERT_TAIL(bb, b);
+ b = apr_bucket_eos_create(r->connection->bucket_alloc);
+ APR_BRIGADE_INSERT_TAIL(bb, b);
+
+ status = ap_scan_script_header_err_brigade_ex(r, bb, NULL,
+ APLOG_MODULE_INDEX);
+ if (status != OK) {
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00860)
+ "error reading response headers from %s:%u",
+ conn->hostname, conn->port);
+ r->status_line = NULL;
+ apr_brigade_destroy(bb);
+ return status;
+ }
+
+ conf = ap_get_module_config(r->per_dir_config, &proxy_scgi_module);
+ if (conf->sendfile && conf->sendfile != scgi_sendfile_off) {
+ short err = 1;
+
+ location = apr_table_get(r->err_headers_out, conf->sendfile);
+ if (!location) {
+ err = 0;
+ location = apr_table_get(r->headers_out, conf->sendfile);
+ }
+ if (location) {
+ scgi_request_config *req_conf = apr_palloc(r->pool,
+ sizeof(*req_conf));
+ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(00861)
+ "Found %s: %s - preparing subrequest.",
+ conf->sendfile, location);
+
+ if (err) {
+ apr_table_unset(r->err_headers_out, conf->sendfile);
+ }
+ else {
+ apr_table_unset(r->headers_out, conf->sendfile);
+ }
+ req_conf->location = location;
+ req_conf->type = scgi_sendfile;
+ ap_set_module_config(r->request_config, &proxy_scgi_module,
+ req_conf);
+ apr_brigade_destroy(bb);
+ return OK;
+ }
+ }
+
+ if (r->status == HTTP_OK
+ && (!conf->internal_redirect /* default === On */
+ || conf->internal_redirect != scgi_internal_redirect_off)) {
+ short err = 1;
+ const char *location_header = conf->internal_redirect ?
+ conf->internal_redirect : scgi_internal_redirect_on;
+
+ location = apr_table_get(r->err_headers_out, location_header);
+ if (!location) {
+ err = 0;
+ location = apr_table_get(r->headers_out, location_header);
+ }
+ if (location && *location == '/') {
+ scgi_request_config *req_conf = apr_palloc(r->pool,
+ sizeof(*req_conf));
+ if (ap_cstr_casecmp(location_header, "Location")) {
+ if (err) {
+ apr_table_unset(r->err_headers_out, location_header);
+ }
+ else {
+ apr_table_unset(r->headers_out, location_header);
+ }
+ }
+ req_conf->location = location;
+ req_conf->type = scgi_internal_redirect;
+ ap_set_module_config(r->request_config, &proxy_scgi_module,
+ req_conf);
+ apr_brigade_destroy(bb);
+ return OK;
+ }
+ }
+
+ if (ap_pass_brigade(r->output_filters, bb)) {
+ return AP_FILTER_ERROR;
+ }
+
+ return OK;
+}
+
+/*
+ * Internal redirect / subrequest handler, working on request_status hook
+ */
+static int scgi_request_status(int *status, request_rec *r)
+{
+ scgi_request_config *req_conf;
+
+ if ( (*status == OK)
+ && (req_conf = ap_get_module_config(r->request_config,
+ &proxy_scgi_module))) {
+ switch (req_conf->type) {
+ case scgi_internal_redirect:
+ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(00862)
+ "Internal redirect to %s", req_conf->location);
+
+ r->status_line = NULL;
+ if (r->method_number != M_GET) {
+ /* keep HEAD, which is passed around as M_GET, too */
+ r->method = "GET";
+ r->method_number = M_GET;
+ }
+ apr_table_unset(r->headers_in, "Content-Length");
+ ap_internal_redirect_handler(req_conf->location, r);
+ return OK;
+ /* break; */
+
+ case scgi_sendfile:
+ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(00863)
+ "File subrequest to %s", req_conf->location);
+ do {
+ request_rec *rr;
+
+ rr = ap_sub_req_lookup_file(req_conf->location, r,
+ r->output_filters);
+ if (rr->status == HTTP_OK && rr->finfo.filetype != APR_NOFILE) {
+ /*
+ * We don't touch Content-Length here. It might be
+ * borked (there's plenty of room for a race condition).
+ * Either the backend sets it or it's gonna be chunked.
+ */
+ ap_run_sub_req(rr);
+ }
+ else {
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00864)
+ "Subrequest to file '%s' not possible. "
+ "(rr->status=%d, rr->finfo.filetype=%d)",
+ req_conf->location, rr->status,
+ rr->finfo.filetype);
+ *status = HTTP_INTERNAL_SERVER_ERROR;
+ return *status;
+ }
+ } while (0);
+
+ return OK;
+ /* break; */
+ }
+ }
+
+ return DECLINED;
+}
+
+
+/*
+ * This handles scgi:(dest) URLs
+ */
+static int scgi_handler(request_rec *r, proxy_worker *worker,
+ proxy_server_conf *conf, char *url,
+ const char *proxyname, apr_port_t proxyport)
+{
+ int status;
+ proxy_conn_rec *backend = NULL;
+ apr_pool_t *p = r->pool;
+ apr_uri_t *uri;
+ char dummy;
+
+ if (ap_cstr_casecmpn(url, SCHEME "://", sizeof(SCHEME) + 2)) {
+ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(00865)
+ "declining URL %s", url);
+ return DECLINED;
+ }
+
+ /* Create space for state information */
+ status = ap_proxy_acquire_connection(PROXY_FUNCTION, &backend, worker,
+ r->server);
+ if (status != OK) {
+ goto cleanup;
+ }
+ backend->is_ssl = 0;
+
+ /* Step One: Determine Who To Connect To */
+ uri = apr_palloc(p, sizeof(*uri));
+ status = ap_proxy_determine_connection(p, r, conf, worker, backend,
+ uri, &url, proxyname, proxyport,
+ &dummy, 1);
+ if (status != OK) {
+ goto cleanup;
+ }
+
+ /* Step Two: Make the Connection */
+ if (ap_proxy_connect_backend(PROXY_FUNCTION, backend, worker, r->server)) {
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00866)
+ "failed to make connection to backend: %s:%u",
+ backend->hostname, backend->port);
+ status = HTTP_SERVICE_UNAVAILABLE;
+ goto cleanup;
+ }
+
+ /* Step Three: Process the Request */
+ if ( ((status = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR)) != OK)
+ || ((status = send_headers(r, backend)) != OK)
+ || ((status = send_request_body(r, backend)) != OK)
+ || ((status = pass_response(r, backend)) != OK)) {
+ goto cleanup;
+ }
+
+cleanup:
+ if (backend) {
+ backend->close = 1; /* always close the socket */
+ ap_proxy_release_connection(PROXY_FUNCTION, backend, r->server);
+ }
+ return status;
+}
+
+
+static void *create_scgi_config(apr_pool_t *p, char *dummy)
+{
+ scgi_config *conf=apr_palloc(p, sizeof(*conf));
+
+ conf->sendfile = NULL; /* === default (off) */
+ conf->internal_redirect = NULL; /* === default (on) */
+
+ return conf;
+}
+
+
+static void *merge_scgi_config(apr_pool_t *p, void *base_, void *add_)
+{
+ scgi_config *base=base_, *add=add_, *conf=apr_palloc(p, sizeof(*conf));
+
+ conf->sendfile = add->sendfile ? add->sendfile: base->sendfile;
+ conf->internal_redirect = add->internal_redirect
+ ? add->internal_redirect
+ : base->internal_redirect;
+ return conf;
+}
+
+
+static const char *scgi_set_send_file(cmd_parms *cmd, void *mconfig,
+ const char *arg)
+{
+ scgi_config *conf=mconfig;
+
+ if (!strcasecmp(arg, "Off")) {
+ conf->sendfile = scgi_sendfile_off;
+ }
+ else if (!strcasecmp(arg, "On")) {
+ conf->sendfile = scgi_sendfile_on;
+ }
+ else {
+ conf->sendfile = arg;
+ }
+ return NULL;
+}
+
+
+static const char *scgi_set_internal_redirect(cmd_parms *cmd, void *mconfig,
+ const char *arg)
+{
+ scgi_config *conf = mconfig;
+
+ if (!strcasecmp(arg, "Off")) {
+ conf->internal_redirect = scgi_internal_redirect_off;
+ }
+ else if (!strcasecmp(arg, "On")) {
+ conf->internal_redirect = scgi_internal_redirect_on;
+ }
+ else {
+ conf->internal_redirect = arg;
+ }
+ return NULL;
+}
+
+
+static const command_rec scgi_cmds[] =
+{
+ AP_INIT_TAKE1("ProxySCGISendfile", scgi_set_send_file, NULL,
+ RSRC_CONF|ACCESS_CONF,
+ "The name of the X-Sendfile pseudo response header or "
+ "On or Off"),
+ AP_INIT_TAKE1("ProxySCGIInternalRedirect", scgi_set_internal_redirect, NULL,
+ RSRC_CONF|ACCESS_CONF,
+ "The name of the pseudo response header or On or Off"),
+ {NULL}
+};
+
+
+static void register_hooks(apr_pool_t *p)
+{
+ proxy_hook_scheme_handler(scgi_handler, NULL, NULL, APR_HOOK_FIRST);
+ proxy_hook_canon_handler(scgi_canon, NULL, NULL, APR_HOOK_FIRST);
+ APR_OPTIONAL_HOOK(proxy, request_status, scgi_request_status, NULL, NULL,
+ APR_HOOK_MIDDLE);
+}
+
+
+AP_DECLARE_MODULE(proxy_scgi) = {
+ STANDARD20_MODULE_STUFF,
+ create_scgi_config, /* create per-directory config structure */
+ merge_scgi_config, /* merge per-directory config structures */
+ NULL, /* create per-server config structure */
+ NULL, /* merge per-server config structures */
+ scgi_cmds, /* command table */
+ register_hooks /* register hooks */
+};