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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 /* FTP routines for Apache proxy */
19 #include "mod_proxy.h"
23 #include "apr_version.h"
25 #if (APR_MAJOR_VERSION < 1)
26 #undef apr_socket_create
27 #define apr_socket_create apr_socket_create_ex
30 #define AUTODETECT_PWD
31 /* Automatic timestamping (Last-Modified header) based on MDTM is used if:
32 * 1) the FTP server supports the MDTM command and
33 * 2) HAVE_TIMEGM (preferred) or HAVE_GMTOFF is available at compile time
38 module AP_MODULE_DECLARE_DATA proxy_ftp_module;
41 int ftp_list_on_wildcard;
42 int ftp_list_on_wildcard_set;
43 int ftp_escape_wildcards;
44 int ftp_escape_wildcards_set;
45 const char *ftp_directory_charset;
48 static void *create_proxy_ftp_dir_config(apr_pool_t *p, char *dummy)
50 proxy_ftp_dir_conf *new =
51 (proxy_ftp_dir_conf *) apr_pcalloc(p, sizeof(proxy_ftp_dir_conf));
53 /* Put these in the dir config so they work inside <Location> */
54 new->ftp_list_on_wildcard = 1;
55 new->ftp_escape_wildcards = 1;
60 static void *merge_proxy_ftp_dir_config(apr_pool_t *p, void *basev, void *addv)
62 proxy_ftp_dir_conf *new = (proxy_ftp_dir_conf *) apr_pcalloc(p, sizeof(proxy_ftp_dir_conf));
63 proxy_ftp_dir_conf *add = (proxy_ftp_dir_conf *) addv;
64 proxy_ftp_dir_conf *base = (proxy_ftp_dir_conf *) basev;
66 /* Put these in the dir config so they work inside <Location> */
67 new->ftp_list_on_wildcard = add->ftp_list_on_wildcard_set ?
68 add->ftp_list_on_wildcard :
69 base->ftp_list_on_wildcard;
70 new->ftp_list_on_wildcard_set = add->ftp_list_on_wildcard_set ?
72 base->ftp_list_on_wildcard_set;
73 new->ftp_escape_wildcards = add->ftp_escape_wildcards_set ?
74 add->ftp_escape_wildcards :
75 base->ftp_escape_wildcards;
76 new->ftp_escape_wildcards_set = add->ftp_escape_wildcards_set ?
78 base->ftp_escape_wildcards_set;
79 new->ftp_directory_charset = add->ftp_directory_charset ?
80 add->ftp_directory_charset :
81 base->ftp_directory_charset;
85 static const char *set_ftp_list_on_wildcard(cmd_parms *cmd, void *dconf,
88 proxy_ftp_dir_conf *conf = dconf;
90 conf->ftp_list_on_wildcard = flag;
91 conf->ftp_list_on_wildcard_set = 1;
95 static const char *set_ftp_escape_wildcards(cmd_parms *cmd, void *dconf,
98 proxy_ftp_dir_conf *conf = dconf;
100 conf->ftp_escape_wildcards = flag;
101 conf->ftp_escape_wildcards_set = 1;
105 static const char *set_ftp_directory_charset(cmd_parms *cmd, void *dconf,
108 proxy_ftp_dir_conf *conf = dconf;
110 conf->ftp_directory_charset = arg;
115 * Decodes a '%' escaped string, and returns the number of characters
117 static int decodeenc(char *x)
122 return 0; /* special case for no characters */
123 for (i = 0, j = 0; x[i] != '\0'; i++, j++) {
124 /* decode it if not already done */
126 if (ch == '%' && apr_isxdigit(x[i + 1]) && apr_isxdigit(x[i + 2])) {
127 ch = ap_proxy_hex2c(&x[i + 1]);
137 * Escape the globbing characters in a path used as argument to
138 * the FTP commands (SIZE, CWD, RETR, MDTM, ...).
139 * ftpd assumes '\\' as a quoting character to escape special characters.
140 * Just returns the original string if ProxyFtpEscapeWildcards has been
142 * Returns: escaped string
144 #define FTP_GLOBBING_CHARS "*?[{~"
145 static const char *ftp_escape_globbingchars(apr_pool_t *p, const char *path, proxy_ftp_dir_conf *dconf)
150 if (!dconf->ftp_escape_wildcards) {
154 ret = apr_palloc(p, 2*strlen(path)+sizeof(""));
155 for (d = ret; *path; ++path) {
156 if (strchr(FTP_GLOBBING_CHARS, *path) != NULL)
165 * Check for globbing characters in a path used as argument to
166 * the FTP commands (SIZE, CWD, RETR, MDTM, ...).
167 * ftpd assumes '\\' as a quoting character to escape special characters.
168 * Returns: 0 (no globbing chars, or all globbing chars escaped), 1 (globbing chars)
170 static int ftp_check_globbingchars(const char *path)
172 for ( ; *path; ++path) {
175 if (*path != '\0' && strchr(FTP_GLOBBING_CHARS, *path) != NULL)
182 * checks an encoded ftp string for bad characters, namely, CR, LF or
183 * non-ascii character
185 static int ftp_check_string(const char *x)
188 #if APR_CHARSET_EBCDIC
192 for (i = 0; x[i] != '\0'; i++) {
194 if (ch == '%' && apr_isxdigit(x[i + 1]) && apr_isxdigit(x[i + 2])) {
195 ch = ap_proxy_hex2c(&x[i + 1]);
198 #if !APR_CHARSET_EBCDIC
199 if (ch == '\015' || ch == '\012' || (ch & 0x80))
200 #else /* APR_CHARSET_EBCDIC */
201 if (ch == '\r' || ch == '\n')
204 ap_xlate_proto_to_ascii(buf, 1);
206 #endif /* APR_CHARSET_EBCDIC */
213 * Canonicalise ftp URLs.
215 static int proxy_ftp_canon(request_rec *r, char *url)
217 char *user, *password, *host, *path, *parms, *strp, sport[7];
218 apr_pool_t *p = r->pool;
220 apr_port_t port, def_port;
223 if (strncasecmp(url, "ftp:", 4) == 0) {
229 def_port = apr_uri_port_of_scheme("ftp");
231 ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, r->server,
232 "proxy: FTP: canonicalising URL %s", url);
235 err = ap_proxy_canon_netloc(p, &url, &user, &password, &host, &port);
237 return HTTP_BAD_REQUEST;
238 if (user != NULL && !ftp_check_string(user))
239 return HTTP_BAD_REQUEST;
240 if (password != NULL && !ftp_check_string(password))
241 return HTTP_BAD_REQUEST;
243 /* now parse path/parameters args, according to rfc1738 */
245 * N.B. if this isn't a true proxy request, then the URL path (but not
246 * query args) has already been decoded. This gives rise to the problem
247 * of a ; being decoded into the path.
249 strp = strchr(url, ';');
252 parms = ap_proxy_canonenc(p, strp, strlen(strp), enc_parm, 0,
255 return HTTP_BAD_REQUEST;
260 path = ap_proxy_canonenc(p, url, strlen(url), enc_path, 0, r->proxyreq);
262 return HTTP_BAD_REQUEST;
263 if (!ftp_check_string(path))
264 return HTTP_BAD_REQUEST;
266 if (r->proxyreq && r->args != NULL) {
268 strp = ap_proxy_canonenc(p, r->args, strlen(r->args), enc_parm, 1, r->proxyreq);
270 return HTTP_BAD_REQUEST;
271 parms = apr_pstrcat(p, parms, "?", strp, NULL);
274 strp = ap_proxy_canonenc(p, r->args, strlen(r->args), enc_fpath, 1, r->proxyreq);
276 return HTTP_BAD_REQUEST;
277 path = apr_pstrcat(p, path, "?", strp, NULL);
282 /* now, rebuild URL */
284 if (port != def_port)
285 apr_snprintf(sport, sizeof(sport), ":%d", port);
289 if (ap_strchr_c(host, ':')) { /* if literal IPv6 address */
290 host = apr_pstrcat(p, "[", host, "]", NULL);
292 r->filename = apr_pstrcat(p, "proxy:ftp://", (user != NULL) ? user : "",
293 (password != NULL) ? ":" : "",
294 (password != NULL) ? password : "",
295 (user != NULL) ? "@" : "", host, sport, "/", path,
296 (parms[0] != '\0') ? ";" : "", parms, NULL);
301 /* we chop lines longer than 80 characters */
302 #define MAX_LINE_LEN 80
305 * Reads response lines, returns both the ftp status code and
306 * remembers the response message in the supplied buffer
308 static int ftp_getrc_msg(conn_rec *ftp_ctrl, apr_bucket_brigade *bb, char *msgbuf, int msglen)
311 char response[MAX_LINE_LEN];
313 char *mb = msgbuf, *me = &msgbuf[msglen];
317 if (APR_SUCCESS != (rv = ap_proxy_string_read(ftp_ctrl, bb, response, sizeof(response), &eos))) {
321 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, NULL,
322 "proxy: <FTP: %s", response);
324 if (!apr_isdigit(response[0]) || !apr_isdigit(response[1]) ||
325 !apr_isdigit(response[2]) || (response[3] != ' ' && response[3] != '-'))
328 status = 100 * response[0] + 10 * response[1] + response[2] - 111 * '0';
330 mb = apr_cpystrn(mb, response + 4, me - mb);
332 if (response[3] == '-') {
333 memcpy(buff, response, 3);
336 if (APR_SUCCESS != (rv = ap_proxy_string_read(ftp_ctrl, bb, response, sizeof(response), &eos))) {
339 mb = apr_cpystrn(mb, response + (' ' == response[0] ? 1 : 4), me - mb);
340 } while (memcmp(response, buff, 4) != 0);
346 /* this is a filter that turns a raw ASCII directory listing into pretty HTML */
348 /* ideally, mod_proxy should simply send the raw directory list up the filter
349 * stack to mod_autoindex, which in theory should turn the raw ascii into
350 * pretty html along with all the bells and whistles it provides...
352 * all in good time...! :)
356 apr_bucket_brigade *in;
357 char buffer[MAX_STRING_LEN];
363 /* fallback regex for ls -s1; ($0..$2) == 3 */
364 #define LS_REG_PATTERN "^ *([0-9]+) +([^ ]+)$"
365 #define LS_REG_MATCH 3
367 static apr_status_t proxy_send_dir_filter(ap_filter_t *f,
368 apr_bucket_brigade *in)
370 request_rec *r = f->r;
371 conn_rec *c = r->connection;
372 apr_pool_t *p = r->pool;
373 apr_bucket_brigade *out = apr_brigade_create(p, c->bucket_alloc);
377 char *dir, *path, *reldir, *site, *str, *type;
379 const char *pwd = apr_table_get(r->notes, "Directory-PWD");
380 const char *readme = apr_table_get(r->notes, "Directory-README");
382 proxy_dir_ctx_t *ctx = f->ctx;
385 f->ctx = ctx = apr_pcalloc(p, sizeof(*ctx));
386 ctx->in = apr_brigade_create(p, c->bucket_alloc);
391 /* combine the stored and the new */
392 APR_BRIGADE_CONCAT(ctx->in, in);
394 if (HEADER == ctx->state) {
396 /* basedir is either "", or "/%2f" for the "squid %2f hack" */
397 const char *basedir = ""; /* By default, path is relative to the $HOME dir */
398 char *wildcard = NULL;
402 * In the reverse proxy case we need to construct our site string
403 * via ap_construct_url. For non anonymous sites apr_uri_unparse would
404 * only supply us with 'username@' which leads to the construction of
405 * an invalid base href later on. Losing the username part of the URL
406 * is no problem in the reverse proxy case as the browser sents the
407 * credentials anyway once entered.
409 if (r->proxyreq == PROXYREQ_REVERSE) {
410 site = ap_construct_url(p, "", r);
413 /* Save "scheme://site" prefix without password */
414 site = apr_uri_unparse(p, &f->r->parsed_uri,
415 APR_URI_UNP_OMITPASSWORD |
416 APR_URI_UNP_OMITPATHINFO);
419 /* ... and path without query args */
420 path = apr_uri_unparse(p, &f->r->parsed_uri, APR_URI_UNP_OMITSITEPART | APR_URI_UNP_OMITQUERY);
422 /* If path began with /%2f, change the basedir */
423 if (strncasecmp(path, "/%2f", 4) == 0) {
427 /* Strip off a type qualifier. It is ignored for dir listings */
428 if ((type = strstr(path, ";type=")) != NULL)
431 (void)decodeenc(path);
433 while (path[1] == '/') /* collapse multiple leading slashes to one */
436 reldir = strrchr(path, '/');
437 if (reldir != NULL && ftp_check_globbingchars(reldir)) {
438 wildcard = &reldir[1];
439 reldir[0] = '\0'; /* strip off the wildcard suffix */
442 /* Copy path, strip (all except the last) trailing slashes */
443 /* (the trailing slash is needed for the dir component loop below) */
444 path = dir = apr_pstrcat(p, path, "/", NULL);
445 for (n = strlen(path); n > 1 && path[n - 1] == '/' && path[n - 2] == '/'; --n)
448 /* Add a link to the root directory (if %2f hack was used) */
449 str = (basedir[0] != '\0') ? "<a href=\"/%2f/\">%2f</a>/" : "";
451 /* print "ftp://host/" */
452 escpath = ap_escape_html(p, path);
453 str = apr_psprintf(p, DOCTYPE_HTML_3_2
454 "<html>\n <head>\n <title>%s%s%s</title>\n"
455 "<base href=\"%s%s%s\">\n"
457 " <body>\n <h2>Directory of "
458 "<a href=\"/\">%s</a>/%s",
459 site, basedir, escpath, site, basedir, escpath, site, str);
461 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str),
462 p, c->bucket_alloc));
464 for (dir = path+1; (dir = strchr(dir, '/')) != NULL; )
467 if ((reldir = strrchr(path+1, '/'))==NULL) {
472 /* print "path/" component */
473 str = apr_psprintf(p, "<a href=\"%s%s/\">%s</a>/", basedir,
474 ap_escape_uri(p, path),
475 ap_escape_html(p, reldir));
479 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str,
483 if (wildcard != NULL) {
484 wildcard = ap_escape_html(p, wildcard);
485 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(wildcard,
490 /* If the caller has determined the current directory, and it differs */
491 /* from what the client requested, then show the real name */
492 if (pwd == NULL || strncmp(pwd, path, strlen(pwd)) == 0) {
493 str = apr_psprintf(p, "</h2>\n\n <hr />\n\n<pre>");
496 str = apr_psprintf(p, "</h2>\n\n(%s)\n\n <hr />\n\n<pre>",
497 ap_escape_html(p, pwd));
499 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str),
500 p, c->bucket_alloc));
504 str = apr_psprintf(p, "%s\n</pre>\n\n<hr />\n\n<pre>\n",
505 ap_escape_html(p, readme));
507 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str,
512 /* make sure page intro gets sent out */
513 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_flush_create(c->bucket_alloc));
514 if (APR_SUCCESS != (rv = ap_pass_brigade(f->next, out))) {
517 apr_brigade_cleanup(out);
522 /* loop through each line of directory */
523 while (BODY == ctx->state) {
528 ap_regex_t *re = NULL;
529 ap_regmatch_t re_result[LS_REG_MATCH];
531 /* Compile the output format of "ls -s1" as a fallback for non-unix ftp listings */
532 re = ap_pregcomp(p, LS_REG_PATTERN, AP_REG_EXTENDED);
533 ap_assert(re != NULL);
535 /* get a complete line */
536 /* if the buffer overruns - throw data away */
537 while (!found && !APR_BRIGADE_EMPTY(ctx->in)) {
538 char *pos, *response;
542 e = APR_BRIGADE_FIRST(ctx->in);
543 if (APR_BUCKET_IS_EOS(e)) {
547 if (APR_SUCCESS != (rv = apr_bucket_read(e, (const char **)&response, &len, APR_BLOCK_READ))) {
550 pos = memchr(response, APR_ASCII_LF, len);
552 if ((response + len) != (pos + 1)) {
553 len = pos - response + 1;
554 apr_bucket_split(e, pos - response + 1);
558 max = sizeof(ctx->buffer) - strlen(ctx->buffer) - 1;
563 /* len+1 to leave space for the trailing nil char */
564 apr_cpystrn(ctx->buffer+strlen(ctx->buffer), response, len+1);
566 APR_BUCKET_REMOVE(e);
567 apr_bucket_destroy(e);
570 /* EOS? jump to footer */
576 /* not complete? leave and try get some more */
582 apr_size_t n = strlen(ctx->buffer);
583 if (ctx->buffer[n-1] == CRLF[1]) /* strip trailing '\n' */
584 ctx->buffer[--n] = '\0';
585 if (ctx->buffer[n-1] == CRLF[0]) /* strip trailing '\r' if present */
586 ctx->buffer[--n] = '\0';
590 if (ctx->buffer[0] == 'l' && (filename = strstr(ctx->buffer, " -> ")) != NULL) {
591 char *link_ptr = filename;
595 } while (filename[0] != ' ' && filename > ctx->buffer);
596 if (filename > ctx->buffer)
597 *(filename++) = '\0';
598 *(link_ptr++) = '\0';
599 str = apr_psprintf(p, "%s <a href=\"%s\">%s %s</a>\n",
600 ap_escape_html(p, ctx->buffer),
601 ap_escape_uri(p, filename),
602 ap_escape_html(p, filename),
603 ap_escape_html(p, link_ptr));
606 /* a directory/file? */
607 else if (ctx->buffer[0] == 'd' || ctx->buffer[0] == '-' || ctx->buffer[0] == 'l' || apr_isdigit(ctx->buffer[0])) {
609 char *searchptr = NULL;
611 if (apr_isdigit(ctx->buffer[0])) { /* handle DOS dir */
612 searchptr = strchr(ctx->buffer, '<');
613 if (searchptr != NULL)
615 searchptr = strchr(ctx->buffer, '>');
616 if (searchptr != NULL)
620 filename = strrchr(ctx->buffer, ' ');
621 if (filename == NULL) {
622 /* Line is broken. Ignore it. */
623 ap_log_error(APLOG_MARK, APLOG_WARNING, 0, r->server,
624 "proxy_ftp: could not parse line %s", ctx->buffer);
625 /* erase buffer for next time around */
627 continue; /* while state is BODY */
629 *(filename++) = '\0';
631 /* handle filenames with spaces in 'em */
632 if (!strcmp(filename, ".") || !strcmp(filename, "..") || firstfile) {
634 searchidx = filename - ctx->buffer;
636 else if (searchidx != 0 && ctx->buffer[searchidx] != 0) {
638 ctx->buffer[searchidx - 1] = '\0';
639 filename = &ctx->buffer[searchidx];
642 /* Append a slash to the HREF link for directories */
643 if (!strcmp(filename, ".") || !strcmp(filename, "..") || ctx->buffer[0] == 'd') {
644 str = apr_psprintf(p, "%s <a href=\"%s/\">%s</a>\n",
645 ap_escape_html(p, ctx->buffer),
646 ap_escape_uri(p, filename),
647 ap_escape_html(p, filename));
650 str = apr_psprintf(p, "%s <a href=\"%s\">%s</a>\n",
651 ap_escape_html(p, ctx->buffer),
652 ap_escape_uri(p, filename),
653 ap_escape_html(p, filename));
656 /* Try a fallback for listings in the format of "ls -s1" */
657 else if (0 == ap_regexec(re, ctx->buffer, LS_REG_MATCH, re_result, 0)) {
659 filename = apr_pstrndup(p, &ctx->buffer[re_result[2].rm_so], re_result[2].rm_eo - re_result[2].rm_so);
661 str = apr_pstrcat(p, ap_escape_html(p, apr_pstrndup(p, ctx->buffer, re_result[2].rm_so)),
662 "<a href=\"", ap_escape_uri(p, filename), "\">",
663 ap_escape_html(p, filename), "</a>\n", NULL);
666 strcat(ctx->buffer, "\n"); /* re-append the newline */
667 str = ap_escape_html(p, ctx->buffer);
670 /* erase buffer for next time around */
673 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str), p,
675 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_flush_create(c->bucket_alloc));
676 if (APR_SUCCESS != (rv = ap_pass_brigade(f->next, out))) {
679 apr_brigade_cleanup(out);
683 if (FOOTER == ctx->state) {
684 str = apr_psprintf(p, "</pre>\n\n <hr />\n\n %s\n\n </body>\n</html>\n", ap_psignature("", r));
685 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str), p,
687 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_flush_create(c->bucket_alloc));
688 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_eos_create(c->bucket_alloc));
689 if (APR_SUCCESS != (rv = ap_pass_brigade(f->next, out))) {
692 apr_brigade_destroy(out);
698 /* Parse EPSV reply and return port, or zero on error. */
699 static apr_port_t parse_epsv_reply(const char *reply)
705 /* Reply syntax per RFC 2428: "229 blah blah (|||port|)" where '|'
706 * can be any character in ASCII from 33-126, obscurely. Verify
708 p = ap_strchr_c(reply, '(');
709 if (p == NULL || !p[1] || p[1] != p[2] || p[1] != p[3]
715 port = strtol(p + 4, &ep, 10);
716 if (errno || port < 1 || port > 65535 || ep[0] != p[1] || ep[1] != ')') {
720 return (apr_port_t)port;
724 * Generic "send FTP command to server" routine, using the control socket.
725 * Returns the FTP returncode (3 digit code)
726 * Allows for tracing the FTP protocol (in LogLevel debug)
729 proxy_ftp_command(const char *cmd, request_rec *r, conn_rec *ftp_ctrl,
730 apr_bucket_brigade *bb, char **pmessage)
734 char message[HUGE_STRING_LEN];
736 /* If cmd == NULL, we retrieve the next ftp response line */
738 conn_rec *c = r->connection;
739 APR_BRIGADE_INSERT_TAIL(bb, apr_bucket_pool_create(cmd, strlen(cmd), r->pool, c->bucket_alloc));
740 APR_BRIGADE_INSERT_TAIL(bb, apr_bucket_flush_create(c->bucket_alloc));
741 ap_pass_brigade(ftp_ctrl->output_filters, bb);
743 /* strip off the CRLF for logging */
744 apr_cpystrn(message, cmd, sizeof(message));
745 if ((crlf = strchr(message, '\r')) != NULL ||
746 (crlf = strchr(message, '\n')) != NULL)
748 if (strncmp(message,"PASS ", 5) == 0)
749 strcpy(&message[5], "****");
750 ap_log_error(APLOG_MARK, APLOG_TRACE2, 0, r->server,
751 "proxy:>FTP: %s", message);
754 rc = ftp_getrc_msg(ftp_ctrl, bb, message, sizeof message);
755 if (rc == -1 || rc == 421)
756 strcpy(message,"<unable to read result>");
757 if ((crlf = strchr(message, '\r')) != NULL ||
758 (crlf = strchr(message, '\n')) != NULL)
760 ap_log_error(APLOG_MARK, APLOG_TRACE2, 0, r->server,
761 "proxy:<FTP: %3.3u %s", rc, message);
763 if (pmessage != NULL)
764 *pmessage = apr_pstrdup(r->pool, message);
769 /* Set ftp server to TYPE {A,I,E} before transfer of a directory or file */
770 static int ftp_set_TYPE(char xfer_type, request_rec *r, conn_rec *ftp_ctrl,
771 apr_bucket_brigade *bb, char **pmessage)
773 char old_type[2] = { 'A', '\0' }; /* After logon, mode is ASCII */
777 /* set desired type */
778 old_type[0] = xfer_type;
780 rc = proxy_ftp_command(apr_pstrcat(r->pool, "TYPE ", old_type, CRLF, NULL),
781 r, ftp_ctrl, bb, pmessage);
782 /* responses: 200, 421, 500, 501, 504, 530 */
783 /* 200 Command okay. */
784 /* 421 Service not available, closing control connection. */
785 /* 500 Syntax error, command unrecognized. */
786 /* 501 Syntax error in parameters or arguments. */
787 /* 504 Command not implemented for that parameter. */
788 /* 530 Not logged in. */
789 if (rc == -1 || rc == 421) {
790 ret = ap_proxyerror(r, HTTP_BAD_GATEWAY,
791 "Error reading from remote server");
793 else if (rc != 200 && rc != 504) {
794 ret = ap_proxyerror(r, HTTP_BAD_GATEWAY,
795 "Unable to set transfer type");
797 /* Allow not implemented */
798 else if (rc == 504) {
799 /* ignore it silently */
806 /* Return the current directory which we have selected on the FTP server, or NULL */
807 static char *ftp_get_PWD(request_rec *r, conn_rec *ftp_ctrl, apr_bucket_brigade *bb)
810 char *ftpmessage = NULL;
812 /* responses: 257, 500, 501, 502, 421, 550 */
813 /* 257 "<directory-name>" <commentary> */
814 /* 421 Service not available, closing control connection. */
815 /* 500 Syntax error, command unrecognized. */
816 /* 501 Syntax error in parameters or arguments. */
817 /* 502 Command not implemented. */
818 /* 550 Requested action not taken. */
819 switch (proxy_ftp_command("PWD" CRLF, r, ftp_ctrl, bb, &ftpmessage)) {
823 ap_proxyerror(r, HTTP_BAD_GATEWAY,
824 "Failed to read PWD on ftp server");
828 const char *dirp = ftpmessage;
829 cwd = ap_getword_conf(r->pool, &dirp);
836 /* Common routine for failed authorization (i.e., missing or wrong password)
837 * to an ftp service. This causes most browsers to retry the request
838 * with username and password (which was presumably queried from the user)
839 * supplied in the Authorization: header.
840 * Note that we "invent" a realm name which consists of the
841 * ftp://user@host part of the reqest (sans password -if supplied but invalid-)
843 static int ftp_unauthorized(request_rec *r, int log_it)
845 r->proxyreq = PROXYREQ_NONE;
847 * Log failed requests if they supplied a password (log username/password
851 ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r,
852 "proxy: missing or failed auth to %s",
853 apr_uri_unparse(r->pool,
854 &r->parsed_uri, APR_URI_UNP_OMITPATHINFO));
856 apr_table_setn(r->err_headers_out, "WWW-Authenticate",
857 apr_pstrcat(r->pool, "Basic realm=\"",
858 apr_uri_unparse(r->pool, &r->parsed_uri,
859 APR_URI_UNP_OMITPASSWORD | APR_URI_UNP_OMITPATHINFO),
862 return HTTP_UNAUTHORIZED;
866 apr_status_t proxy_ftp_cleanup(request_rec *r, proxy_conn_rec *backend)
870 ap_set_module_config(r->connection->conn_config, &proxy_ftp_module, NULL);
871 ap_proxy_release_connection("FTP", backend, r->server);
877 int ftp_proxyerror(request_rec *r, proxy_conn_rec *conn, int statuscode, const char *message)
879 proxy_ftp_cleanup(r, conn);
880 return ap_proxyerror(r, statuscode, message);
883 * Handles direct access of ftp:// URLs
884 * Original (Non-PASV) version from
885 * Troy Morrison <spiffnet@zoom.com>
886 * PASV added by Chuck
887 * Filters by [Graham Leggett <minfrin@sharp.fm>]
889 static int proxy_ftp_handler(request_rec *r, proxy_worker *worker,
890 proxy_server_conf *conf, char *url,
891 const char *proxyhost, apr_port_t proxyport)
893 apr_pool_t *p = r->pool;
894 conn_rec *c = r->connection;
895 proxy_conn_rec *backend;
896 apr_socket_t *sock, *local_sock, *data_sock = NULL;
897 apr_sockaddr_t *connect_addr = NULL;
899 conn_rec *origin, *data = NULL;
900 apr_status_t err = APR_SUCCESS;
901 apr_status_t uerr = APR_SUCCESS;
902 apr_bucket_brigade *bb = apr_brigade_create(p, c->bucket_alloc);
903 char *buf, *connectname;
904 apr_port_t connectport;
905 char buffer[MAX_STRING_LEN];
906 char *ftpmessage = NULL;
907 char *path, *strp, *type_suffix, *cwd = NULL;
910 /* char *account = NULL; how to supply an account in a URL? */
911 const char *password = NULL;
915 char xfer_type = 'A'; /* after ftp login, the default is ASCII */
917 #if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
918 apr_time_t mtime = 0L;
920 proxy_ftp_dir_conf *fdconf = ap_get_module_config(r->per_dir_config,
923 /* stuff for PASV mode */
924 int connect = 0, use_port = 0;
925 char dates[APR_RFC822_DATE_LEN];
927 apr_pool_t *address_pool;
929 /* is this for us? */
931 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
932 "proxy: FTP: declining URL %s - proxyhost %s specified:", url, proxyhost);
933 return DECLINED; /* proxy connections are via HTTP */
935 if (strncasecmp(url, "ftp:", 4)) {
936 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
937 "proxy: FTP: declining URL %s - not ftp:", url);
938 return DECLINED; /* only interested in FTP */
940 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
941 "proxy: FTP: serving URL %s", url);
945 * I: Who Do I Connect To? -----------------------
947 * Break up the URL to determine the host to connect to
950 /* we only support GET and HEAD */
951 if (r->method_number != M_GET)
952 return HTTP_NOT_IMPLEMENTED;
954 /* We break the URL into host, port, path-search */
955 if (r->parsed_uri.hostname == NULL) {
956 if (APR_SUCCESS != apr_uri_parse(p, url, &uri)) {
957 return ap_proxyerror(r, HTTP_BAD_REQUEST,
958 apr_psprintf(p, "URI cannot be parsed: %s", url));
960 connectname = uri.hostname;
961 connectport = uri.port;
962 path = apr_pstrdup(p, uri.path);
965 connectname = r->parsed_uri.hostname;
966 connectport = r->parsed_uri.port;
967 path = apr_pstrdup(p, r->parsed_uri.path);
969 if (connectport == 0) {
970 connectport = apr_uri_port_of_scheme("ftp");
972 path = (path != NULL && path[0] != '\0') ? &path[1] : "";
974 type_suffix = strchr(path, ';');
975 if (type_suffix != NULL)
976 *(type_suffix++) = '\0';
978 if (type_suffix != NULL && strncmp(type_suffix, "type=", 5) == 0
979 && apr_isalpha(type_suffix[5])) {
980 /* "type=d" forces a dir listing.
981 * The other types (i|a|e) are directly used for the ftp TYPE command
983 if ( ! (dirlisting = (apr_tolower(type_suffix[5]) == 'd')))
984 xfer_type = apr_toupper(type_suffix[5]);
986 /* Check valid types, rather than ignoring invalid types silently: */
987 if (strchr("AEI", xfer_type) == NULL)
988 return ap_proxyerror(r, HTTP_BAD_REQUEST, apr_pstrcat(r->pool,
989 "ftp proxy supports only types 'a', 'i', or 'e': \"",
990 type_suffix, "\" is invalid.", NULL));
993 /* make binary transfers the default */
999 * The "Authorization:" header must be checked first. We allow the user
1000 * to "override" the URL-coded user [ & password ] in the Browsers'
1001 * User&Password Dialog. NOTE that this is only marginally more secure
1002 * than having the password travel in plain as part of the URL, because
1003 * Basic Auth simply uuencodes the plain text password. But chances are
1004 * still smaller that the URL is logged regularly.
1006 if ((password = apr_table_get(r->headers_in, "Authorization")) != NULL
1007 && strcasecmp(ap_getword(r->pool, &password, ' '), "Basic") == 0
1008 && (password = ap_pbase64decode(r->pool, password))[0] != ':') {
1009 /* Check the decoded string for special characters. */
1010 if (!ftp_check_string(password)) {
1011 return ap_proxyerror(r, HTTP_BAD_REQUEST,
1012 "user credentials contained invalid character");
1015 * Note that this allocation has to be made from r->connection->pool
1016 * because it has the lifetime of the connection. The other
1017 * allocations are temporary and can be tossed away any time.
1019 user = ap_getword_nulls(r->connection->pool, &password, ':');
1020 r->ap_auth_type = "Basic";
1021 r->user = r->parsed_uri.user = user;
1023 else if ((user = r->parsed_uri.user) != NULL) {
1024 user = apr_pstrdup(p, user);
1026 if ((password = r->parsed_uri.password) != NULL) {
1027 char *tmp = apr_pstrdup(p, password);
1034 password = "apache-proxy@";
1037 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1038 "proxy: FTP: connecting %s to %s:%d", url, connectname, connectport);
1040 if (worker->is_address_reusable) {
1041 if (!worker->cp->addr) {
1042 if ((err = PROXY_THREAD_LOCK(worker)) != APR_SUCCESS) {
1043 ap_log_error(APLOG_MARK, APLOG_ERR, err, r->server,
1044 "proxy: FTP: lock");
1045 return HTTP_INTERNAL_SERVER_ERROR;
1048 connect_addr = worker->cp->addr;
1049 address_pool = worker->cp->pool;
1052 address_pool = r->pool;
1054 /* do a DNS lookup for the destination host */
1056 err = apr_sockaddr_info_get(&(connect_addr),
1057 connectname, APR_UNSPEC,
1060 if (worker->is_address_reusable && !worker->cp->addr) {
1061 worker->cp->addr = connect_addr;
1062 if ((uerr = PROXY_THREAD_UNLOCK(worker)) != APR_SUCCESS) {
1063 ap_log_error(APLOG_MARK, APLOG_ERR, uerr, r->server,
1064 "proxy: FTP: unlock");
1068 * get all the possible IP addresses for the destname and loop through
1069 * them until we get a successful connection
1071 if (APR_SUCCESS != err) {
1072 return ap_proxyerror(r, HTTP_BAD_GATEWAY, apr_pstrcat(p,
1073 "DNS lookup failure for: ",
1074 connectname, NULL));
1077 /* check if ProxyBlock directive on this host */
1078 if (OK != ap_proxy_checkproxyblock(r, conf, connect_addr)) {
1079 return ap_proxyerror(r, HTTP_FORBIDDEN,
1080 "Connect to remote machine blocked");
1083 /* create space for state information */
1084 backend = (proxy_conn_rec *) ap_get_module_config(c->conn_config, &proxy_ftp_module);
1086 status = ap_proxy_acquire_connection("FTP", &backend, worker, r->server);
1090 ap_proxy_release_connection("FTP", backend, r->server);
1094 /* TODO: see if ftp could use determine_connection */
1095 backend->addr = connect_addr;
1096 ap_set_module_config(c->conn_config, &proxy_ftp_module, backend);
1101 * II: Make the Connection -----------------------
1103 * We have determined who to connect to. Now make the connection.
1107 if (ap_proxy_connect_backend("FTP", backend, worker, r->server)) {
1108 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1109 "proxy: FTP: an error occurred creating a new connection to %pI (%s)",
1110 connect_addr, connectname);
1111 proxy_ftp_cleanup(r, backend);
1112 return HTTP_SERVICE_UNAVAILABLE;
1115 if (!backend->connection) {
1116 status = ap_proxy_connection_create("FTP", backend, c, r->server);
1118 proxy_ftp_cleanup(r, backend);
1123 /* Use old naming */
1124 origin = backend->connection;
1125 sock = backend->sock;
1127 ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, r->server,
1128 "proxy: FTP: control connection complete");
1132 * III: Send Control Request -------------------------
1134 * Log into the ftp server, send the username & password, change to the
1135 * correct directory...
1139 /* possible results: */
1140 /* 120 Service ready in nnn minutes. */
1141 /* 220 Service ready for new user. */
1142 /* 421 Service not available, closing control connection. */
1143 rc = proxy_ftp_command(NULL, r, origin, bb, &ftpmessage);
1144 if (rc == -1 || rc == 421) {
1145 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, "Error reading from remote server");
1149 * RFC2616 states: 14.37 Retry-After
1151 * The Retry-After response-header field can be used with a 503 (Service
1152 * Unavailable) response to indicate how long the service is expected
1153 * to be unavailable to the requesting client. [...] The value of
1154 * this field can be either an HTTP-date or an integer number of
1155 * seconds (in decimal) after the time of the response. Retry-After
1156 * = "Retry-After" ":" ( HTTP-date | delta-seconds )
1158 char *secs_str = ftpmessage;
1161 /* Look for a number, preceded by whitespace */
1163 if ((secs_str==ftpmessage || apr_isspace(secs_str[-1])) &&
1164 apr_isdigit(secs_str[0]))
1166 if (*secs_str != '\0') {
1167 secs = atol(secs_str);
1168 apr_table_add(r->headers_out, "Retry-After",
1169 apr_psprintf(p, "%lu", (unsigned long)(60 * secs)));
1171 return ftp_proxyerror(r, backend, HTTP_SERVICE_UNAVAILABLE, ftpmessage);
1174 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1177 rc = proxy_ftp_command(apr_pstrcat(p, "USER ", user, CRLF, NULL),
1178 r, origin, bb, &ftpmessage);
1179 /* possible results; 230, 331, 332, 421, 500, 501, 530 */
1180 /* states: 1 - error, 2 - success; 3 - send password, 4,5 fail */
1181 /* 230 User logged in, proceed. */
1182 /* 331 User name okay, need password. */
1183 /* 332 Need account for login. */
1184 /* 421 Service not available, closing control connection. */
1185 /* 500 Syntax error, command unrecognized. */
1186 /* (This may include errors such as command line too long.) */
1187 /* 501 Syntax error in parameters or arguments. */
1188 /* 530 Not logged in. */
1189 if (rc == -1 || rc == 421) {
1190 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, "Error reading from remote server");
1193 proxy_ftp_cleanup(r, backend);
1194 return ftp_unauthorized(r, 1); /* log it: user name guessing
1197 if (rc != 230 && rc != 331) {
1198 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1201 if (rc == 331) { /* send password */
1202 if (password == NULL) {
1203 proxy_ftp_cleanup(r, backend);
1204 return ftp_unauthorized(r, 0);
1207 rc = proxy_ftp_command(apr_pstrcat(p, "PASS ", password, CRLF, NULL),
1208 r, origin, bb, &ftpmessage);
1209 /* possible results 202, 230, 332, 421, 500, 501, 503, 530 */
1210 /* 230 User logged in, proceed. */
1211 /* 332 Need account for login. */
1212 /* 421 Service not available, closing control connection. */
1213 /* 500 Syntax error, command unrecognized. */
1214 /* 501 Syntax error in parameters or arguments. */
1215 /* 503 Bad sequence of commands. */
1216 /* 530 Not logged in. */
1217 if (rc == -1 || rc == 421) {
1218 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1219 "Error reading from remote server");
1222 return ftp_proxyerror(r, backend, HTTP_UNAUTHORIZED,
1223 apr_pstrcat(p, "Need account for login: ", ftpmessage, NULL));
1225 /* @@@ questionable -- we might as well return a 403 Forbidden here */
1227 proxy_ftp_cleanup(r, backend);
1228 return ftp_unauthorized(r, 1); /* log it: passwd guessing
1231 if (rc != 230 && rc != 202) {
1232 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1235 apr_table_set(r->notes, "Directory-README", ftpmessage);
1238 /* Special handling for leading "%2f": this enforces a "cwd /"
1239 * out of the $HOME directory which was the starting point after login
1241 if (strncasecmp(path, "%2f", 3) == 0) {
1243 while (*path == '/') /* skip leading '/' (after root %2f) */
1246 rc = proxy_ftp_command("CWD /" CRLF, r, origin, bb, &ftpmessage);
1247 if (rc == -1 || rc == 421)
1248 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1249 "Error reading from remote server");
1253 * set the directory (walk directory component by component): this is
1254 * what we must do if we don't know the OS type of the remote machine
1257 strp = strchr(path, '/');
1262 len = decodeenc(path); /* Note! This decodes a %2f -> "/" */
1264 if (strchr(path, '/')) { /* are there now any '/' characters? */
1265 return ftp_proxyerror(r, backend, HTTP_BAD_REQUEST,
1266 "Use of /%2f is only allowed at the base directory");
1269 /* NOTE: FTP servers do globbing on the path.
1270 * So we need to escape the URI metacharacters.
1271 * We use a special glob-escaping routine to escape globbing chars.
1272 * We could also have extended gen_test_char.c with a special T_ESCAPE_FTP_PATH
1274 rc = proxy_ftp_command(apr_pstrcat(p, "CWD ",
1275 ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
1276 r, origin, bb, &ftpmessage);
1278 /* responses: 250, 421, 500, 501, 502, 530, 550 */
1279 /* 250 Requested file action okay, completed. */
1280 /* 421 Service not available, closing control connection. */
1281 /* 500 Syntax error, command unrecognized. */
1282 /* 501 Syntax error in parameters or arguments. */
1283 /* 502 Command not implemented. */
1284 /* 530 Not logged in. */
1285 /* 550 Requested action not taken. */
1286 if (rc == -1 || rc == 421) {
1287 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1288 "Error reading from remote server");
1291 return ftp_proxyerror(r, backend, HTTP_NOT_FOUND, ftpmessage);
1294 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1301 * IV: Make Data Connection? -------------------------
1303 * Try EPSV, if that fails... try PASV, if that fails... try PORT.
1305 /* this temporarily switches off EPSV/PASV */
1308 /* set up data connection - EPSV */
1310 apr_sockaddr_t *data_addr;
1312 apr_port_t data_port;
1315 * The EPSV command replaces PASV where both IPV4 and IPV6 is
1316 * supported. Only the port is returned, the IP address is always the
1317 * same as that on the control connection. Example: Entering Extended
1318 * Passive Mode (|||6446|)
1320 rc = proxy_ftp_command("EPSV" CRLF,
1321 r, origin, bb, &ftpmessage);
1322 /* possible results: 227, 421, 500, 501, 502, 530 */
1323 /* 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). */
1324 /* 421 Service not available, closing control connection. */
1325 /* 500 Syntax error, command unrecognized. */
1326 /* 501 Syntax error in parameters or arguments. */
1327 /* 502 Command not implemented. */
1328 /* 530 Not logged in. */
1329 if (rc == -1 || rc == 421) {
1330 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1331 "Error reading from remote server");
1333 if (rc != 229 && rc != 500 && rc != 501 && rc != 502) {
1334 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1336 else if (rc == 229) {
1337 /* Parse the port out of the EPSV reply. */
1338 data_port = parse_epsv_reply(ftpmessage);
1341 apr_sockaddr_t *epsv_addr;
1343 ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, r->server,
1344 "proxy: FTP: EPSV contacting remote host on port %d",
1347 if ((rv = apr_socket_create(&data_sock, connect_addr->family, SOCK_STREAM, 0, r->pool)) != APR_SUCCESS) {
1348 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1349 "proxy: FTP: error creating EPSV socket");
1350 proxy_ftp_cleanup(r, backend);
1351 return HTTP_INTERNAL_SERVER_ERROR;
1354 if (conf->recv_buffer_size > 0
1355 && (rv = apr_socket_opt_set(data_sock, APR_SO_RCVBUF,
1356 conf->recv_buffer_size))) {
1357 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1358 "proxy: FTP: apr_socket_opt_set(SO_RCVBUF): Failed to set ProxyReceiveBufferSize, using default");
1361 rv = apr_socket_opt_set(data_sock, APR_TCP_NODELAY, 1);
1362 if (rv != APR_SUCCESS && rv != APR_ENOTIMPL) {
1363 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1364 "apr_socket_opt_set(APR_TCP_NODELAY): Failed to set");
1367 /* make the connection */
1368 apr_socket_addr_get(&data_addr, APR_REMOTE, sock);
1369 apr_sockaddr_ip_get(&data_ip, data_addr);
1370 apr_sockaddr_info_get(&epsv_addr, data_ip, connect_addr->family, data_port, 0, p);
1371 rv = apr_socket_connect(data_sock, epsv_addr);
1372 if (rv != APR_SUCCESS) {
1373 ap_log_error(APLOG_MARK, APLOG_ERR, rv, r->server,
1374 "proxy: FTP: EPSV attempt to connect to %pI failed - Firewall/NAT?", epsv_addr);
1375 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, apr_psprintf(r->pool,
1376 "EPSV attempt to connect to %pI failed - firewall/NAT?", epsv_addr));
1385 /* set up data connection - PASV */
1387 rc = proxy_ftp_command("PASV" CRLF,
1388 r, origin, bb, &ftpmessage);
1389 /* possible results: 227, 421, 500, 501, 502, 530 */
1390 /* 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). */
1391 /* 421 Service not available, closing control connection. */
1392 /* 500 Syntax error, command unrecognized. */
1393 /* 501 Syntax error in parameters or arguments. */
1394 /* 502 Command not implemented. */
1395 /* 530 Not logged in. */
1396 if (rc == -1 || rc == 421) {
1397 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1398 "Error reading from remote server");
1400 if (rc != 227 && rc != 502) {
1401 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1403 else if (rc == 227) {
1404 unsigned int h0, h1, h2, h3, p0, p1;
1408 /* FIXME: Check PASV against RFC1123 */
1411 pstr = apr_strtok(pstr, " ", &tok_cntx); /* separate result code */
1413 if (*(pstr + strlen(pstr) + 1) == '=') {
1414 pstr += strlen(pstr) + 2;
1417 pstr = apr_strtok(NULL, "(", &tok_cntx); /* separate address &
1420 pstr = apr_strtok(NULL, ")", &tok_cntx);
1424 /* FIXME: Only supports IPV4 - fix in RFC2428 */
1426 if (pstr != NULL && (sscanf(pstr,
1427 "%d,%d,%d,%d,%d,%d", &h3, &h2, &h1, &h0, &p1, &p0) == 6)) {
1429 apr_sockaddr_t *pasv_addr;
1430 apr_port_t pasvport = (p1 << 8) + p0;
1431 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1432 "proxy: FTP: PASV contacting host %d.%d.%d.%d:%d",
1433 h3, h2, h1, h0, pasvport);
1435 if ((rv = apr_socket_create(&data_sock, connect_addr->family, SOCK_STREAM, 0, r->pool)) != APR_SUCCESS) {
1436 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1437 "proxy: error creating PASV socket");
1438 proxy_ftp_cleanup(r, backend);
1439 return HTTP_INTERNAL_SERVER_ERROR;
1442 if (conf->recv_buffer_size > 0
1443 && (rv = apr_socket_opt_set(data_sock, APR_SO_RCVBUF,
1444 conf->recv_buffer_size))) {
1445 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1446 "proxy: FTP: apr_socket_opt_set(SO_RCVBUF): Failed to set ProxyReceiveBufferSize, using default");
1449 rv = apr_socket_opt_set(data_sock, APR_TCP_NODELAY, 1);
1450 if (rv != APR_SUCCESS && rv != APR_ENOTIMPL) {
1451 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1452 "apr_socket_opt_set(APR_TCP_NODELAY): Failed to set");
1455 /* make the connection */
1456 apr_sockaddr_info_get(&pasv_addr, apr_psprintf(p, "%d.%d.%d.%d", h3, h2, h1, h0), connect_addr->family, pasvport, 0, p);
1457 rv = apr_socket_connect(data_sock, pasv_addr);
1458 if (rv != APR_SUCCESS) {
1459 ap_log_error(APLOG_MARK, APLOG_ERR, rv, r->server,
1460 "proxy: FTP: PASV attempt to connect to %pI failed - Firewall/NAT?", pasv_addr);
1461 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, apr_psprintf(r->pool,
1462 "PASV attempt to connect to %pI failed - firewall/NAT?", pasv_addr));
1472 /* set up data connection - PORT */
1474 apr_sockaddr_t *local_addr;
1476 apr_port_t local_port;
1477 unsigned int h0, h1, h2, h3, p0, p1;
1479 if ((rv = apr_socket_create(&local_sock, connect_addr->family, SOCK_STREAM, 0, r->pool)) != APR_SUCCESS) {
1480 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1481 "proxy: FTP: error creating local socket");
1482 proxy_ftp_cleanup(r, backend);
1483 return HTTP_INTERNAL_SERVER_ERROR;
1485 apr_socket_addr_get(&local_addr, APR_LOCAL, sock);
1486 local_port = local_addr->port;
1487 apr_sockaddr_ip_get(&local_ip, local_addr);
1489 if ((rv = apr_socket_opt_set(local_sock, APR_SO_REUSEADDR, one))
1491 #ifndef _OSD_POSIX /* BS2000 has this option "always on" */
1492 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1493 "proxy: FTP: error setting reuseaddr option");
1494 proxy_ftp_cleanup(r, backend);
1495 return HTTP_INTERNAL_SERVER_ERROR;
1496 #endif /* _OSD_POSIX */
1499 apr_sockaddr_info_get(&local_addr, local_ip, APR_UNSPEC, local_port, 0, r->pool);
1501 if ((rv = apr_socket_bind(local_sock, local_addr)) != APR_SUCCESS) {
1502 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1503 "proxy: FTP: error binding to ftp data socket %pI", local_addr);
1504 proxy_ftp_cleanup(r, backend);
1505 return HTTP_INTERNAL_SERVER_ERROR;
1508 /* only need a short queue */
1509 if ((rv = apr_socket_listen(local_sock, 2)) != APR_SUCCESS) {
1510 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1511 "proxy: FTP: error listening to ftp data socket %pI", local_addr);
1512 proxy_ftp_cleanup(r, backend);
1513 return HTTP_INTERNAL_SERVER_ERROR;
1516 /* FIXME: Sent PORT here */
1518 if (local_ip && (sscanf(local_ip,
1519 "%d.%d.%d.%d", &h3, &h2, &h1, &h0) == 4)) {
1520 p1 = (local_port >> 8);
1521 p0 = (local_port & 0xFF);
1523 rc = proxy_ftp_command(apr_psprintf(p, "PORT %d,%d,%d,%d,%d,%d" CRLF, h3, h2, h1, h0, p1, p0),
1524 r, origin, bb, &ftpmessage);
1525 /* possible results: 200, 421, 500, 501, 502, 530 */
1526 /* 200 Command okay. */
1527 /* 421 Service not available, closing control connection. */
1528 /* 500 Syntax error, command unrecognized. */
1529 /* 501 Syntax error in parameters or arguments. */
1530 /* 502 Command not implemented. */
1531 /* 530 Not logged in. */
1532 if (rc == -1 || rc == 421) {
1533 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1534 "Error reading from remote server");
1537 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, buffer);
1540 /* signal that we must use the EPRT/PORT loop */
1545 * The EPRT command replaces PORT where both IPV4 and IPV6 is supported. The first
1546 * number (1,2) indicates the protocol type. Examples:
1547 * EPRT |1|132.235.1.2|6275|
1548 * EPRT |2|1080::8:800:200C:417A|5282|
1550 return ftp_proxyerror(r, backend, HTTP_NOT_IMPLEMENTED,
1551 "Connect to IPV6 ftp server using EPRT not supported. Enable EPSV.");
1557 * V: Set The Headers -------------------
1559 * Get the size of the request, set up the environment for HTTP.
1562 /* set request; "path" holds last path component */
1563 len = decodeenc(path);
1565 if (strchr(path, '/')) { /* are there now any '/' characters? */
1566 return ftp_proxyerror(r, backend, HTTP_BAD_REQUEST,
1567 "Use of /%2f is only allowed at the base directory");
1570 /* If len == 0 then it must be a directory (you can't RETR nothing)
1571 * Also, don't allow to RETR by wildcard. Instead, create a dirlisting,
1572 * unless ProxyFtpListOnWildcard is off.
1574 if (len == 0 || (ftp_check_globbingchars(path) && fdconf->ftp_list_on_wildcard)) {
1578 /* (from FreeBSD ftpd):
1579 * SIZE is not in RFC959, but Postel has blessed it and
1580 * it will be in the updated RFC.
1582 * Return size of file in a format suitable for
1583 * using with RESTART (we just count bytes).
1585 /* from draft-ietf-ftpext-mlst-14.txt:
1587 * change depending on the current STRUcture, MODE and TYPE of the data
1588 * connection, or a data connection which would be created were one
1589 * created now. Thus, the result of the SIZE command is dependent on
1590 * the currently established STRU, MODE and TYPE parameters.
1592 /* Therefore: switch to binary if the user did not specify ";type=a" */
1593 ftp_set_TYPE(xfer_type, r, origin, bb, &ftpmessage);
1594 rc = proxy_ftp_command(apr_pstrcat(p, "SIZE ",
1595 ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
1596 r, origin, bb, &ftpmessage);
1597 if (rc == -1 || rc == 421) {
1598 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1599 "Error reading from remote server");
1601 else if (rc == 213) {/* Size command ok */
1603 for (j = 0; apr_isdigit(ftpmessage[j]); j++)
1605 ftpmessage[j] = '\0';
1606 if (ftpmessage[0] != '\0')
1607 size = ftpmessage; /* already pstrdup'ed: no copy necessary */
1609 else if (rc == 550) { /* Not a regular file */
1610 ap_log_error(APLOG_MARK, APLOG_TRACE4, 0, r->server,
1611 "proxy: FTP: SIZE shows this is a directory");
1613 rc = proxy_ftp_command(apr_pstrcat(p, "CWD ",
1614 ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
1615 r, origin, bb, &ftpmessage);
1616 /* possible results: 250, 421, 500, 501, 502, 530, 550 */
1617 /* 250 Requested file action okay, completed. */
1618 /* 421 Service not available, closing control connection. */
1619 /* 500 Syntax error, command unrecognized. */
1620 /* 501 Syntax error in parameters or arguments. */
1621 /* 502 Command not implemented. */
1622 /* 530 Not logged in. */
1623 /* 550 Requested action not taken. */
1624 if (rc == -1 || rc == 421) {
1625 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1626 "Error reading from remote server");
1629 return ftp_proxyerror(r, backend, HTTP_NOT_FOUND, ftpmessage);
1632 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1639 cwd = ftp_get_PWD(r, origin, bb);
1641 apr_table_set(r->notes, "Directory-PWD", cwd);
1645 ftp_set_TYPE('A', r, origin, bb, NULL);
1646 /* If the current directory contains no slash, we are talking to
1647 * a non-unix ftp system. Try LIST instead of "LIST -lag", it
1648 * should return a long listing anyway (unlike NLST).
1649 * Some exotic FTP servers might choke on the "-lag" switch.
1651 /* Note that we do not escape the path here, to allow for
1652 * queries like: ftp://user@host/apache/src/server/http_*.c
1655 buf = apr_pstrcat(p, "LIST ", path, CRLF, NULL);
1656 else if (cwd == NULL || strchr(cwd, '/') != NULL)
1657 buf = apr_pstrcat(p, "LIST -lag", CRLF, NULL);
1662 /* switch to binary if the user did not specify ";type=a" */
1663 ftp_set_TYPE(xfer_type, r, origin, bb, &ftpmessage);
1664 #if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
1665 /* from draft-ietf-ftpext-mlst-14.txt:
1666 * The FTP command, MODIFICATION TIME (MDTM), can be used to determine
1667 * when a file in the server NVFS was last modified. <..>
1668 * The syntax of a time value is:
1669 * time-val = 14DIGIT [ "." 1*DIGIT ] <..>
1670 * Symbolically, a time-val may be viewed as
1671 * YYYYMMDDHHMMSS.sss
1672 * The "." and subsequent digits ("sss") are optional. <..>
1673 * Time values are always represented in UTC (GMT)
1675 rc = proxy_ftp_command(apr_pstrcat(p, "MDTM ", ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
1676 r, origin, bb, &ftpmessage);
1677 /* then extract the Last-Modified time from it (YYYYMMDDhhmmss or YYYYMMDDhhmmss.xxx GMT). */
1687 if (6 == sscanf(ftpmessage, "%4[0-9]%2[0-9]%2[0-9]%2[0-9]%2[0-9]%2[0-9]",
1688 time_val.YYYY, time_val.MM, time_val.DD, time_val.hh, time_val.mm, time_val.ss)) {
1690 memset (&tms, '\0', sizeof tms);
1691 tms.tm_year = atoi(time_val.YYYY) - 1900;
1692 tms.tm_mon = atoi(time_val.MM) - 1;
1693 tms.tm_mday = atoi(time_val.DD);
1694 tms.tm_hour = atoi(time_val.hh);
1695 tms.tm_min = atoi(time_val.mm);
1696 tms.tm_sec = atoi(time_val.ss);
1697 #ifdef HAVE_TIMEGM /* Does system have timegm()? */
1698 mtime = timegm(&tms);
1699 mtime *= APR_USEC_PER_SEC;
1700 #elif HAVE_GMTOFF /* does struct tm have a member tm_gmtoff? */
1701 /* mktime will subtract the local timezone, which is not what we want.
1702 * Add it again because the MDTM string is GMT
1704 mtime = mktime(&tms);
1705 mtime += tms.tm_gmtoff;
1706 mtime *= APR_USEC_PER_SEC;
1712 #endif /* USE_MDTM */
1713 /* FIXME: Handle range requests - send REST */
1714 buf = apr_pstrcat(p, "RETR ", ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL);
1716 rc = proxy_ftp_command(buf, r, origin, bb, &ftpmessage);
1717 /* rc is an intermediate response for the LIST or RETR commands */
1720 * RETR: 110, 125, 150, 226, 250, 421, 425, 426, 450, 451, 500, 501, 530,
1721 * 550 NLST: 125, 150, 226, 250, 421, 425, 426, 450, 451, 500, 501, 502,
1724 /* 110 Restart marker reply. */
1725 /* 125 Data connection already open; transfer starting. */
1726 /* 150 File status okay; about to open data connection. */
1727 /* 226 Closing data connection. */
1728 /* 250 Requested file action okay, completed. */
1729 /* 421 Service not available, closing control connection. */
1730 /* 425 Can't open data connection. */
1731 /* 426 Connection closed; transfer aborted. */
1732 /* 450 Requested file action not taken. */
1733 /* 451 Requested action aborted. Local error in processing. */
1734 /* 500 Syntax error, command unrecognized. */
1735 /* 501 Syntax error in parameters or arguments. */
1736 /* 530 Not logged in. */
1737 /* 550 Requested action not taken. */
1738 if (rc == -1 || rc == 421) {
1739 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1740 "Error reading from remote server");
1743 ap_log_error(APLOG_MARK, APLOG_TRACE4, 0, r->server,
1744 "proxy: FTP: RETR failed, trying LIST instead");
1746 /* Directory Listings should always be fetched in ASCII mode */
1748 ftp_set_TYPE('A', r, origin, bb, NULL);
1750 rc = proxy_ftp_command(apr_pstrcat(p, "CWD ",
1751 ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
1752 r, origin, bb, &ftpmessage);
1753 /* possible results: 250, 421, 500, 501, 502, 530, 550 */
1754 /* 250 Requested file action okay, completed. */
1755 /* 421 Service not available, closing control connection. */
1756 /* 500 Syntax error, command unrecognized. */
1757 /* 501 Syntax error in parameters or arguments. */
1758 /* 502 Command not implemented. */
1759 /* 530 Not logged in. */
1760 /* 550 Requested action not taken. */
1761 if (rc == -1 || rc == 421) {
1762 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1763 "Error reading from remote server");
1766 return ftp_proxyerror(r, backend, HTTP_NOT_FOUND, ftpmessage);
1769 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1772 /* Update current directory after CWD */
1773 cwd = ftp_get_PWD(r, origin, bb);
1775 apr_table_set(r->notes, "Directory-PWD", cwd);
1778 /* See above for the "LIST" vs. "LIST -lag" discussion. */
1779 rc = proxy_ftp_command((cwd == NULL || strchr(cwd, '/') != NULL)
1780 ? "LIST -lag" CRLF : "LIST" CRLF,
1781 r, origin, bb, &ftpmessage);
1783 /* rc is an intermediate response for the LIST command (125 transfer starting, 150 opening data connection) */
1784 if (rc == -1 || rc == 421)
1785 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1786 "Error reading from remote server");
1788 if (rc != 125 && rc != 150 && rc != 226 && rc != 250) {
1789 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1792 r->status = HTTP_OK;
1793 r->status_line = "200 OK";
1795 apr_rfc822_date(dates, r->request_time);
1796 apr_table_setn(r->headers_out, "Date", dates);
1797 apr_table_setn(r->headers_out, "Server", ap_get_server_description());
1799 /* set content-type */
1801 ap_set_content_type(r, apr_pstrcat(p, "text/html;charset=",
1802 fdconf->ftp_directory_charset ?
1803 fdconf->ftp_directory_charset :
1804 "ISO-8859-1", NULL));
1807 if (xfer_type != 'A' && size != NULL) {
1808 /* We "trust" the ftp server to really serve (size) bytes... */
1809 apr_table_setn(r->headers_out, "Content-Length", size);
1810 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
1811 "proxy: FTP: Content-Length set to %s", size);
1814 if (r->content_type) {
1815 apr_table_setn(r->headers_out, "Content-Type", r->content_type);
1816 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
1817 "proxy: FTP: Content-Type set to %s", r->content_type);
1820 #if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
1822 char datestr[APR_RFC822_DATE_LEN];
1823 apr_rfc822_date(datestr, mtime);
1824 apr_table_set(r->headers_out, "Last-Modified", datestr);
1825 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
1826 "proxy: FTP: Last-Modified set to %s", datestr);
1828 #endif /* USE_MDTM */
1830 /* If an encoding has been set by mistake, delete it.
1831 * @@@ FIXME (e.g., for ftp://user@host/file*.tar.gz,
1832 * @@@ the encoding is currently set to x-gzip)
1834 if (dirlisting && r->content_encoding != NULL)
1835 r->content_encoding = NULL;
1837 /* set content-encoding (not for dir listings, they are uncompressed)*/
1838 if (r->content_encoding != NULL && r->content_encoding[0] != '\0') {
1839 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
1840 "proxy: FTP: Content-Encoding set to %s",
1841 r->content_encoding);
1842 apr_table_setn(r->headers_out, "Content-Encoding", r->content_encoding);
1845 /* wait for connection */
1848 rv = apr_socket_accept(&data_sock, local_sock, r->pool);
1849 if (rv == APR_EINTR) {
1852 else if (rv == APR_SUCCESS) {
1856 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1857 "proxy: FTP: failed to accept data connection");
1858 proxy_ftp_cleanup(r, backend);
1859 return HTTP_BAD_GATEWAY;
1864 /* the transfer socket is now open, create a new connection */
1865 data = ap_run_create_connection(p, r->server, data_sock, r->connection->id,
1866 r->connection->sbh, c->bucket_alloc);
1869 * the peer reset the connection already; ap_run_create_connection() closed
1872 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1873 "proxy: FTP: an error occurred creating the transfer connection");
1874 proxy_ftp_cleanup(r, backend);
1875 return HTTP_INTERNAL_SERVER_ERROR;
1879 * We do not do SSL over the data connection, even if the virtual host we
1880 * are in might have SSL enabled
1882 ap_proxy_ssl_disable(data);
1883 /* set up the connection filters */
1884 rc = ap_run_pre_connection(data, data_sock);
1885 if (rc != OK && rc != DONE) {
1886 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1887 "proxy: FTP: pre_connection setup failed (%d)",
1890 proxy_ftp_cleanup(r, backend);
1895 * VI: Receive the Response ------------------------
1897 * Get response from the remote ftp socket, and pass it up the filter chain.
1904 /* insert directory filter */
1905 ap_add_output_filter("PROXY_SEND_DIR", NULL, r, r->connection);
1909 if (!r->header_only) {
1913 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
1914 "proxy: FTP: start body send");
1916 /* read the body, pass it to the output filters */
1917 while (ap_get_brigade(data->input_filters,
1921 conf->io_buffer_size) == APR_SUCCESS) {
1924 apr_off_t readbytes;
1925 apr_brigade_length(bb, 0, &readbytes);
1926 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0,
1927 r->server, "proxy (PID %d): readbytes: %#x",
1928 getpid(), readbytes);
1932 if (APR_BRIGADE_EMPTY(bb)) {
1933 apr_brigade_cleanup(bb);
1937 /* found the last brigade? */
1938 if (APR_BUCKET_IS_EOS(APR_BRIGADE_LAST(bb))) {
1939 /* if this is the last brigade, cleanup the
1940 * backend connection first to prevent the
1941 * backend server from hanging around waiting
1942 * for a slow client to eat these bytes
1944 ap_flush_conn(data);
1946 apr_socket_close(data_sock);
1949 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1950 "proxy: FTP: data connection closed");
1951 /* signal that we must leave */
1955 /* if no EOS yet, then we must flush */
1956 if (FALSE == finish) {
1957 e = apr_bucket_flush_create(c->bucket_alloc);
1958 APR_BRIGADE_INSERT_TAIL(bb, e);
1961 /* try send what we read */
1962 if (ap_pass_brigade(r->output_filters, bb) != APR_SUCCESS
1964 /* Ack! Phbtt! Die! User aborted! */
1968 /* make sure we always clean up after ourselves */
1969 apr_brigade_cleanup(bb);
1971 /* if we are done, leave */
1972 if (TRUE == finish) {
1976 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
1977 "proxy: FTP: end body send");
1981 ap_flush_conn(data);
1982 apr_socket_close(data_sock);
1983 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1984 "proxy: FTP: data connection closed");
1987 /* Retrieve the final response for the RETR or LIST commands */
1988 rc = proxy_ftp_command(NULL, r, origin, bb, &ftpmessage);
1989 apr_brigade_cleanup(bb);
1992 * VII: Clean Up -------------
1994 * If there are no KeepAlives, or if the connection has been signalled to
1995 * close, close the socket and clean up
1999 rc = proxy_ftp_command("QUIT" CRLF,
2000 r, origin, bb, &ftpmessage);
2001 /* responses: 221, 500 */
2002 /* 221 Service closing control connection. */
2003 /* 500 Syntax error, command unrecognized. */
2004 ap_flush_conn(origin);
2005 proxy_ftp_cleanup(r, backend);
2007 apr_brigade_destroy(bb);
2011 static void ap_proxy_ftp_register_hook(apr_pool_t *p)
2014 proxy_hook_scheme_handler(proxy_ftp_handler, NULL, NULL, APR_HOOK_MIDDLE);
2015 proxy_hook_canon_handler(proxy_ftp_canon, NULL, NULL, APR_HOOK_MIDDLE);
2017 ap_register_output_filter("PROXY_SEND_DIR", proxy_send_dir_filter,
2018 NULL, AP_FTYPE_RESOURCE);
2021 static const command_rec proxy_ftp_cmds[] =
2023 AP_INIT_FLAG("ProxyFtpListOnWildcard", set_ftp_list_on_wildcard, NULL,
2024 RSRC_CONF|ACCESS_CONF, "Whether wildcard characters in a path cause mod_proxy_ftp to list the files instead of trying to get them. Defaults to on."),
2025 AP_INIT_FLAG("ProxyFtpEscapeWildcards", set_ftp_escape_wildcards, NULL,
2026 RSRC_CONF|ACCESS_CONF, "Whether the proxy should escape wildcards in paths before sending them to the FTP server. Defaults to on, but most FTP servers will need it turned off if you need to manage paths that contain wildcard characters."),
2027 AP_INIT_TAKE1("ProxyFtpDirCharset", set_ftp_directory_charset, NULL,
2028 RSRC_CONF|ACCESS_CONF, "Define the character set for proxied FTP listings"),
2033 AP_DECLARE_MODULE(proxy_ftp) = {
2034 STANDARD20_MODULE_STUFF,
2035 create_proxy_ftp_dir_config,/* create per-directory config structure */
2036 merge_proxy_ftp_dir_config, /* merge per-directory config structures */
2037 NULL, /* create per-server config structure */
2038 NULL, /* merge per-server config structures */
2039 proxy_ftp_cmds, /* command apr_table_t */
2040 ap_proxy_ftp_register_hook /* register hooks */