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