1 /* Licensed to the Apache Software Foundation (ASF) under one or more
2  * contributor license agreements.  See the NOTICE file distributed with
3  * this work for additional information regarding copyright ownership.
4  * The ASF licenses this file to You under the Apache License, Version 2.0
5  * (the "License"); you may not use this file except in compliance with
6  * the License.  You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 /*
18  * mod_proxy_scgi.c
19  * Proxy backend module for the SCGI protocol
20  * (http://python.ca/scgi/protocol.txt)
21  *
22  * Andr� Malo (nd/perlig.de), August 2007
23  */
24 
25 #define APR_WANT_MEMFUNC
26 #define APR_WANT_STRFUNC
27 #include "apr_strings.h"
28 #include "ap_hooks.h"
29 #include "apr_optional_hooks.h"
30 #include "apr_buckets.h"
31 
32 #include "httpd.h"
33 #include "http_config.h"
34 #include "http_log.h"
35 #include "http_protocol.h"
36 #include "http_request.h"
37 #include "util_script.h"
38 
39 #include "mod_proxy.h"
40 #include "scgi.h"
41 
42 
43 #define SCHEME "scgi"
44 #define PROXY_FUNCTION "SCGI"
45 #define SCGI_MAGIC "SCGI"
46 #define SCGI_PROTOCOL_VERSION "1"
47 
48 /* just protect from typos */
49 #define CONTENT_LENGTH "CONTENT_LENGTH"
50 #define GATEWAY_INTERFACE "GATEWAY_INTERFACE"
51 
52 module AP_MODULE_DECLARE_DATA proxy_scgi_module;
53 
54 
55 typedef enum {
56     scgi_internal_redirect,
57     scgi_sendfile
58 } scgi_request_type;
59 
60 typedef struct {
61     const char *location;    /* target URL */
62     scgi_request_type type;  /* type of request */
63 } scgi_request_config;
64 
65 const char *scgi_sendfile_off = "off";
66 const char *scgi_sendfile_on = "X-Sendfile";
67 const char *scgi_internal_redirect_off = "off";
68 const char *scgi_internal_redirect_on = "Location";
69 
70 typedef struct {
71     const char *sendfile;
72     const char *internal_redirect;
73 } scgi_config;
74 
75 
76 /*
77  * We create our own bucket type, which is actually derived (c&p) from the
78  * socket bucket.
79  * Maybe some time this should be made more abstract (like passing an
80  * interception function to read or something) and go into the ap_ or
81  * even apr_ namespace.
82  */
83 
84 typedef struct {
85     apr_socket_t *sock;
86     apr_off_t *counter;
87 } socket_ex_data;
88 
89 static apr_bucket *bucket_socket_ex_create(socket_ex_data *data,
90                                            apr_bucket_alloc_t *list);
91 
92 
bucket_socket_ex_read(apr_bucket * a,const char ** str,apr_size_t * len,apr_read_type_e block)93 static apr_status_t bucket_socket_ex_read(apr_bucket *a, const char **str,
94                                           apr_size_t *len,
95                                           apr_read_type_e block)
96 {
97     socket_ex_data *data = a->data;
98     apr_socket_t *p = data->sock;
99     char *buf;
100     apr_status_t rv;
101     apr_interval_time_t timeout;
102 
103     if (block == APR_NONBLOCK_READ) {
104         apr_socket_timeout_get(p, &timeout);
105         apr_socket_timeout_set(p, 0);
106     }
107 
108     *str = NULL;
109     *len = APR_BUCKET_BUFF_SIZE;
110     buf = apr_bucket_alloc(*len, a->list);
111 
112     rv = apr_socket_recv(p, buf, len);
113 
114     if (block == APR_NONBLOCK_READ) {
115         apr_socket_timeout_set(p, timeout);
116     }
117 
118     if (rv != APR_SUCCESS && rv != APR_EOF) {
119         apr_bucket_free(buf);
120         return rv;
121     }
122 
123     if (*len > 0) {
124         apr_bucket_heap *h;
125 
126         /* count for stats */
127         *data->counter += *len;
128 
129         /* Change the current bucket to refer to what we read */
130         a = apr_bucket_heap_make(a, buf, *len, apr_bucket_free);
131         h = a->data;
132         h->alloc_len = APR_BUCKET_BUFF_SIZE; /* note the real buffer size */
133         *str = buf;
134         APR_BUCKET_INSERT_AFTER(a, bucket_socket_ex_create(data, a->list));
135     }
136     else {
137         apr_bucket_free(buf);
138         a = apr_bucket_immortal_make(a, "", 0);
139         *str = a->data;
140     }
141     return APR_SUCCESS;
142 }
143 
144 static const apr_bucket_type_t bucket_type_socket_ex = {
145     "SOCKET_EX", 5, APR_BUCKET_DATA,
146     apr_bucket_destroy_noop,
147     bucket_socket_ex_read,
148     apr_bucket_setaside_notimpl,
149     apr_bucket_split_notimpl,
150     apr_bucket_copy_notimpl
151 };
152 
bucket_socket_ex_make(apr_bucket * b,socket_ex_data * data)153 static apr_bucket *bucket_socket_ex_make(apr_bucket *b, socket_ex_data *data)
154 {
155     b->type        = &bucket_type_socket_ex;
156     b->length      = (apr_size_t)(-1);
157     b->start       = -1;
158     b->data        = data;
159     return b;
160 }
161 
bucket_socket_ex_create(socket_ex_data * data,apr_bucket_alloc_t * list)162 static apr_bucket *bucket_socket_ex_create(socket_ex_data *data,
163                                            apr_bucket_alloc_t *list)
164 {
165     apr_bucket *b = apr_bucket_alloc(sizeof(*b), list);
166 
167     APR_BUCKET_INIT(b);
168     b->free = apr_bucket_free;
169     b->list = list;
170     return bucket_socket_ex_make(b, data);
171 }
172 
173 
174 /*
175  * Canonicalize scgi-like URLs.
176  */
scgi_canon(request_rec * r,char * url)177 static int scgi_canon(request_rec *r, char *url)
178 {
179     char *host, sport[sizeof(":65535")];
180     const char *err, *path;
181     apr_port_t port, def_port;
182 
183     if (ap_cstr_casecmpn(url, SCHEME "://", sizeof(SCHEME) + 2)) {
184         return DECLINED;
185     }
186     url += sizeof(SCHEME); /* Keep slashes */
187 
188     port = def_port = SCGI_DEF_PORT;
189 
190     err = ap_proxy_canon_netloc(r->pool, &url, NULL, NULL, &host, &port);
191     if (err) {
192         ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00857)
193                       "error parsing URL %s: %s", url, err);
194         return HTTP_BAD_REQUEST;
195     }
196 
197     if (port != def_port) {
198         apr_snprintf(sport, sizeof(sport), ":%u", port);
199     }
200     else {
201         sport[0] = '\0';
202     }
203 
204     if (ap_strchr(host, ':')) { /* if literal IPv6 address */
205         host = apr_pstrcat(r->pool, "[", host, "]", NULL);
206     }
207 
208     path = ap_proxy_canonenc(r->pool, url, strlen(url), enc_path, 0,
209                              r->proxyreq);
210     if (!path) {
211         return HTTP_BAD_REQUEST;
212     }
213 
214     r->filename = apr_pstrcat(r->pool, "proxy:" SCHEME "://", host, sport, "/",
215                               path, NULL);
216 
217     if (apr_table_get(r->subprocess_env, "proxy-scgi-pathinfo")) {
218         r->path_info = apr_pstrcat(r->pool, "/", path, NULL);
219     }
220 
221     return OK;
222 }
223 
224 
225 /*
226  * Send a block of data, ensure, everything is sent
227  */
sendall(proxy_conn_rec * conn,const char * buf,apr_size_t length,request_rec * r)228 static int sendall(proxy_conn_rec *conn, const char *buf, apr_size_t length,
229                    request_rec *r)
230 {
231     apr_status_t rv;
232     apr_size_t written;
233 
234     while (length > 0) {
235         written = length;
236         if ((rv = apr_socket_send(conn->sock, buf, &written)) != APR_SUCCESS) {
237             ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(00858)
238                           "sending data to %s:%u failed",
239                           conn->hostname, conn->port);
240             return HTTP_SERVICE_UNAVAILABLE;
241         }
242 
243         /* count for stats */
244         conn->worker->s->transferred += written;
245         buf += written;
246         length -= written;
247     }
248 
249     return OK;
250 }
251 
252 
253 /*
254  * Send SCGI header block
255  */
send_headers(request_rec * r,proxy_conn_rec * conn)256 static int send_headers(request_rec *r, proxy_conn_rec *conn)
257 {
258     char *buf, *cp, *bodylen;
259     const char *ns_len;
260     const apr_array_header_t *env_table;
261     const apr_table_entry_t *env;
262     int j;
263     apr_size_t len, bodylen_size;
264     apr_size_t headerlen =   sizeof(CONTENT_LENGTH)
265                            + sizeof(SCGI_MAGIC)
266                            + sizeof(SCGI_PROTOCOL_VERSION);
267 
268     ap_add_common_vars(r);
269     ap_add_cgi_vars(r);
270 
271     /*
272      * The header blob basically takes the environment and concatenates
273      * keys and values using 0 bytes. There are special treatments here:
274      *   - GATEWAY_INTERFACE and SCGI_MAGIC are dropped
275      *   - CONTENT_LENGTH is always set and must be sent as the very first
276      *     variable
277      *
278      * Additionally it's wrapped into a so-called netstring (see SCGI spec)
279      */
280     env_table = apr_table_elts(r->subprocess_env);
281     env = (apr_table_entry_t *)env_table->elts;
282     for (j = 0; j < env_table->nelts; ++j) {
283         if (   (!strcmp(env[j].key, GATEWAY_INTERFACE))
284             || (!strcmp(env[j].key, CONTENT_LENGTH))
285             || (!strcmp(env[j].key, SCGI_MAGIC))) {
286             continue;
287         }
288         headerlen += strlen(env[j].key) + strlen(env[j].val) + 2;
289     }
290     bodylen = apr_psprintf(r->pool, "%" APR_OFF_T_FMT, r->remaining);
291     bodylen_size = strlen(bodylen) + 1;
292     headerlen += bodylen_size;
293 
294     ns_len = apr_psprintf(r->pool, "%" APR_SIZE_T_FMT ":", headerlen);
295     len = strlen(ns_len);
296     headerlen += len + 1; /* 1 == , */
297     cp = buf = apr_palloc(r->pool, headerlen);
298     memcpy(cp, ns_len, len);
299     cp += len;
300 
301     memcpy(cp, CONTENT_LENGTH, sizeof(CONTENT_LENGTH));
302     cp += sizeof(CONTENT_LENGTH);
303     memcpy(cp, bodylen, bodylen_size);
304     cp += bodylen_size;
305     memcpy(cp, SCGI_MAGIC, sizeof(SCGI_MAGIC));
306     cp += sizeof(SCGI_MAGIC);
307     memcpy(cp, SCGI_PROTOCOL_VERSION, sizeof(SCGI_PROTOCOL_VERSION));
308     cp += sizeof(SCGI_PROTOCOL_VERSION);
309 
310     for (j = 0; j < env_table->nelts; ++j) {
311         if (   (!strcmp(env[j].key, GATEWAY_INTERFACE))
312             || (!strcmp(env[j].key, CONTENT_LENGTH))
313             || (!strcmp(env[j].key, SCGI_MAGIC))) {
314             continue;
315         }
316         len = strlen(env[j].key) + 1;
317         memcpy(cp, env[j].key, len);
318         cp += len;
319         len = strlen(env[j].val) + 1;
320         memcpy(cp, env[j].val, len);
321         cp += len;
322     }
323     *cp++ = ',';
324 
325     return sendall(conn, buf, headerlen, r);
326 }
327 
328 
329 /*
330  * Send request body (if any)
331  */
send_request_body(request_rec * r,proxy_conn_rec * conn)332 static int send_request_body(request_rec *r, proxy_conn_rec *conn)
333 {
334     if (ap_should_client_block(r)) {
335         char *buf = apr_palloc(r->pool, AP_IOBUFSIZE);
336         int status;
337         long readlen;
338 
339         readlen = ap_get_client_block(r, buf, AP_IOBUFSIZE);
340         while (readlen > 0) {
341             status = sendall(conn, buf, (apr_size_t)readlen, r);
342             if (status != OK) {
343                 return HTTP_SERVICE_UNAVAILABLE;
344             }
345             readlen = ap_get_client_block(r, buf, AP_IOBUFSIZE);
346         }
347         if (readlen == -1) {
348             ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00859)
349                           "receiving request body failed");
350             return HTTP_INTERNAL_SERVER_ERROR;
351         }
352     }
353 
354     return OK;
355 }
356 
357 
358 /*
359  * Fetch response from backend and pass back to the front
360  */
pass_response(request_rec * r,proxy_conn_rec * conn)361 static int pass_response(request_rec *r, proxy_conn_rec *conn)
362 {
363     apr_bucket_brigade *bb;
364     apr_bucket *b;
365     const char *location;
366     scgi_config *conf;
367     socket_ex_data *sock_data;
368     int status;
369 
370     sock_data = apr_palloc(r->pool, sizeof(*sock_data));
371     sock_data->sock = conn->sock;
372     sock_data->counter = &conn->worker->s->read;
373 
374     bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
375     b = bucket_socket_ex_create(sock_data, r->connection->bucket_alloc);
376     APR_BRIGADE_INSERT_TAIL(bb, b);
377     b = apr_bucket_eos_create(r->connection->bucket_alloc);
378     APR_BRIGADE_INSERT_TAIL(bb, b);
379 
380     status = ap_scan_script_header_err_brigade_ex(r, bb, NULL,
381                                                   APLOG_MODULE_INDEX);
382     if (status != OK) {
383         ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00860)
384                       "error reading response headers from %s:%u",
385                       conn->hostname, conn->port);
386         r->status_line = NULL;
387         apr_brigade_destroy(bb);
388         return status;
389     }
390 
391     conf = ap_get_module_config(r->per_dir_config, &proxy_scgi_module);
392     if (conf->sendfile && conf->sendfile != scgi_sendfile_off) {
393         short err = 1;
394 
395         location = apr_table_get(r->err_headers_out, conf->sendfile);
396         if (!location) {
397             err = 0;
398             location = apr_table_get(r->headers_out, conf->sendfile);
399         }
400         if (location) {
401             scgi_request_config *req_conf = apr_palloc(r->pool,
402                                                        sizeof(*req_conf));
403             ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(00861)
404                           "Found %s: %s - preparing subrequest.",
405                           conf->sendfile, location);
406 
407             if (err) {
408                 apr_table_unset(r->err_headers_out, conf->sendfile);
409             }
410             else {
411                 apr_table_unset(r->headers_out, conf->sendfile);
412             }
413             req_conf->location = location;
414             req_conf->type = scgi_sendfile;
415             ap_set_module_config(r->request_config, &proxy_scgi_module,
416                                  req_conf);
417             apr_brigade_destroy(bb);
418             return OK;
419         }
420     }
421 
422     if (r->status == HTTP_OK
423         && (!conf->internal_redirect /* default === On */
424             || conf->internal_redirect != scgi_internal_redirect_off)) {
425         short err = 1;
426         const char *location_header = conf->internal_redirect ?
427             conf->internal_redirect : scgi_internal_redirect_on;
428 
429         location = apr_table_get(r->err_headers_out, location_header);
430         if (!location) {
431             err = 0;
432             location = apr_table_get(r->headers_out, location_header);
433         }
434         if (location && *location == '/') {
435             scgi_request_config *req_conf = apr_palloc(r->pool,
436                                                        sizeof(*req_conf));
437             if (ap_cstr_casecmp(location_header, "Location")) {
438                 if (err) {
439                     apr_table_unset(r->err_headers_out, location_header);
440                 }
441                 else {
442                     apr_table_unset(r->headers_out, location_header);
443                 }
444             }
445             req_conf->location = location;
446             req_conf->type = scgi_internal_redirect;
447             ap_set_module_config(r->request_config, &proxy_scgi_module,
448                                  req_conf);
449             apr_brigade_destroy(bb);
450             return OK;
451         }
452     }
453 
454     if (ap_pass_brigade(r->output_filters, bb)) {
455         return AP_FILTER_ERROR;
456     }
457 
458     return OK;
459 }
460 
461 /*
462  * Internal redirect / subrequest handler, working on request_status hook
463  */
scgi_request_status(int * status,request_rec * r)464 static int scgi_request_status(int *status, request_rec *r)
465 {
466     scgi_request_config *req_conf;
467 
468     if (   (*status == OK)
469         && (req_conf = ap_get_module_config(r->request_config,
470                                             &proxy_scgi_module))) {
471         switch (req_conf->type) {
472         case scgi_internal_redirect:
473             ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(00862)
474                           "Internal redirect to %s", req_conf->location);
475 
476             r->status_line = NULL;
477             if (r->method_number != M_GET) {
478                 /* keep HEAD, which is passed around as M_GET, too */
479                 r->method = "GET";
480                 r->method_number = M_GET;
481             }
482             apr_table_unset(r->headers_in, "Content-Length");
483             ap_internal_redirect_handler(req_conf->location, r);
484             return OK;
485             /* break; */
486 
487         case scgi_sendfile:
488             ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(00863)
489                           "File subrequest to %s", req_conf->location);
490             do {
491                 request_rec *rr;
492 
493                 rr = ap_sub_req_lookup_file(req_conf->location, r,
494                                             r->output_filters);
495                 if (rr->status == HTTP_OK && rr->finfo.filetype != APR_NOFILE) {
496                     /*
497                      * We don't touch Content-Length here. It might be
498                      * borked (there's plenty of room for a race condition).
499                      * Either the backend sets it or it's gonna be chunked.
500                      */
501                     ap_run_sub_req(rr);
502                 }
503                 else {
504                     ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00864)
505                                   "Subrequest to file '%s' not possible. "
506                                   "(rr->status=%d, rr->finfo.filetype=%d)",
507                                   req_conf->location, rr->status,
508                                   rr->finfo.filetype);
509                     *status = HTTP_INTERNAL_SERVER_ERROR;
510                     return *status;
511                 }
512             } while (0);
513 
514             return OK;
515             /* break; */
516         }
517     }
518 
519     return DECLINED;
520 }
521 
522 
523 /*
524  * This handles scgi:(dest) URLs
525  */
scgi_handler(request_rec * r,proxy_worker * worker,proxy_server_conf * conf,char * url,const char * proxyname,apr_port_t proxyport)526 static int scgi_handler(request_rec *r, proxy_worker *worker,
527                         proxy_server_conf *conf, char *url,
528                         const char *proxyname, apr_port_t proxyport)
529 {
530     int status;
531     proxy_conn_rec *backend = NULL;
532     apr_pool_t *p = r->pool;
533     apr_uri_t *uri;
534     char dummy;
535 
536     if (ap_cstr_casecmpn(url, SCHEME "://", sizeof(SCHEME) + 2)) {
537         ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(00865)
538                       "declining URL %s", url);
539         return DECLINED;
540     }
541 
542     /* Create space for state information */
543     status = ap_proxy_acquire_connection(PROXY_FUNCTION, &backend, worker,
544                                          r->server);
545     if (status != OK) {
546         goto cleanup;
547     }
548     backend->is_ssl = 0;
549 
550     /* Step One: Determine Who To Connect To */
551     uri = apr_palloc(p, sizeof(*uri));
552     status = ap_proxy_determine_connection(p, r, conf, worker, backend,
553                                            uri, &url, proxyname, proxyport,
554                                            &dummy, 1);
555     if (status != OK) {
556         goto cleanup;
557     }
558 
559     /* Step Two: Make the Connection */
560     if (ap_proxy_connect_backend(PROXY_FUNCTION, backend, worker, r->server)) {
561         ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00866)
562                       "failed to make connection to backend: %s:%u",
563                       backend->hostname, backend->port);
564         status = HTTP_SERVICE_UNAVAILABLE;
565         goto cleanup;
566     }
567 
568     /* Step Three: Process the Request */
569     if (   ((status = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR)) != OK)
570         || ((status = send_headers(r, backend)) != OK)
571         || ((status = send_request_body(r, backend)) != OK)
572         || ((status = pass_response(r, backend)) != OK)) {
573         goto cleanup;
574     }
575 
576 cleanup:
577     if (backend) {
578         backend->close = 1; /* always close the socket */
579         ap_proxy_release_connection(PROXY_FUNCTION, backend, r->server);
580     }
581     return status;
582 }
583 
584 
create_scgi_config(apr_pool_t * p,char * dummy)585 static void *create_scgi_config(apr_pool_t *p, char *dummy)
586 {
587     scgi_config *conf=apr_palloc(p, sizeof(*conf));
588 
589     conf->sendfile = NULL; /* === default (off) */
590     conf->internal_redirect = NULL; /* === default (on) */
591 
592     return conf;
593 }
594 
595 
merge_scgi_config(apr_pool_t * p,void * base_,void * add_)596 static void *merge_scgi_config(apr_pool_t *p, void *base_, void *add_)
597 {
598     scgi_config *base=base_, *add=add_, *conf=apr_palloc(p, sizeof(*conf));
599 
600     conf->sendfile = add->sendfile ? add->sendfile: base->sendfile;
601     conf->internal_redirect = add->internal_redirect
602                               ? add->internal_redirect
603                               : base->internal_redirect;
604     return conf;
605 }
606 
607 
scgi_set_send_file(cmd_parms * cmd,void * mconfig,const char * arg)608 static const char *scgi_set_send_file(cmd_parms *cmd, void *mconfig,
609                                       const char *arg)
610 {
611     scgi_config *conf=mconfig;
612 
613     if (!strcasecmp(arg, "Off")) {
614         conf->sendfile = scgi_sendfile_off;
615     }
616     else if (!strcasecmp(arg, "On")) {
617         conf->sendfile = scgi_sendfile_on;
618     }
619     else {
620         conf->sendfile = arg;
621     }
622     return NULL;
623 }
624 
625 
scgi_set_internal_redirect(cmd_parms * cmd,void * mconfig,const char * arg)626 static const char *scgi_set_internal_redirect(cmd_parms *cmd, void *mconfig,
627                                               const char *arg)
628 {
629     scgi_config *conf = mconfig;
630 
631     if (!strcasecmp(arg, "Off")) {
632         conf->internal_redirect = scgi_internal_redirect_off;
633     }
634     else if (!strcasecmp(arg, "On")) {
635         conf->internal_redirect = scgi_internal_redirect_on;
636     }
637     else {
638         conf->internal_redirect = arg;
639     }
640     return NULL;
641 }
642 
643 
644 static const command_rec scgi_cmds[] =
645 {
646     AP_INIT_TAKE1("ProxySCGISendfile", scgi_set_send_file, NULL,
647                   RSRC_CONF|ACCESS_CONF,
648                   "The name of the X-Sendfile pseudo response header or "
649                   "On or Off"),
650     AP_INIT_TAKE1("ProxySCGIInternalRedirect", scgi_set_internal_redirect, NULL,
651                   RSRC_CONF|ACCESS_CONF,
652                   "The name of the pseudo response header or On or Off"),
653     {NULL}
654 };
655 
656 
register_hooks(apr_pool_t * p)657 static void register_hooks(apr_pool_t *p)
658 {
659     proxy_hook_scheme_handler(scgi_handler, NULL, NULL, APR_HOOK_FIRST);
660     proxy_hook_canon_handler(scgi_canon, NULL, NULL, APR_HOOK_FIRST);
661     APR_OPTIONAL_HOOK(proxy, request_status, scgi_request_status, NULL, NULL,
662                       APR_HOOK_MIDDLE);
663 }
664 
665 
666 AP_DECLARE_MODULE(proxy_scgi) = {
667     STANDARD20_MODULE_STUFF,
668     create_scgi_config,  /* create per-directory config structure */
669     merge_scgi_config,   /* merge per-directory config structures */
670     NULL,                /* create per-server config structure */
671     NULL,                /* merge per-server config structures */
672     scgi_cmds,           /* command table */
673     register_hooks       /* register hooks */
674 };
675