]> granicus.if.org Git - apache/blob - modules/proxy/mod_proxy_ftp.c
Strip useless apr_brigade_cleanup() calls.
[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 #define APR_WANT_BYTEFUNC
20 #include "mod_proxy.h"
21 #if APR_HAVE_TIME_H
22 #include <time.h>
23 #endif
24 #include "apr_version.h"
25
26 #if (APR_MAJOR_VERSION < 1)
27 #undef apr_socket_create
28 #define apr_socket_create apr_socket_create_ex
29 #endif
30
31 #define AUTODETECT_PWD
32 /* Automatic timestamping (Last-Modified header) based on MDTM is used if:
33  * 1) the FTP server supports the MDTM command and
34  * 2) HAVE_TIMEGM (preferred) or HAVE_GMTOFF is available at compile time
35  */
36 #define USE_MDTM
37
38
39 module AP_MODULE_DECLARE_DATA proxy_ftp_module;
40
41 typedef struct {
42     int ftp_list_on_wildcard;
43     int ftp_list_on_wildcard_set;
44     int ftp_escape_wildcards;
45     int ftp_escape_wildcards_set;
46     const char *ftp_directory_charset;
47 } proxy_ftp_dir_conf;
48
49 static void *create_proxy_ftp_dir_config(apr_pool_t *p, char *dummy)
50 {
51     proxy_ftp_dir_conf *new =
52         (proxy_ftp_dir_conf *) apr_pcalloc(p, sizeof(proxy_ftp_dir_conf));
53
54     /* Put these in the dir config so they work inside <Location> */
55     new->ftp_list_on_wildcard = 1;
56     new->ftp_escape_wildcards = 1;
57
58     return (void *) new;
59 }
60
61 static void *merge_proxy_ftp_dir_config(apr_pool_t *p, void *basev, void *addv)
62 {
63     proxy_ftp_dir_conf *new = (proxy_ftp_dir_conf *) apr_pcalloc(p, sizeof(proxy_ftp_dir_conf));
64     proxy_ftp_dir_conf *add = (proxy_ftp_dir_conf *) addv;
65     proxy_ftp_dir_conf *base = (proxy_ftp_dir_conf *) basev;
66
67     /* Put these in the dir config so they work inside <Location> */
68     new->ftp_list_on_wildcard = add->ftp_list_on_wildcard_set ?
69                                 add->ftp_list_on_wildcard :
70                                 base->ftp_list_on_wildcard;
71     new->ftp_list_on_wildcard_set = add->ftp_list_on_wildcard_set ?
72                                 1 :
73                                 base->ftp_list_on_wildcard_set;
74     new->ftp_escape_wildcards = add->ftp_escape_wildcards_set ?
75                                 add->ftp_escape_wildcards :
76                                 base->ftp_escape_wildcards;
77     new->ftp_escape_wildcards_set = add->ftp_escape_wildcards_set ?
78                                 1 :
79                                 base->ftp_escape_wildcards_set;
80     new->ftp_directory_charset = add->ftp_directory_charset ?
81                                  add->ftp_directory_charset :
82                                  base->ftp_directory_charset;
83     return new;
84 }
85
86 static const char *set_ftp_list_on_wildcard(cmd_parms *cmd, void *dconf,
87                                             int flag)
88 {
89     proxy_ftp_dir_conf *conf = dconf;
90
91     conf->ftp_list_on_wildcard = flag;
92     conf->ftp_list_on_wildcard_set = 1;
93     return NULL;
94 }
95
96 static const char *set_ftp_escape_wildcards(cmd_parms *cmd, void *dconf,
97                                             int flag)
98 {
99     proxy_ftp_dir_conf *conf = dconf;
100
101     conf->ftp_escape_wildcards = flag;
102     conf->ftp_escape_wildcards_set = 1;
103     return NULL;
104 }
105
106 static const char *set_ftp_directory_charset(cmd_parms *cmd, void *dconf,
107                                              const char *arg)
108 {
109     proxy_ftp_dir_conf *conf = dconf;
110
111     conf->ftp_directory_charset = arg;
112     return NULL;
113 }
114
115 /*
116  * Decodes a '%' escaped string, and returns the number of characters
117  */
118 static int decodeenc(char *x)
119 {
120     int i, j, ch;
121
122     if (x[0] == '\0')
123         return 0;               /* special case for no characters */
124     for (i = 0, j = 0; x[i] != '\0'; i++, j++) {
125         /* decode it if not already done */
126         ch = x[i];
127         if (ch == '%' && apr_isxdigit(x[i + 1]) && apr_isxdigit(x[i + 2])) {
128             ch = ap_proxy_hex2c(&x[i + 1]);
129             i += 2;
130         }
131         x[j] = ch;
132     }
133     x[j] = '\0';
134     return j;
135 }
136
137 /*
138  * Escape the globbing characters in a path used as argument to
139  * the FTP commands (SIZE, CWD, RETR, MDTM, ...).
140  * ftpd assumes '\\' as a quoting character to escape special characters.
141  * Just returns the original string if ProxyFtpEscapeWildcards has been
142  * configured "off".
143  * Returns: escaped string
144  */
145 #define FTP_GLOBBING_CHARS "*?[{~"
146 static const char *ftp_escape_globbingchars(apr_pool_t *p, const char *path, proxy_ftp_dir_conf *dconf)
147 {
148     char *ret;
149     char *d;
150
151     if (!dconf->ftp_escape_wildcards) {
152         return path;
153     }
154
155     ret = apr_palloc(p, 2*strlen(path)+sizeof(""));
156     for (d = ret; *path; ++path) {
157         if (strchr(FTP_GLOBBING_CHARS, *path) != NULL)
158             *d++ = '\\';
159         *d++ = *path;
160     }
161     *d = '\0';
162     return ret;
163 }
164
165 /*
166  * Check for globbing characters in a path used as argument to
167  * the FTP commands (SIZE, CWD, RETR, MDTM, ...).
168  * ftpd assumes '\\' as a quoting character to escape special characters.
169  * Returns: 0 (no globbing chars, or all globbing chars escaped), 1 (globbing chars)
170  */
171 static int ftp_check_globbingchars(const char *path)
172 {
173     for ( ; *path; ++path) {
174         if (*path == '\\')
175         ++path;
176         if (*path != '\0' && strchr(FTP_GLOBBING_CHARS, *path) != NULL)
177             return TRUE;
178     }
179     return FALSE;
180 }
181
182 /*
183  * checks an encoded ftp string for bad characters, namely, CR, LF or
184  * non-ascii character
185  */
186 static int ftp_check_string(const char *x)
187 {
188     int i, ch = 0;
189 #if APR_CHARSET_EBCDIC
190     char buf[1];
191 #endif
192
193     for (i = 0; x[i] != '\0'; i++) {
194         ch = x[i];
195         if (ch == '%' && apr_isxdigit(x[i + 1]) && apr_isxdigit(x[i + 2])) {
196             ch = ap_proxy_hex2c(&x[i + 1]);
197             i += 2;
198         }
199 #if !APR_CHARSET_EBCDIC
200         if (ch == '\015' || ch == '\012' || (ch & 0x80))
201 #else                           /* APR_CHARSET_EBCDIC */
202         if (ch == '\r' || ch == '\n')
203             return 0;
204         buf[0] = ch;
205         ap_xlate_proto_to_ascii(buf, 1);
206         if (buf[0] & 0x80)
207 #endif                          /* APR_CHARSET_EBCDIC */
208             return 0;
209     }
210     return 1;
211 }
212
213 /*
214  * converts a series of buckets into a string
215  * XXX: BillS says this function performs essentially the same function as
216  * ap_rgetline() in protocol.c. Deprecate this function and use ap_rgetline()
217  * instead? I think ftp_string_read() will not work properly on non ASCII
218  * (EBCDIC) machines either.
219  */
220 static apr_status_t ftp_string_read(conn_rec *c, apr_bucket_brigade *bb,
221         char *buff, apr_size_t bufflen, int *eos)
222 {
223     apr_bucket *e;
224     apr_status_t rv;
225     char *pos = buff;
226     char *response;
227     int found = 0;
228     apr_size_t len;
229
230     /* start with an empty string */
231     buff[0] = 0;
232     *eos = 0;
233
234     /* loop through each brigade */
235     while (!found) {
236         /* get brigade from network one line at a time */
237         if (APR_SUCCESS != (rv = ap_get_brigade(c->input_filters, bb,
238                                                 AP_MODE_GETLINE,
239                                                 APR_BLOCK_READ,
240                                                 0))) {
241             return rv;
242         }
243         /* loop through each bucket */
244         while (!found) {
245             if (*eos || APR_BRIGADE_EMPTY(bb)) {
246                 /* The connection aborted or timed out */
247                 return APR_ECONNABORTED;
248             }
249             e = APR_BRIGADE_FIRST(bb);
250             if (APR_BUCKET_IS_EOS(e)) {
251                 *eos = 1;
252             }
253             else {
254                 if (APR_SUCCESS != (rv = apr_bucket_read(e,
255                                                          (const char **)&response,
256                                                          &len,
257                                                          APR_BLOCK_READ))) {
258                     return rv;
259                 }
260                 /*
261                  * is string LF terminated?
262                  * XXX: This check can be made more efficient by simply checking
263                  * if the last character in the 'response' buffer is an ASCII_LF.
264                  * See ap_rgetline() for an example.
265                  */
266                 if (memchr(response, APR_ASCII_LF, len)) {
267                     found = 1;
268                 }
269                 /* concat strings until buff is full - then throw the data away */
270                 if (len > ((bufflen-1)-(pos-buff))) {
271                     len = (bufflen-1)-(pos-buff);
272                 }
273                 if (len > 0) {
274                     memcpy(pos, response, len);
275                     pos += len;
276                 }
277             }
278             APR_BUCKET_REMOVE(e);
279             apr_bucket_destroy(e);
280         }
281         *pos = '\0';
282     }
283
284     return APR_SUCCESS;
285 }
286
287 /*
288  * Canonicalise ftp URLs.
289  */
290 static int proxy_ftp_canon(request_rec *r, char *url)
291 {
292     char *user, *password, *host, *path, *parms, *strp, sport[7];
293     apr_pool_t *p = r->pool;
294     const char *err;
295     apr_port_t port, def_port;
296
297     /* */
298     if (strncasecmp(url, "ftp:", 4) == 0) {
299         url += 4;
300     }
301     else {
302         return DECLINED;
303     }
304     def_port = apr_uri_port_of_scheme("ftp");
305
306     ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "canonicalising URL %s", url);
307
308     port = def_port;
309     err = ap_proxy_canon_netloc(p, &url, &user, &password, &host, &port);
310     if (err)
311         return HTTP_BAD_REQUEST;
312     if (user != NULL && !ftp_check_string(user))
313         return HTTP_BAD_REQUEST;
314     if (password != NULL && !ftp_check_string(password))
315         return HTTP_BAD_REQUEST;
316
317     /* now parse path/parameters args, according to rfc1738 */
318     /*
319      * N.B. if this isn't a true proxy request, then the URL path (but not
320      * query args) has already been decoded. This gives rise to the problem
321      * of a ; being decoded into the path.
322      */
323     strp = strchr(url, ';');
324     if (strp != NULL) {
325         *(strp++) = '\0';
326         parms = ap_proxy_canonenc(p, strp, strlen(strp), enc_parm, 0,
327                                   r->proxyreq);
328         if (parms == NULL)
329             return HTTP_BAD_REQUEST;
330     }
331     else
332         parms = "";
333
334     path = ap_proxy_canonenc(p, url, strlen(url), enc_path, 0, r->proxyreq);
335     if (path == NULL)
336         return HTTP_BAD_REQUEST;
337     if (!ftp_check_string(path))
338         return HTTP_BAD_REQUEST;
339
340     if (r->proxyreq && r->args != NULL) {
341         if (strp != NULL) {
342             strp = ap_proxy_canonenc(p, r->args, strlen(r->args), enc_parm, 1, r->proxyreq);
343             if (strp == NULL)
344                 return HTTP_BAD_REQUEST;
345             parms = apr_pstrcat(p, parms, "?", strp, NULL);
346         }
347         else {
348             strp = ap_proxy_canonenc(p, r->args, strlen(r->args), enc_fpath, 1, r->proxyreq);
349             if (strp == NULL)
350                 return HTTP_BAD_REQUEST;
351             path = apr_pstrcat(p, path, "?", strp, NULL);
352         }
353         r->args = NULL;
354     }
355
356 /* now, rebuild URL */
357
358     if (port != def_port)
359         apr_snprintf(sport, sizeof(sport), ":%d", port);
360     else
361         sport[0] = '\0';
362
363     if (ap_strchr_c(host, ':')) { /* if literal IPv6 address */
364         host = apr_pstrcat(p, "[", host, "]", NULL);
365     }
366     r->filename = apr_pstrcat(p, "proxy:ftp://", (user != NULL) ? user : "",
367                               (password != NULL) ? ":" : "",
368                               (password != NULL) ? password : "",
369                           (user != NULL) ? "@" : "", host, sport, "/", path,
370                               (parms[0] != '\0') ? ";" : "", parms, NULL);
371
372     return OK;
373 }
374
375 /* we chop lines longer than 80 characters */
376 #define MAX_LINE_LEN 80
377
378 /*
379  * Reads response lines, returns both the ftp status code and
380  * remembers the response message in the supplied buffer
381  */
382 static int ftp_getrc_msg(conn_rec *ftp_ctrl, apr_bucket_brigade *bb, char *msgbuf, int msglen)
383 {
384     int status;
385     char response[MAX_LINE_LEN];
386     char buff[5];
387     char *mb = msgbuf, *me = &msgbuf[msglen];
388     apr_status_t rv;
389     int eos;
390
391     if (APR_SUCCESS != (rv = ftp_string_read(ftp_ctrl, bb, response, sizeof(response), &eos))) {
392         return -1;
393     }
394 /*
395     ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, NULL,
396                  "<%s", response);
397 */
398     if (!apr_isdigit(response[0]) || !apr_isdigit(response[1]) ||
399     !apr_isdigit(response[2]) || (response[3] != ' ' && response[3] != '-'))
400         status = 0;
401     else
402         status = 100 * response[0] + 10 * response[1] + response[2] - 111 * '0';
403
404     mb = apr_cpystrn(mb, response + 4, me - mb);
405
406     if (response[3] == '-') {
407         memcpy(buff, response, 3);
408         buff[3] = ' ';
409         do {
410             if (APR_SUCCESS != (rv = ftp_string_read(ftp_ctrl, bb, response, sizeof(response), &eos))) {
411                 return -1;
412             }
413             mb = apr_cpystrn(mb, response + (' ' == response[0] ? 1 : 4), me - mb);
414         } while (memcmp(response, buff, 4) != 0);
415     }
416
417     return status;
418 }
419
420 /* this is a filter that turns a raw ASCII directory listing into pretty HTML */
421
422 /* ideally, mod_proxy should simply send the raw directory list up the filter
423  * stack to mod_autoindex, which in theory should turn the raw ascii into
424  * pretty html along with all the bells and whistles it provides...
425  *
426  * all in good time...! :)
427  */
428
429 typedef struct {
430     apr_bucket_brigade *in;
431     char buffer[MAX_STRING_LEN];
432     enum {
433         HEADER, BODY, FOOTER
434     }    state;
435 }      proxy_dir_ctx_t;
436
437 /* fallback regex for ls -s1;  ($0..$2) == 3 */
438 #define LS_REG_PATTERN "^ *([0-9]+) +([^ ]+)$"
439 #define LS_REG_MATCH   3
440 static ap_regex_t *ls_regex;
441
442 static apr_status_t proxy_send_dir_filter(ap_filter_t *f,
443                                           apr_bucket_brigade *in)
444 {
445     request_rec *r = f->r;
446     conn_rec *c = r->connection;
447     apr_pool_t *p = r->pool;
448     apr_bucket_brigade *out = apr_brigade_create(p, c->bucket_alloc);
449     apr_status_t rv;
450
451     register int n;
452     char *dir, *path, *reldir, *site, *str, *type;
453
454     const char *pwd = apr_table_get(r->notes, "Directory-PWD");
455     const char *readme = apr_table_get(r->notes, "Directory-README");
456
457     proxy_dir_ctx_t *ctx = f->ctx;
458
459     if (!ctx) {
460         f->ctx = ctx = apr_pcalloc(p, sizeof(*ctx));
461         ctx->in = apr_brigade_create(p, c->bucket_alloc);
462         ctx->buffer[0] = 0;
463         ctx->state = HEADER;
464     }
465
466     /* combine the stored and the new */
467     APR_BRIGADE_CONCAT(ctx->in, in);
468
469     if (HEADER == ctx->state) {
470
471         /* basedir is either "", or "/%2f" for the "squid %2f hack" */
472         const char *basedir = "";  /* By default, path is relative to the $HOME dir */
473         char *wildcard = NULL;
474         const char *escpath;
475
476         /*
477          * In the reverse proxy case we need to construct our site string
478          * via ap_construct_url. For non anonymous sites apr_uri_unparse would
479          * only supply us with 'username@' which leads to the construction of
480          * an invalid base href later on. Losing the username part of the URL
481          * is no problem in the reverse proxy case as the browser sents the
482          * credentials anyway once entered.
483          */
484         if (r->proxyreq == PROXYREQ_REVERSE) {
485             site = ap_construct_url(p, "", r);
486         }
487         else {
488             /* Save "scheme://site" prefix without password */
489             site = apr_uri_unparse(p, &f->r->parsed_uri,
490                                    APR_URI_UNP_OMITPASSWORD |
491                                    APR_URI_UNP_OMITPATHINFO);
492         }
493
494         /* ... and path without query args */
495         path = apr_uri_unparse(p, &f->r->parsed_uri, APR_URI_UNP_OMITSITEPART | APR_URI_UNP_OMITQUERY);
496
497         /* If path began with /%2f, change the basedir */
498         if (strncasecmp(path, "/%2f", 4) == 0) {
499             basedir = "/%2f";
500         }
501
502         /* Strip off a type qualifier. It is ignored for dir listings */
503         if ((type = strstr(path, ";type=")) != NULL)
504             *type++ = '\0';
505
506         (void)decodeenc(path);
507
508         while (path[1] == '/') /* collapse multiple leading slashes to one */
509             ++path;
510
511         reldir = strrchr(path, '/');
512         if (reldir != NULL && ftp_check_globbingchars(reldir)) {
513             wildcard = &reldir[1];
514             reldir[0] = '\0'; /* strip off the wildcard suffix */
515         }
516
517         /* Copy path, strip (all except the last) trailing slashes */
518         /* (the trailing slash is needed for the dir component loop below) */
519         path = dir = apr_pstrcat(p, path, "/", NULL);
520         for (n = strlen(path); n > 1 && path[n - 1] == '/' && path[n - 2] == '/'; --n)
521             path[n - 1] = '\0';
522
523         /* Add a link to the root directory (if %2f hack was used) */
524         str = (basedir[0] != '\0') ? "<a href=\"/%2f/\">%2f</a>/" : "";
525
526         /* print "ftp://host/" */
527         escpath = ap_escape_html(p, path);
528         str = apr_psprintf(p, DOCTYPE_HTML_3_2
529                 "<html>\n <head>\n  <title>%s%s%s</title>\n"
530                 "<base href=\"%s%s%s\">\n"
531                 " </head>\n"
532                 " <body>\n  <h2>Directory of "
533                 "<a href=\"/\">%s</a>/%s",
534                 ap_escape_html(p, site), basedir, escpath,
535                 ap_escape_uri(p, site), basedir, escpath,
536                 ap_escape_uri(p, site), str);
537
538         APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str),
539                                                           p, c->bucket_alloc));
540
541         for (dir = path+1; (dir = strchr(dir, '/')) != NULL; )
542         {
543             *dir = '\0';
544             if ((reldir = strrchr(path+1, '/'))==NULL) {
545                 reldir = path+1;
546             }
547             else
548                 ++reldir;
549             /* print "path/" component */
550             str = apr_psprintf(p, "<a href=\"%s%s/\">%s</a>/", basedir,
551                         ap_escape_uri(p, path),
552                         ap_escape_html(p, reldir));
553             *dir = '/';
554             while (*dir == '/')
555               ++dir;
556             APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str,
557                                                            strlen(str), p,
558                                                            c->bucket_alloc));
559         }
560         if (wildcard != NULL) {
561             wildcard = ap_escape_html(p, wildcard);
562             APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(wildcard,
563                                                            strlen(wildcard), p,
564                                                            c->bucket_alloc));
565         }
566
567         /* If the caller has determined the current directory, and it differs */
568         /* from what the client requested, then show the real name */
569         if (pwd == NULL || strncmp(pwd, path, strlen(pwd)) == 0) {
570             str = apr_psprintf(p, "</h2>\n\n  <hr />\n\n<pre>");
571         }
572         else {
573             str = apr_psprintf(p, "</h2>\n\n(%s)\n\n  <hr />\n\n<pre>",
574                                ap_escape_html(p, pwd));
575         }
576         APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str),
577                                                            p, c->bucket_alloc));
578
579         /* print README */
580         if (readme) {
581             str = apr_psprintf(p, "%s\n</pre>\n\n<hr />\n\n<pre>\n",
582                                ap_escape_html(p, readme));
583
584             APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str,
585                                                            strlen(str), p,
586                                                            c->bucket_alloc));
587         }
588
589         /* make sure page intro gets sent out */
590         APR_BRIGADE_INSERT_TAIL(out, apr_bucket_flush_create(c->bucket_alloc));
591         if (APR_SUCCESS != (rv = ap_pass_brigade(f->next, out))) {
592             return rv;
593         }
594         apr_brigade_cleanup(out);
595
596         ctx->state = BODY;
597     }
598
599     /* loop through each line of directory */
600     while (BODY == ctx->state) {
601         char *filename;
602         int found = 0;
603         int eos = 0;
604         ap_regmatch_t re_result[LS_REG_MATCH];
605
606         /* get a complete line */
607         /* if the buffer overruns - throw data away */
608         while (!found && !APR_BRIGADE_EMPTY(ctx->in)) {
609             char *pos, *response;
610             apr_size_t len, max;
611             apr_bucket *e;
612
613             e = APR_BRIGADE_FIRST(ctx->in);
614             if (APR_BUCKET_IS_EOS(e)) {
615                 eos = 1;
616                 break;
617             }
618             if (APR_SUCCESS != (rv = apr_bucket_read(e, (const char **)&response, &len, APR_BLOCK_READ))) {
619                 return rv;
620             }
621             pos = memchr(response, APR_ASCII_LF, len);
622             if (pos != NULL) {
623                 if ((response + len) != (pos + 1)) {
624                     len = pos - response + 1;
625                     apr_bucket_split(e, pos - response + 1);
626                 }
627                 found = 1;
628             }
629             max = sizeof(ctx->buffer) - strlen(ctx->buffer) - 1;
630             if (len > max) {
631                 len = max;
632             }
633
634             /* len+1 to leave space for the trailing nil char */
635             apr_cpystrn(ctx->buffer+strlen(ctx->buffer), response, len+1);
636
637             APR_BUCKET_REMOVE(e);
638             apr_bucket_destroy(e);
639         }
640
641         /* EOS? jump to footer */
642         if (eos) {
643             ctx->state = FOOTER;
644             break;
645         }
646
647         /* not complete? leave and try get some more */
648         if (!found) {
649             return APR_SUCCESS;
650         }
651
652         {
653             apr_size_t n = strlen(ctx->buffer);
654             if (ctx->buffer[n-1] == CRLF[1])  /* strip trailing '\n' */
655                 ctx->buffer[--n] = '\0';
656             if (ctx->buffer[n-1] == CRLF[0])  /* strip trailing '\r' if present */
657                 ctx->buffer[--n] = '\0';
658         }
659
660         /* a symlink? */
661         if (ctx->buffer[0] == 'l' && (filename = strstr(ctx->buffer, " -> ")) != NULL) {
662             char *link_ptr = filename;
663
664             do {
665                 filename--;
666             } while (filename[0] != ' ' && filename > ctx->buffer);
667             if (filename > ctx->buffer)
668                 *(filename++) = '\0';
669             *(link_ptr++) = '\0';
670             str = apr_psprintf(p, "%s <a href=\"%s\">%s %s</a>\n",
671                                ap_escape_html(p, ctx->buffer),
672                                ap_escape_uri(p, filename),
673                                ap_escape_html(p, filename),
674                                ap_escape_html(p, link_ptr));
675         }
676
677         /* a directory/file? */
678         else if (ctx->buffer[0] == 'd' || ctx->buffer[0] == '-' || ctx->buffer[0] == 'l' || apr_isdigit(ctx->buffer[0])) {
679             int searchidx = 0;
680             char *searchptr = NULL;
681             int firstfile = 1;
682             if (apr_isdigit(ctx->buffer[0])) {  /* handle DOS dir */
683                 searchptr = strchr(ctx->buffer, '<');
684                 if (searchptr != NULL)
685                     *searchptr = '[';
686                 searchptr = strchr(ctx->buffer, '>');
687                 if (searchptr != NULL)
688                     *searchptr = ']';
689             }
690
691             filename = strrchr(ctx->buffer, ' ');
692             if (filename == NULL) {
693                 /* Line is broken.  Ignore it. */
694                 ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(01034)
695                               "proxy_ftp: could not parse line %s",
696                               ctx->buffer);
697                 /* erase buffer for next time around */
698                 ctx->buffer[0] = 0;
699                 continue;  /* while state is BODY */
700             }
701             *(filename++) = '\0';
702
703             /* handle filenames with spaces in 'em */
704             if (!strcmp(filename, ".") || !strcmp(filename, "..") || firstfile) {
705                 firstfile = 0;
706                 searchidx = filename - ctx->buffer;
707             }
708             else if (searchidx != 0 && ctx->buffer[searchidx] != 0) {
709                 *(--filename) = ' ';
710                 ctx->buffer[searchidx - 1] = '\0';
711                 filename = &ctx->buffer[searchidx];
712             }
713
714             /* Append a slash to the HREF link for directories */
715             if (!strcmp(filename, ".") || !strcmp(filename, "..") || ctx->buffer[0] == 'd') {
716                 str = apr_psprintf(p, "%s <a href=\"%s/\">%s</a>\n",
717                                    ap_escape_html(p, ctx->buffer),
718                                    ap_escape_uri(p, filename),
719                                    ap_escape_html(p, filename));
720             }
721             else {
722                 str = apr_psprintf(p, "%s <a href=\"%s\">%s</a>\n",
723                                    ap_escape_html(p, ctx->buffer),
724                                    ap_escape_uri(p, filename),
725                                    ap_escape_html(p, filename));
726             }
727         }
728         /* Try a fallback for listings in the format of "ls -s1" */
729         else if (0 == ap_regexec(ls_regex, ctx->buffer, LS_REG_MATCH, re_result, 0)) {
730             /*
731              * We don't need to check for rm_eo == rm_so == -1 here since ls_regex
732              * is such that $2 cannot be unset if we have a match.
733              */
734             filename = apr_pstrndup(p, &ctx->buffer[re_result[2].rm_so], re_result[2].rm_eo - re_result[2].rm_so);
735
736             str = apr_pstrcat(p, ap_escape_html(p, apr_pstrndup(p, ctx->buffer, re_result[2].rm_so)),
737                               "<a href=\"", ap_escape_uri(p, filename), "\">",
738                               ap_escape_html(p, filename), "</a>\n", NULL);
739         }
740         else {
741             strcat(ctx->buffer, "\n"); /* re-append the newline */
742             str = ap_escape_html(p, ctx->buffer);
743         }
744
745         /* erase buffer for next time around */
746         ctx->buffer[0] = 0;
747
748         APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str), p,
749                                                             c->bucket_alloc));
750         APR_BRIGADE_INSERT_TAIL(out, apr_bucket_flush_create(c->bucket_alloc));
751         if (APR_SUCCESS != (rv = ap_pass_brigade(f->next, out))) {
752             return rv;
753         }
754         apr_brigade_cleanup(out);
755
756     }
757
758     if (FOOTER == ctx->state) {
759         str = apr_psprintf(p, "</pre>\n\n  <hr />\n\n  %s\n\n </body>\n</html>\n", ap_psignature("", r));
760         APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str), p,
761                                                             c->bucket_alloc));
762         APR_BRIGADE_INSERT_TAIL(out, apr_bucket_flush_create(c->bucket_alloc));
763         APR_BRIGADE_INSERT_TAIL(out, apr_bucket_eos_create(c->bucket_alloc));
764         if (APR_SUCCESS != (rv = ap_pass_brigade(f->next, out))) {
765             return rv;
766         }
767         apr_brigade_destroy(out);
768     }
769
770     return APR_SUCCESS;
771 }
772
773 /* Parse EPSV reply and return port, or zero on error. */
774 static apr_port_t parse_epsv_reply(const char *reply)
775 {
776     const char *p;
777     char *ep;
778     long port;
779
780     /* Reply syntax per RFC 2428: "229 blah blah (|||port|)" where '|'
781      * can be any character in ASCII from 33-126, obscurely.  Verify
782      * the syntax. */
783     p = ap_strchr_c(reply, '(');
784     if (p == NULL || !p[1] || p[1] != p[2] || p[1] != p[3]
785         || p[4] == p[1]) {
786         return 0;
787     }
788
789     errno = 0;
790     port = strtol(p + 4, &ep, 10);
791     if (errno || port < 1 || port > 65535 || ep[0] != p[1] || ep[1] != ')') {
792         return 0;
793     }
794
795     return (apr_port_t)port;
796 }
797
798 /*
799  * Generic "send FTP command to server" routine, using the control socket.
800  * Returns the FTP returncode (3 digit code)
801  * Allows for tracing the FTP protocol (in LogLevel debug)
802  */
803 static int
804 proxy_ftp_command(const char *cmd, request_rec *r, conn_rec *ftp_ctrl,
805                   apr_bucket_brigade *bb, char **pmessage)
806 {
807     char *crlf;
808     int rc;
809     char message[HUGE_STRING_LEN];
810
811     /* If cmd == NULL, we retrieve the next ftp response line */
812     if (cmd != NULL) {
813         conn_rec *c = r->connection;
814         APR_BRIGADE_INSERT_TAIL(bb, apr_bucket_pool_create(cmd, strlen(cmd), r->pool, c->bucket_alloc));
815         APR_BRIGADE_INSERT_TAIL(bb, apr_bucket_flush_create(c->bucket_alloc));
816         ap_pass_brigade(ftp_ctrl->output_filters, bb);
817
818         /* strip off the CRLF for logging */
819         apr_cpystrn(message, cmd, sizeof(message));
820         if ((crlf = strchr(message, '\r')) != NULL ||
821             (crlf = strchr(message, '\n')) != NULL)
822             *crlf = '\0';
823         if (strncmp(message,"PASS ", 5) == 0)
824             strcpy(&message[5], "****");
825         ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, ">%s", message);
826     }
827
828     rc = ftp_getrc_msg(ftp_ctrl, bb, message, sizeof message);
829     if (rc == -1 || rc == 421)
830         strcpy(message,"<unable to read result>");
831     if ((crlf = strchr(message, '\r')) != NULL ||
832         (crlf = strchr(message, '\n')) != NULL)
833         *crlf = '\0';
834     ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "<%3.3u %s", rc, message);
835
836     if (pmessage != NULL)
837         *pmessage = apr_pstrdup(r->pool, message);
838
839     return rc;
840 }
841
842 /* Set ftp server to TYPE {A,I,E} before transfer of a directory or file */
843 static int ftp_set_TYPE(char xfer_type, request_rec *r, conn_rec *ftp_ctrl,
844                   apr_bucket_brigade *bb, char **pmessage)
845 {
846     char old_type[2] = { 'A', '\0' }; /* After logon, mode is ASCII */
847     int ret = HTTP_OK;
848     int rc;
849
850     /* set desired type */
851     old_type[0] = xfer_type;
852
853     rc = proxy_ftp_command(apr_pstrcat(r->pool, "TYPE ", old_type, CRLF, NULL),
854                            r, ftp_ctrl, bb, pmessage);
855 /* responses: 200, 421, 500, 501, 504, 530 */
856     /* 200 Command okay. */
857     /* 421 Service not available, closing control connection. */
858     /* 500 Syntax error, command unrecognized. */
859     /* 501 Syntax error in parameters or arguments. */
860     /* 504 Command not implemented for that parameter. */
861     /* 530 Not logged in. */
862     if (rc == -1) {
863         ret = ap_proxyerror(r, HTTP_GATEWAY_TIME_OUT,
864                              "Error reading from remote server");
865     }
866     else if (rc == 421) {
867         ret = ap_proxyerror(r, HTTP_BAD_GATEWAY,
868                              "Error reading from remote server");
869     }
870     else if (rc != 200 && rc != 504) {
871         ret = ap_proxyerror(r, HTTP_BAD_GATEWAY,
872                              "Unable to set transfer type");
873     }
874 /* Allow not implemented */
875     else if (rc == 504) {
876         /* ignore it silently */
877     }
878
879     return ret;
880 }
881
882
883 /* Return the current directory which we have selected on the FTP server, or NULL */
884 static char *ftp_get_PWD(request_rec *r, conn_rec *ftp_ctrl, apr_bucket_brigade *bb)
885 {
886     char *cwd = NULL;
887     char *ftpmessage = NULL;
888
889     /* responses: 257, 500, 501, 502, 421, 550 */
890     /* 257 "<directory-name>" <commentary> */
891     /* 421 Service not available, closing control connection. */
892     /* 500 Syntax error, command unrecognized. */
893     /* 501 Syntax error in parameters or arguments. */
894     /* 502 Command not implemented. */
895     /* 550 Requested action not taken. */
896     switch (proxy_ftp_command("PWD" CRLF, r, ftp_ctrl, bb, &ftpmessage)) {
897         case -1:
898             ap_proxyerror(r, HTTP_GATEWAY_TIME_OUT,
899                              "Failed to read PWD on ftp server");
900             break;
901
902         case 421:
903         case 550:
904             ap_proxyerror(r, HTTP_BAD_GATEWAY,
905                              "Failed to read PWD on ftp server");
906             break;
907
908         case 257: {
909             const char *dirp = ftpmessage;
910             cwd = ap_getword_conf(r->pool, &dirp);
911         }
912     }
913     return cwd;
914 }
915
916
917 /* Common routine for failed authorization (i.e., missing or wrong password)
918  * to an ftp service. This causes most browsers to retry the request
919  * with username and password (which was presumably queried from the user)
920  * supplied in the Authorization: header.
921  * Note that we "invent" a realm name which consists of the
922  * ftp://user@host part of the reqest (sans password -if supplied but invalid-)
923  */
924 static int ftp_unauthorized(request_rec *r, int log_it)
925 {
926     r->proxyreq = PROXYREQ_NONE;
927     /*
928      * Log failed requests if they supplied a password (log username/password
929      * guessing attempts)
930      */
931     if (log_it)
932         ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(01035)
933                       "missing or failed auth to %s",
934                       apr_uri_unparse(r->pool,
935                                  &r->parsed_uri, APR_URI_UNP_OMITPATHINFO));
936
937     apr_table_setn(r->err_headers_out, "WWW-Authenticate",
938                    apr_pstrcat(r->pool, "Basic realm=\"",
939                                apr_uri_unparse(r->pool, &r->parsed_uri,
940                        APR_URI_UNP_OMITPASSWORD | APR_URI_UNP_OMITPATHINFO),
941                                "\"", NULL));
942
943     return HTTP_UNAUTHORIZED;
944 }
945
946 static
947 apr_status_t proxy_ftp_cleanup(request_rec *r, proxy_conn_rec *backend)
948 {
949
950     backend->close = 1;
951     ap_set_module_config(r->connection->conn_config, &proxy_ftp_module, NULL);
952     ap_proxy_release_connection("FTP", backend, r->server);
953
954     return OK;
955 }
956
957 static
958 int ftp_proxyerror(request_rec *r, proxy_conn_rec *conn, int statuscode, const char *message)
959 {
960     proxy_ftp_cleanup(r, conn);
961     return ap_proxyerror(r, statuscode, message);
962 }
963 /*
964  * Handles direct access of ftp:// URLs
965  * Original (Non-PASV) version from
966  * Troy Morrison <spiffnet@zoom.com>
967  * PASV added by Chuck
968  * Filters by [Graham Leggett <minfrin@sharp.fm>]
969  */
970 static int proxy_ftp_handler(request_rec *r, proxy_worker *worker,
971                              proxy_server_conf *conf, char *url,
972                              const char *proxyhost, apr_port_t proxyport)
973 {
974     apr_pool_t *p = r->pool;
975     conn_rec *c = r->connection;
976     proxy_conn_rec *backend;
977     apr_socket_t *sock, *local_sock, *data_sock = NULL;
978     apr_sockaddr_t *connect_addr = NULL;
979     apr_status_t rv;
980     conn_rec *origin, *data = NULL;
981     apr_status_t err = APR_SUCCESS;
982     apr_status_t uerr = APR_SUCCESS;
983     apr_bucket_brigade *bb = apr_brigade_create(p, c->bucket_alloc);
984     char *buf, *connectname;
985     apr_port_t connectport;
986     char *ftpmessage = NULL;
987     char *path, *strp, *type_suffix, *cwd = NULL;
988     apr_uri_t uri;
989     char *user = NULL;
990 /*    char *account = NULL; how to supply an account in a URL? */
991     const char *password = NULL;
992     int len, rc;
993     int one = 1;
994     char *size = NULL;
995     char xfer_type = 'A'; /* after ftp login, the default is ASCII */
996     int  dirlisting = 0;
997 #if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
998     apr_time_t mtime = 0L;
999 #endif
1000     proxy_ftp_dir_conf *fdconf = ap_get_module_config(r->per_dir_config,
1001                                                       &proxy_ftp_module);
1002
1003     /* stuff for PASV mode */
1004     int connect = 0, use_port = 0;
1005     char dates[APR_RFC822_DATE_LEN];
1006     int status;
1007     apr_pool_t *address_pool;
1008
1009     /* is this for us? */
1010     if (proxyhost) {
1011         ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r,
1012                       "declining URL %s - proxyhost %s specified:", url,
1013                       proxyhost);
1014         return DECLINED;        /* proxy connections are via HTTP */
1015     }
1016     if (strncasecmp(url, "ftp:", 4)) {
1017         ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r,
1018                       "declining URL %s - not ftp:", url);
1019         return DECLINED;        /* only interested in FTP */
1020     }
1021     ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r, "serving URL %s", url);
1022
1023
1024     /*
1025      * I: Who Do I Connect To? -----------------------
1026      *
1027      * Break up the URL to determine the host to connect to
1028      */
1029
1030     /* we only support GET and HEAD */
1031     if (r->method_number != M_GET)
1032         return HTTP_NOT_IMPLEMENTED;
1033
1034     /* We break the URL into host, port, path-search */
1035     if (r->parsed_uri.hostname == NULL) {
1036         if (APR_SUCCESS != apr_uri_parse(p, url, &uri)) {
1037             return ap_proxyerror(r, HTTP_BAD_REQUEST,
1038                 apr_psprintf(p, "URI cannot be parsed: %s", url));
1039         }
1040         connectname = uri.hostname;
1041         connectport = uri.port;
1042         path = apr_pstrdup(p, uri.path);
1043     }
1044     else {
1045         connectname = r->parsed_uri.hostname;
1046         connectport = r->parsed_uri.port;
1047         path = apr_pstrdup(p, r->parsed_uri.path);
1048     }
1049     if (connectport == 0) {
1050         connectport = apr_uri_port_of_scheme("ftp");
1051     }
1052     path = (path != NULL && path[0] != '\0') ? &path[1] : "";
1053
1054     type_suffix = strchr(path, ';');
1055     if (type_suffix != NULL)
1056         *(type_suffix++) = '\0';
1057
1058     if (type_suffix != NULL && strncmp(type_suffix, "type=", 5) == 0
1059         && apr_isalpha(type_suffix[5])) {
1060         /* "type=d" forces a dir listing.
1061          * The other types (i|a|e) are directly used for the ftp TYPE command
1062          */
1063         if ( ! (dirlisting = (apr_tolower(type_suffix[5]) == 'd')))
1064             xfer_type = apr_toupper(type_suffix[5]);
1065
1066         /* Check valid types, rather than ignoring invalid types silently: */
1067         if (strchr("AEI", xfer_type) == NULL)
1068             return ap_proxyerror(r, HTTP_BAD_REQUEST, apr_pstrcat(r->pool,
1069                                     "ftp proxy supports only types 'a', 'i', or 'e': \"",
1070                                     type_suffix, "\" is invalid.", NULL));
1071     }
1072     else {
1073         /* make binary transfers the default */
1074         xfer_type = 'I';
1075     }
1076
1077
1078     /*
1079      * The "Authorization:" header must be checked first. We allow the user
1080      * to "override" the URL-coded user [ & password ] in the Browsers'
1081      * User&Password Dialog. NOTE that this is only marginally more secure
1082      * than having the password travel in plain as part of the URL, because
1083      * Basic Auth simply uuencodes the plain text password. But chances are
1084      * still smaller that the URL is logged regularly.
1085      */
1086     if ((password = apr_table_get(r->headers_in, "Authorization")) != NULL
1087         && strcasecmp(ap_getword(r->pool, &password, ' '), "Basic") == 0
1088         && (password = ap_pbase64decode(r->pool, password))[0] != ':') {
1089         /* Check the decoded string for special characters. */
1090         if (!ftp_check_string(password)) {
1091             return ap_proxyerror(r, HTTP_BAD_REQUEST,
1092                                  "user credentials contained invalid character");
1093         }
1094         /*
1095          * Note that this allocation has to be made from r->connection->pool
1096          * because it has the lifetime of the connection.  The other
1097          * allocations are temporary and can be tossed away any time.
1098          */
1099         user = ap_getword_nulls(r->connection->pool, &password, ':');
1100         r->ap_auth_type = "Basic";
1101         r->user = r->parsed_uri.user = user;
1102     }
1103     else if ((user = r->parsed_uri.user) != NULL) {
1104         user = apr_pstrdup(p, user);
1105         decodeenc(user);
1106         if ((password = r->parsed_uri.password) != NULL) {
1107             char *tmp = apr_pstrdup(p, password);
1108             decodeenc(tmp);
1109             password = tmp;
1110         }
1111     }
1112     else {
1113         user = "anonymous";
1114         password = "apache-proxy@";
1115     }
1116
1117     ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01036)
1118                   "connecting %s to %s:%d", url, connectname, connectport);
1119
1120     if (worker->s->is_address_reusable) {
1121         if (!worker->cp->addr) {
1122             if ((err = PROXY_THREAD_LOCK(worker->balancer)) != APR_SUCCESS) {
1123                 ap_log_rerror(APLOG_MARK, APLOG_ERR, err, r, APLOGNO(01037) "lock");
1124                 return HTTP_INTERNAL_SERVER_ERROR;
1125             }
1126         }
1127         connect_addr = worker->cp->addr;
1128         address_pool = worker->cp->pool;
1129     }
1130     else
1131         address_pool = r->pool;
1132
1133     /* do a DNS lookup for the destination host */
1134     if (!connect_addr)
1135         err = apr_sockaddr_info_get(&(connect_addr),
1136                                     connectname, APR_UNSPEC,
1137                                     connectport, 0,
1138                                     address_pool);
1139     if (worker->s->is_address_reusable && !worker->cp->addr) {
1140         worker->cp->addr = connect_addr;
1141         if ((uerr = PROXY_THREAD_UNLOCK(worker->balancer)) != APR_SUCCESS) {
1142             ap_log_rerror(APLOG_MARK, APLOG_ERR, uerr, r, APLOGNO(01038) "unlock");
1143         }
1144     }
1145     /*
1146      * get all the possible IP addresses for the destname and loop through
1147      * them until we get a successful connection
1148      */
1149     if (APR_SUCCESS != err) {
1150         return ap_proxyerror(r, HTTP_GATEWAY_TIME_OUT, apr_pstrcat(p,
1151                                                  "DNS lookup failure for: ",
1152                                                         connectname, NULL));
1153     }
1154
1155     /* check if ProxyBlock directive on this host */
1156     if (OK != ap_proxy_checkproxyblock(r, conf, connectname, connect_addr)) {
1157         return ap_proxyerror(r, HTTP_FORBIDDEN,
1158                              "Connect to remote machine blocked");
1159     }
1160
1161     /* create space for state information */
1162     backend = (proxy_conn_rec *) ap_get_module_config(c->conn_config, &proxy_ftp_module);
1163     if (!backend) {
1164         status = ap_proxy_acquire_connection("FTP", &backend, worker, r->server);
1165         if (status != OK) {
1166             if (backend) {
1167                 backend->close = 1;
1168                 ap_proxy_release_connection("FTP", backend, r->server);
1169             }
1170             return status;
1171         }
1172         /* TODO: see if ftp could use determine_connection */
1173         backend->addr = connect_addr;
1174         ap_set_module_config(c->conn_config, &proxy_ftp_module, backend);
1175     }
1176
1177
1178     /*
1179      * II: Make the Connection -----------------------
1180      *
1181      * We have determined who to connect to. Now make the connection.
1182      */
1183
1184
1185     if (ap_proxy_connect_backend("FTP", backend, worker, r->server)) {
1186         ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01039)
1187                       "an error occurred creating a new connection to %pI (%s)",
1188                       connect_addr, connectname);
1189         proxy_ftp_cleanup(r, backend);
1190         return HTTP_SERVICE_UNAVAILABLE;
1191     }
1192
1193     if (!backend->connection) {
1194         status = ap_proxy_connection_create("FTP", backend, c, r->server);
1195         if (status != OK) {
1196             proxy_ftp_cleanup(r, backend);
1197             return status;
1198         }
1199     }
1200
1201     /* Use old naming */
1202     origin = backend->connection;
1203     sock = backend->sock;
1204
1205     ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
1206                   "control connection complete");
1207
1208
1209     /*
1210      * III: Send Control Request -------------------------
1211      *
1212      * Log into the ftp server, send the username & password, change to the
1213      * correct directory...
1214      */
1215
1216
1217     /* possible results: */
1218     /* 120 Service ready in nnn minutes. */
1219     /* 220 Service ready for new user. */
1220     /* 421 Service not available, closing control connection. */
1221     rc = proxy_ftp_command(NULL, r, origin, bb, &ftpmessage);
1222     if (rc == -1) {
1223         return ftp_proxyerror(r, backend, HTTP_GATEWAY_TIME_OUT,
1224                 "Error reading from remote server");
1225     }
1226     else if (rc == 421) {
1227         return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1228                 "Error reading from remote server");
1229     }
1230     else if (rc == 120) {
1231         /*
1232          * RFC2616 states: 14.37 Retry-After
1233          *
1234          * The Retry-After response-header field can be used with a 503 (Service
1235          * Unavailable) response to indicate how long the service is expected
1236          * to be unavailable to the requesting client. [...] The value of
1237          * this field can be either an HTTP-date or an integer number of
1238          * seconds (in decimal) after the time of the response. Retry-After
1239          * = "Retry-After" ":" ( HTTP-date | delta-seconds )
1240          */
1241         char *secs_str = ftpmessage;
1242         time_t secs;
1243
1244         /* Look for a number, preceded by whitespace */
1245         while (*secs_str)
1246             if ((secs_str==ftpmessage || apr_isspace(secs_str[-1])) &&
1247                 apr_isdigit(secs_str[0]))
1248                 break;
1249         if (*secs_str != '\0') {
1250             secs = atol(secs_str);
1251             apr_table_addn(r->headers_out, "Retry-After",
1252                            apr_psprintf(p, "%lu", (unsigned long)(60 * secs)));
1253         }
1254         return ftp_proxyerror(r, backend, HTTP_SERVICE_UNAVAILABLE, ftpmessage);
1255     }
1256     else if (rc != 220) {
1257         return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1258     }
1259
1260     rc = proxy_ftp_command(apr_pstrcat(p, "USER ", user, CRLF, NULL),
1261                            r, origin, bb, &ftpmessage);
1262     /* possible results; 230, 331, 332, 421, 500, 501, 530 */
1263     /* states: 1 - error, 2 - success; 3 - send password, 4,5 fail */
1264     /* 230 User logged in, proceed. */
1265     /* 331 User name okay, need password. */
1266     /* 332 Need account for login. */
1267     /* 421 Service not available, closing control connection. */
1268     /* 500 Syntax error, command unrecognized. */
1269     /* (This may include errors such as command line too long.) */
1270     /* 501 Syntax error in parameters or arguments. */
1271     /* 530 Not logged in. */
1272     if (rc == -1) {
1273         return ftp_proxyerror(r, backend, HTTP_GATEWAY_TIME_OUT,
1274                 "Error reading from remote server");
1275     }
1276     else if (rc == 421) {
1277         return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1278                 "Error reading from remote server");
1279     }
1280     else if (rc == 530) {
1281         proxy_ftp_cleanup(r, backend);
1282         return ftp_unauthorized(r, 1);  /* log it: user name guessing
1283                                          * attempt? */
1284     }
1285     else if (rc != 230 && rc != 331) {
1286         return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1287     }
1288
1289     if (rc == 331) {            /* send password */
1290         if (password == NULL) {
1291             proxy_ftp_cleanup(r, backend);
1292             return ftp_unauthorized(r, 0);
1293         }
1294
1295         rc = proxy_ftp_command(apr_pstrcat(p, "PASS ", password, CRLF, NULL),
1296                            r, origin, bb, &ftpmessage);
1297         /* possible results 202, 230, 332, 421, 500, 501, 503, 530 */
1298         /* 230 User logged in, proceed. */
1299         /* 332 Need account for login. */
1300         /* 421 Service not available, closing control connection. */
1301         /* 500 Syntax error, command unrecognized. */
1302         /* 501 Syntax error in parameters or arguments. */
1303         /* 503 Bad sequence of commands. */
1304         /* 530 Not logged in. */
1305         if (rc == -1) {
1306             return ftp_proxyerror(r, backend, HTTP_GATEWAY_TIME_OUT,
1307                                   "Error reading from remote server");
1308         }
1309         else if (rc == 421) {
1310             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1311                                   "Error reading from remote server");
1312         }
1313         else if (rc == 332) {
1314             return ftp_proxyerror(r, backend, HTTP_UNAUTHORIZED,
1315                   apr_pstrcat(p, "Need account for login: ", ftpmessage, NULL));
1316         }
1317         /* @@@ questionable -- we might as well return a 403 Forbidden here */
1318         else if (rc == 530) {
1319             proxy_ftp_cleanup(r, backend);
1320             return ftp_unauthorized(r, 1);      /* log it: passwd guessing
1321                                                  * attempt? */
1322         }
1323         else if (rc != 230 && rc != 202) {
1324             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1325         }
1326     }
1327     apr_table_set(r->notes, "Directory-README", ftpmessage);
1328
1329
1330     /* Special handling for leading "%2f": this enforces a "cwd /"
1331      * out of the $HOME directory which was the starting point after login
1332      */
1333     if (strncasecmp(path, "%2f", 3) == 0) {
1334         path += 3;
1335         while (*path == '/') /* skip leading '/' (after root %2f) */
1336             ++path;
1337
1338         rc = proxy_ftp_command("CWD /" CRLF, r, origin, bb, &ftpmessage);
1339         if (rc == -1) {
1340             return ftp_proxyerror(r, backend, HTTP_GATEWAY_TIME_OUT,
1341                     "Error reading from remote server");
1342         }
1343         else if (rc == 421) {
1344             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1345                     "Error reading from remote server");
1346         }
1347     }
1348
1349     /*
1350      * set the directory (walk directory component by component): this is
1351      * what we must do if we don't know the OS type of the remote machine
1352      */
1353     for (;;) {
1354         strp = strchr(path, '/');
1355         if (strp == NULL)
1356             break;
1357         *strp = '\0';
1358
1359         decodeenc(path); /* Note! This decodes a %2f -> "/" */
1360
1361         if (strchr(path, '/')) { /* are there now any '/' characters? */
1362             return ftp_proxyerror(r, backend, HTTP_BAD_REQUEST,
1363                                   "Use of /%2f is only allowed at the base directory");
1364         }
1365
1366         /* NOTE: FTP servers do globbing on the path.
1367          * So we need to escape the URI metacharacters.
1368          * We use a special glob-escaping routine to escape globbing chars.
1369          * We could also have extended gen_test_char.c with a special T_ESCAPE_FTP_PATH
1370          */
1371         rc = proxy_ftp_command(apr_pstrcat(p, "CWD ",
1372                            ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
1373                            r, origin, bb, &ftpmessage);
1374         *strp = '/';
1375         /* responses: 250, 421, 500, 501, 502, 530, 550 */
1376         /* 250 Requested file action okay, completed. */
1377         /* 421 Service not available, closing control connection. */
1378         /* 500 Syntax error, command unrecognized. */
1379         /* 501 Syntax error in parameters or arguments. */
1380         /* 502 Command not implemented. */
1381         /* 530 Not logged in. */
1382         /* 550 Requested action not taken. */
1383         if (rc == -1) {
1384             return ftp_proxyerror(r, backend, HTTP_GATEWAY_TIME_OUT,
1385                     "Error reading from remote server");
1386         }
1387         else if (rc == 421) {
1388             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1389                     "Error reading from remote server");
1390         }
1391         else if (rc == 550) {
1392             return ftp_proxyerror(r, backend, HTTP_NOT_FOUND, ftpmessage);
1393         }
1394         else if (rc != 250) {
1395             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1396         }
1397
1398         path = strp + 1;
1399     }
1400
1401     /*
1402      * IV: Make Data Connection? -------------------------
1403      *
1404      * Try EPSV, if that fails... try PASV, if that fails... try PORT.
1405      */
1406 /* this temporarily switches off EPSV/PASV */
1407 /*goto bypass;*/
1408
1409     /* set up data connection - EPSV */
1410     {
1411         apr_port_t data_port;
1412
1413         /*
1414          * The EPSV command replaces PASV where both IPV4 and IPV6 is
1415          * supported. Only the port is returned, the IP address is always the
1416          * same as that on the control connection. Example: Entering Extended
1417          * Passive Mode (|||6446|)
1418          */
1419         rc = proxy_ftp_command("EPSV" CRLF,
1420                            r, origin, bb, &ftpmessage);
1421         /* possible results: 227, 421, 500, 501, 502, 530 */
1422         /* 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). */
1423         /* 421 Service not available, closing control connection. */
1424         /* 500 Syntax error, command unrecognized. */
1425         /* 501 Syntax error in parameters or arguments. */
1426         /* 502 Command not implemented. */
1427         /* 530 Not logged in. */
1428         if (rc == -1) {
1429             return ftp_proxyerror(r, backend, HTTP_GATEWAY_TIME_OUT,
1430                     "Error reading from remote server");
1431         }
1432         else if (rc == 421) {
1433             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1434                     "Error reading from remote server");
1435         }
1436         else if (rc != 229 && rc != 500 && rc != 501 && rc != 502) {
1437             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1438         }
1439         else if (rc == 229) {
1440             /* Parse the port out of the EPSV reply. */
1441             data_port = parse_epsv_reply(ftpmessage);
1442
1443             if (data_port) {
1444                 apr_sockaddr_t *remote_addr, epsv_addr;
1445
1446                 ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
1447                               "EPSV contacting remote host on port %d", data_port);
1448
1449                 /* Retrieve the client's address. */
1450                 rv = apr_socket_addr_get(&remote_addr, APR_REMOTE, sock);
1451                 if (rv == APR_SUCCESS) {
1452                     /* Take a shallow copy of the server address to
1453                      * modify; the _addr_get function gives back a
1454                      * pointer to the socket's internal structure.
1455                      * This is awkward given current APR network
1456                      * interfaces. */
1457                     epsv_addr = *remote_addr;
1458                     epsv_addr.port = data_port;
1459 #if APR_HAVE_IPV6
1460                     if (epsv_addr.family == APR_INET6) {
1461                         epsv_addr.sa.sin6.sin6_port = htons(data_port);
1462                     }
1463                     else
1464 #endif
1465                     {
1466                         epsv_addr.sa.sin.sin_port = htons(data_port);
1467                     }
1468                     rv = apr_socket_create(&data_sock, epsv_addr.family, SOCK_STREAM, 0, r->pool);
1469                 }
1470
1471                 if (rv != APR_SUCCESS) {
1472                     ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01040) 
1473                                   "could not establish socket for client data connection");
1474                     proxy_ftp_cleanup(r, backend);
1475                     return HTTP_INTERNAL_SERVER_ERROR;
1476                 }
1477
1478                 if (conf->recv_buffer_size > 0
1479                         && (rv = apr_socket_opt_set(data_sock, APR_SO_RCVBUF,
1480                                                     conf->recv_buffer_size))) {
1481                     ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01041)
1482                                   "apr_socket_opt_set(SO_RCVBUF): Failed to "
1483                                   "set ProxyReceiveBufferSize, using default");
1484                 }
1485
1486                 rv = apr_socket_opt_set(data_sock, APR_TCP_NODELAY, 1);
1487                 if (rv != APR_SUCCESS && rv != APR_ENOTIMPL) {
1488                     ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01042)
1489                                   "apr_socket_opt_set(APR_TCP_NODELAY): "
1490                                   "Failed to set");
1491                 }
1492
1493                 rv = apr_socket_connect(data_sock, &epsv_addr);
1494                 if (rv != APR_SUCCESS) {
1495                     ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01043)
1496                                   "EPSV attempt to connect to %pI failed - "
1497                                   "Firewall/NAT?", &epsv_addr);
1498                     return ftp_proxyerror(r, backend, HTTP_GATEWAY_TIME_OUT,
1499                             apr_psprintf(r->pool,
1500                                     "EPSV attempt to connect to %pI failed - firewall/NAT?",
1501                                     &epsv_addr));
1502                 }
1503                 else {
1504                     ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
1505                                   "connected data socket to %pI", &epsv_addr);
1506                     connect = 1;
1507                 }
1508             }
1509         }
1510     }
1511
1512     /* set up data connection - PASV */
1513     if (!connect) {
1514         rc = proxy_ftp_command("PASV" CRLF,
1515                            r, origin, bb, &ftpmessage);
1516         /* possible results: 227, 421, 500, 501, 502, 530 */
1517         /* 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). */
1518         /* 421 Service not available, closing control connection. */
1519         /* 500 Syntax error, command unrecognized. */
1520         /* 501 Syntax error in parameters or arguments. */
1521         /* 502 Command not implemented. */
1522         /* 530 Not logged in. */
1523         if (rc == -1) {
1524             return ftp_proxyerror(r, backend, HTTP_GATEWAY_TIME_OUT,
1525                     "Error reading from remote server");
1526         }
1527         else if (rc == 421) {
1528             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1529                     "Error reading from remote server");
1530         }
1531         else if (rc != 227 && rc != 502) {
1532             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1533         }
1534         else if (rc == 227) {
1535             unsigned int h0, h1, h2, h3, p0, p1;
1536             char *pstr;
1537             char *tok_cntx;
1538
1539 /* FIXME: Check PASV against RFC1123 */
1540
1541             pstr = ftpmessage;
1542             pstr = apr_strtok(pstr, " ", &tok_cntx);    /* separate result code */
1543             if (pstr != NULL) {
1544                 if (*(pstr + strlen(pstr) + 1) == '=') {
1545                     pstr += strlen(pstr) + 2;
1546                 }
1547                 else {
1548                     pstr = apr_strtok(NULL, "(", &tok_cntx);    /* separate address &
1549                                                                  * port params */
1550                     if (pstr != NULL)
1551                         pstr = apr_strtok(NULL, ")", &tok_cntx);
1552                 }
1553             }
1554
1555 /* FIXME: Only supports IPV4 - fix in RFC2428 */
1556
1557             if (pstr != NULL && (sscanf(pstr,
1558                  "%d,%d,%d,%d,%d,%d", &h3, &h2, &h1, &h0, &p1, &p0) == 6)) {
1559
1560                 apr_sockaddr_t *pasv_addr;
1561                 apr_port_t pasvport = (p1 << 8) + p0;
1562                 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01044)
1563                               "PASV contacting host %d.%d.%d.%d:%d",
1564                               h3, h2, h1, h0, pasvport);
1565
1566                 if ((rv = apr_socket_create(&data_sock, connect_addr->family, SOCK_STREAM, 0, r->pool)) != APR_SUCCESS) {
1567                     ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01045)
1568                                   "error creating PASV socket");
1569                     proxy_ftp_cleanup(r, backend);
1570                     return HTTP_INTERNAL_SERVER_ERROR;
1571                 }
1572
1573                 if (conf->recv_buffer_size > 0
1574                         && (rv = apr_socket_opt_set(data_sock, APR_SO_RCVBUF,
1575                                                     conf->recv_buffer_size))) {
1576                     ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01046)
1577                                   "apr_socket_opt_set(SO_RCVBUF): Failed to set ProxyReceiveBufferSize, using default");
1578                 }
1579
1580                 rv = apr_socket_opt_set(data_sock, APR_TCP_NODELAY, 1);
1581                 if (rv != APR_SUCCESS && rv != APR_ENOTIMPL) {
1582                     ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01047)
1583                                   "apr_socket_opt_set(APR_TCP_NODELAY): "
1584                                   "Failed to set");
1585                 }
1586
1587                 /* make the connection */
1588                 apr_sockaddr_info_get(&pasv_addr, apr_psprintf(p, "%d.%d.%d.%d", h3, h2, h1, h0), connect_addr->family, pasvport, 0, p);
1589                 rv = apr_socket_connect(data_sock, pasv_addr);
1590                 if (rv != APR_SUCCESS) {
1591                     ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01048)
1592                                   "PASV attempt to connect to %pI failed - Firewall/NAT?", pasv_addr);
1593                     return ftp_proxyerror(r, backend, HTTP_GATEWAY_TIME_OUT,
1594                             apr_psprintf(r->pool,
1595                                     "PASV attempt to connect to %pI failed - firewall/NAT?",
1596                                     pasv_addr));
1597                 }
1598                 else {
1599                     connect = 1;
1600                 }
1601             }
1602         }
1603     }
1604 /*bypass:*/
1605
1606     /* set up data connection - PORT */
1607     if (!connect) {
1608         apr_sockaddr_t *local_addr;
1609         char *local_ip;
1610         apr_port_t local_port;
1611         unsigned int h0, h1, h2, h3, p0, p1;
1612
1613         if ((rv = apr_socket_create(&local_sock, connect_addr->family, SOCK_STREAM, 0, r->pool)) != APR_SUCCESS) {
1614             ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01049)
1615                           "error creating local socket");
1616             proxy_ftp_cleanup(r, backend);
1617             return HTTP_INTERNAL_SERVER_ERROR;
1618         }
1619         apr_socket_addr_get(&local_addr, APR_LOCAL, sock);
1620         local_port = local_addr->port;
1621         apr_sockaddr_ip_get(&local_ip, local_addr);
1622
1623         if ((rv = apr_socket_opt_set(local_sock, APR_SO_REUSEADDR, one))
1624                 != APR_SUCCESS) {
1625 #ifndef _OSD_POSIX              /* BS2000 has this option "always on" */
1626             ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01050)
1627                           "error setting reuseaddr option");
1628             proxy_ftp_cleanup(r, backend);
1629             return HTTP_INTERNAL_SERVER_ERROR;
1630 #endif                          /* _OSD_POSIX */
1631         }
1632
1633         apr_sockaddr_info_get(&local_addr, local_ip, APR_UNSPEC, local_port, 0, r->pool);
1634
1635         if ((rv = apr_socket_bind(local_sock, local_addr)) != APR_SUCCESS) {
1636             ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01051)
1637                           "error binding to ftp data socket %pI", local_addr);
1638             proxy_ftp_cleanup(r, backend);
1639             return HTTP_INTERNAL_SERVER_ERROR;
1640         }
1641
1642         /* only need a short queue */
1643         if ((rv = apr_socket_listen(local_sock, 2)) != APR_SUCCESS) {
1644             ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01052)
1645                           "error listening to ftp data socket %pI", local_addr);
1646             proxy_ftp_cleanup(r, backend);
1647             return HTTP_INTERNAL_SERVER_ERROR;
1648         }
1649
1650 /* FIXME: Sent PORT here */
1651
1652         if (local_ip && (sscanf(local_ip,
1653                                 "%d.%d.%d.%d", &h3, &h2, &h1, &h0) == 4)) {
1654             p1 = (local_port >> 8);
1655             p0 = (local_port & 0xFF);
1656
1657             rc = proxy_ftp_command(apr_psprintf(p, "PORT %d,%d,%d,%d,%d,%d" CRLF, h3, h2, h1, h0, p1, p0),
1658                            r, origin, bb, &ftpmessage);
1659             /* possible results: 200, 421, 500, 501, 502, 530 */
1660             /* 200 Command okay. */
1661             /* 421 Service not available, closing control connection. */
1662             /* 500 Syntax error, command unrecognized. */
1663             /* 501 Syntax error in parameters or arguments. */
1664             /* 502 Command not implemented. */
1665             /* 530 Not logged in. */
1666             if (rc == -1) {
1667                 return ftp_proxyerror(r, backend, HTTP_GATEWAY_TIME_OUT,
1668                         "Error reading from remote server");
1669             }
1670             else if (rc == 421) {
1671                 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1672                         "Error reading from remote server");
1673             }
1674             else if (rc != 200) {
1675                 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1676             }
1677
1678             /* signal that we must use the EPRT/PORT loop */
1679             use_port = 1;
1680         }
1681         else {
1682 /* IPV6 FIXME:
1683  * The EPRT command replaces PORT where both IPV4 and IPV6 is supported. The first
1684  * number (1,2) indicates the protocol type. Examples:
1685  *   EPRT |1|132.235.1.2|6275|
1686  *   EPRT |2|1080::8:800:200C:417A|5282|
1687  */
1688             return ftp_proxyerror(r, backend, HTTP_NOT_IMPLEMENTED,
1689                                   "Connect to IPV6 ftp server using EPRT not supported. Enable EPSV.");
1690         }
1691     }
1692
1693
1694     /*
1695      * V: Set The Headers -------------------
1696      *
1697      * Get the size of the request, set up the environment for HTTP.
1698      */
1699
1700     /* set request; "path" holds last path component */
1701     len = decodeenc(path);
1702
1703     if (strchr(path, '/')) { /* are there now any '/' characters? */
1704        return ftp_proxyerror(r, backend, HTTP_BAD_REQUEST,
1705                              "Use of /%2f is only allowed at the base directory");
1706     }
1707
1708     /* If len == 0 then it must be a directory (you can't RETR nothing)
1709      * Also, don't allow to RETR by wildcard. Instead, create a dirlisting,
1710      * unless ProxyFtpListOnWildcard is off.
1711      */
1712     if (len == 0 || (ftp_check_globbingchars(path) && fdconf->ftp_list_on_wildcard)) {
1713         dirlisting = 1;
1714     }
1715     else {
1716         /* (from FreeBSD ftpd):
1717          * SIZE is not in RFC959, but Postel has blessed it and
1718          * it will be in the updated RFC.
1719          *
1720          * Return size of file in a format suitable for
1721          * using with RESTART (we just count bytes).
1722          */
1723         /* from draft-ietf-ftpext-mlst-14.txt:
1724          * This value will
1725          * change depending on the current STRUcture, MODE and TYPE of the data
1726          * connection, or a data connection which would be created were one
1727          * created now.  Thus, the result of the SIZE command is dependent on
1728          * the currently established STRU, MODE and TYPE parameters.
1729          */
1730         /* Therefore: switch to binary if the user did not specify ";type=a" */
1731         ftp_set_TYPE(xfer_type, r, origin, bb, &ftpmessage);
1732         rc = proxy_ftp_command(apr_pstrcat(p, "SIZE ",
1733                            ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
1734                            r, origin, bb, &ftpmessage);
1735         if (rc == -1) {
1736             return ftp_proxyerror(r, backend, HTTP_GATEWAY_TIME_OUT,
1737                     "Error reading from remote server");
1738         }
1739         else if (rc == 421) {
1740             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1741                     "Error reading from remote server");
1742         }
1743         else if (rc == 213) {/* Size command ok */
1744             int j;
1745             for (j = 0; apr_isdigit(ftpmessage[j]); j++)
1746                 ;
1747             ftpmessage[j] = '\0';
1748             if (ftpmessage[0] != '\0')
1749                  size = ftpmessage; /* already pstrdup'ed: no copy necessary */
1750         }
1751         else if (rc == 550) {    /* Not a regular file */
1752             ap_log_rerror(APLOG_MARK, APLOG_TRACE4, 0, r,
1753                           "SIZE shows this is a directory");
1754             dirlisting = 1;
1755             rc = proxy_ftp_command(apr_pstrcat(p, "CWD ",
1756                            ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
1757                            r, origin, bb, &ftpmessage);
1758             /* possible results: 250, 421, 500, 501, 502, 530, 550 */
1759             /* 250 Requested file action okay, completed. */
1760             /* 421 Service not available, closing control connection. */
1761             /* 500 Syntax error, command unrecognized. */
1762             /* 501 Syntax error in parameters or arguments. */
1763             /* 502 Command not implemented. */
1764             /* 530 Not logged in. */
1765             /* 550 Requested action not taken. */
1766             if (rc == -1) {
1767                 return ftp_proxyerror(r, backend, HTTP_GATEWAY_TIME_OUT,
1768                         "Error reading from remote server");
1769             }
1770             else if (rc == 421) {
1771                 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1772                         "Error reading from remote server");
1773             }
1774             else if (rc == 550) {
1775                 return ftp_proxyerror(r, backend, HTTP_NOT_FOUND, ftpmessage);
1776             }
1777             else if (rc != 250) {
1778                 return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1779             }
1780             path = "";
1781             len = 0;
1782         }
1783     }
1784
1785     cwd = ftp_get_PWD(r, origin, bb);
1786     if (cwd != NULL) {
1787         apr_table_set(r->notes, "Directory-PWD", cwd);
1788     }
1789
1790     if (dirlisting) {
1791         ftp_set_TYPE('A', r, origin, bb, NULL);
1792         /* If the current directory contains no slash, we are talking to
1793          * a non-unix ftp system. Try LIST instead of "LIST -lag", it
1794          * should return a long listing anyway (unlike NLST).
1795          * Some exotic FTP servers might choke on the "-lag" switch.
1796          */
1797         /* Note that we do not escape the path here, to allow for
1798          * queries like: ftp://user@host/apache/src/server/http_*.c
1799          */
1800         if (len != 0)
1801             buf = apr_pstrcat(p, "LIST ", path, CRLF, NULL);
1802         else if (cwd == NULL || strchr(cwd, '/') != NULL)
1803             buf = "LIST -lag" CRLF;
1804         else
1805             buf = "LIST" CRLF;
1806     }
1807     else {
1808         /* switch to binary if the user did not specify ";type=a" */
1809         ftp_set_TYPE(xfer_type, r, origin, bb, &ftpmessage);
1810 #if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
1811         /* from draft-ietf-ftpext-mlst-14.txt:
1812          *   The FTP command, MODIFICATION TIME (MDTM), can be used to determine
1813          *   when a file in the server NVFS was last modified.     <..>
1814          *   The syntax of a time value is:
1815          *           time-val       = 14DIGIT [ "." 1*DIGIT ]      <..>
1816          *     Symbolically, a time-val may be viewed as
1817          *           YYYYMMDDHHMMSS.sss
1818          *     The "." and subsequent digits ("sss") are optional. <..>
1819          *     Time values are always represented in UTC (GMT)
1820          */
1821         rc = proxy_ftp_command(apr_pstrcat(p, "MDTM ", ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
1822                                r, origin, bb, &ftpmessage);
1823         /* then extract the Last-Modified time from it (YYYYMMDDhhmmss or YYYYMMDDhhmmss.xxx GMT). */
1824         if (rc == 213) {
1825         struct {
1826             char YYYY[4+1];
1827         char MM[2+1];
1828         char DD[2+1];
1829         char hh[2+1];
1830         char mm[2+1];
1831         char ss[2+1];
1832         } time_val;
1833         if (6 == sscanf(ftpmessage, "%4[0-9]%2[0-9]%2[0-9]%2[0-9]%2[0-9]%2[0-9]",
1834             time_val.YYYY, time_val.MM, time_val.DD, time_val.hh, time_val.mm, time_val.ss)) {
1835                 struct tm tms;
1836         memset (&tms, '\0', sizeof tms);
1837         tms.tm_year = atoi(time_val.YYYY) - 1900;
1838         tms.tm_mon  = atoi(time_val.MM)   - 1;
1839         tms.tm_mday = atoi(time_val.DD);
1840         tms.tm_hour = atoi(time_val.hh);
1841         tms.tm_min  = atoi(time_val.mm);
1842         tms.tm_sec  = atoi(time_val.ss);
1843 #ifdef HAVE_TIMEGM /* Does system have timegm()? */
1844         mtime = timegm(&tms);
1845         mtime *= APR_USEC_PER_SEC;
1846 #elif HAVE_GMTOFF /* does struct tm have a member tm_gmtoff? */
1847                 /* mktime will subtract the local timezone, which is not what we want.
1848          * Add it again because the MDTM string is GMT
1849          */
1850         mtime = mktime(&tms);
1851         mtime += tms.tm_gmtoff;
1852         mtime *= APR_USEC_PER_SEC;
1853 #else
1854         mtime = 0L;
1855 #endif
1856             }
1857     }
1858 #endif /* USE_MDTM */
1859 /* FIXME: Handle range requests - send REST */
1860         buf = apr_pstrcat(p, "RETR ", ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL);
1861     }
1862     rc = proxy_ftp_command(buf, r, origin, bb, &ftpmessage);
1863     /* rc is an intermediate response for the LIST or RETR commands */
1864
1865     /*
1866      * RETR: 110, 125, 150, 226, 250, 421, 425, 426, 450, 451, 500, 501, 530,
1867      * 550 NLST: 125, 150, 226, 250, 421, 425, 426, 450, 451, 500, 501, 502,
1868      * 530
1869      */
1870     /* 110 Restart marker reply. */
1871     /* 125 Data connection already open; transfer starting. */
1872     /* 150 File status okay; about to open data connection. */
1873     /* 226 Closing data connection. */
1874     /* 250 Requested file action okay, completed. */
1875     /* 421 Service not available, closing control connection. */
1876     /* 425 Can't open data connection. */
1877     /* 426 Connection closed; transfer aborted. */
1878     /* 450 Requested file action not taken. */
1879     /* 451 Requested action aborted. Local error in processing. */
1880     /* 500 Syntax error, command unrecognized. */
1881     /* 501 Syntax error in parameters or arguments. */
1882     /* 530 Not logged in. */
1883     /* 550 Requested action not taken. */
1884     if (rc == -1) {
1885         return ftp_proxyerror(r, backend, HTTP_GATEWAY_TIME_OUT,
1886                               "Error reading from remote server");
1887     }
1888     else if (rc == 421) {
1889         return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1890                               "Error reading from remote server");
1891     }
1892     else if (rc == 550) {
1893         ap_log_rerror(APLOG_MARK, APLOG_TRACE4, 0, r,
1894                       "RETR failed, trying LIST instead");
1895
1896         /* Directory Listings should always be fetched in ASCII mode */
1897         dirlisting = 1;
1898         ftp_set_TYPE('A', r, origin, bb, NULL);
1899
1900         rc = proxy_ftp_command(apr_pstrcat(p, "CWD ",
1901                                ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
1902                                r, origin, bb, &ftpmessage);
1903         /* possible results: 250, 421, 500, 501, 502, 530, 550 */
1904         /* 250 Requested file action okay, completed. */
1905         /* 421 Service not available, closing control connection. */
1906         /* 500 Syntax error, command unrecognized. */
1907         /* 501 Syntax error in parameters or arguments. */
1908         /* 502 Command not implemented. */
1909         /* 530 Not logged in. */
1910         /* 550 Requested action not taken. */
1911         if (rc == -1) {
1912             return ftp_proxyerror(r, backend, HTTP_GATEWAY_TIME_OUT,
1913                     "Error reading from remote server");
1914         }
1915         else if (rc == 421) {
1916             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1917                     "Error reading from remote server");
1918         }
1919         else if (rc == 550) {
1920             return ftp_proxyerror(r, backend, HTTP_NOT_FOUND, ftpmessage);
1921         }
1922         else if (rc != 250) {
1923             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1924         }
1925
1926         /* Update current directory after CWD */
1927         cwd = ftp_get_PWD(r, origin, bb);
1928         if (cwd != NULL) {
1929             apr_table_set(r->notes, "Directory-PWD", cwd);
1930         }
1931
1932         /* See above for the "LIST" vs. "LIST -lag" discussion. */
1933         rc = proxy_ftp_command((cwd == NULL || strchr(cwd, '/') != NULL)
1934                                ? "LIST -lag" CRLF : "LIST" CRLF,
1935                                r, origin, bb, &ftpmessage);
1936
1937         /* rc is an intermediate response for the LIST command (125 transfer starting, 150 opening data connection) */
1938         if (rc == -1) {
1939             return ftp_proxyerror(r, backend, HTTP_GATEWAY_TIME_OUT,
1940                     "Error reading from remote server");
1941         }
1942         else if (rc == 421) {
1943             return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
1944                     "Error reading from remote server");
1945         }
1946     }
1947     if (rc != 125 && rc != 150 && rc != 226 && rc != 250) {
1948         return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
1949     }
1950
1951     r->status = HTTP_OK;
1952     r->status_line = "200 OK";
1953
1954     apr_rfc822_date(dates, r->request_time);
1955     apr_table_setn(r->headers_out, "Date", dates);
1956     apr_table_setn(r->headers_out, "Server", ap_get_server_description());
1957
1958     /* set content-type */
1959     if (dirlisting) {
1960         ap_set_content_type(r, apr_pstrcat(p, "text/html;charset=",
1961                                            fdconf->ftp_directory_charset ?
1962                                            fdconf->ftp_directory_charset :
1963                                            "ISO-8859-1",  NULL));
1964     }
1965     else {
1966         if (xfer_type != 'A' && size != NULL) {
1967             /* We "trust" the ftp server to really serve (size) bytes... */
1968             apr_table_setn(r->headers_out, "Content-Length", size);
1969             ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r,
1970                           "Content-Length set to %s", size);
1971         }
1972     }
1973     if (r->content_type) {
1974         apr_table_setn(r->headers_out, "Content-Type", r->content_type);
1975         ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r,
1976                       "Content-Type set to %s", r->content_type);
1977     }
1978
1979 #if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
1980     if (mtime != 0L) {
1981         char datestr[APR_RFC822_DATE_LEN];
1982         apr_rfc822_date(datestr, mtime);
1983         apr_table_set(r->headers_out, "Last-Modified", datestr);
1984         ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r,
1985                       "Last-Modified set to %s", datestr);
1986     }
1987 #endif /* USE_MDTM */
1988
1989     /* If an encoding has been set by mistake, delete it.
1990      * @@@ FIXME (e.g., for ftp://user@host/file*.tar.gz,
1991      * @@@        the encoding is currently set to x-gzip)
1992      */
1993     if (dirlisting && r->content_encoding != NULL)
1994         r->content_encoding = NULL;
1995
1996     /* set content-encoding (not for dir listings, they are uncompressed)*/
1997     if (r->content_encoding != NULL && r->content_encoding[0] != '\0') {
1998         ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r,
1999                       "Content-Encoding set to %s", r->content_encoding);
2000         apr_table_setn(r->headers_out, "Content-Encoding", r->content_encoding);
2001     }
2002
2003     /* wait for connection */
2004     if (use_port) {
2005         for (;;) {
2006             rv = apr_socket_accept(&data_sock, local_sock, r->pool);
2007             if (APR_STATUS_IS_EINTR(rv)) {
2008                 continue;
2009             }
2010             else if (rv == APR_SUCCESS) {
2011                 break;
2012             }
2013             else {
2014                 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01053)
2015                               "failed to accept data connection");
2016                 proxy_ftp_cleanup(r, backend);
2017                 return HTTP_GATEWAY_TIME_OUT;
2018             }
2019         }
2020     }
2021
2022     /* the transfer socket is now open, create a new connection */
2023     data = ap_run_create_connection(p, r->server, data_sock, r->connection->id,
2024                                     r->connection->sbh, c->bucket_alloc);
2025     if (!data) {
2026         /*
2027          * the peer reset the connection already; ap_run_create_connection() closed
2028          * the socket
2029          */
2030         ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01054)
2031                       "an error occurred creating the transfer connection");
2032         proxy_ftp_cleanup(r, backend);
2033         return HTTP_INTERNAL_SERVER_ERROR;
2034     }
2035
2036     /*
2037      * We do not do SSL over the data connection, even if the virtual host we
2038      * are in might have SSL enabled
2039      */
2040     ap_proxy_ssl_disable(data);
2041     /* set up the connection filters */
2042     rc = ap_run_pre_connection(data, data_sock);
2043     if (rc != OK && rc != DONE) {
2044         ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01055)
2045                       "pre_connection setup failed (%d)", rc);
2046         data->aborted = 1;
2047         proxy_ftp_cleanup(r, backend);
2048         return rc;
2049     }
2050
2051     /*
2052      * VI: Receive the Response ------------------------
2053      *
2054      * Get response from the remote ftp socket, and pass it up the filter chain.
2055      */
2056
2057     /* send response */
2058     r->sent_bodyct = 1;
2059
2060     if (dirlisting) {
2061         /* insert directory filter */
2062         ap_add_output_filter("PROXY_SEND_DIR", NULL, r, r->connection);
2063     }
2064
2065     /* send body */
2066     if (!r->header_only) {
2067         apr_bucket *e;
2068         int finish = FALSE;
2069
2070         ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r, "start body send");
2071
2072         /* read the body, pass it to the output filters */
2073         while (ap_get_brigade(data->input_filters,
2074                               bb,
2075                               AP_MODE_READBYTES,
2076                               APR_BLOCK_READ,
2077                               conf->io_buffer_size) == APR_SUCCESS) {
2078 #if DEBUGGING
2079             {
2080                 apr_off_t readbytes;
2081                 apr_brigade_length(bb, 0, &readbytes);
2082                 ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, APLOGNO(01056)
2083                              "proxy: readbytes: %#x", readbytes);
2084             }
2085 #endif
2086             /* sanity check */
2087             if (APR_BRIGADE_EMPTY(bb)) {
2088                 break;
2089             }
2090
2091             /* found the last brigade? */
2092             if (APR_BUCKET_IS_EOS(APR_BRIGADE_LAST(bb))) {
2093                 /* if this is the last brigade, cleanup the
2094                  * backend connection first to prevent the
2095                  * backend server from hanging around waiting
2096                  * for a slow client to eat these bytes
2097                  */
2098                 ap_flush_conn(data);
2099                 if (data_sock) {
2100                     apr_socket_close(data_sock);
2101                 }
2102                 data_sock = NULL;
2103                 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01057)
2104                               "data connection closed");
2105                 /* signal that we must leave */
2106                 finish = TRUE;
2107             }
2108
2109             /* if no EOS yet, then we must flush */
2110             if (FALSE == finish) {
2111                 e = apr_bucket_flush_create(c->bucket_alloc);
2112                 APR_BRIGADE_INSERT_TAIL(bb, e);
2113             }
2114
2115             /* try send what we read */
2116             if (ap_pass_brigade(r->output_filters, bb) != APR_SUCCESS
2117                 || c->aborted) {
2118                 /* Ack! Phbtt! Die! User aborted! */
2119                 finish = TRUE;
2120             }
2121
2122             /* make sure we always clean up after ourselves */
2123             apr_brigade_cleanup(bb);
2124
2125             /* if we are done, leave */
2126             if (TRUE == finish) {
2127                 break;
2128             }
2129         }
2130         ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r, "end body send");
2131
2132     }
2133     if (data_sock) {
2134         ap_flush_conn(data);
2135         apr_socket_close(data_sock);
2136         ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01058) "data connection closed");
2137     }
2138
2139     /* Retrieve the final response for the RETR or LIST commands */
2140     proxy_ftp_command(NULL, r, origin, bb, &ftpmessage);
2141     apr_brigade_cleanup(bb);
2142
2143     /*
2144      * VII: Clean Up -------------
2145      *
2146      * If there are no KeepAlives, or if the connection has been signalled to
2147      * close, close the socket and clean up
2148      */
2149
2150     /* finish */
2151     proxy_ftp_command("QUIT" CRLF, r, origin, bb, &ftpmessage);
2152     /* responses: 221, 500 */
2153     /* 221 Service closing control connection. */
2154     /* 500 Syntax error, command unrecognized. */
2155     ap_flush_conn(origin);
2156     proxy_ftp_cleanup(r, backend);
2157
2158     apr_brigade_destroy(bb);
2159     return OK;
2160 }
2161
2162 static void ap_proxy_ftp_register_hook(apr_pool_t *p)
2163 {
2164     /* hooks */
2165     proxy_hook_scheme_handler(proxy_ftp_handler, NULL, NULL, APR_HOOK_MIDDLE);
2166     proxy_hook_canon_handler(proxy_ftp_canon, NULL, NULL, APR_HOOK_MIDDLE);
2167     /* filters */
2168     ap_register_output_filter("PROXY_SEND_DIR", proxy_send_dir_filter,
2169                               NULL, AP_FTYPE_RESOURCE);
2170     /* Compile the output format of "ls -s1" as a fallback for non-unix ftp listings */
2171     ls_regex = ap_pregcomp(p, LS_REG_PATTERN, AP_REG_EXTENDED);
2172     ap_assert(ls_regex != NULL);
2173 }
2174
2175 static const command_rec proxy_ftp_cmds[] =
2176 {
2177     AP_INIT_FLAG("ProxyFtpListOnWildcard", set_ftp_list_on_wildcard, NULL,
2178      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."),
2179     AP_INIT_FLAG("ProxyFtpEscapeWildcards", set_ftp_escape_wildcards, NULL,
2180      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."),
2181     AP_INIT_TAKE1("ProxyFtpDirCharset", set_ftp_directory_charset, NULL,
2182      RSRC_CONF|ACCESS_CONF, "Define the character set for proxied FTP listings"),
2183     {NULL}
2184 };
2185
2186
2187 AP_DECLARE_MODULE(proxy_ftp) = {
2188     STANDARD20_MODULE_STUFF,
2189     create_proxy_ftp_dir_config,/* create per-directory config structure */
2190     merge_proxy_ftp_dir_config, /* merge per-directory config structures */
2191     NULL,                       /* create per-server config structure */
2192     NULL,                       /* merge per-server config structures */
2193     proxy_ftp_cmds,             /* command apr_table_t */
2194     ap_proxy_ftp_register_hook  /* register hooks */
2195 };