]> granicus.if.org Git - apache/blob - modules/proxy/mod_proxy_ftp.c
e7b50f476bdd7d9d666555b2ce000483713ad709
[apache] / modules / proxy / mod_proxy_ftp.c
1 /* Licensed to the Apache Software Foundation (ASF) under one or more
2  * contributor license agreements.  See the NOTICE file distributed with
3  * this work for additional information regarding copyright ownership.
4  * The ASF licenses this file to You under the Apache License, Version 2.0
5  * (the "License"); you may not use this file except in compliance with
6  * the License.  You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /* FTP routines for Apache proxy */
18
19 #include "mod_proxy.h"
20 #if APR_HAVE_TIME_H
21 #include <time.h>
22 #endif
23 #include "apr_version.h"
24
25 #if (APR_MAJOR_VERSION < 1)
26 #undef apr_socket_create
27 #define apr_socket_create apr_socket_create_ex
28 #endif
29
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
34  */
35 #define USE_MDTM
36
37
38 module AP_MODULE_DECLARE_DATA proxy_ftp_module;
39
40 typedef struct {
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;
46 } proxy_ftp_dir_conf;
47
48 static void *create_proxy_ftp_dir_config(apr_pool_t *p, char *dummy)
49 {
50     proxy_ftp_dir_conf *new =
51         (proxy_ftp_dir_conf *) apr_pcalloc(p, sizeof(proxy_ftp_dir_conf));
52
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;
56
57     return (void *) new;
58 }
59
60 static void *merge_proxy_ftp_dir_config(apr_pool_t *p, void *basev, void *addv)
61 {
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;
65
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 ?
71                                 1 :
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 ?
77                                 1 :
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;
82     return new;
83 }
84
85 static const char *set_ftp_list_on_wildcard(cmd_parms *cmd, void *dconf,
86                                             int flag)
87 {
88     proxy_ftp_dir_conf *conf = dconf;
89
90     conf->ftp_list_on_wildcard = flag;
91     conf->ftp_list_on_wildcard_set = 1;
92     return NULL;
93 }
94
95 static const char *set_ftp_escape_wildcards(cmd_parms *cmd, void *dconf,
96                                             int flag)
97 {
98     proxy_ftp_dir_conf *conf = dconf;
99
100     conf->ftp_escape_wildcards = flag;
101     conf->ftp_escape_wildcards_set = 1;
102     return NULL;
103 }
104
105 static const char *set_ftp_directory_charset(cmd_parms *cmd, void *dconf,
106                                              const char *arg)
107 {
108     proxy_ftp_dir_conf *conf = dconf;
109
110     conf->ftp_directory_charset = arg;
111     return NULL;
112 }
113
114 /*
115  * Decodes a '%' escaped string, and returns the number of characters
116  */
117 static int decodeenc(char *x)
118 {
119     int i, j, ch;
120
121     if (x[0] == '\0')
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 */
125         ch = x[i];
126         if (ch == '%' && apr_isxdigit(x[i + 1]) && apr_isxdigit(x[i + 2])) {
127             ch = ap_proxy_hex2c(&x[i + 1]);
128             i += 2;
129         }
130         x[j] = ch;
131     }
132     x[j] = '\0';
133     return j;
134 }
135
136 /*
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
141  * configured "off".
142  * Returns: escaped string
143  */
144 #define FTP_GLOBBING_CHARS "*?[{~"
145 static const char *ftp_escape_globbingchars(apr_pool_t *p, const char *path, proxy_ftp_dir_conf *dconf)
146 {
147     char *ret;
148     char *d;
149     
150     if (!dconf->ftp_escape_wildcards) {
151         return path;
152     }
153
154     ret = apr_palloc(p, 2*strlen(path)+sizeof(""));
155     for (d = ret; *path; ++path) {
156         if (strchr(FTP_GLOBBING_CHARS, *path) != NULL)
157             *d++ = '\\';
158         *d++ = *path;
159     }
160     *d = '\0';
161     return ret;
162 }
163
164 /*
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)
169  */
170 static int ftp_check_globbingchars(const char *path)
171 {
172     for ( ; *path; ++path) {
173         if (*path == '\\')
174         ++path;
175         if (*path != '\0' && strchr(FTP_GLOBBING_CHARS, *path) != NULL)
176             return TRUE;
177     }
178     return FALSE;
179 }
180
181 /*
182  * checks an encoded ftp string for bad characters, namely, CR, LF or
183  * non-ascii character
184  */
185 static int ftp_check_string(const char *x)
186 {
187     int i, ch = 0;
188 #if APR_CHARSET_EBCDIC
189     char buf[1];
190 #endif
191
192     for (i = 0; x[i] != '\0'; i++) {
193         ch = x[i];
194         if (ch == '%' && apr_isxdigit(x[i + 1]) && apr_isxdigit(x[i + 2])) {
195             ch = ap_proxy_hex2c(&x[i + 1]);
196             i += 2;
197         }
198 #if !APR_CHARSET_EBCDIC
199         if (ch == '\015' || ch == '\012' || (ch & 0x80))
200 #else                           /* APR_CHARSET_EBCDIC */
201         if (ch == '\r' || ch == '\n')
202             return 0;
203         buf[0] = ch;
204         ap_xlate_proto_to_ascii(buf, 1);
205         if (buf[0] & 0x80)
206 #endif                          /* APR_CHARSET_EBCDIC */
207             return 0;
208     }
209     return 1;
210 }
211
212 /*
213  * Canonicalise ftp URLs.
214  */
215 static int proxy_ftp_canon(request_rec *r, char *url)
216 {
217     char *user, *password, *host, *path, *parms, *strp, sport[7];
218     apr_pool_t *p = r->pool;
219     const char *err;
220     apr_port_t port, def_port;
221
222     /* */
223     if (strncasecmp(url, "ftp:", 4) == 0) {
224         url += 4;
225     }
226     else {
227         return DECLINED;
228     }
229     def_port = apr_uri_port_of_scheme("ftp");
230
231     ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, r->server,
232                  "proxy: FTP: canonicalising URL %s", url);
233
234     port = def_port;
235     err = ap_proxy_canon_netloc(p, &url, &user, &password, &host, &port);
236     if (err)
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;
242
243     /* now parse path/parameters args, according to rfc1738 */
244     /*
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.
248      */
249     strp = strchr(url, ';');
250     if (strp != NULL) {
251         *(strp++) = '\0';
252         parms = ap_proxy_canonenc(p, strp, strlen(strp), enc_parm, 0,
253                                   r->proxyreq);
254         if (parms == NULL)
255             return HTTP_BAD_REQUEST;
256     }
257     else
258         parms = "";
259
260     path = ap_proxy_canonenc(p, url, strlen(url), enc_path, 0, r->proxyreq);
261     if (path == NULL)
262         return HTTP_BAD_REQUEST;
263     if (!ftp_check_string(path))
264         return HTTP_BAD_REQUEST;
265
266     if (r->proxyreq && r->args != NULL) {
267         if (strp != NULL) {
268             strp = ap_proxy_canonenc(p, r->args, strlen(r->args), enc_parm, 1, r->proxyreq);
269             if (strp == NULL)
270                 return HTTP_BAD_REQUEST;
271             parms = apr_pstrcat(p, parms, "?", strp, NULL);
272         }
273         else {
274             strp = ap_proxy_canonenc(p, r->args, strlen(r->args), enc_fpath, 1, r->proxyreq);
275             if (strp == NULL)
276                 return HTTP_BAD_REQUEST;
277             path = apr_pstrcat(p, path, "?", strp, NULL);
278         }
279         r->args = NULL;
280     }
281
282 /* now, rebuild URL */
283
284     if (port != def_port)
285         apr_snprintf(sport, sizeof(sport), ":%d", port);
286     else
287         sport[0] = '\0';
288
289     if (ap_strchr_c(host, ':')) { /* if literal IPv6 address */
290         host = apr_pstrcat(p, "[", host, "]", NULL);
291     }
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);
297
298     return OK;
299 }
300
301 /* we chop lines longer than 80 characters */
302 #define MAX_LINE_LEN 80
303
304 /*
305  * Reads response lines, returns both the ftp status code and
306  * remembers the response message in the supplied buffer
307  */
308 static int ftp_getrc_msg(conn_rec *ftp_ctrl, apr_bucket_brigade *bb, char *msgbuf, int msglen)
309 {
310     int status;
311     char response[MAX_LINE_LEN];
312     char buff[5];
313     char *mb = msgbuf, *me = &msgbuf[msglen];
314     apr_status_t rv;
315     int eos;
316
317     if (APR_SUCCESS != (rv = ap_proxy_string_read(ftp_ctrl, bb, response, sizeof(response), &eos))) {
318         return -1;
319     }
320 /*
321     ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, NULL,
322                  "proxy: <FTP: %s", response);
323 */
324     if (!apr_isdigit(response[0]) || !apr_isdigit(response[1]) ||
325     !apr_isdigit(response[2]) || (response[3] != ' ' && response[3] != '-'))
326         status = 0;
327     else
328         status = 100 * response[0] + 10 * response[1] + response[2] - 111 * '0';
329
330     mb = apr_cpystrn(mb, response + 4, me - mb);
331
332     if (response[3] == '-') {
333         memcpy(buff, response, 3);
334         buff[3] = ' ';
335         do {
336             if (APR_SUCCESS != (rv = ap_proxy_string_read(ftp_ctrl, bb, response, sizeof(response), &eos))) {
337                 return -1;
338             }
339             mb = apr_cpystrn(mb, response + (' ' == response[0] ? 1 : 4), me - mb);
340         } while (memcmp(response, buff, 4) != 0);
341     }
342
343     return status;
344 }
345
346 /* this is a filter that turns a raw ASCII directory listing into pretty HTML */
347
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...
351  *
352  * all in good time...! :)
353  */
354
355 typedef struct {
356     apr_bucket_brigade *in;
357     char buffer[MAX_STRING_LEN];
358     enum {
359         HEADER, BODY, FOOTER
360     }    state;
361 }      proxy_dir_ctx_t;
362
363 /* fallback regex for ls -s1;  ($0..$2) == 3 */
364 #define LS_REG_PATTERN "^ *([0-9]+) +([^ ]+)$"
365 #define LS_REG_MATCH   3
366
367 static apr_status_t proxy_send_dir_filter(ap_filter_t *f,
368                                           apr_bucket_brigade *in)
369 {
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);
374     apr_status_t rv;
375
376     register int n;
377     char *dir, *path, *reldir, *site, *str, *type;
378
379     const char *pwd = apr_table_get(r->notes, "Directory-PWD");
380     const char *readme = apr_table_get(r->notes, "Directory-README");
381
382     proxy_dir_ctx_t *ctx = f->ctx;
383
384     if (!ctx) {
385         f->ctx = ctx = apr_pcalloc(p, sizeof(*ctx));
386         ctx->in = apr_brigade_create(p, c->bucket_alloc);
387         ctx->buffer[0] = 0;
388         ctx->state = HEADER;
389     }
390
391     /* combine the stored and the new */
392     APR_BRIGADE_CONCAT(ctx->in, in);
393
394     if (HEADER == ctx->state) {
395
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;
399         const char *escpath;
400
401         /*
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.
408          */
409         if (r->proxyreq == PROXYREQ_REVERSE) {
410             site = ap_construct_url(p, "", r);
411         }
412         else {
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);
417         }
418
419         /* ... and path without query args */
420         path = apr_uri_unparse(p, &f->r->parsed_uri, APR_URI_UNP_OMITSITEPART | APR_URI_UNP_OMITQUERY);
421
422         /* If path began with /%2f, change the basedir */
423         if (strncasecmp(path, "/%2f", 4) == 0) {
424             basedir = "/%2f";
425         }
426
427         /* Strip off a type qualifier. It is ignored for dir listings */
428         if ((type = strstr(path, ";type=")) != NULL)
429             *type++ = '\0';
430
431         (void)decodeenc(path);
432
433         while (path[1] == '/') /* collapse multiple leading slashes to one */
434             ++path;
435
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 */
440         }
441
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)
446             path[n - 1] = '\0';
447
448         /* Add a link to the root directory (if %2f hack was used) */
449         str = (basedir[0] != '\0') ? "<a href=\"/%2f/\">%2f</a>/" : "";
450
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"
456                 " </head>\n"
457                 " <body>\n  <h2>Directory of "
458                 "<a href=\"/\">%s</a>/%s",
459                 site, basedir, escpath, site, basedir, escpath, site, str);
460
461         APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str),
462                                                           p, c->bucket_alloc));
463
464         for (dir = path+1; (dir = strchr(dir, '/')) != NULL; )
465         {
466             *dir = '\0';
467             if ((reldir = strrchr(path+1, '/'))==NULL) {
468                 reldir = path+1;
469             }
470             else
471                 ++reldir;
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));
476             *dir = '/';
477             while (*dir == '/')
478               ++dir;
479             APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str,
480                                                            strlen(str), p,
481                                                            c->bucket_alloc));
482         }
483         if (wildcard != NULL) {
484             wildcard = ap_escape_html(p, wildcard);
485             APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(wildcard,
486                                                            strlen(wildcard), p,
487                                                            c->bucket_alloc));
488         }
489
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>");
494         }
495         else {
496             str = apr_psprintf(p, "</h2>\n\n(%s)\n\n  <hr />\n\n<pre>",
497                                ap_escape_html(p, pwd));
498         }
499         APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str),
500                                                            p, c->bucket_alloc));
501
502         /* print README */
503         if (readme) {
504             str = apr_psprintf(p, "%s\n</pre>\n\n<hr />\n\n<pre>\n",
505                                ap_escape_html(p, readme));
506
507             APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str,
508                                                            strlen(str), p,
509                                                            c->bucket_alloc));
510         }
511
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))) {
515             return rv;
516         }
517         apr_brigade_cleanup(out);
518
519         ctx->state = BODY;
520     }
521
522     /* loop through each line of directory */
523     while (BODY == ctx->state) {
524         char *filename;
525         int found = 0;
526         int eos = 0;
527
528         ap_regex_t *re = NULL;
529         ap_regmatch_t re_result[LS_REG_MATCH];
530
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);
534
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;
539             apr_size_t len, max;
540             apr_bucket *e;
541
542             e = APR_BRIGADE_FIRST(ctx->in);
543             if (APR_BUCKET_IS_EOS(e)) {
544                 eos = 1;
545                 break;
546             }
547             if (APR_SUCCESS != (rv = apr_bucket_read(e, (const char **)&response, &len, APR_BLOCK_READ))) {
548                 return rv;
549             }
550             pos = memchr(response, APR_ASCII_LF, len);
551             if (pos != NULL) {
552                 if ((response + len) != (pos + 1)) {
553                     len = pos - response + 1;
554                     apr_bucket_split(e, pos - response + 1);
555                 }
556                 found = 1;
557             }
558             max = sizeof(ctx->buffer) - strlen(ctx->buffer) - 1;
559             if (len > max) {
560                 len = max;
561             }
562
563             /* len+1 to leave space for the trailing nil char */
564             apr_cpystrn(ctx->buffer+strlen(ctx->buffer), response, len+1);
565
566             APR_BUCKET_REMOVE(e);
567             apr_bucket_destroy(e);
568         }
569
570         /* EOS? jump to footer */
571         if (eos) {
572             ctx->state = FOOTER;
573             break;
574         }
575
576         /* not complete? leave and try get some more */
577         if (!found) {
578             return APR_SUCCESS;
579         }
580
581         {
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';
587         }
588
589         /* a symlink? */
590         if (ctx->buffer[0] == 'l' && (filename = strstr(ctx->buffer, " -> ")) != NULL) {
591             char *link_ptr = filename;
592
593             do {
594                 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));
604         }
605
606         /* a directory/file? */
607         else if (ctx->buffer[0] == 'd' || ctx->buffer[0] == '-' || ctx->buffer[0] == 'l' || apr_isdigit(ctx->buffer[0])) {
608             int searchidx = 0;
609             char *searchptr = NULL;
610             int firstfile = 1;
611             if (apr_isdigit(ctx->buffer[0])) {  /* handle DOS dir */
612                 searchptr = strchr(ctx->buffer, '<');
613                 if (searchptr != NULL)
614                     *searchptr = '[';
615                 searchptr = strchr(ctx->buffer, '>');
616                 if (searchptr != NULL)
617                     *searchptr = ']';
618             }
619
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 */
626                 ctx->buffer[0] = 0;
627                 continue;  /* while state is BODY */
628             }
629             *(filename++) = '\0';
630
631             /* handle filenames with spaces in 'em */
632             if (!strcmp(filename, ".") || !strcmp(filename, "..") || firstfile) {
633                 firstfile = 0;
634                 searchidx = filename - ctx->buffer;
635             }
636             else if (searchidx != 0 && ctx->buffer[searchidx] != 0) {
637                 *(--filename) = ' ';
638                 ctx->buffer[searchidx - 1] = '\0';
639                 filename = &ctx->buffer[searchidx];
640             }
641
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));
648             }
649             else {
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));
654             }
655         }
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)) {
658
659             filename = apr_pstrndup(p, &ctx->buffer[re_result[2].rm_so], re_result[2].rm_eo - re_result[2].rm_so);
660
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);
664         }
665         else {
666             strcat(ctx->buffer, "\n"); /* re-append the newline */
667             str = ap_escape_html(p, ctx->buffer);
668         }
669
670         /* erase buffer for next time around */
671         ctx->buffer[0] = 0;
672
673         APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str), p,
674                                                             c->bucket_alloc));
675         APR_BRIGADE_INSERT_TAIL(out, apr_bucket_flush_create(c->bucket_alloc));
676         if (APR_SUCCESS != (rv = ap_pass_brigade(f->next, out))) {
677             return rv;
678         }
679         apr_brigade_cleanup(out);
680
681     }
682
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,
686                                                             c->bucket_alloc));
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))) {
690             return rv;
691         }
692         apr_brigade_destroy(out);
693     }
694
695     return APR_SUCCESS;
696 }
697
698 /* Parse EPSV reply and return port, or zero on error. */
699 static apr_port_t parse_epsv_reply(const char *reply)
700 {
701     const char *p;
702     char *ep;
703     long port;
704
705     /* Reply syntax per RFC 2428: "229 blah blah (|||port|)" where '|'
706      * can be any character in ASCII from 33-126, obscurely.  Verify
707      * the syntax. */
708     p = ap_strchr_c(reply, '(');
709     if (p == NULL || !p[1] || p[1] != p[2] || p[1] != p[3]
710         || p[4] == p[1]) {
711         return 0;
712     }
713
714     errno = 0;
715     port = strtol(p + 4, &ep, 10);
716     if (errno || port < 1 || port > 65535 || ep[0] != p[1] || ep[1] != ')') {
717         return 0;
718     }
719
720     return (apr_port_t)port;
721 }
722
723 /*
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)
727  */
728 static int
729 proxy_ftp_command(const char *cmd, request_rec *r, conn_rec *ftp_ctrl,
730                   apr_bucket_brigade *bb, char **pmessage)
731 {
732     char *crlf;
733     int rc;
734     char message[HUGE_STRING_LEN];
735
736     /* If cmd == NULL, we retrieve the next ftp response line */
737     if (cmd != NULL) {
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);
742
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)
747             *crlf = '\0';
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);
752     }
753
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)
759         *crlf = '\0';
760     ap_log_error(APLOG_MARK, APLOG_TRACE2, 0, r->server,
761                  "proxy:<FTP: %3.3u %s", rc, message);
762
763     if (pmessage != NULL)
764         *pmessage = apr_pstrdup(r->pool, message);
765
766     return rc;
767 }
768
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)
772 {
773     char old_type[2] = { 'A', '\0' }; /* After logon, mode is ASCII */
774     int ret = HTTP_OK;
775     int rc;
776
777     /* set desired type */
778     old_type[0] = xfer_type;
779
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");
792     }
793     else if (rc != 200 && rc != 504) {
794         ret = ap_proxyerror(r, HTTP_BAD_GATEWAY,
795                              "Unable to set transfer type");
796     }
797 /* Allow not implemented */
798     else if (rc == 504)
799         /* ignore it silently */;
800
801     return ret;
802 }
803
804
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)
807 {
808     char *cwd = NULL;
809     char *ftpmessage = NULL;
810
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)) {
819         case -1:
820         case 421:
821         case 550:
822             ap_proxyerror(r, HTTP_BAD_GATEWAY,
823                              "Failed to read PWD on ftp server");
824             break;
825
826         case 257: {
827             const char *dirp = ftpmessage;
828             cwd = ap_getword_conf(r->pool, &dirp);
829         }
830     }
831     return cwd;
832 }
833
834
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-)
841  */
842 static int ftp_unauthorized(request_rec *r, int log_it)
843 {
844     r->proxyreq = PROXYREQ_NONE;
845     /*
846      * Log failed requests if they supplied a password (log username/password
847      * guessing attempts)
848      */
849     if (log_it)
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));
854
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),
859                                "\"", NULL));
860
861     return HTTP_UNAUTHORIZED;
862 }
863
864 static
865 apr_status_t proxy_ftp_cleanup(request_rec *r, proxy_conn_rec *backend)
866 {
867
868     backend->close = 1;
869     ap_set_module_config(r->connection->conn_config, &proxy_ftp_module, NULL);
870     ap_proxy_release_connection("FTP", backend, r->server);
871
872     return OK;
873 }
874
875 static
876 int ftp_proxyerror(request_rec *r, proxy_conn_rec *conn, int statuscode, const char *message)
877 {
878     proxy_ftp_cleanup(r, conn);
879     return ap_proxyerror(r, statuscode, message);
880 }
881 /*
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>]
887  */
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)
891 {
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;
897     apr_status_t rv;
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;
907     apr_uri_t uri;
908     char *user = NULL;
909 /*    char *account = NULL; how to supply an account in a URL? */
910     const char *password = NULL;
911     int len, rc;
912     int one = 1;
913     char *size = NULL;
914     char xfer_type = 'A'; /* after ftp login, the default is ASCII */
915     int  dirlisting = 0;
916 #if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
917     apr_time_t mtime = 0L;
918 #endif
919     proxy_ftp_dir_conf *fdconf = ap_get_module_config(r->per_dir_config,
920                                                       &proxy_ftp_module);
921
922     /* stuff for PASV mode */
923     int connect = 0, use_port = 0;
924     char dates[APR_RFC822_DATE_LEN];
925     int status;
926     apr_pool_t *address_pool;
927
928     /* is this for us? */
929     if (proxyhost) {
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 */
933     }
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 */
938     }
939     ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
940                  "proxy: FTP: serving URL %s", url);
941
942
943     /*
944      * I: Who Do I Connect To? -----------------------
945      *
946      * Break up the URL to determine the host to connect to
947      */
948
949     /* we only support GET and HEAD */
950     if (r->method_number != M_GET)
951         return HTTP_NOT_IMPLEMENTED;
952
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));
958         }
959         connectname = uri.hostname;
960         connectport = uri.port;
961         path = apr_pstrdup(p, uri.path);
962     }
963     else {
964         connectname = r->parsed_uri.hostname;
965         connectport = r->parsed_uri.port;
966         path = apr_pstrdup(p, r->parsed_uri.path);
967     }
968     if (connectport == 0) {
969         connectport = apr_uri_port_of_scheme("ftp");
970     }
971     path = (path != NULL && path[0] != '\0') ? &path[1] : "";
972
973     type_suffix = strchr(path, ';');
974     if (type_suffix != NULL)
975         *(type_suffix++) = '\0';
976
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
981          */
982         if ( ! (dirlisting = (apr_tolower(type_suffix[5]) == 'd')))
983             xfer_type = apr_toupper(type_suffix[5]);
984
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));
990     }
991     else {
992         /* make binary transfers the default */
993         xfer_type = 'I';
994     }
995
996
997     /*
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.
1004      */
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");
1012         } 
1013         /*
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.
1017          */
1018         user = ap_getword_nulls(r->connection->pool, &password, ':');
1019         r->ap_auth_type = "Basic";
1020         r->user = r->parsed_uri.user = user;
1021     }
1022     else if ((user = r->parsed_uri.user) != NULL) {
1023         user = apr_pstrdup(p, user);
1024         decodeenc(user);
1025         if ((password = r->parsed_uri.password) != NULL) {
1026             char *tmp = apr_pstrdup(p, password);
1027             decodeenc(tmp);
1028             password = tmp;
1029         }
1030     }
1031     else {
1032         user = "anonymous";
1033         password = "apache-proxy@";
1034     }
1035
1036     ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1037        "proxy: FTP: connecting %s to %s:%d", url, connectname, connectport);
1038
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;
1045             }
1046         }
1047         connect_addr = worker->cp->addr;
1048         address_pool = worker->cp->pool;
1049     }
1050     else
1051         address_pool = r->pool;
1052
1053     /* do a DNS lookup for the destination host */
1054     if (!connect_addr)
1055         err = apr_sockaddr_info_get(&(connect_addr),
1056                                     connectname, APR_UNSPEC,
1057                                     connectport, 0,
1058                                     address_pool);
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");
1064         }
1065     }
1066     /*
1067      * get all the possible IP addresses for the destname and loop through
1068      * them until we get a successful connection
1069      */
1070     if (APR_SUCCESS != err) {
1071         return ap_proxyerror(r, HTTP_BAD_GATEWAY, apr_pstrcat(p,
1072                                                  "DNS lookup failure for: ",
1073                                                         connectname, NULL));
1074     }
1075
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");
1080     }
1081
1082     /* create space for state information */
1083     backend = (proxy_conn_rec *) ap_get_module_config(c->conn_config, &proxy_ftp_module);
1084     if (!backend) {
1085         status = ap_proxy_acquire_connection("FTP", &backend, worker, r->server);
1086         if (status != OK) {
1087             if (backend) {
1088                 backend->close = 1;
1089                 ap_proxy_release_connection("FTP", backend, r->server);
1090             }
1091             return status;
1092         }
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);
1096     }
1097
1098
1099     /*
1100      * II: Make the Connection -----------------------
1101      *
1102      * We have determined who to connect to. Now make the connection.
1103      */
1104
1105
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;
1112     }
1113
1114     if (!backend->connection) {
1115         status = ap_proxy_connection_create("FTP", backend, c, r->server);
1116         if (status != OK) {
1117             proxy_ftp_cleanup(r, backend);
1118             return status;
1119         }
1120     }
1121
1122     /* Use old naming */
1123     origin = backend->connection;
1124     sock = backend->sock;
1125
1126     ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, r->server,
1127                  "proxy: FTP: control connection complete");
1128
1129
1130     /*
1131      * III: Send Control Request -------------------------
1132      *
1133      * Log into the ftp server, send the username & password, change to the
1134      * correct directory...
1135      */
1136
1137
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");
1145     }
1146     if (rc == 120) {
1147         /*
1148          * RFC2616 states: 14.37 Retry-After
1149          *
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 )
1156          */
1157         char *secs_str = ftpmessage;
1158         time_t secs;
1159
1160         /* Look for a number, preceded by whitespace */
1161         while (*secs_str)
1162             if ((secs_str==ftpmessage || apr_isspace(secs_str[-1])) &&
1163                 apr_isdigit(secs_str[0]))
1164                 break;
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)));
1169         }
1170         return ftp_proxyerror(r, backend, HTTP_SERVICE_UNAVAILABLE, ftpmessage);
1171     }
1172     if (rc != 220) {
1173         return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1174     }
1175
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");
1190     }
1191     if (rc == 530) {
1192         proxy_ftp_cleanup(r, backend);
1193         return ftp_unauthorized(r, 1);  /* log it: user name guessing
1194                                          * attempt? */
1195     }
1196     if (rc != 230 && rc != 331) {
1197         return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1198     }
1199
1200     if (rc == 331) {            /* send password */
1201         if (password == NULL) {
1202             proxy_ftp_cleanup(r, backend);
1203             return ftp_unauthorized(r, 0);
1204         }
1205
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");
1219         }
1220         if (rc == 332) {
1221             return ftp_proxyerror(r, backend, HTTP_UNAUTHORIZED,
1222                   apr_pstrcat(p, "Need account for login: ", ftpmessage, NULL));
1223         }
1224         /* @@@ questionable -- we might as well return a 403 Forbidden here */
1225         if (rc == 530) {
1226             proxy_ftp_cleanup(r, backend);
1227             return ftp_unauthorized(r, 1);      /* log it: passwd guessing
1228                                                  * attempt? */
1229         }
1230         if (rc != 230 && rc != 202) {
1231             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1232         }
1233     }
1234     apr_table_set(r->notes, "Directory-README", ftpmessage);
1235
1236
1237     /* Special handling for leading "%2f": this enforces a "cwd /"
1238      * out of the $HOME directory which was the starting point after login
1239      */
1240     if (strncasecmp(path, "%2f", 3) == 0) {
1241         path += 3;
1242         while (*path == '/') /* skip leading '/' (after root %2f) */
1243             ++path;
1244
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");
1249     }
1250
1251     /*
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
1254      */
1255     for (;;) {
1256         strp = strchr(path, '/');
1257         if (strp == NULL)
1258             break;
1259         *strp = '\0';
1260
1261         len = decodeenc(path); /* Note! This decodes a %2f -> "/" */
1262
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");
1266         }
1267
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
1272          */
1273         rc = proxy_ftp_command(apr_pstrcat(p, "CWD ",
1274                            ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
1275                            r, origin, bb, &ftpmessage);
1276         *strp = '/';
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");
1288         }
1289         if (rc == 550) {
1290             return ftp_proxyerror(r, backend, HTTP_NOT_FOUND, ftpmessage);
1291         }
1292         if (rc != 250) {
1293             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1294         }
1295
1296         path = strp + 1;
1297     }
1298
1299     /*
1300      * IV: Make Data Connection? -------------------------
1301      *
1302      * Try EPSV, if that fails... try PASV, if that fails... try PORT.
1303      */
1304 /* this temporarily switches off EPSV/PASV */
1305 /*goto bypass;*/
1306
1307     /* set up data connection - EPSV */
1308     {
1309         apr_sockaddr_t *data_addr;
1310         char *data_ip;
1311         apr_port_t data_port;
1312
1313         /*
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|)
1318          */
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");
1331         }
1332         if (rc != 229 && rc != 500 && rc != 501 && rc != 502) {
1333             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1334         }
1335         else if (rc == 229) {
1336             /* Parse the port out of the EPSV reply. */
1337             data_port = parse_epsv_reply(ftpmessage);
1338
1339             if (data_port) {
1340                 apr_sockaddr_t *epsv_addr;
1341
1342                 ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, r->server,
1343                              "proxy: FTP: EPSV contacting remote host on port %d",
1344                              data_port);
1345
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;
1351                 }
1352
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");
1358                 }
1359
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");
1364                 }
1365
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));
1376                 }
1377                 else {
1378                     connect = 1;
1379                 }
1380             }
1381         }
1382     }
1383
1384     /* set up data connection - PASV */
1385     if (!connect) {
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");
1398         }
1399         if (rc != 227 && rc != 502) {
1400             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1401         }
1402         else if (rc == 227) {
1403             unsigned int h0, h1, h2, h3, p0, p1;
1404             char *pstr;
1405             char *tok_cntx;
1406
1407 /* FIXME: Check PASV against RFC1123 */
1408
1409             pstr = ftpmessage;
1410             pstr = apr_strtok(pstr, " ", &tok_cntx);    /* separate result code */
1411             if (pstr != NULL) {
1412                 if (*(pstr + strlen(pstr) + 1) == '=') {
1413                     pstr += strlen(pstr) + 2;
1414                 }
1415                 else {
1416                     pstr = apr_strtok(NULL, "(", &tok_cntx);    /* separate address &
1417                                                                  * port params */
1418                     if (pstr != NULL)
1419                         pstr = apr_strtok(NULL, ")", &tok_cntx);
1420                 }
1421             }
1422
1423 /* FIXME: Only supports IPV4 - fix in RFC2428 */
1424
1425             if (pstr != NULL && (sscanf(pstr,
1426                  "%d,%d,%d,%d,%d,%d", &h3, &h2, &h1, &h0, &p1, &p0) == 6)) {
1427
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);
1433
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;
1439                 }
1440
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");
1446                 }
1447
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");
1452                 }
1453
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));
1462                 }
1463                 else {
1464                     connect = 1;
1465                 }
1466             }
1467         }
1468     }
1469 /*bypass:*/
1470
1471     /* set up data connection - PORT */
1472     if (!connect) {
1473         apr_sockaddr_t *local_addr;
1474         char *local_ip;
1475         apr_port_t local_port;
1476         unsigned int h0, h1, h2, h3, p0, p1;
1477
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;
1483         }
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);
1487
1488         if ((rv = apr_socket_opt_set(local_sock, APR_SO_REUSEADDR, one))
1489                 != APR_SUCCESS) {
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 */
1496         }
1497
1498         apr_sockaddr_info_get(&local_addr, local_ip, APR_UNSPEC, local_port, 0, r->pool);
1499
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;
1505         }
1506
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;
1513         }
1514
1515 /* FIXME: Sent PORT here */
1516
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);
1521
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");
1534             }
1535             if (rc != 200) {
1536                 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, buffer);
1537             }
1538
1539             /* signal that we must use the EPRT/PORT loop */
1540             use_port = 1;
1541         }
1542         else {
1543 /* IPV6 FIXME:
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|
1548  */
1549             return ftp_proxyerror(r, backend, HTTP_NOT_IMPLEMENTED,
1550                                   "Connect to IPV6 ftp server using EPRT not supported. Enable EPSV.");
1551         }
1552     }
1553
1554
1555     /*
1556      * V: Set The Headers -------------------
1557      *
1558      * Get the size of the request, set up the environment for HTTP.
1559      */
1560
1561     /* set request; "path" holds last path component */
1562     len = decodeenc(path);
1563
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");
1567     }
1568
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.
1572      */
1573     if (len == 0 || (ftp_check_globbingchars(path) && fdconf->ftp_list_on_wildcard)) {
1574         dirlisting = 1;
1575     }
1576     else {
1577         /* (from FreeBSD ftpd):
1578          * SIZE is not in RFC959, but Postel has blessed it and
1579          * it will be in the updated RFC.
1580          *
1581          * Return size of file in a format suitable for
1582          * using with RESTART (we just count bytes).
1583          */
1584         /* from draft-ietf-ftpext-mlst-14.txt:
1585          * This value will
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.
1590          */
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");
1599         }
1600         else if (rc == 213) {/* Size command ok */
1601             int j;
1602             for (j = 0; apr_isdigit(ftpmessage[j]); j++)
1603                 ;
1604             ftpmessage[j] = '\0';
1605             if (ftpmessage[0] != '\0')
1606                  size = ftpmessage; /* already pstrdup'ed: no copy necessary */
1607         }
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");
1611             dirlisting = 1;
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");
1626             }
1627             if (rc == 550) {
1628                 return ftp_proxyerror(r, backend, HTTP_NOT_FOUND, ftpmessage);
1629             }
1630             if (rc != 250) {
1631                 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1632             }
1633             path = "";
1634             len = 0;
1635         }
1636     }
1637
1638     cwd = ftp_get_PWD(r, origin, bb);
1639     if (cwd != NULL) {
1640         apr_table_set(r->notes, "Directory-PWD", cwd);
1641     }
1642
1643     if (dirlisting) {
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.
1649          */
1650         /* Note that we do not escape the path here, to allow for
1651          * queries like: ftp://user@host/apache/src/server/http_*.c
1652          */
1653         if (len != 0)
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);
1657         else
1658             buf = "LIST" CRLF;
1659     }
1660     else {
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)
1673          */
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). */
1677         if (rc == 213) {
1678         struct {
1679             char YYYY[4+1];
1680         char MM[2+1];
1681         char DD[2+1];
1682         char hh[2+1];
1683         char mm[2+1];
1684         char ss[2+1];
1685         } time_val;
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)) {
1688                 struct tm tms;
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
1702          */
1703         mtime = mktime(&tms);
1704         mtime += tms.tm_gmtoff;
1705         mtime *= APR_USEC_PER_SEC;
1706 #else
1707         mtime = 0L;
1708 #endif
1709             }
1710     }
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);
1714     }
1715     rc = proxy_ftp_command(buf, r, origin, bb, &ftpmessage);
1716     /* rc is an intermediate response for the LIST or RETR commands */
1717
1718     /*
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,
1721      * 530
1722      */
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");
1740     }
1741     if (rc == 550) {
1742         ap_log_error(APLOG_MARK, APLOG_TRACE4, 0, r->server,
1743                      "proxy: FTP: RETR failed, trying LIST instead");
1744
1745         /* Directory Listings should always be fetched in ASCII mode */
1746         dirlisting = 1;
1747         ftp_set_TYPE('A', r, origin, bb, NULL);
1748
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");
1763         }
1764         if (rc == 550) {
1765             return ftp_proxyerror(r, backend, HTTP_NOT_FOUND, ftpmessage);
1766         }
1767         if (rc != 250) {
1768             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1769         }
1770
1771         /* Update current directory after CWD */
1772         cwd = ftp_get_PWD(r, origin, bb);
1773         if (cwd != NULL) {
1774             apr_table_set(r->notes, "Directory-PWD", cwd);
1775         }
1776
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);
1781
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");
1786     }
1787     if (rc != 125 && rc != 150 && rc != 226 && rc != 250) {
1788         return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1789     }
1790
1791     r->status = HTTP_OK;
1792     r->status_line = "200 OK";
1793
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());
1797
1798     /* set content-type */
1799     if (dirlisting) {
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));
1804     }
1805     else {
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);
1811         }
1812     }
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);
1817     }
1818
1819 #if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
1820     if (mtime != 0L) {
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);
1826     }
1827 #endif /* USE_MDTM */
1828
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)
1832      */
1833     if (dirlisting && r->content_encoding != NULL)
1834         r->content_encoding = NULL;
1835
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);
1842     }
1843
1844     /* wait for connection */
1845     if (use_port) {
1846         for (;;) {
1847             rv = apr_socket_accept(&data_sock, local_sock, r->pool);
1848             if (rv == APR_EINTR) {
1849                 continue;
1850             }
1851             else if (rv == APR_SUCCESS) {
1852                 break;
1853             }
1854             else {
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;
1859             }
1860         }
1861     }
1862
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);
1866     if (!data) {
1867         /*
1868          * the peer reset the connection already; ap_run_create_connection() closed
1869          * the socket
1870          */
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;
1875     }
1876
1877     /*
1878      * We do not do SSL over the data connection, even if the virtual host we
1879      * are in might have SSL enabled
1880      */
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)",
1887                      rc);
1888         data->aborted = 1;
1889         proxy_ftp_cleanup(r, backend);
1890         return rc;
1891     }
1892
1893     /*
1894      * VI: Receive the Response ------------------------
1895      *
1896      * Get response from the remote ftp socket, and pass it up the filter chain.
1897      */
1898
1899     /* send response */
1900     r->sent_bodyct = 1;
1901
1902     if (dirlisting) {
1903         /* insert directory filter */
1904         ap_add_output_filter("PROXY_SEND_DIR", NULL, r, r->connection);
1905     }
1906
1907     /* send body */
1908     if (!r->header_only) {
1909         apr_bucket *e;
1910         int finish = FALSE;
1911
1912         ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
1913                      "proxy: FTP: start body send");
1914
1915         /* read the body, pass it to the output filters */
1916         while (ap_get_brigade(data->input_filters,
1917                               bb,
1918                               AP_MODE_READBYTES,
1919                               APR_BLOCK_READ,
1920                               conf->io_buffer_size) == APR_SUCCESS) {
1921 #if DEBUGGING
1922             {
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);
1928             }
1929 #endif
1930             /* sanity check */
1931             if (APR_BRIGADE_EMPTY(bb)) {
1932                 apr_brigade_cleanup(bb);
1933                 break;
1934             }
1935
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
1942                  */
1943                 ap_flush_conn(data);
1944                 if (data_sock) {
1945                     apr_socket_close(data_sock);
1946                 }
1947                 data_sock = NULL;
1948                 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
1949                              "proxy: FTP: data connection closed");
1950                 /* signal that we must leave */
1951                 finish = TRUE;
1952             }
1953
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);
1958             }
1959
1960             /* try send what we read */
1961             if (ap_pass_brigade(r->output_filters, bb) != APR_SUCCESS
1962                 || c->aborted) {
1963                 /* Ack! Phbtt! Die! User aborted! */
1964                 finish = TRUE;
1965             }
1966
1967             /* make sure we always clean up after ourselves */
1968             apr_brigade_cleanup(bb);
1969
1970             /* if we are done, leave */
1971             if (TRUE == finish) {
1972                 break;
1973             }
1974         }
1975         ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, r->server,
1976                      "proxy: FTP: end body send");
1977
1978     }
1979     if (data_sock) {
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");
1984     }
1985
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);
1989
1990     /*
1991      * VII: Clean Up -------------
1992      *
1993      * If there are no KeepAlives, or if the connection has been signalled to
1994      * close, close the socket and clean up
1995      */
1996
1997     /* finish */
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);
2005
2006     apr_brigade_destroy(bb);
2007     return OK;
2008 }
2009
2010 static void ap_proxy_ftp_register_hook(apr_pool_t *p)
2011 {
2012     /* hooks */
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);
2015     /* filters */
2016     ap_register_output_filter("PROXY_SEND_DIR", proxy_send_dir_filter,
2017                               NULL, AP_FTYPE_RESOURCE);
2018 }
2019
2020 static const command_rec proxy_ftp_cmds[] =
2021 {
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"),
2028     {NULL}
2029 };
2030
2031
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 */
2040 };