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