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 */
799 /* ignore it silently */;
805 /* Return the current directory which we have selected on the FTP server, or NULL */
806 static char *ftp_get_PWD(request_rec *r, conn_rec *ftp_ctrl, apr_bucket_brigade *bb)
809 char *ftpmessage = NULL;
811 /* responses: 257, 500, 501, 502, 421, 550 */
812 /* 257 "<directory-name>" <commentary> */
813 /* 421 Service not available, closing control connection. */
814 /* 500 Syntax error, command unrecognized. */
815 /* 501 Syntax error in parameters or arguments. */
816 /* 502 Command not implemented. */
817 /* 550 Requested action not taken. */
818 switch (proxy_ftp_command("PWD" CRLF, r, ftp_ctrl, bb, &ftpmessage)) {
822 ap_proxyerror(r, HTTP_BAD_GATEWAY,
823 "Failed to read PWD on ftp server");
827 const char *dirp = ftpmessage;
828 cwd = ap_getword_conf(r->pool, &dirp);
835 /* Common routine for failed authorization (i.e., missing or wrong password)
836 * to an ftp service. This causes most browsers to retry the request
837 * with username and password (which was presumably queried from the user)
838 * supplied in the Authorization: header.
839 * Note that we "invent" a realm name which consists of the
840 * ftp://user@host part of the reqest (sans password -if supplied but invalid-)
842 static int ftp_unauthorized(request_rec *r, int log_it)
844 r->proxyreq = PROXYREQ_NONE;
846 * Log failed requests if they supplied a password (log username/password
850 ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r,
851 "proxy: missing or failed auth to %s",
852 apr_uri_unparse(r->pool,
853 &r->parsed_uri, APR_URI_UNP_OMITPATHINFO));
855 apr_table_setn(r->err_headers_out, "WWW-Authenticate",
856 apr_pstrcat(r->pool, "Basic realm=\"",
857 apr_uri_unparse(r->pool, &r->parsed_uri,
858 APR_URI_UNP_OMITPASSWORD | APR_URI_UNP_OMITPATHINFO),
861 return HTTP_UNAUTHORIZED;
865 apr_status_t proxy_ftp_cleanup(request_rec *r, proxy_conn_rec *backend)
869 ap_set_module_config(r->connection->conn_config, &proxy_ftp_module, NULL);
870 ap_proxy_release_connection("FTP", backend, r->server);
876 int ftp_proxyerror(request_rec *r, proxy_conn_rec *conn, int statuscode, const char *message)
878 proxy_ftp_cleanup(r, conn);
879 return ap_proxyerror(r, statuscode, message);
882 * Handles direct access of ftp:// URLs
883 * Original (Non-PASV) version from
884 * Troy Morrison <spiffnet@zoom.com>
885 * PASV added by Chuck
886 * Filters by [Graham Leggett <minfrin@sharp.fm>]
888 static int proxy_ftp_handler(request_rec *r, proxy_worker *worker,
889 proxy_server_conf *conf, char *url,
890 const char *proxyhost, apr_port_t proxyport)
892 apr_pool_t *p = r->pool;
893 conn_rec *c = r->connection;
894 proxy_conn_rec *backend;
895 apr_socket_t *sock, *local_sock, *data_sock = NULL;
896 apr_sockaddr_t *connect_addr = NULL;
898 conn_rec *origin, *data = NULL;
899 apr_status_t err = APR_SUCCESS;
900 apr_status_t uerr = APR_SUCCESS;
901 apr_bucket_brigade *bb = apr_brigade_create(p, c->bucket_alloc);
902 char *buf, *connectname;
903 apr_port_t connectport;
904 char buffer[MAX_STRING_LEN];
905 char *ftpmessage = NULL;
906 char *path, *strp, *type_suffix, *cwd = NULL;
909 /* char *account = NULL; how to supply an account in a URL? */
910 const char *password = NULL;
914 char xfer_type = 'A'; /* after ftp login, the default is ASCII */
916 #if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
917 apr_time_t mtime = 0L;
919 proxy_ftp_dir_conf *fdconf = ap_get_module_config(r->per_dir_config,
922 /* stuff for PASV mode */
923 int connect = 0, use_port = 0;
924 char dates[APR_RFC822_DATE_LEN];
926 apr_pool_t *address_pool;
928 /* is this for us? */
930 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
931 "proxy: FTP: declining URL %s - proxyhost %s specified:", url, proxyhost);
932 return DECLINED; /* proxy connections are via HTTP */
934 if (strncasecmp(url, "ftp:", 4)) {
935 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
936 "proxy: FTP: declining URL %s - not ftp:", url);
937 return DECLINED; /* only interested in FTP */
939 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
940 "proxy: FTP: serving URL %s", url);
944 * I: Who Do I Connect To? -----------------------
946 * Break up the URL to determine the host to connect to
949 /* we only support GET and HEAD */
950 if (r->method_number != M_GET)
951 return HTTP_NOT_IMPLEMENTED;
953 /* We break the URL into host, port, path-search */
954 if (r->parsed_uri.hostname == NULL) {
955 if (APR_SUCCESS != apr_uri_parse(p, url, &uri)) {
956 return ap_proxyerror(r, HTTP_BAD_REQUEST,
957 apr_psprintf(p, "URI cannot be parsed: %s", url));
959 connectname = uri.hostname;
960 connectport = uri.port;
961 path = apr_pstrdup(p, uri.path);
964 connectname = r->parsed_uri.hostname;
965 connectport = r->parsed_uri.port;
966 path = apr_pstrdup(p, r->parsed_uri.path);
968 if (connectport == 0) {
969 connectport = apr_uri_port_of_scheme("ftp");
971 path = (path != NULL && path[0] != '\0') ? &path[1] : "";
973 type_suffix = strchr(path, ';');
974 if (type_suffix != NULL)
975 *(type_suffix++) = '\0';
977 if (type_suffix != NULL && strncmp(type_suffix, "type=", 5) == 0
978 && apr_isalpha(type_suffix[5])) {
979 /* "type=d" forces a dir listing.
980 * The other types (i|a|e) are directly used for the ftp TYPE command
982 if ( ! (dirlisting = (apr_tolower(type_suffix[5]) == 'd')))
983 xfer_type = apr_toupper(type_suffix[5]);
985 /* Check valid types, rather than ignoring invalid types silently: */
986 if (strchr("AEI", xfer_type) == NULL)
987 return ap_proxyerror(r, HTTP_BAD_REQUEST, apr_pstrcat(r->pool,
988 "ftp proxy supports only types 'a', 'i', or 'e': \"",
989 type_suffix, "\" is invalid.", NULL));
992 /* make binary transfers the default */
998 * The "Authorization:" header must be checked first. We allow the user
999 * to "override" the URL-coded user [ & password ] in the Browsers'
1000 * User&Password Dialog. NOTE that this is only marginally more secure
1001 * than having the password travel in plain as part of the URL, because
1002 * Basic Auth simply uuencodes the plain text password. But chances are
1003 * still smaller that the URL is logged regularly.
1005 if ((password = apr_table_get(r->headers_in, "Authorization")) != NULL
1006 && strcasecmp(ap_getword(r->pool, &password, ' '), "Basic") == 0
1007 && (password = ap_pbase64decode(r->pool, password))[0] != ':') {
1008 /* Check the decoded string for special characters. */
1009 if (!ftp_check_string(password)) {
1010 return ap_proxyerror(r, HTTP_BAD_REQUEST,
1011 "user credentials contained invalid character");
1014 * Note that this allocation has to be made from r->connection->pool
1015 * because it has the lifetime of the connection. The other
1016 * allocations are temporary and can be tossed away any time.
1018 user = ap_getword_nulls(r->connection->pool, &password, ':');
1019 r->ap_auth_type = "Basic";
1020 r->user = r->parsed_uri.user = user;
1022 else if ((user = r->parsed_uri.user) != NULL) {
1023 user = apr_pstrdup(p, user);
1025 if ((password = r->parsed_uri.password) != NULL) {
1026 char *tmp = apr_pstrdup(p, password);
1033 password = "apache-proxy@";
1036 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1037 "proxy: FTP: connecting %s to %s:%d", url, connectname, connectport);
1039 if (worker->is_address_reusable) {
1040 if (!worker->cp->addr) {
1041 if ((err = PROXY_THREAD_LOCK(worker)) != APR_SUCCESS) {
1042 ap_log_error(APLOG_MARK, APLOG_ERR, err, r->server,
1043 "proxy: FTP: lock");
1044 return HTTP_INTERNAL_SERVER_ERROR;
1047 connect_addr = worker->cp->addr;
1048 address_pool = worker->cp->pool;
1051 address_pool = r->pool;
1053 /* do a DNS lookup for the destination host */
1055 err = apr_sockaddr_info_get(&(connect_addr),
1056 connectname, APR_UNSPEC,
1059 if (worker->is_address_reusable && !worker->cp->addr) {
1060 worker->cp->addr = connect_addr;
1061 if ((uerr = PROXY_THREAD_UNLOCK(worker)) != APR_SUCCESS) {
1062 ap_log_error(APLOG_MARK, APLOG_ERR, uerr, r->server,
1063 "proxy: FTP: unlock");
1067 * get all the possible IP addresses for the destname and loop through
1068 * them until we get a successful connection
1070 if (APR_SUCCESS != err) {
1071 return ap_proxyerror(r, HTTP_BAD_GATEWAY, apr_pstrcat(p,
1072 "DNS lookup failure for: ",
1073 connectname, NULL));
1076 /* check if ProxyBlock directive on this host */
1077 if (OK != ap_proxy_checkproxyblock(r, conf, connect_addr)) {
1078 return ap_proxyerror(r, HTTP_FORBIDDEN,
1079 "Connect to remote machine blocked");
1082 /* create space for state information */
1083 backend = (proxy_conn_rec *) ap_get_module_config(c->conn_config, &proxy_ftp_module);
1085 status = ap_proxy_acquire_connection("FTP", &backend, worker, r->server);
1089 ap_proxy_release_connection("FTP", backend, r->server);
1093 /* TODO: see if ftp could use determine_connection */
1094 backend->addr = connect_addr;
1095 ap_set_module_config(c->conn_config, &proxy_ftp_module, backend);
1100 * II: Make the Connection -----------------------
1102 * We have determined who to connect to. Now make the connection.
1106 if (ap_proxy_connect_backend("FTP", backend, worker, r->server)) {
1107 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1108 "proxy: FTP: an error occurred creating a new connection to %pI (%s)",
1109 connect_addr, connectname);
1110 proxy_ftp_cleanup(r, backend);
1111 return HTTP_SERVICE_UNAVAILABLE;
1114 if (!backend->connection) {
1115 status = ap_proxy_connection_create("FTP", backend, c, r->server);
1117 proxy_ftp_cleanup(r, backend);
1122 /* Use old naming */
1123 origin = backend->connection;
1124 sock = backend->sock;
1126 ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, r->server,
1127 "proxy: FTP: control connection complete");
1131 * III: Send Control Request -------------------------
1133 * Log into the ftp server, send the username & password, change to the
1134 * correct directory...
1138 /* possible results: */
1139 /* 120 Service ready in nnn minutes. */
1140 /* 220 Service ready for new user. */
1141 /* 421 Service not available, closing control connection. */
1142 rc = proxy_ftp_command(NULL, r, origin, bb, &ftpmessage);
1143 if (rc == -1 || rc == 421) {
1144 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, "Error reading from remote server");
1148 * RFC2616 states: 14.37 Retry-After
1150 * The Retry-After response-header field can be used with a 503 (Service
1151 * Unavailable) response to indicate how long the service is expected
1152 * to be unavailable to the requesting client. [...] The value of
1153 * this field can be either an HTTP-date or an integer number of
1154 * seconds (in decimal) after the time of the response. Retry-After
1155 * = "Retry-After" ":" ( HTTP-date | delta-seconds )
1157 char *secs_str = ftpmessage;
1160 /* Look for a number, preceded by whitespace */
1162 if ((secs_str==ftpmessage || apr_isspace(secs_str[-1])) &&
1163 apr_isdigit(secs_str[0]))
1165 if (*secs_str != '\0') {
1166 secs = atol(secs_str);
1167 apr_table_add(r->headers_out, "Retry-After",
1168 apr_psprintf(p, "%lu", (unsigned long)(60 * secs)));
1170 return ftp_proxyerror(r, backend, HTTP_SERVICE_UNAVAILABLE, ftpmessage);
1173 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1176 rc = proxy_ftp_command(apr_pstrcat(p, "USER ", user, CRLF, NULL),
1177 r, origin, bb, &ftpmessage);
1178 /* possible results; 230, 331, 332, 421, 500, 501, 530 */
1179 /* states: 1 - error, 2 - success; 3 - send password, 4,5 fail */
1180 /* 230 User logged in, proceed. */
1181 /* 331 User name okay, need password. */
1182 /* 332 Need account for login. */
1183 /* 421 Service not available, closing control connection. */
1184 /* 500 Syntax error, command unrecognized. */
1185 /* (This may include errors such as command line too long.) */
1186 /* 501 Syntax error in parameters or arguments. */
1187 /* 530 Not logged in. */
1188 if (rc == -1 || rc == 421) {
1189 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, "Error reading from remote server");
1192 proxy_ftp_cleanup(r, backend);
1193 return ftp_unauthorized(r, 1); /* log it: user name guessing
1196 if (rc != 230 && rc != 331) {
1197 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1200 if (rc == 331) { /* send password */
1201 if (password == NULL) {
1202 proxy_ftp_cleanup(r, backend);
1203 return ftp_unauthorized(r, 0);
1206 rc = proxy_ftp_command(apr_pstrcat(p, "PASS ", password, CRLF, NULL),
1207 r, origin, bb, &ftpmessage);
1208 /* possible results 202, 230, 332, 421, 500, 501, 503, 530 */
1209 /* 230 User logged in, proceed. */
1210 /* 332 Need account for login. */
1211 /* 421 Service not available, closing control connection. */
1212 /* 500 Syntax error, command unrecognized. */
1213 /* 501 Syntax error in parameters or arguments. */
1214 /* 503 Bad sequence of commands. */
1215 /* 530 Not logged in. */
1216 if (rc == -1 || rc == 421) {
1217 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1218 "Error reading from remote server");
1221 return ftp_proxyerror(r, backend, HTTP_UNAUTHORIZED,
1222 apr_pstrcat(p, "Need account for login: ", ftpmessage, NULL));
1224 /* @@@ questionable -- we might as well return a 403 Forbidden here */
1226 proxy_ftp_cleanup(r, backend);
1227 return ftp_unauthorized(r, 1); /* log it: passwd guessing
1230 if (rc != 230 && rc != 202) {
1231 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1234 apr_table_set(r->notes, "Directory-README", ftpmessage);
1237 /* Special handling for leading "%2f": this enforces a "cwd /"
1238 * out of the $HOME directory which was the starting point after login
1240 if (strncasecmp(path, "%2f", 3) == 0) {
1242 while (*path == '/') /* skip leading '/' (after root %2f) */
1245 rc = proxy_ftp_command("CWD /" CRLF, r, origin, bb, &ftpmessage);
1246 if (rc == -1 || rc == 421)
1247 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1248 "Error reading from remote server");
1252 * set the directory (walk directory component by component): this is
1253 * what we must do if we don't know the OS type of the remote machine
1256 strp = strchr(path, '/');
1261 len = decodeenc(path); /* Note! This decodes a %2f -> "/" */
1263 if (strchr(path, '/')) { /* are there now any '/' characters? */
1264 return ftp_proxyerror(r, backend, HTTP_BAD_REQUEST,
1265 "Use of /%2f is only allowed at the base directory");
1268 /* NOTE: FTP servers do globbing on the path.
1269 * So we need to escape the URI metacharacters.
1270 * We use a special glob-escaping routine to escape globbing chars.
1271 * We could also have extended gen_test_char.c with a special T_ESCAPE_FTP_PATH
1273 rc = proxy_ftp_command(apr_pstrcat(p, "CWD ",
1274 ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
1275 r, origin, bb, &ftpmessage);
1277 /* responses: 250, 421, 500, 501, 502, 530, 550 */
1278 /* 250 Requested file action okay, completed. */
1279 /* 421 Service not available, closing control connection. */
1280 /* 500 Syntax error, command unrecognized. */
1281 /* 501 Syntax error in parameters or arguments. */
1282 /* 502 Command not implemented. */
1283 /* 530 Not logged in. */
1284 /* 550 Requested action not taken. */
1285 if (rc == -1 || rc == 421) {
1286 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1287 "Error reading from remote server");
1290 return ftp_proxyerror(r, backend, HTTP_NOT_FOUND, ftpmessage);
1293 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1300 * IV: Make Data Connection? -------------------------
1302 * Try EPSV, if that fails... try PASV, if that fails... try PORT.
1304 /* this temporarily switches off EPSV/PASV */
1307 /* set up data connection - EPSV */
1309 apr_sockaddr_t *data_addr;
1311 apr_port_t data_port;
1314 * The EPSV command replaces PASV where both IPV4 and IPV6 is
1315 * supported. Only the port is returned, the IP address is always the
1316 * same as that on the control connection. Example: Entering Extended
1317 * Passive Mode (|||6446|)
1319 rc = proxy_ftp_command("EPSV" CRLF,
1320 r, origin, bb, &ftpmessage);
1321 /* possible results: 227, 421, 500, 501, 502, 530 */
1322 /* 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). */
1323 /* 421 Service not available, closing control connection. */
1324 /* 500 Syntax error, command unrecognized. */
1325 /* 501 Syntax error in parameters or arguments. */
1326 /* 502 Command not implemented. */
1327 /* 530 Not logged in. */
1328 if (rc == -1 || rc == 421) {
1329 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1330 "Error reading from remote server");
1332 if (rc != 229 && rc != 500 && rc != 501 && rc != 502) {
1333 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1335 else if (rc == 229) {
1336 /* Parse the port out of the EPSV reply. */
1337 data_port = parse_epsv_reply(ftpmessage);
1340 apr_sockaddr_t *epsv_addr;
1342 ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, r->server,
1343 "proxy: FTP: EPSV contacting remote host on port %d",
1346 if ((rv = apr_socket_create(&data_sock, connect_addr->family, SOCK_STREAM, 0, r->pool)) != APR_SUCCESS) {
1347 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1348 "proxy: FTP: error creating EPSV socket");
1349 proxy_ftp_cleanup(r, backend);
1350 return HTTP_INTERNAL_SERVER_ERROR;
1353 if (conf->recv_buffer_size > 0
1354 && (rv = apr_socket_opt_set(data_sock, APR_SO_RCVBUF,
1355 conf->recv_buffer_size))) {
1356 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1357 "proxy: FTP: apr_socket_opt_set(SO_RCVBUF): Failed to set ProxyReceiveBufferSize, using default");
1360 rv = apr_socket_opt_set(data_sock, APR_TCP_NODELAY, 1);
1361 if (rv != APR_SUCCESS && rv != APR_ENOTIMPL) {
1362 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1363 "apr_socket_opt_set(APR_TCP_NODELAY): Failed to set");
1366 /* make the connection */
1367 apr_socket_addr_get(&data_addr, APR_REMOTE, sock);
1368 apr_sockaddr_ip_get(&data_ip, data_addr);
1369 apr_sockaddr_info_get(&epsv_addr, data_ip, connect_addr->family, data_port, 0, p);
1370 rv = apr_socket_connect(data_sock, epsv_addr);
1371 if (rv != APR_SUCCESS) {
1372 ap_log_error(APLOG_MARK, APLOG_ERR, rv, r->server,
1373 "proxy: FTP: EPSV attempt to connect to %pI failed - Firewall/NAT?", epsv_addr);
1374 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, apr_psprintf(r->pool,
1375 "EPSV attempt to connect to %pI failed - firewall/NAT?", epsv_addr));
1384 /* set up data connection - PASV */
1386 rc = proxy_ftp_command("PASV" CRLF,
1387 r, origin, bb, &ftpmessage);
1388 /* possible results: 227, 421, 500, 501, 502, 530 */
1389 /* 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). */
1390 /* 421 Service not available, closing control connection. */
1391 /* 500 Syntax error, command unrecognized. */
1392 /* 501 Syntax error in parameters or arguments. */
1393 /* 502 Command not implemented. */
1394 /* 530 Not logged in. */
1395 if (rc == -1 || rc == 421) {
1396 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1397 "Error reading from remote server");
1399 if (rc != 227 && rc != 502) {
1400 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1402 else if (rc == 227) {
1403 unsigned int h0, h1, h2, h3, p0, p1;
1407 /* FIXME: Check PASV against RFC1123 */
1410 pstr = apr_strtok(pstr, " ", &tok_cntx); /* separate result code */
1412 if (*(pstr + strlen(pstr) + 1) == '=') {
1413 pstr += strlen(pstr) + 2;
1416 pstr = apr_strtok(NULL, "(", &tok_cntx); /* separate address &
1419 pstr = apr_strtok(NULL, ")", &tok_cntx);
1423 /* FIXME: Only supports IPV4 - fix in RFC2428 */
1425 if (pstr != NULL && (sscanf(pstr,
1426 "%d,%d,%d,%d,%d,%d", &h3, &h2, &h1, &h0, &p1, &p0) == 6)) {
1428 apr_sockaddr_t *pasv_addr;
1429 apr_port_t pasvport = (p1 << 8) + p0;
1430 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1431 "proxy: FTP: PASV contacting host %d.%d.%d.%d:%d",
1432 h3, h2, h1, h0, pasvport);
1434 if ((rv = apr_socket_create(&data_sock, connect_addr->family, SOCK_STREAM, 0, r->pool)) != APR_SUCCESS) {
1435 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1436 "proxy: error creating PASV socket");
1437 proxy_ftp_cleanup(r, backend);
1438 return HTTP_INTERNAL_SERVER_ERROR;
1441 if (conf->recv_buffer_size > 0
1442 && (rv = apr_socket_opt_set(data_sock, APR_SO_RCVBUF,
1443 conf->recv_buffer_size))) {
1444 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1445 "proxy: FTP: apr_socket_opt_set(SO_RCVBUF): Failed to set ProxyReceiveBufferSize, using default");
1448 rv = apr_socket_opt_set(data_sock, APR_TCP_NODELAY, 1);
1449 if (rv != APR_SUCCESS && rv != APR_ENOTIMPL) {
1450 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1451 "apr_socket_opt_set(APR_TCP_NODELAY): Failed to set");
1454 /* make the connection */
1455 apr_sockaddr_info_get(&pasv_addr, apr_psprintf(p, "%d.%d.%d.%d", h3, h2, h1, h0), connect_addr->family, pasvport, 0, p);
1456 rv = apr_socket_connect(data_sock, pasv_addr);
1457 if (rv != APR_SUCCESS) {
1458 ap_log_error(APLOG_MARK, APLOG_ERR, rv, r->server,
1459 "proxy: FTP: PASV attempt to connect to %pI failed - Firewall/NAT?", pasv_addr);
1460 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, apr_psprintf(r->pool,
1461 "PASV attempt to connect to %pI failed - firewall/NAT?", pasv_addr));
1471 /* set up data connection - PORT */
1473 apr_sockaddr_t *local_addr;
1475 apr_port_t local_port;
1476 unsigned int h0, h1, h2, h3, p0, p1;
1478 if ((rv = apr_socket_create(&local_sock, connect_addr->family, SOCK_STREAM, 0, r->pool)) != APR_SUCCESS) {
1479 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1480 "proxy: FTP: error creating local socket");
1481 proxy_ftp_cleanup(r, backend);
1482 return HTTP_INTERNAL_SERVER_ERROR;
1484 apr_socket_addr_get(&local_addr, APR_LOCAL, sock);
1485 local_port = local_addr->port;
1486 apr_sockaddr_ip_get(&local_ip, local_addr);
1488 if ((rv = apr_socket_opt_set(local_sock, APR_SO_REUSEADDR, one))
1490 #ifndef _OSD_POSIX /* BS2000 has this option "always on" */
1491 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1492 "proxy: FTP: error setting reuseaddr option");
1493 proxy_ftp_cleanup(r, backend);
1494 return HTTP_INTERNAL_SERVER_ERROR;
1495 #endif /* _OSD_POSIX */
1498 apr_sockaddr_info_get(&local_addr, local_ip, APR_UNSPEC, local_port, 0, r->pool);
1500 if ((rv = apr_socket_bind(local_sock, local_addr)) != APR_SUCCESS) {
1501 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1502 "proxy: FTP: error binding to ftp data socket %pI", local_addr);
1503 proxy_ftp_cleanup(r, backend);
1504 return HTTP_INTERNAL_SERVER_ERROR;
1507 /* only need a short queue */
1508 if ((rv = apr_socket_listen(local_sock, 2)) != APR_SUCCESS) {
1509 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1510 "proxy: FTP: error listening to ftp data socket %pI", local_addr);
1511 proxy_ftp_cleanup(r, backend);
1512 return HTTP_INTERNAL_SERVER_ERROR;
1515 /* FIXME: Sent PORT here */
1517 if (local_ip && (sscanf(local_ip,
1518 "%d.%d.%d.%d", &h3, &h2, &h1, &h0) == 4)) {
1519 p1 = (local_port >> 8);
1520 p0 = (local_port & 0xFF);
1522 rc = proxy_ftp_command(apr_psprintf(p, "PORT %d,%d,%d,%d,%d,%d" CRLF, h3, h2, h1, h0, p1, p0),
1523 r, origin, bb, &ftpmessage);
1524 /* possible results: 200, 421, 500, 501, 502, 530 */
1525 /* 200 Command okay. */
1526 /* 421 Service not available, closing control connection. */
1527 /* 500 Syntax error, command unrecognized. */
1528 /* 501 Syntax error in parameters or arguments. */
1529 /* 502 Command not implemented. */
1530 /* 530 Not logged in. */
1531 if (rc == -1 || rc == 421) {
1532 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1533 "Error reading from remote server");
1536 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, buffer);
1539 /* signal that we must use the EPRT/PORT loop */
1544 * The EPRT command replaces PORT where both IPV4 and IPV6 is supported. The first
1545 * number (1,2) indicates the protocol type. Examples:
1546 * EPRT |1|132.235.1.2|6275|
1547 * EPRT |2|1080::8:800:200C:417A|5282|
1549 return ftp_proxyerror(r, backend, HTTP_NOT_IMPLEMENTED,
1550 "Connect to IPV6 ftp server using EPRT not supported. Enable EPSV.");
1556 * V: Set The Headers -------------------
1558 * Get the size of the request, set up the environment for HTTP.
1561 /* set request; "path" holds last path component */
1562 len = decodeenc(path);
1564 if (strchr(path, '/')) { /* are there now any '/' characters? */
1565 return ftp_proxyerror(r, backend, HTTP_BAD_REQUEST,
1566 "Use of /%2f is only allowed at the base directory");
1569 /* If len == 0 then it must be a directory (you can't RETR nothing)
1570 * Also, don't allow to RETR by wildcard. Instead, create a dirlisting,
1571 * unless ProxyFtpListOnWildcard is off.
1573 if (len == 0 || (ftp_check_globbingchars(path) && fdconf->ftp_list_on_wildcard)) {
1577 /* (from FreeBSD ftpd):
1578 * SIZE is not in RFC959, but Postel has blessed it and
1579 * it will be in the updated RFC.
1581 * Return size of file in a format suitable for
1582 * using with RESTART (we just count bytes).
1584 /* from draft-ietf-ftpext-mlst-14.txt:
1586 * change depending on the current STRUcture, MODE and TYPE of the data
1587 * connection, or a data connection which would be created were one
1588 * created now. Thus, the result of the SIZE command is dependent on
1589 * the currently established STRU, MODE and TYPE parameters.
1591 /* Therefore: switch to binary if the user did not specify ";type=a" */
1592 ftp_set_TYPE(xfer_type, r, origin, bb, &ftpmessage);
1593 rc = proxy_ftp_command(apr_pstrcat(p, "SIZE ",
1594 ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
1595 r, origin, bb, &ftpmessage);
1596 if (rc == -1 || rc == 421) {
1597 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1598 "Error reading from remote server");
1600 else if (rc == 213) {/* Size command ok */
1602 for (j = 0; apr_isdigit(ftpmessage[j]); j++)
1604 ftpmessage[j] = '\0';
1605 if (ftpmessage[0] != '\0')
1606 size = ftpmessage; /* already pstrdup'ed: no copy necessary */
1608 else if (rc == 550) { /* Not a regular file */
1609 ap_log_error(APLOG_MARK, APLOG_TRACE4, 0, r->server,
1610 "proxy: FTP: SIZE shows this is a directory");
1612 rc = proxy_ftp_command(apr_pstrcat(p, "CWD ",
1613 ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
1614 r, origin, bb, &ftpmessage);
1615 /* possible results: 250, 421, 500, 501, 502, 530, 550 */
1616 /* 250 Requested file action okay, completed. */
1617 /* 421 Service not available, closing control connection. */
1618 /* 500 Syntax error, command unrecognized. */
1619 /* 501 Syntax error in parameters or arguments. */
1620 /* 502 Command not implemented. */
1621 /* 530 Not logged in. */
1622 /* 550 Requested action not taken. */
1623 if (rc == -1 || rc == 421) {
1624 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1625 "Error reading from remote server");
1628 return ftp_proxyerror(r, backend, HTTP_NOT_FOUND, ftpmessage);
1631 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1638 cwd = ftp_get_PWD(r, origin, bb);
1640 apr_table_set(r->notes, "Directory-PWD", cwd);
1644 ftp_set_TYPE('A', r, origin, bb, NULL);
1645 /* If the current directory contains no slash, we are talking to
1646 * a non-unix ftp system. Try LIST instead of "LIST -lag", it
1647 * should return a long listing anyway (unlike NLST).
1648 * Some exotic FTP servers might choke on the "-lag" switch.
1650 /* Note that we do not escape the path here, to allow for
1651 * queries like: ftp://user@host/apache/src/server/http_*.c
1654 buf = apr_pstrcat(p, "LIST ", path, CRLF, NULL);
1655 else if (cwd == NULL || strchr(cwd, '/') != NULL)
1656 buf = apr_pstrcat(p, "LIST -lag", CRLF, NULL);
1661 /* switch to binary if the user did not specify ";type=a" */
1662 ftp_set_TYPE(xfer_type, r, origin, bb, &ftpmessage);
1663 #if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
1664 /* from draft-ietf-ftpext-mlst-14.txt:
1665 * The FTP command, MODIFICATION TIME (MDTM), can be used to determine
1666 * when a file in the server NVFS was last modified. <..>
1667 * The syntax of a time value is:
1668 * time-val = 14DIGIT [ "." 1*DIGIT ] <..>
1669 * Symbolically, a time-val may be viewed as
1670 * YYYYMMDDHHMMSS.sss
1671 * The "." and subsequent digits ("sss") are optional. <..>
1672 * Time values are always represented in UTC (GMT)
1674 rc = proxy_ftp_command(apr_pstrcat(p, "MDTM ", ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
1675 r, origin, bb, &ftpmessage);
1676 /* then extract the Last-Modified time from it (YYYYMMDDhhmmss or YYYYMMDDhhmmss.xxx GMT). */
1686 if (6 == sscanf(ftpmessage, "%4[0-9]%2[0-9]%2[0-9]%2[0-9]%2[0-9]%2[0-9]",
1687 time_val.YYYY, time_val.MM, time_val.DD, time_val.hh, time_val.mm, time_val.ss)) {
1689 memset (&tms, '\0', sizeof tms);
1690 tms.tm_year = atoi(time_val.YYYY) - 1900;
1691 tms.tm_mon = atoi(time_val.MM) - 1;
1692 tms.tm_mday = atoi(time_val.DD);
1693 tms.tm_hour = atoi(time_val.hh);
1694 tms.tm_min = atoi(time_val.mm);
1695 tms.tm_sec = atoi(time_val.ss);
1696 #ifdef HAVE_TIMEGM /* Does system have timegm()? */
1697 mtime = timegm(&tms);
1698 mtime *= APR_USEC_PER_SEC;
1699 #elif HAVE_GMTOFF /* does struct tm have a member tm_gmtoff? */
1700 /* mktime will subtract the local timezone, which is not what we want.
1701 * Add it again because the MDTM string is GMT
1703 mtime = mktime(&tms);
1704 mtime += tms.tm_gmtoff;
1705 mtime *= APR_USEC_PER_SEC;
1711 #endif /* USE_MDTM */
1712 /* FIXME: Handle range requests - send REST */
1713 buf = apr_pstrcat(p, "RETR ", ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL);
1715 rc = proxy_ftp_command(buf, r, origin, bb, &ftpmessage);
1716 /* rc is an intermediate response for the LIST or RETR commands */
1719 * RETR: 110, 125, 150, 226, 250, 421, 425, 426, 450, 451, 500, 501, 530,
1720 * 550 NLST: 125, 150, 226, 250, 421, 425, 426, 450, 451, 500, 501, 502,
1723 /* 110 Restart marker reply. */
1724 /* 125 Data connection already open; transfer starting. */
1725 /* 150 File status okay; about to open data connection. */
1726 /* 226 Closing data connection. */
1727 /* 250 Requested file action okay, completed. */
1728 /* 421 Service not available, closing control connection. */
1729 /* 425 Can't open data connection. */
1730 /* 426 Connection closed; transfer aborted. */
1731 /* 450 Requested file action not taken. */
1732 /* 451 Requested action aborted. Local error in processing. */
1733 /* 500 Syntax error, command unrecognized. */
1734 /* 501 Syntax error in parameters or arguments. */
1735 /* 530 Not logged in. */
1736 /* 550 Requested action not taken. */
1737 if (rc == -1 || rc == 421) {
1738 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1739 "Error reading from remote server");
1742 ap_log_error(APLOG_MARK, APLOG_TRACE4, 0, r->server,
1743 "proxy: FTP: RETR failed, trying LIST instead");
1745 /* Directory Listings should always be fetched in ASCII mode */
1747 ftp_set_TYPE('A', r, origin, bb, NULL);
1749 rc = proxy_ftp_command(apr_pstrcat(p, "CWD ",
1750 ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
1751 r, origin, bb, &ftpmessage);
1752 /* possible results: 250, 421, 500, 501, 502, 530, 550 */
1753 /* 250 Requested file action okay, completed. */
1754 /* 421 Service not available, closing control connection. */
1755 /* 500 Syntax error, command unrecognized. */
1756 /* 501 Syntax error in parameters or arguments. */
1757 /* 502 Command not implemented. */
1758 /* 530 Not logged in. */
1759 /* 550 Requested action not taken. */
1760 if (rc == -1 || rc == 421) {
1761 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1762 "Error reading from remote server");
1765 return ftp_proxyerror(r, backend, HTTP_NOT_FOUND, ftpmessage);
1768 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1771 /* Update current directory after CWD */
1772 cwd = ftp_get_PWD(r, origin, bb);
1774 apr_table_set(r->notes, "Directory-PWD", cwd);
1777 /* See above for the "LIST" vs. "LIST -lag" discussion. */
1778 rc = proxy_ftp_command((cwd == NULL || strchr(cwd, '/') != NULL)
1779 ? "LIST -lag" CRLF : "LIST" CRLF,
1780 r, origin, bb, &ftpmessage);
1782 /* rc is an intermediate response for the LIST command (125 transfer starting, 150 opening data connection) */
1783 if (rc == -1 || rc == 421)
1784 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1785 "Error reading from remote server");
1787 if (rc != 125 && rc != 150 && rc != 226 && rc != 250) {
1788 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1791 r->status = HTTP_OK;
1792 r->status_line = "200 OK";
1794 apr_rfc822_date(dates, r->request_time);
1795 apr_table_setn(r->headers_out, "Date", dates);
1796 apr_table_setn(r->headers_out, "Server", ap_get_server_description());
1798 /* set content-type */
1800 ap_set_content_type(r, apr_pstrcat(p, "text/html;charset=",
1801 fdconf->ftp_directory_charset ?
1802 fdconf->ftp_directory_charset :
1803 "ISO-8859-1", NULL));
1806 if (xfer_type != 'A' && size != NULL) {
1807 /* We "trust" the ftp server to really serve (size) bytes... */
1808 apr_table_setn(r->headers_out, "Content-Length", size);
1809 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
1810 "proxy: FTP: Content-Length set to %s", size);
1813 if (r->content_type) {
1814 apr_table_setn(r->headers_out, "Content-Type", r->content_type);
1815 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
1816 "proxy: FTP: Content-Type set to %s", r->content_type);
1819 #if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
1821 char datestr[APR_RFC822_DATE_LEN];
1822 apr_rfc822_date(datestr, mtime);
1823 apr_table_set(r->headers_out, "Last-Modified", datestr);
1824 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
1825 "proxy: FTP: Last-Modified set to %s", datestr);
1827 #endif /* USE_MDTM */
1829 /* If an encoding has been set by mistake, delete it.
1830 * @@@ FIXME (e.g., for ftp://user@host/file*.tar.gz,
1831 * @@@ the encoding is currently set to x-gzip)
1833 if (dirlisting && r->content_encoding != NULL)
1834 r->content_encoding = NULL;
1836 /* set content-encoding (not for dir listings, they are uncompressed)*/
1837 if (r->content_encoding != NULL && r->content_encoding[0] != '\0') {
1838 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
1839 "proxy: FTP: Content-Encoding set to %s",
1840 r->content_encoding);
1841 apr_table_setn(r->headers_out, "Content-Encoding", r->content_encoding);
1844 /* wait for connection */
1847 rv = apr_socket_accept(&data_sock, local_sock, r->pool);
1848 if (rv == APR_EINTR) {
1851 else if (rv == APR_SUCCESS) {
1855 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1856 "proxy: FTP: failed to accept data connection");
1857 proxy_ftp_cleanup(r, backend);
1858 return HTTP_BAD_GATEWAY;
1863 /* the transfer socket is now open, create a new connection */
1864 data = ap_run_create_connection(p, r->server, data_sock, r->connection->id,
1865 r->connection->sbh, c->bucket_alloc);
1868 * the peer reset the connection already; ap_run_create_connection() closed
1871 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1872 "proxy: FTP: an error occurred creating the transfer connection");
1873 proxy_ftp_cleanup(r, backend);
1874 return HTTP_INTERNAL_SERVER_ERROR;
1878 * We do not do SSL over the data connection, even if the virtual host we
1879 * are in might have SSL enabled
1881 ap_proxy_ssl_disable(data);
1882 /* set up the connection filters */
1883 rc = ap_run_pre_connection(data, data_sock);
1884 if (rc != OK && rc != DONE) {
1885 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1886 "proxy: FTP: pre_connection setup failed (%d)",
1889 proxy_ftp_cleanup(r, backend);
1894 * VI: Receive the Response ------------------------
1896 * Get response from the remote ftp socket, and pass it up the filter chain.
1903 /* insert directory filter */
1904 ap_add_output_filter("PROXY_SEND_DIR", NULL, r, r->connection);
1908 if (!r->header_only) {
1912 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
1913 "proxy: FTP: start body send");
1915 /* read the body, pass it to the output filters */
1916 while (ap_get_brigade(data->input_filters,
1920 conf->io_buffer_size) == APR_SUCCESS) {
1923 apr_off_t readbytes;
1924 apr_brigade_length(bb, 0, &readbytes);
1925 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0,
1926 r->server, "proxy (PID %d): readbytes: %#x",
1927 getpid(), readbytes);
1931 if (APR_BRIGADE_EMPTY(bb)) {
1932 apr_brigade_cleanup(bb);
1936 /* found the last brigade? */
1937 if (APR_BUCKET_IS_EOS(APR_BRIGADE_LAST(bb))) {
1938 /* if this is the last brigade, cleanup the
1939 * backend connection first to prevent the
1940 * backend server from hanging around waiting
1941 * for a slow client to eat these bytes
1943 ap_flush_conn(data);
1945 apr_socket_close(data_sock);
1948 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1949 "proxy: FTP: data connection closed");
1950 /* signal that we must leave */
1954 /* if no EOS yet, then we must flush */
1955 if (FALSE == finish) {
1956 e = apr_bucket_flush_create(c->bucket_alloc);
1957 APR_BRIGADE_INSERT_TAIL(bb, e);
1960 /* try send what we read */
1961 if (ap_pass_brigade(r->output_filters, bb) != APR_SUCCESS
1963 /* Ack! Phbtt! Die! User aborted! */
1967 /* make sure we always clean up after ourselves */
1968 apr_brigade_cleanup(bb);
1970 /* if we are done, leave */
1971 if (TRUE == finish) {
1975 ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
1976 "proxy: FTP: end body send");
1980 ap_flush_conn(data);
1981 apr_socket_close(data_sock);
1982 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1983 "proxy: FTP: data connection closed");
1986 /* Retrieve the final response for the RETR or LIST commands */
1987 rc = proxy_ftp_command(NULL, r, origin, bb, &ftpmessage);
1988 apr_brigade_cleanup(bb);
1991 * VII: Clean Up -------------
1993 * If there are no KeepAlives, or if the connection has been signalled to
1994 * close, close the socket and clean up
1998 rc = proxy_ftp_command("QUIT" CRLF,
1999 r, origin, bb, &ftpmessage);
2000 /* responses: 221, 500 */
2001 /* 221 Service closing control connection. */
2002 /* 500 Syntax error, command unrecognized. */
2003 ap_flush_conn(origin);
2004 proxy_ftp_cleanup(r, backend);
2006 apr_brigade_destroy(bb);
2010 static void ap_proxy_ftp_register_hook(apr_pool_t *p)
2013 proxy_hook_scheme_handler(proxy_ftp_handler, NULL, NULL, APR_HOOK_MIDDLE);
2014 proxy_hook_canon_handler(proxy_ftp_canon, NULL, NULL, APR_HOOK_MIDDLE);
2016 ap_register_output_filter("PROXY_SEND_DIR", proxy_send_dir_filter,
2017 NULL, AP_FTYPE_RESOURCE);
2020 static const command_rec proxy_ftp_cmds[] =
2022 AP_INIT_FLAG("ProxyFtpListOnWildcard", set_ftp_list_on_wildcard, NULL,
2023 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."),
2024 AP_INIT_FLAG("ProxyFtpEscapeWildcards", set_ftp_escape_wildcards, NULL,
2025 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."),
2026 AP_INIT_TAKE1("ProxyFtpDirCharset", set_ftp_directory_charset, NULL,
2027 RSRC_CONF|ACCESS_CONF, "Define the character set for proxied FTP listings"),
2032 AP_DECLARE_MODULE(proxy_ftp) = {
2033 STANDARD20_MODULE_STUFF,
2034 create_proxy_ftp_dir_config,/* create per-directory config structure */
2035 merge_proxy_ftp_dir_config, /* merge per-directory config structures */
2036 NULL, /* create per-server config structure */
2037 NULL, /* merge per-server config structures */
2038 proxy_ftp_cmds, /* command apr_table_t */
2039 ap_proxy_ftp_register_hook /* register hooks */