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