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"
24 #define AUTODETECT_PWD
25 /* Automatic timestamping (Last-Modified header) based on MDTM is used if:
26 * 1) the FTP server supports the MDTM command and
27 * 2) HAVE_TIMEGM (preferred) or HAVE_GMTOFF is available at compile time
32 module AP_MODULE_DECLARE_DATA proxy_ftp_module;
34 int ap_proxy_ftp_canon(request_rec *r, char *url);
35 int ap_proxy_ftp_handler(request_rec *r, proxy_server_conf *conf,
36 char *url, const char *proxyhost,
37 apr_port_t proxyport);
38 apr_status_t ap_proxy_send_dir_filter(ap_filter_t * f,
39 apr_bucket_brigade *bb);
43 * Decodes a '%' escaped string, and returns the number of characters
45 static int decodeenc(char *x)
50 return 0; /* special case for no characters */
51 for (i = 0, j = 0; x[i] != '\0'; i++, j++) {
52 /* decode it if not already done */
54 if (ch == '%' && apr_isxdigit(x[i + 1]) && apr_isxdigit(x[i + 2])) {
55 ch = ap_proxy_hex2c(&x[i + 1]);
65 * Escape the globbing characters in a path used as argument to
66 * the FTP commands (SIZE, CWD, RETR, MDTM, ...).
67 * ftpd assumes '\\' as a quoting character to escape special characters.
68 * Returns: escaped string
70 #define FTP_GLOBBING_CHARS "*?[{~"
71 static char *ftp_escape_globbingchars(apr_pool_t *p, const char *path)
73 char *ret = apr_palloc(p, 2*strlen(path)+sizeof(""));
75 for (d = ret; *path; ++path) {
76 if (strchr(FTP_GLOBBING_CHARS, *path) != NULL)
85 * Check for globbing characters in a path used as argument to
86 * the FTP commands (SIZE, CWD, RETR, MDTM, ...).
87 * ftpd assumes '\\' as a quoting character to escape special characters.
88 * Returns: 0 (no globbing chars, or all globbing chars escaped), 1 (globbing chars)
90 static int ftp_check_globbingchars(const char *path)
92 for ( ; *path; ++path) {
95 if (*path != '\0' && strchr(FTP_GLOBBING_CHARS, *path) != NULL)
102 * checks an encoded ftp string for bad characters, namely, CR, LF or
103 * non-ascii character
105 static int ftp_check_string(const char *x)
108 #if APR_CHARSET_EBCDIC
112 for (i = 0; x[i] != '\0'; i++) {
114 if (ch == '%' && apr_isxdigit(x[i + 1]) && apr_isxdigit(x[i + 2])) {
115 ch = ap_proxy_hex2c(&x[i + 1]);
118 #if !APR_CHARSET_EBCDIC
119 if (ch == '\015' || ch == '\012' || (ch & 0x80))
120 #else /* APR_CHARSET_EBCDIC */
121 if (ch == '\r' || ch == '\n')
124 ap_xlate_proto_to_ascii(buf, 1);
126 #endif /* APR_CHARSET_EBCDIC */
133 * Canonicalise ftp URLs.
135 int ap_proxy_ftp_canon(request_rec *r, char *url)
137 char *user, *password, *host, *path, *parms, *strp, sport[7];
138 apr_pool_t *p = r->pool;
140 apr_port_t port, def_port;
143 if (strncasecmp(url, "ftp:", 4) == 0) {
149 def_port = apr_uri_port_of_scheme("ftp");
151 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
152 "proxy: FTP: canonicalising URL %s", url);
155 err = ap_proxy_canon_netloc(p, &url, &user, &password, &host, &port);
157 return HTTP_BAD_REQUEST;
158 if (user != NULL && !ftp_check_string(user))
159 return HTTP_BAD_REQUEST;
160 if (password != NULL && !ftp_check_string(password))
161 return HTTP_BAD_REQUEST;
163 /* now parse path/parameters args, according to rfc1738 */
165 * N.B. if this isn't a true proxy request, then the URL path (but not
166 * query args) has already been decoded. This gives rise to the problem
167 * of a ; being decoded into the path.
169 strp = strchr(url, ';');
172 parms = ap_proxy_canonenc(p, strp, strlen(strp), enc_parm,
175 return HTTP_BAD_REQUEST;
180 path = ap_proxy_canonenc(p, url, strlen(url), enc_path, r->proxyreq);
182 return HTTP_BAD_REQUEST;
183 if (!ftp_check_string(path))
184 return HTTP_BAD_REQUEST;
186 if (r->proxyreq && r->args != NULL) {
188 strp = ap_proxy_canonenc(p, r->args, strlen(r->args), enc_parm, 1);
190 return HTTP_BAD_REQUEST;
191 parms = apr_pstrcat(p, parms, "?", strp, NULL);
194 strp = ap_proxy_canonenc(p, r->args, strlen(r->args), enc_fpath, 1);
196 return HTTP_BAD_REQUEST;
197 path = apr_pstrcat(p, path, "?", strp, NULL);
202 /* now, rebuild URL */
204 if (port != def_port)
205 apr_snprintf(sport, sizeof(sport), ":%d", port);
209 if (ap_strchr_c(host, ':')) { /* if literal IPv6 address */
210 host = apr_pstrcat(p, "[", host, "]", NULL);
212 r->filename = apr_pstrcat(p, "proxy:ftp://", (user != NULL) ? user : "",
213 (password != NULL) ? ":" : "",
214 (password != NULL) ? password : "",
215 (user != NULL) ? "@" : "", host, sport, "/", path,
216 (parms[0] != '\0') ? ";" : "", parms, NULL);
221 /* we chop lines longer than 80 characters */
222 #define MAX_LINE_LEN 80
225 * Reads response lines, returns both the ftp status code and
226 * remembers the response message in the supplied buffer
228 static int ftp_getrc_msg(conn_rec *ftp_ctrl, apr_bucket_brigade *bb, char *msgbuf, int msglen)
231 char response[MAX_LINE_LEN];
233 char *mb = msgbuf, *me = &msgbuf[msglen];
237 if (APR_SUCCESS != (rv = ap_proxy_string_read(ftp_ctrl, bb, response, sizeof(response), &eos))) {
241 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, NULL,
242 "proxy: <FTP: %s", response);
244 if (!apr_isdigit(response[0]) || !apr_isdigit(response[1]) ||
245 !apr_isdigit(response[2]) || (response[3] != ' ' && response[3] != '-'))
248 status = 100 * response[0] + 10 * response[1] + response[2] - 111 * '0';
250 mb = apr_cpystrn(mb, response + 4, me - mb);
252 if (response[3] == '-') {
253 memcpy(buff, response, 3);
256 if (APR_SUCCESS != (rv = ap_proxy_string_read(ftp_ctrl, bb, response, sizeof(response), &eos))) {
259 mb = apr_cpystrn(mb, response + (' ' == response[0] ? 1 : 4), me - mb);
260 } while (memcmp(response, buff, 4) != 0);
266 /* this is a filter that turns a raw ASCII directory listing into pretty HTML */
268 /* ideally, mod_proxy should simply send the raw directory list up the filter
269 * stack to mod_autoindex, which in theory should turn the raw ascii into
270 * pretty html along with all the bells and whistles it provides...
272 * all in good time...! :)
276 apr_bucket_brigade *in;
277 char buffer[MAX_STRING_LEN];
283 /* fallback regex for ls -s1; ($0..$2) == 3 */
284 #define LS_REG_PATTERN "^ *([0-9]+) +([^ ]+)$"
285 #define LS_REG_MATCH 3
287 apr_status_t ap_proxy_send_dir_filter(ap_filter_t *f, apr_bucket_brigade *in)
289 request_rec *r = f->r;
290 conn_rec *c = r->connection;
291 apr_pool_t *p = r->pool;
292 apr_bucket_brigade *out = apr_brigade_create(p, c->bucket_alloc);
296 char *dir, *path, *reldir, *site, *str, *type;
298 const char *pwd = apr_table_get(r->notes, "Directory-PWD");
299 const char *readme = apr_table_get(r->notes, "Directory-README");
301 proxy_dir_ctx_t *ctx = f->ctx;
304 f->ctx = ctx = apr_pcalloc(p, sizeof(*ctx));
305 ctx->in = apr_brigade_create(p, c->bucket_alloc);
310 /* combine the stored and the new */
311 APR_BRIGADE_CONCAT(ctx->in, in);
313 if (HEADER == ctx->state) {
315 /* basedir is either "", or "/%2f" for the "squid %2f hack" */
316 const char *basedir = ""; /* By default, path is relative to the $HOME dir */
317 char *wildcard = NULL;
319 /* Save "scheme://site" prefix without password */
320 site = apr_uri_unparse(p, &f->r->parsed_uri, APR_URI_UNP_OMITPASSWORD | APR_URI_UNP_OMITPATHINFO);
321 /* ... and path without query args */
322 path = apr_uri_unparse(p, &f->r->parsed_uri, APR_URI_UNP_OMITSITEPART | APR_URI_UNP_OMITQUERY);
324 /* If path began with /%2f, change the basedir */
325 if (strncasecmp(path, "/%2f", 4) == 0) {
329 /* Strip off a type qualifier. It is ignored for dir listings */
330 if ((type = strstr(path, ";type=")) != NULL)
333 (void)decodeenc(path);
335 while (path[1] == '/') /* collapse multiple leading slashes to one */
338 reldir = strrchr(path, '/');
339 if (reldir != NULL && ftp_check_globbingchars(reldir)) {
340 wildcard = &reldir[1];
341 reldir[0] = '\0'; /* strip off the wildcard suffix */
344 /* Copy path, strip (all except the last) trailing slashes */
345 /* (the trailing slash is needed for the dir component loop below) */
346 path = dir = apr_pstrcat(p, path, "/", NULL);
347 for (n = strlen(path); n > 1 && path[n - 1] == '/' && path[n - 2] == '/'; --n)
350 /* Add a link to the root directory (if %2f hack was used) */
351 str = (basedir[0] != '\0') ? "<a href=\"/%2f/\">%2f</a>/" : "";
353 /* print "ftp://host/" */
354 str = apr_psprintf(p, DOCTYPE_HTML_3_2
355 "<html>\n <head>\n <title>%s%s%s</title>\n"
357 " <body>\n <h2>Directory of "
358 "<a href=\"/\">%s</a>/%s",
359 site, basedir, ap_escape_html(p, path),
362 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str),
363 p, c->bucket_alloc));
365 for (dir = path+1; (dir = strchr(dir, '/')) != NULL; )
368 if ((reldir = strrchr(path+1, '/'))==NULL) {
373 /* print "path/" component */
374 str = apr_psprintf(p, "<a href=\"%s%s/\">%s</a>/", basedir,
375 ap_escape_uri(p, path),
376 ap_escape_html(p, reldir));
380 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str,
384 if (wildcard != NULL) {
385 wildcard = ap_escape_html(p, wildcard);
386 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(wildcard,
391 /* If the caller has determined the current directory, and it differs */
392 /* from what the client requested, then show the real name */
393 if (pwd == NULL || strncmp(pwd, path, strlen(pwd)) == 0) {
394 str = apr_psprintf(p, "</h2>\n\n <hr />\n\n<pre>");
397 str = apr_psprintf(p, "</h2>\n\n(%s)\n\n <hr />\n\n<pre>",
398 ap_escape_html(p, pwd));
400 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str),
401 p, c->bucket_alloc));
405 str = apr_psprintf(p, "%s\n</pre>\n\n<hr />\n\n<pre>\n",
406 ap_escape_html(p, readme));
408 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str,
413 /* make sure page intro gets sent out */
414 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_flush_create(c->bucket_alloc));
415 if (APR_SUCCESS != (rv = ap_pass_brigade(f->next, out))) {
418 apr_brigade_cleanup(out);
423 /* loop through each line of directory */
424 while (BODY == ctx->state) {
430 regmatch_t re_result[LS_REG_MATCH];
432 /* Compile the output format of "ls -s1" as a fallback for non-unix ftp listings */
433 re = ap_pregcomp(p, LS_REG_PATTERN, REG_EXTENDED);
434 ap_assert(re != NULL);
436 /* get a complete line */
437 /* if the buffer overruns - throw data away */
438 while (!found && !APR_BRIGADE_EMPTY(ctx->in)) {
439 char *pos, *response;
443 e = APR_BRIGADE_FIRST(ctx->in);
444 if (APR_BUCKET_IS_EOS(e)) {
448 if (APR_SUCCESS != (rv = apr_bucket_read(e, (const char **)&response, &len, APR_BLOCK_READ))) {
451 pos = memchr(response, APR_ASCII_LF, len);
453 if ((response + len) != (pos + 1)) {
454 len = pos - response + 1;
455 apr_bucket_split(e, pos - response + 1);
459 max = sizeof(ctx->buffer) - strlen(ctx->buffer) - 1;
464 /* len+1 to leave space for the trailing nil char */
465 apr_cpystrn(ctx->buffer+strlen(ctx->buffer), response, len+1);
467 APR_BUCKET_REMOVE(e);
468 apr_bucket_destroy(e);
471 /* EOS? jump to footer */
477 /* not complete? leave and try get some more */
483 apr_size_t n = strlen(ctx->buffer);
484 if (ctx->buffer[n-1] == CRLF[1]) /* strip trailing '\n' */
485 ctx->buffer[--n] = '\0';
486 if (ctx->buffer[n-1] == CRLF[0]) /* strip trailing '\r' if present */
487 ctx->buffer[--n] = '\0';
491 if (ctx->buffer[0] == 'l' && (filename = strstr(ctx->buffer, " -> ")) != NULL) {
492 char *link_ptr = filename;
496 } while (filename[0] != ' ' && filename > ctx->buffer);
497 if (filename > ctx->buffer)
498 *(filename++) = '\0';
499 *(link_ptr++) = '\0';
500 str = apr_psprintf(p, "%s <a href=\"%s\">%s %s</a>\n",
501 ap_escape_html(p, ctx->buffer),
502 ap_escape_uri(p, filename),
503 ap_escape_html(p, filename),
504 ap_escape_html(p, link_ptr));
507 /* a directory/file? */
508 else if (ctx->buffer[0] == 'd' || ctx->buffer[0] == '-' || ctx->buffer[0] == 'l' || apr_isdigit(ctx->buffer[0])) {
510 char *searchptr = NULL;
512 if (apr_isdigit(ctx->buffer[0])) { /* handle DOS dir */
513 searchptr = strchr(ctx->buffer, '<');
514 if (searchptr != NULL)
516 searchptr = strchr(ctx->buffer, '>');
517 if (searchptr != NULL)
521 filename = strrchr(ctx->buffer, ' ');
522 *(filename++) = '\0';
524 /* handle filenames with spaces in 'em */
525 if (!strcmp(filename, ".") || !strcmp(filename, "..") || firstfile) {
527 searchidx = filename - ctx->buffer;
529 else if (searchidx != 0 && ctx->buffer[searchidx] != 0) {
531 ctx->buffer[searchidx - 1] = '\0';
532 filename = &ctx->buffer[searchidx];
535 /* Append a slash to the HREF link for directories */
536 if (!strcmp(filename, ".") || !strcmp(filename, "..") || ctx->buffer[0] == 'd') {
537 str = apr_psprintf(p, "%s <a href=\"%s/\">%s</a>\n",
538 ap_escape_html(p, ctx->buffer),
539 ap_escape_uri(p, filename),
540 ap_escape_html(p, filename));
543 str = apr_psprintf(p, "%s <a href=\"%s\">%s</a>\n",
544 ap_escape_html(p, ctx->buffer),
545 ap_escape_uri(p, filename),
546 ap_escape_html(p, filename));
549 /* Try a fallback for listings in the format of "ls -s1" */
550 else if (0 == ap_regexec(re, ctx->buffer, LS_REG_MATCH, re_result, 0)) {
552 filename = apr_pstrndup(p, &ctx->buffer[re_result[2].rm_so], re_result[2].rm_eo - re_result[2].rm_so);
554 str = apr_pstrcat(p, ap_escape_html(p, apr_pstrndup(p, ctx->buffer, re_result[2].rm_so)),
555 "<a href=\"", ap_escape_uri(p, filename), "\">",
556 ap_escape_html(p, filename), "</a>\n", NULL);
559 strcat(ctx->buffer, "\n"); /* re-append the newline */
560 str = ap_escape_html(p, ctx->buffer);
563 /* erase buffer for next time around */
566 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str), p,
568 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_flush_create(c->bucket_alloc));
569 if (APR_SUCCESS != (rv = ap_pass_brigade(f->next, out))) {
572 apr_brigade_cleanup(out);
576 if (FOOTER == ctx->state) {
577 str = apr_psprintf(p, "</pre>\n\n <hr />\n\n %s\n\n </body>\n</html>\n", ap_psignature("", r));
578 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str), p,
580 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_flush_create(c->bucket_alloc));
581 APR_BRIGADE_INSERT_TAIL(out, apr_bucket_eos_create(c->bucket_alloc));
582 if (APR_SUCCESS != (rv = ap_pass_brigade(f->next, out))) {
585 apr_brigade_destroy(out);
591 /* Parse EPSV reply and return port, or zero on error. */
592 static apr_port_t parse_epsv_reply(const char *reply)
598 /* Reply syntax per RFC 2428: "229 blah blah (|||port|)" where '|'
599 * can be any character in ASCII from 33-126, obscurely. Verify
601 p = ap_strchr_c(reply, '(');
602 if (p == NULL || !p[1] || p[1] != p[2] || p[1] != p[3]
608 port = strtol(p + 4, &ep, 10);
609 if (errno || port < 1 || port > 65535 || ep[0] != p[1] || ep[1] != ')') {
613 return (apr_port_t)port;
617 * Generic "send FTP command to server" routine, using the control socket.
618 * Returns the FTP returncode (3 digit code)
619 * Allows for tracing the FTP protocol (in LogLevel debug)
622 proxy_ftp_command(const char *cmd, request_rec *r, conn_rec *ftp_ctrl,
623 apr_bucket_brigade *bb, char **pmessage)
627 char message[HUGE_STRING_LEN];
629 /* If cmd == NULL, we retrieve the next ftp response line */
631 conn_rec *c = r->connection;
632 APR_BRIGADE_INSERT_TAIL(bb, apr_bucket_pool_create(cmd, strlen(cmd), r->pool, c->bucket_alloc));
633 APR_BRIGADE_INSERT_TAIL(bb, apr_bucket_flush_create(c->bucket_alloc));
634 ap_pass_brigade(ftp_ctrl->output_filters, bb);
636 /* strip off the CRLF for logging */
637 apr_cpystrn(message, cmd, sizeof(message));
638 if ((crlf = strchr(message, '\r')) != NULL ||
639 (crlf = strchr(message, '\n')) != NULL)
641 if (strncmp(message,"PASS ", 5) == 0)
642 strcpy(&message[5], "****");
643 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
644 "proxy:>FTP: %s", message);
647 rc = ftp_getrc_msg(ftp_ctrl, bb, message, sizeof message);
648 if (rc == -1 || rc == 421)
649 strcpy(message,"<unable to read result>");
650 if ((crlf = strchr(message, '\r')) != NULL ||
651 (crlf = strchr(message, '\n')) != NULL)
653 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
654 "proxy:<FTP: %3.3u %s", rc, message);
656 if (pmessage != NULL)
657 *pmessage = apr_pstrdup(r->pool, message);
662 /* Set ftp server to TYPE {A,I,E} before transfer of a directory or file */
663 static int ftp_set_TYPE(char xfer_type, request_rec *r, conn_rec *ftp_ctrl,
664 apr_bucket_brigade *bb, char **pmessage)
666 char old_type[2] = { 'A', '\0' }; /* After logon, mode is ASCII */
670 /* set desired type */
671 old_type[0] = xfer_type;
673 rc = proxy_ftp_command(apr_pstrcat(r->pool, "TYPE ", old_type, CRLF, NULL),
674 r, ftp_ctrl, bb, pmessage);
675 /* responses: 200, 421, 500, 501, 504, 530 */
676 /* 200 Command okay. */
677 /* 421 Service not available, closing control connection. */
678 /* 500 Syntax error, command unrecognized. */
679 /* 501 Syntax error in parameters or arguments. */
680 /* 504 Command not implemented for that parameter. */
681 /* 530 Not logged in. */
682 if (rc == -1 || rc == 421) {
683 ret = ap_proxyerror(r, HTTP_BAD_GATEWAY,
684 "Error reading from remote server");
686 else if (rc != 200 && rc != 504) {
687 ret = ap_proxyerror(r, HTTP_BAD_GATEWAY,
688 "Unable to set transfer type");
690 /* Allow not implemented */
692 /* ignore it silently */;
698 /* Return the current directory which we have selected on the FTP server, or NULL */
699 static char *ftp_get_PWD(request_rec *r, conn_rec *ftp_ctrl, apr_bucket_brigade *bb)
702 char *ftpmessage = NULL;
704 /* responses: 257, 500, 501, 502, 421, 550 */
705 /* 257 "<directory-name>" <commentary> */
706 /* 421 Service not available, closing control connection. */
707 /* 500 Syntax error, command unrecognized. */
708 /* 501 Syntax error in parameters or arguments. */
709 /* 502 Command not implemented. */
710 /* 550 Requested action not taken. */
711 switch (proxy_ftp_command("PWD" CRLF, r, ftp_ctrl, bb, &ftpmessage)) {
715 ap_proxyerror(r, HTTP_BAD_GATEWAY,
716 "Failed to read PWD on ftp server");
720 const char *dirp = ftpmessage;
721 cwd = ap_getword_conf(r->pool, &dirp);
728 /* Common routine for failed authorization (i.e., missing or wrong password)
729 * to an ftp service. This causes most browsers to retry the request
730 * with username and password (which was presumably queried from the user)
731 * supplied in the Authorization: header.
732 * Note that we "invent" a realm name which consists of the
733 * ftp://user@host part of the reqest (sans password -if supplied but invalid-)
735 static int ftp_unauthorized(request_rec *r, int log_it)
737 r->proxyreq = PROXYREQ_NONE;
739 * Log failed requests if they supplied a password (log username/password
743 ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r,
744 "proxy: missing or failed auth to %s",
745 apr_uri_unparse(r->pool,
746 &r->parsed_uri, APR_URI_UNP_OMITPATHINFO));
748 apr_table_setn(r->err_headers_out, "WWW-Authenticate",
749 apr_pstrcat(r->pool, "Basic realm=\"",
750 apr_uri_unparse(r->pool, &r->parsed_uri,
751 APR_URI_UNP_OMITPASSWORD | APR_URI_UNP_OMITPATHINFO),
754 return HTTP_UNAUTHORIZED;
759 * Handles direct access of ftp:// URLs
760 * Original (Non-PASV) version from
761 * Troy Morrison <spiffnet@zoom.com>
762 * PASV added by Chuck
763 * Filters by [Graham Leggett <minfrin@sharp.fm>]
765 int ap_proxy_ftp_handler(request_rec *r, proxy_server_conf *conf,
766 char *url, const char *proxyhost,
767 apr_port_t proxyport)
769 apr_pool_t *p = r->pool;
770 conn_rec *c = r->connection;
771 proxy_conn_rec *backend;
772 apr_socket_t *sock, *local_sock, *data_sock = NULL;
773 apr_sockaddr_t *connect_addr;
775 conn_rec *origin, *data = NULL;
777 apr_bucket_brigade *bb = apr_brigade_create(p, c->bucket_alloc);
778 char *buf, *connectname;
779 apr_port_t connectport;
780 char buffer[MAX_STRING_LEN];
781 char *ftpmessage = NULL;
782 char *path, *strp, *type_suffix, *cwd = NULL;
785 /* char *account = NULL; how to supply an account in a URL? */
786 const char *password = NULL;
790 apr_socket_t *origin_sock = NULL;
791 char xfer_type = 'A'; /* after ftp login, the default is ASCII */
793 #if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
794 apr_time_t mtime = 0L;
797 /* stuff for PASV mode */
798 int connect = 0, use_port = 0;
799 char dates[APR_RFC822_DATE_LEN];
801 /* is this for us? */
803 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
804 "proxy: FTP: declining URL %s - proxyhost %s specified:", url, proxyhost);
805 return DECLINED; /* proxy connections are via HTTP */
807 if (strncasecmp(url, "ftp:", 4)) {
808 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
809 "proxy: FTP: declining URL %s - not ftp:", url);
810 return DECLINED; /* only interested in FTP */
812 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
813 "proxy: FTP: serving URL %s", url);
815 /* create space for state information */
816 backend = (proxy_conn_rec *) ap_get_module_config(c->conn_config, &proxy_ftp_module);
818 backend = apr_pcalloc(c->pool, sizeof(proxy_conn_rec));
819 backend->connection = NULL;
820 backend->hostname = NULL;
822 ap_set_module_config(c->conn_config, &proxy_ftp_module, backend);
824 if (backend->connection)
825 origin_sock = ap_get_module_config(backend->connection->conn_config, &core_module);
829 * I: Who Do I Connect To? -----------------------
831 * Break up the URL to determine the host to connect to
834 /* we only support GET and HEAD */
835 if (r->method_number != M_GET)
836 return HTTP_NOT_IMPLEMENTED;
838 /* We break the URL into host, port, path-search */
839 if (r->parsed_uri.hostname == NULL) {
840 if (APR_SUCCESS != apr_uri_parse(p, url, &uri)) {
841 return ap_proxyerror(r, HTTP_BAD_REQUEST,
842 apr_psprintf(p, "URI cannot be parsed: %s", url));
844 connectname = uri.hostname;
845 connectport = uri.port;
846 path = apr_pstrdup(p, uri.path);
849 connectname = r->parsed_uri.hostname;
850 connectport = r->parsed_uri.port;
851 path = apr_pstrdup(p, r->parsed_uri.path);
853 if (connectport == 0) {
854 connectport = apr_uri_port_of_scheme("ftp");
856 path = (path != NULL && path[0] != '\0') ? &path[1] : "";
858 type_suffix = strchr(path, ';');
859 if (type_suffix != NULL)
860 *(type_suffix++) = '\0';
862 if (type_suffix != NULL && strncmp(type_suffix, "type=", 5) == 0
863 && apr_isalpha(type_suffix[5])) {
864 /* "type=d" forces a dir listing.
865 * The other types (i|a|e) are directly used for the ftp TYPE command
867 if ( ! (dirlisting = (apr_tolower(type_suffix[5]) == 'd')))
868 xfer_type = apr_toupper(type_suffix[5]);
870 /* Check valid types, rather than ignoring invalid types silently: */
871 if (strchr("AEI", xfer_type) == NULL)
872 return ap_proxyerror(r, HTTP_BAD_REQUEST, apr_pstrcat(r->pool,
873 "ftp proxy supports only types 'a', 'i', or 'e': \"",
874 type_suffix, "\" is invalid.", NULL));
877 /* make binary transfers the default */
883 * The "Authorization:" header must be checked first. We allow the user
884 * to "override" the URL-coded user [ & password ] in the Browsers'
885 * User&Password Dialog. NOTE that this is only marginally more secure
886 * than having the password travel in plain as part of the URL, because
887 * Basic Auth simply uuencodes the plain text password. But chances are
888 * still smaller that the URL is logged regularly.
890 if ((password = apr_table_get(r->headers_in, "Authorization")) != NULL
891 && strcasecmp(ap_getword(r->pool, &password, ' '), "Basic") == 0
892 && (password = ap_pbase64decode(r->pool, password))[0] != ':') {
893 /* Check the decoded string for special characters. */
894 if (!ftp_check_string(password)) {
895 return ap_proxyerror(r, HTTP_BAD_REQUEST,
896 "user credentials contained invalid character");
899 * Note that this allocation has to be made from r->connection->pool
900 * because it has the lifetime of the connection. The other
901 * allocations are temporary and can be tossed away any time.
903 user = ap_getword_nulls(r->connection->pool, &password, ':');
904 r->ap_auth_type = "Basic";
905 r->user = r->parsed_uri.user = user;
907 else if ((user = r->parsed_uri.user) != NULL) {
908 user = apr_pstrdup(p, user);
910 if ((password = r->parsed_uri.password) != NULL) {
911 char *tmp = apr_pstrdup(p, password);
918 password = "apache-proxy@";
921 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
922 "proxy: FTP: connecting %s to %s:%d", url, connectname, connectport);
924 /* do a DNS lookup for the destination host */
925 err = apr_sockaddr_info_get(&connect_addr, connectname, APR_UNSPEC, connectport, 0, p);
927 /* check if ProxyBlock directive on this host */
928 if (OK != ap_proxy_checkproxyblock(r, conf, connect_addr)) {
929 return ap_proxyerror(r, HTTP_FORBIDDEN,
930 "Connect to remote machine blocked");
935 * II: Make the Connection -----------------------
937 * We have determined who to connect to. Now make the connection.
941 * get all the possible IP addresses for the destname and loop through
942 * them until we get a successful connection
944 if (APR_SUCCESS != err) {
945 return ap_proxyerror(r, HTTP_BAD_GATEWAY, apr_pstrcat(p,
946 "DNS lookup failure for: ",
951 * At this point we have a list of one or more IP addresses of the
952 * machine to connect to. If configured, reorder this list so that the
953 * "best candidate" is first try. "best candidate" could mean the least
954 * loaded server, the fastest responding server, whatever.
956 * For now we do nothing, ie we get DNS round robin. XXX FIXME
960 /* try each IP address until we connect successfully */
963 while (connect_addr) {
965 if ((rv = apr_socket_create(&sock, connect_addr->family, SOCK_STREAM, r->pool)) != APR_SUCCESS) {
966 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
967 "proxy: FTP: error creating socket");
968 connect_addr = connect_addr->next;
972 #if !defined(TPF) && !defined(BEOS)
973 if (conf->recv_buffer_size > 0
974 && (rv = apr_socket_opt_set(sock, APR_SO_RCVBUF,
975 conf->recv_buffer_size))) {
976 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
977 "apr_socket_opt_set(APR_SO_RCVBUF): Failed to set ProxyReceiveBufferSize, using default");
981 if (APR_SUCCESS != (rv = apr_socket_opt_set(sock, APR_SO_REUSEADDR, one))) {
982 apr_socket_close(sock);
983 #ifndef _OSD_POSIX /* BS2000 has this option "always on" */
984 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
985 "proxy: FTP: error setting reuseaddr option: apr_socket_opt_set(APR_SO_REUSEADDR)");
986 connect_addr = connect_addr->next;
988 #endif /* _OSD_POSIX */
991 /* Set a timeout on the socket */
992 if (conf->timeout_set == 1) {
993 apr_socket_timeout_set(sock, conf->timeout);
996 apr_socket_timeout_set(sock, r->server->timeout);
999 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1000 "proxy: FTP: fam %d socket created, trying to connect to %pI (%s)...",
1001 connect_addr->family, connect_addr, connectname);
1003 /* make the connection out of the socket */
1004 rv = apr_connect(sock, connect_addr);
1006 /* if an error occurred, loop round and try again */
1007 if (rv != APR_SUCCESS) {
1008 apr_socket_close(sock);
1009 ap_log_error(APLOG_MARK, APLOG_ERR, rv, r->server,
1010 "proxy: FTP: attempt to connect to %pI (%s) failed", connect_addr, connectname);
1011 connect_addr = connect_addr->next;
1015 /* if we get here, all is well */
1020 /* handle a permanent error from the above loop */
1022 return ap_proxyerror(r, HTTP_BAD_GATEWAY, apr_psprintf(r->pool,
1023 "Could not connect to remote machine: %s port %d",
1024 connectname, connectport));
1028 /* the socket is now open, create a new connection */
1029 origin = ap_run_create_connection(p, r->server, sock, r->connection->id,
1030 r->connection->sbh, c->bucket_alloc);
1033 * the peer reset the connection already; ap_run_create_connection() closed
1036 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1037 "proxy: FTP: an error occurred creating a new connection to %pI (%s)", connect_addr, connectname);
1038 return HTTP_INTERNAL_SERVER_ERROR;
1041 /* if a keepalive connection is floating around, close it first! */
1042 /* we might support ftp keepalives later, but not now... */
1043 if (backend->connection) {
1044 apr_socket_close(origin_sock);
1045 backend->connection = NULL;
1049 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1050 "proxy: FTP: control connection complete");
1054 * III: Send Control Request -------------------------
1056 * Log into the ftp server, send the username & password, change to the
1057 * correct directory...
1060 /* set up the connection filters */
1061 rc = ap_run_pre_connection(origin, sock);
1062 if (rc != OK && rc != DONE) {
1063 origin->aborted = 1;
1064 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1065 "proxy: FTP: pre_connection setup failed (%d)",
1070 /* possible results: */
1071 /* 120 Service ready in nnn minutes. */
1072 /* 220 Service ready for new user. */
1073 /* 421 Service not available, closing control connection. */
1074 rc = proxy_ftp_command(NULL, r, origin, bb, &ftpmessage);
1075 if (rc == -1 || rc == 421) {
1076 return ap_proxyerror(r, HTTP_BAD_GATEWAY, "Error reading from remote server");
1080 * RFC2616 states: 14.37 Retry-After
1082 * The Retry-After response-header field can be used with a 503 (Service
1083 * Unavailable) response to indicate how long the service is expected
1084 * to be unavailable to the requesting client. [...] The value of
1085 * this field can be either an HTTP-date or an integer number of
1086 * seconds (in decimal) after the time of the response. Retry-After
1087 * = "Retry-After" ":" ( HTTP-date | delta-seconds )
1089 char *secs_str = ftpmessage;
1092 /* Look for a number, preceded by whitespace */
1094 if ((secs_str==ftpmessage || apr_isspace(secs_str[-1])) &&
1095 apr_isdigit(secs_str[0]))
1097 if (*secs_str != '\0') {
1098 secs = atol(secs_str);
1099 apr_table_add(r->headers_out, "Retry-After",
1100 apr_psprintf(p, "%lu", (unsigned long)(60 * secs)));
1102 return ap_proxyerror(r, HTTP_SERVICE_UNAVAILABLE, ftpmessage);
1105 return ap_proxyerror(r, HTTP_BAD_GATEWAY, ftpmessage);
1108 rc = proxy_ftp_command(apr_pstrcat(p, "USER ", user, CRLF, NULL),
1109 r, origin, bb, &ftpmessage);
1110 /* possible results; 230, 331, 332, 421, 500, 501, 530 */
1111 /* states: 1 - error, 2 - success; 3 - send password, 4,5 fail */
1112 /* 230 User logged in, proceed. */
1113 /* 331 User name okay, need password. */
1114 /* 332 Need account for login. */
1115 /* 421 Service not available, closing control connection. */
1116 /* 500 Syntax error, command unrecognized. */
1117 /* (This may include errors such as command line too long.) */
1118 /* 501 Syntax error in parameters or arguments. */
1119 /* 530 Not logged in. */
1120 if (rc == -1 || rc == 421) {
1121 return ap_proxyerror(r, HTTP_BAD_GATEWAY, "Error reading from remote server");
1124 return ftp_unauthorized(r, 1); /* log it: user name guessing
1127 if (rc != 230 && rc != 331) {
1128 return ap_proxyerror(r, HTTP_BAD_GATEWAY, ftpmessage);
1131 if (rc == 331) { /* send password */
1132 if (password == NULL) {
1133 return ftp_unauthorized(r, 0);
1136 rc = proxy_ftp_command(apr_pstrcat(p, "PASS ", password, CRLF, NULL),
1137 r, origin, bb, &ftpmessage);
1138 /* possible results 202, 230, 332, 421, 500, 501, 503, 530 */
1139 /* 230 User logged in, proceed. */
1140 /* 332 Need account for login. */
1141 /* 421 Service not available, closing control connection. */
1142 /* 500 Syntax error, command unrecognized. */
1143 /* 501 Syntax error in parameters or arguments. */
1144 /* 503 Bad sequence of commands. */
1145 /* 530 Not logged in. */
1146 if (rc == -1 || rc == 421) {
1147 return ap_proxyerror(r, HTTP_BAD_GATEWAY,
1148 "Error reading from remote server");
1151 return ap_proxyerror(r, HTTP_UNAUTHORIZED,
1152 apr_pstrcat(p, "Need account for login: ", ftpmessage, NULL));
1154 /* @@@ questionable -- we might as well return a 403 Forbidden here */
1156 return ftp_unauthorized(r, 1); /* log it: passwd guessing
1159 if (rc != 230 && rc != 202) {
1160 return ap_proxyerror(r, HTTP_BAD_GATEWAY, ftpmessage);
1163 apr_table_set(r->notes, "Directory-README", ftpmessage);
1166 /* Special handling for leading "%2f": this enforces a "cwd /"
1167 * out of the $HOME directory which was the starting point after login
1169 if (strncasecmp(path, "%2f", 3) == 0) {
1171 while (*path == '/') /* skip leading '/' (after root %2f) */
1174 rc = proxy_ftp_command("CWD /" CRLF, r, origin, bb, &ftpmessage);
1175 if (rc == -1 || rc == 421)
1176 return ap_proxyerror(r, HTTP_BAD_GATEWAY,
1177 "Error reading from remote server");
1181 * set the directory (walk directory component by component): this is
1182 * what we must do if we don't know the OS type of the remote machine
1185 strp = strchr(path, '/');
1190 len = decodeenc(path); /* Note! This decodes a %2f -> "/" */
1192 if (strchr(path, '/')) { /* are there now any '/' characters? */
1193 return ap_proxyerror(r, HTTP_BAD_REQUEST,
1194 "Use of /%2f is only allowed at the base directory");
1197 /* NOTE: FTP servers do globbing on the path.
1198 * So we need to escape the URI metacharacters.
1199 * We use a special glob-escaping routine to escape globbing chars.
1200 * We could also have extended gen_test_char.c with a special T_ESCAPE_FTP_PATH
1202 rc = proxy_ftp_command(apr_pstrcat(p, "CWD ",
1203 ftp_escape_globbingchars(p, path), CRLF, NULL),
1204 r, origin, bb, &ftpmessage);
1206 /* responses: 250, 421, 500, 501, 502, 530, 550 */
1207 /* 250 Requested file action okay, completed. */
1208 /* 421 Service not available, closing control connection. */
1209 /* 500 Syntax error, command unrecognized. */
1210 /* 501 Syntax error in parameters or arguments. */
1211 /* 502 Command not implemented. */
1212 /* 530 Not logged in. */
1213 /* 550 Requested action not taken. */
1214 if (rc == -1 || rc == 421) {
1215 return ap_proxyerror(r, HTTP_BAD_GATEWAY,
1216 "Error reading from remote server");
1219 return ap_proxyerror(r, HTTP_NOT_FOUND, ftpmessage);
1222 return ap_proxyerror(r, HTTP_BAD_GATEWAY, ftpmessage);
1229 * IV: Make Data Connection? -------------------------
1231 * Try EPSV, if that fails... try PASV, if that fails... try PORT.
1233 /* this temporarily switches off EPSV/PASV */
1236 /* set up data connection - EPSV */
1238 apr_sockaddr_t *data_addr;
1240 apr_port_t data_port;
1243 * The EPSV command replaces PASV where both IPV4 and IPV6 is
1244 * supported. Only the port is returned, the IP address is always the
1245 * same as that on the control connection. Example: Entering Extended
1246 * Passive Mode (|||6446|)
1248 rc = proxy_ftp_command("EPSV" CRLF,
1249 r, origin, bb, &ftpmessage);
1250 /* possible results: 227, 421, 500, 501, 502, 530 */
1251 /* 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). */
1252 /* 421 Service not available, closing control connection. */
1253 /* 500 Syntax error, command unrecognized. */
1254 /* 501 Syntax error in parameters or arguments. */
1255 /* 502 Command not implemented. */
1256 /* 530 Not logged in. */
1257 if (rc == -1 || rc == 421) {
1258 return ap_proxyerror(r, HTTP_BAD_GATEWAY,
1259 "Error reading from remote server");
1261 if (rc != 229 && rc != 500 && rc != 501 && rc != 502) {
1262 return ap_proxyerror(r, HTTP_BAD_GATEWAY, ftpmessage);
1264 else if (rc == 229) {
1265 /* Parse the port out of the EPSV reply. */
1266 data_port = parse_epsv_reply(ftpmessage);
1269 apr_sockaddr_t *epsv_addr;
1271 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1272 "proxy: FTP: EPSV contacting remote host on port %d",
1275 if ((rv = apr_socket_create(&data_sock, connect_addr->family, SOCK_STREAM, r->pool)) != APR_SUCCESS) {
1276 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1277 "proxy: FTP: error creating EPSV socket");
1278 return HTTP_INTERNAL_SERVER_ERROR;
1281 #if !defined (TPF) && !defined(BEOS)
1282 if (conf->recv_buffer_size > 0
1283 && (rv = apr_socket_opt_set(data_sock, APR_SO_RCVBUF,
1284 conf->recv_buffer_size))) {
1285 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1286 "proxy: FTP: apr_socket_opt_set(SO_RCVBUF): Failed to set ProxyReceiveBufferSize, using default");
1290 /* make the connection */
1291 apr_socket_addr_get(&data_addr, APR_REMOTE, sock);
1292 apr_sockaddr_ip_get(&data_ip, data_addr);
1293 apr_sockaddr_info_get(&epsv_addr, data_ip, connect_addr->family, data_port, 0, p);
1294 rv = apr_connect(data_sock, epsv_addr);
1295 if (rv != APR_SUCCESS) {
1296 ap_log_error(APLOG_MARK, APLOG_ERR, rv, r->server,
1297 "proxy: FTP: EPSV attempt to connect to %pI failed - Firewall/NAT?", epsv_addr);
1298 return ap_proxyerror(r, HTTP_BAD_GATEWAY, apr_psprintf(r->pool,
1299 "EPSV attempt to connect to %pI failed - firewall/NAT?", epsv_addr));
1308 /* set up data connection - PASV */
1310 rc = proxy_ftp_command("PASV" CRLF,
1311 r, origin, bb, &ftpmessage);
1312 /* possible results: 227, 421, 500, 501, 502, 530 */
1313 /* 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). */
1314 /* 421 Service not available, closing control connection. */
1315 /* 500 Syntax error, command unrecognized. */
1316 /* 501 Syntax error in parameters or arguments. */
1317 /* 502 Command not implemented. */
1318 /* 530 Not logged in. */
1319 if (rc == -1 || rc == 421) {
1320 return ap_proxyerror(r, HTTP_BAD_GATEWAY,
1321 "Error reading from remote server");
1323 if (rc != 227 && rc != 502) {
1324 return ap_proxyerror(r, HTTP_BAD_GATEWAY, ftpmessage);
1326 else if (rc == 227) {
1327 unsigned int h0, h1, h2, h3, p0, p1;
1331 /* FIXME: Check PASV against RFC1123 */
1334 pstr = apr_strtok(pstr, " ", &tok_cntx); /* separate result code */
1336 if (*(pstr + strlen(pstr) + 1) == '=') {
1337 pstr += strlen(pstr) + 2;
1340 pstr = apr_strtok(NULL, "(", &tok_cntx); /* separate address &
1343 pstr = apr_strtok(NULL, ")", &tok_cntx);
1347 /* FIXME: Only supports IPV4 - fix in RFC2428 */
1349 if (pstr != NULL && (sscanf(pstr,
1350 "%d,%d,%d,%d,%d,%d", &h3, &h2, &h1, &h0, &p1, &p0) == 6)) {
1352 apr_sockaddr_t *pasv_addr;
1353 apr_port_t pasvport = (p1 << 8) + p0;
1354 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1355 "proxy: FTP: PASV contacting host %d.%d.%d.%d:%d",
1356 h3, h2, h1, h0, pasvport);
1358 if ((rv = apr_socket_create(&data_sock, connect_addr->family, SOCK_STREAM, r->pool)) != APR_SUCCESS) {
1359 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1360 "proxy: error creating PASV socket");
1361 return HTTP_INTERNAL_SERVER_ERROR;
1364 #if !defined (TPF) && !defined(BEOS)
1365 if (conf->recv_buffer_size > 0
1366 && (rv = apr_socket_opt_set(data_sock, APR_SO_RCVBUF,
1367 conf->recv_buffer_size))) {
1368 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1369 "proxy: FTP: apr_socket_opt_set(SO_RCVBUF): Failed to set ProxyReceiveBufferSize, using default");
1373 /* make the connection */
1374 apr_sockaddr_info_get(&pasv_addr, apr_psprintf(p, "%d.%d.%d.%d", h3, h2, h1, h0), connect_addr->family, pasvport, 0, p);
1375 rv = apr_connect(data_sock, pasv_addr);
1376 if (rv != APR_SUCCESS) {
1377 ap_log_error(APLOG_MARK, APLOG_ERR, rv, r->server,
1378 "proxy: FTP: PASV attempt to connect to %pI failed - Firewall/NAT?", pasv_addr);
1379 return ap_proxyerror(r, HTTP_BAD_GATEWAY, apr_psprintf(r->pool,
1380 "PASV attempt to connect to %pI failed - firewall/NAT?", pasv_addr));
1390 /* set up data connection - PORT */
1392 apr_sockaddr_t *local_addr;
1394 apr_port_t local_port;
1395 unsigned int h0, h1, h2, h3, p0, p1;
1397 if ((rv = apr_socket_create(&local_sock, connect_addr->family, SOCK_STREAM, r->pool)) != APR_SUCCESS) {
1398 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1399 "proxy: FTP: error creating local socket");
1400 return HTTP_INTERNAL_SERVER_ERROR;
1402 apr_socket_addr_get(&local_addr, APR_LOCAL, sock);
1403 apr_sockaddr_port_get(&local_port, local_addr);
1404 apr_sockaddr_ip_get(&local_ip, local_addr);
1406 if ((rv = apr_socket_opt_set(local_sock, APR_SO_REUSEADDR, one))
1408 #ifndef _OSD_POSIX /* BS2000 has this option "always on" */
1409 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1410 "proxy: FTP: error setting reuseaddr option");
1411 return HTTP_INTERNAL_SERVER_ERROR;
1412 #endif /* _OSD_POSIX */
1415 apr_sockaddr_info_get(&local_addr, local_ip, APR_UNSPEC, local_port, 0, r->pool);
1417 if ((rv = apr_bind(local_sock, local_addr)) != APR_SUCCESS) {
1418 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1419 "proxy: FTP: error binding to ftp data socket %pI", local_addr);
1420 return HTTP_INTERNAL_SERVER_ERROR;
1423 /* only need a short queue */
1424 if ((rv = apr_listen(local_sock, 2)) != APR_SUCCESS) {
1425 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1426 "proxy: FTP: error listening to ftp data socket %pI", local_addr);
1427 return HTTP_INTERNAL_SERVER_ERROR;
1430 /* FIXME: Sent PORT here */
1432 if (local_ip && (sscanf(local_ip,
1433 "%d.%d.%d.%d", &h3, &h2, &h1, &h0) == 4)) {
1434 p1 = (local_port >> 8);
1435 p0 = (local_port & 0xFF);
1437 rc = proxy_ftp_command(apr_psprintf(p, "PORT %d,%d,%d,%d,%d,%d" CRLF, h3, h2, h1, h0, p1, p0),
1438 r, origin, bb, &ftpmessage);
1439 /* possible results: 200, 421, 500, 501, 502, 530 */
1440 /* 200 Command okay. */
1441 /* 421 Service not available, closing control connection. */
1442 /* 500 Syntax error, command unrecognized. */
1443 /* 501 Syntax error in parameters or arguments. */
1444 /* 502 Command not implemented. */
1445 /* 530 Not logged in. */
1446 if (rc == -1 || rc == 421) {
1447 return ap_proxyerror(r, HTTP_BAD_GATEWAY,
1448 "Error reading from remote server");
1451 return ap_proxyerror(r, HTTP_BAD_GATEWAY, buffer);
1454 /* signal that we must use the EPRT/PORT loop */
1459 * The EPRT command replaces PORT where both IPV4 and IPV6 is supported. The first
1460 * number (1,2) indicates the protocol type. Examples:
1461 * EPRT |1|132.235.1.2|6275|
1462 * EPRT |2|1080::8:800:200C:417A|5282|
1464 return ap_proxyerror(r, HTTP_NOT_IMPLEMENTED, "Connect to IPV6 ftp server using EPRT not supported. Enable EPSV.");
1470 * V: Set The Headers -------------------
1472 * Get the size of the request, set up the environment for HTTP.
1475 /* set request; "path" holds last path component */
1476 len = decodeenc(path);
1478 if (strchr(path, '/')) { /* are there now any '/' characters? */
1479 return ap_proxyerror(r, HTTP_BAD_REQUEST,
1480 "Use of /%2f is only allowed at the base directory");
1483 /* If len == 0 then it must be a directory (you can't RETR nothing)
1484 * Also, don't allow to RETR by wildcard. Instead, create a dirlisting
1486 if (len == 0 || ftp_check_globbingchars(path)) {
1490 /* (from FreeBSD ftpd):
1491 * SIZE is not in RFC959, but Postel has blessed it and
1492 * it will be in the updated RFC.
1494 * Return size of file in a format suitable for
1495 * using with RESTART (we just count bytes).
1497 /* from draft-ietf-ftpext-mlst-14.txt:
1499 * change depending on the current STRUcture, MODE and TYPE of the data
1500 * connection, or a data connection which would be created were one
1501 * created now. Thus, the result of the SIZE command is dependent on
1502 * the currently established STRU, MODE and TYPE parameters.
1504 /* Therefore: switch to binary if the user did not specify ";type=a" */
1505 ftp_set_TYPE(xfer_type, r, origin, bb, &ftpmessage);
1506 rc = proxy_ftp_command(apr_pstrcat(p, "SIZE ",
1507 ftp_escape_globbingchars(p, path), CRLF, NULL),
1508 r, origin, bb, &ftpmessage);
1509 if (rc == -1 || rc == 421) {
1510 return ap_proxyerror(r, HTTP_BAD_GATEWAY,
1511 "Error reading from remote server");
1513 else if (rc == 213) {/* Size command ok */
1515 for (j = 0; apr_isdigit(ftpmessage[j]); j++)
1517 ftpmessage[j] = '\0';
1518 if (ftpmessage[0] != '\0')
1519 size = ftpmessage; /* already pstrdup'ed: no copy necessary */
1521 else if (rc == 550) { /* Not a regular file */
1522 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1523 "proxy: FTP: SIZE shows this is a directory");
1525 rc = proxy_ftp_command(apr_pstrcat(p, "CWD ",
1526 ftp_escape_globbingchars(p, path), CRLF, NULL),
1527 r, origin, bb, &ftpmessage);
1528 /* possible results: 250, 421, 500, 501, 502, 530, 550 */
1529 /* 250 Requested file action okay, completed. */
1530 /* 421 Service not available, closing control connection. */
1531 /* 500 Syntax error, command unrecognized. */
1532 /* 501 Syntax error in parameters or arguments. */
1533 /* 502 Command not implemented. */
1534 /* 530 Not logged in. */
1535 /* 550 Requested action not taken. */
1536 if (rc == -1 || rc == 421) {
1537 return ap_proxyerror(r, HTTP_BAD_GATEWAY,
1538 "Error reading from remote server");
1541 return ap_proxyerror(r, HTTP_NOT_FOUND, ftpmessage);
1544 return ap_proxyerror(r, HTTP_BAD_GATEWAY, ftpmessage);
1551 cwd = ftp_get_PWD(r, origin, bb);
1553 apr_table_set(r->notes, "Directory-PWD", cwd);
1557 ftp_set_TYPE('A', r, origin, bb, NULL);
1558 /* If the current directory contains no slash, we are talking to
1559 * a non-unix ftp system. Try LIST instead of "LIST -lag", it
1560 * should return a long listing anyway (unlike NLST).
1561 * Some exotic FTP servers might choke on the "-lag" switch.
1563 /* Note that we do not escape the path here, to allow for
1564 * queries like: ftp://user@host/apache/src/server/http_*.c
1567 buf = apr_pstrcat(p, "LIST ", path, CRLF, NULL);
1568 else if (cwd == NULL || strchr(cwd, '/') != NULL)
1569 buf = apr_pstrcat(p, "LIST -lag", CRLF, NULL);
1574 /* switch to binary if the user did not specify ";type=a" */
1575 ftp_set_TYPE(xfer_type, r, origin, bb, &ftpmessage);
1576 #if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
1577 /* from draft-ietf-ftpext-mlst-14.txt:
1578 * The FTP command, MODIFICATION TIME (MDTM), can be used to determine
1579 * when a file in the server NVFS was last modified. <..>
1580 * The syntax of a time value is:
1581 * time-val = 14DIGIT [ "." 1*DIGIT ] <..>
1582 * Symbolically, a time-val may be viewed as
1583 * YYYYMMDDHHMMSS.sss
1584 * The "." and subsequent digits ("sss") are optional. <..>
1585 * Time values are always represented in UTC (GMT)
1587 rc = proxy_ftp_command(apr_pstrcat(p, "MDTM ", ftp_escape_globbingchars(p, path), CRLF, NULL),
1588 r, origin, bb, &ftpmessage);
1589 /* then extract the Last-Modified time from it (YYYYMMDDhhmmss or YYYYMMDDhhmmss.xxx GMT). */
1599 if (6 == sscanf(ftpmessage, "%4[0-9]%2[0-9]%2[0-9]%2[0-9]%2[0-9]%2[0-9]",
1600 time_val.YYYY, time_val.MM, time_val.DD, time_val.hh, time_val.mm, time_val.ss)) {
1602 memset (&tms, '\0', sizeof tms);
1603 tms.tm_year = atoi(time_val.YYYY) - 1900;
1604 tms.tm_mon = atoi(time_val.MM) - 1;
1605 tms.tm_mday = atoi(time_val.DD);
1606 tms.tm_hour = atoi(time_val.hh);
1607 tms.tm_min = atoi(time_val.mm);
1608 tms.tm_sec = atoi(time_val.ss);
1609 #ifdef HAVE_TIMEGM /* Does system have timegm()? */
1610 mtime = timegm(&tms);
1611 mtime *= APR_USEC_PER_SEC;
1612 #elif HAVE_GMTOFF /* does struct tm have a member tm_gmtoff? */
1613 /* mktime will subtract the local timezone, which is not what we want.
1614 * Add it again because the MDTM string is GMT
1616 mtime = mktime(&tms);
1617 mtime += tms.tm_gmtoff;
1618 mtime *= APR_USEC_PER_SEC;
1624 #endif /* USE_MDTM */
1625 /* FIXME: Handle range requests - send REST */
1626 buf = apr_pstrcat(p, "RETR ", ftp_escape_globbingchars(p, path), CRLF, NULL);
1628 rc = proxy_ftp_command(buf, r, origin, bb, &ftpmessage);
1629 /* rc is an intermediate response for the LIST or RETR commands */
1632 * RETR: 110, 125, 150, 226, 250, 421, 425, 426, 450, 451, 500, 501, 530,
1633 * 550 NLST: 125, 150, 226, 250, 421, 425, 426, 450, 451, 500, 501, 502,
1636 /* 110 Restart marker reply. */
1637 /* 125 Data connection already open; transfer starting. */
1638 /* 150 File status okay; about to open data connection. */
1639 /* 226 Closing data connection. */
1640 /* 250 Requested file action okay, completed. */
1641 /* 421 Service not available, closing control connection. */
1642 /* 425 Can't open data connection. */
1643 /* 426 Connection closed; transfer aborted. */
1644 /* 450 Requested file action not taken. */
1645 /* 451 Requested action aborted. Local error in processing. */
1646 /* 500 Syntax error, command unrecognized. */
1647 /* 501 Syntax error in parameters or arguments. */
1648 /* 530 Not logged in. */
1649 /* 550 Requested action not taken. */
1650 if (rc == -1 || rc == 421) {
1651 return ap_proxyerror(r, HTTP_BAD_GATEWAY,
1652 "Error reading from remote server");
1655 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1656 "proxy: FTP: RETR failed, trying LIST instead");
1658 /* Directory Listings should always be fetched in ASCII mode */
1660 ftp_set_TYPE('A', r, origin, bb, NULL);
1662 rc = proxy_ftp_command(apr_pstrcat(p, "CWD ",
1663 ftp_escape_globbingchars(p, path), CRLF, NULL),
1664 r, origin, bb, &ftpmessage);
1665 /* possible results: 250, 421, 500, 501, 502, 530, 550 */
1666 /* 250 Requested file action okay, completed. */
1667 /* 421 Service not available, closing control connection. */
1668 /* 500 Syntax error, command unrecognized. */
1669 /* 501 Syntax error in parameters or arguments. */
1670 /* 502 Command not implemented. */
1671 /* 530 Not logged in. */
1672 /* 550 Requested action not taken. */
1673 if (rc == -1 || rc == 421) {
1674 return ap_proxyerror(r, HTTP_BAD_GATEWAY,
1675 "Error reading from remote server");
1678 return ap_proxyerror(r, HTTP_NOT_FOUND, ftpmessage);
1681 return ap_proxyerror(r, HTTP_BAD_GATEWAY, ftpmessage);
1684 /* Update current directory after CWD */
1685 cwd = ftp_get_PWD(r, origin, bb);
1687 apr_table_set(r->notes, "Directory-PWD", cwd);
1690 /* See above for the "LIST" vs. "LIST -lag" discussion. */
1691 rc = proxy_ftp_command((cwd == NULL || strchr(cwd, '/') != NULL)
1692 ? "LIST -lag" CRLF : "LIST" CRLF,
1693 r, origin, bb, &ftpmessage);
1695 /* rc is an intermediate response for the LIST command (125 transfer starting, 150 opening data connection) */
1696 if (rc == -1 || rc == 421)
1697 return ap_proxyerror(r, HTTP_BAD_GATEWAY,
1698 "Error reading from remote server");
1700 if (rc != 125 && rc != 150 && rc != 226 && rc != 250) {
1701 return ap_proxyerror(r, HTTP_BAD_GATEWAY, ftpmessage);
1704 r->status = HTTP_OK;
1705 r->status_line = "200 OK";
1707 apr_rfc822_date(dates, r->request_time);
1708 apr_table_setn(r->headers_out, "Date", dates);
1709 apr_table_setn(r->headers_out, "Server", ap_get_server_version());
1711 /* set content-type */
1713 ap_set_content_type(r, "text/html; charset=ISO-8859-1");
1716 if (r->content_type) {
1717 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1718 "proxy: FTP: Content-Type set to %s", r->content_type);
1721 ap_set_content_type(r, ap_default_type(r));
1723 if (xfer_type != 'A' && size != NULL) {
1724 /* We "trust" the ftp server to really serve (size) bytes... */
1725 apr_table_setn(r->headers_out, "Content-Length", size);
1726 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1727 "proxy: FTP: Content-Length set to %s", size);
1730 apr_table_setn(r->headers_out, "Content-Type", r->content_type);
1731 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1732 "proxy: FTP: Content-Type set to %s", r->content_type);
1734 #if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
1736 char datestr[APR_RFC822_DATE_LEN];
1737 apr_rfc822_date(datestr, mtime);
1738 apr_table_set(r->headers_out, "Last-Modified", datestr);
1739 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1740 "proxy: FTP: Last-Modified set to %s", datestr);
1742 #endif /* USE_MDTM */
1744 /* If an encoding has been set by mistake, delete it.
1745 * @@@ FIXME (e.g., for ftp://user@host/file*.tar.gz,
1746 * @@@ the encoding is currently set to x-gzip)
1748 if (dirlisting && r->content_encoding != NULL)
1749 r->content_encoding = NULL;
1751 /* set content-encoding (not for dir listings, they are uncompressed)*/
1752 if (r->content_encoding != NULL && r->content_encoding[0] != '\0') {
1753 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1754 "proxy: FTP: Content-Encoding set to %s", r->content_encoding);
1755 apr_table_setn(r->headers_out, "Content-Encoding", r->content_encoding);
1758 /* wait for connection */
1761 rv = apr_accept(&data_sock, local_sock, r->pool);
1762 if (rv == APR_EINTR) {
1765 else if (rv == APR_SUCCESS) {
1769 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
1770 "proxy: FTP: failed to accept data connection");
1771 return HTTP_BAD_GATEWAY;
1776 /* the transfer socket is now open, create a new connection */
1777 data = ap_run_create_connection(p, r->server, data_sock, r->connection->id,
1778 r->connection->sbh, c->bucket_alloc);
1781 * the peer reset the connection already; ap_run_create_connection() closed
1784 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1785 "proxy: FTP: an error occurred creating the transfer connection");
1786 return HTTP_INTERNAL_SERVER_ERROR;
1789 /* set up the connection filters */
1790 rc = ap_run_pre_connection(data, data_sock);
1791 if (rc != OK && rc != DONE) {
1793 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1794 "proxy: FTP: pre_connection setup failed (%d)",
1800 * VI: Receive the Response ------------------------
1802 * Get response from the remote ftp socket, and pass it up the filter chain.
1809 /* insert directory filter */
1810 ap_add_output_filter("PROXY_SEND_DIR", NULL, r, r->connection);
1814 if (!r->header_only) {
1818 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1819 "proxy: FTP: start body send");
1821 /* read the body, pass it to the output filters */
1822 while (ap_get_brigade(data->input_filters,
1826 conf->io_buffer_size) == APR_SUCCESS) {
1829 apr_off_t readbytes;
1830 apr_brigade_length(bb, 0, &readbytes);
1831 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0,
1832 r->server, "proxy (PID %d): readbytes: %#x",
1833 getpid(), readbytes);
1837 if (APR_BRIGADE_EMPTY(bb)) {
1838 apr_brigade_cleanup(bb);
1842 /* found the last brigade? */
1843 if (APR_BUCKET_IS_EOS(APR_BRIGADE_LAST(bb))) {
1844 /* if this is the last brigade, cleanup the
1845 * backend connection first to prevent the
1846 * backend server from hanging around waiting
1847 * for a slow client to eat these bytes
1849 ap_flush_conn(data);
1851 apr_socket_close(data_sock);
1854 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1855 "proxy: FTP: data connection closed");
1856 /* signal that we must leave */
1860 /* if no EOS yet, then we must flush */
1861 if (FALSE == finish) {
1862 e = apr_bucket_flush_create(c->bucket_alloc);
1863 APR_BRIGADE_INSERT_TAIL(bb, e);
1866 /* try send what we read */
1867 if (ap_pass_brigade(r->output_filters, bb) != APR_SUCCESS
1869 /* Ack! Phbtt! Die! User aborted! */
1873 /* make sure we always clean up after ourselves */
1874 apr_brigade_cleanup(bb);
1876 /* if we are done, leave */
1877 if (TRUE == finish) {
1881 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1882 "proxy: FTP: end body send");
1886 ap_flush_conn(data);
1887 apr_socket_close(data_sock);
1888 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1889 "proxy: FTP: data connection closed");
1892 /* Retrieve the final response for the RETR or LIST commands */
1893 rc = proxy_ftp_command(NULL, r, origin, bb, &ftpmessage);
1894 apr_brigade_cleanup(bb);
1897 * VII: Clean Up -------------
1899 * If there are no KeepAlives, or if the connection has been signalled to
1900 * close, close the socket and clean up
1904 rc = proxy_ftp_command("QUIT" CRLF,
1905 r, origin, bb, &ftpmessage);
1906 /* responses: 221, 500 */
1907 /* 221 Service closing control connection. */
1908 /* 500 Syntax error, command unrecognized. */
1909 ap_flush_conn(origin);
1911 apr_socket_close(origin_sock);
1914 apr_brigade_destroy(bb);
1918 static void ap_proxy_ftp_register_hook(apr_pool_t *p)
1921 proxy_hook_scheme_handler(ap_proxy_ftp_handler, NULL, NULL, APR_HOOK_MIDDLE);
1922 proxy_hook_canon_handler(ap_proxy_ftp_canon, NULL, NULL, APR_HOOK_MIDDLE);
1924 ap_register_output_filter("PROXY_SEND_DIR", ap_proxy_send_dir_filter,
1925 NULL, AP_FTYPE_RESOURCE);
1928 module AP_MODULE_DECLARE_DATA proxy_ftp_module = {
1929 STANDARD20_MODULE_STUFF,
1930 NULL, /* create per-directory config structure */
1931 NULL, /* merge per-directory config structures */
1932 NULL, /* create per-server config structure */
1933 NULL, /* merge per-server config structures */
1934 NULL, /* command apr_table_t */
1935 ap_proxy_ftp_register_hook /* register hooks */