mod_proxy_ftp.c revision cf8d02ea0c91653917b044529f3133c5a1bb9200
57221209d11b05aa0373cc3892d5df89ba96ebf9Christian Maeder/* Licensed to the Apache Software Foundation (ASF) under one or more
53bd0c89aa4743dc41a6394db5a90717c1ca4517Liam O'Reilly * contributor license agreements. See the NOTICE file distributed with
53bd0c89aa4743dc41a6394db5a90717c1ca4517Liam O'Reilly * this work for additional information regarding copyright ownership.
53bd0c89aa4743dc41a6394db5a90717c1ca4517Liam O'Reilly * The ASF licenses this file to You under the Apache License, Version 2.0
53bd0c89aa4743dc41a6394db5a90717c1ca4517Liam O'Reilly * (the "License"); you may not use this file except in compliance with
98890889ffb2e8f6f722b00e265a211f13b5a861Corneliu-Claudiu Prodescu * the License. You may obtain a copy of the License at
53bd0c89aa4743dc41a6394db5a90717c1ca4517Liam O'Reilly * Unless required by applicable law or agreed to in writing, software
53bd0c89aa4743dc41a6394db5a90717c1ca4517Liam O'Reilly * distributed under the License is distributed on an "AS IS" BASIS,
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
53bd0c89aa4743dc41a6394db5a90717c1ca4517Liam O'Reilly * See the License for the specific language governing permissions and
53bd0c89aa4743dc41a6394db5a90717c1ca4517Liam O'Reilly * limitations under the License.
57221209d11b05aa0373cc3892d5df89ba96ebf9Christian Maeder/* FTP routines for Apache proxy */
7830e8fa7442fb7452af7ecdba102bc297ae367eChristian Maeder/* Automatic timestamping (Last-Modified header) based on MDTM is used if:
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder * 1) the FTP server supports the MDTM command and
d5833d2ee7bafcbf2fdd2bdfd9a728c769b100c7Christian Maeder * 2) HAVE_TIMEGM (preferred) or HAVE_GMTOFF is available at compile time
7830e8fa7442fb7452af7ecdba102bc297ae367eChristian Maedermodule AP_MODULE_DECLARE_DATA proxy_ftp_module;
036ecbd8f721096321f47cf6a354a9d1bf3d032fChristian Maeder * Decodes a '%' escaped string, and returns the number of characters
648fe1220044aac847acbdfbc4155af5556063ebChristian Maederstatic int decodeenc(char *x)
53bd0c89aa4743dc41a6394db5a90717c1ca4517Liam O'Reilly if (x[0] == '\0')
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly return 0; /* special case for no characters */
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly for (i = 0, j = 0; x[i] != '\0'; i++, j++) {
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder /* decode it if not already done */
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder if (ch == '%' && apr_isxdigit(x[i + 1]) && apr_isxdigit(x[i + 2])) {
d5833d2ee7bafcbf2fdd2bdfd9a728c769b100c7Christian Maeder * Escape the globbing characters in a path used as argument to
d5833d2ee7bafcbf2fdd2bdfd9a728c769b100c7Christian Maeder * the FTP commands (SIZE, CWD, RETR, MDTM, ...).
9aeda2b3ae8ce0b018955521e4ca835a8ba8a27bLiam O'Reilly * ftpd assumes '\\' as a quoting character to escape special characters.
9aeda2b3ae8ce0b018955521e4ca835a8ba8a27bLiam O'Reilly * Returns: escaped string
648fe1220044aac847acbdfbc4155af5556063ebChristian Maederstatic char *ftp_escape_globbingchars(apr_pool_t *p, const char *path)
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder char *ret = apr_palloc(p, 2*strlen(path)+sizeof(""));
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly if (strchr(FTP_GLOBBING_CHARS, *path) != NULL)
fa373bc327620e08861294716b4454be8d25669fChristian Maeder * Check for globbing characters in a path used as argument to
fa373bc327620e08861294716b4454be8d25669fChristian Maeder * the FTP commands (SIZE, CWD, RETR, MDTM, ...).
fa373bc327620e08861294716b4454be8d25669fChristian Maeder * ftpd assumes '\\' as a quoting character to escape special characters.
fa373bc327620e08861294716b4454be8d25669fChristian Maeder * Returns: 0 (no globbing chars, or all globbing chars escaped), 1 (globbing chars)
648fe1220044aac847acbdfbc4155af5556063ebChristian Maederstatic int ftp_check_globbingchars(const char *path)
9aeda2b3ae8ce0b018955521e4ca835a8ba8a27bLiam O'Reilly if (*path != '\0' && strchr(FTP_GLOBBING_CHARS, *path) != NULL)
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder * checks an encoded ftp string for bad characters, namely, CR, LF or
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder * non-ascii character
648fe1220044aac847acbdfbc4155af5556063ebChristian Maederstatic int ftp_check_string(const char *x)
eb48217dfa67ddb87b8fbd846de293d0636bd578Christian Maeder for (i = 0; x[i] != '\0'; i++) {
eb48217dfa67ddb87b8fbd846de293d0636bd578Christian Maeder if (ch == '%' && apr_isxdigit(x[i + 1]) && apr_isxdigit(x[i + 2])) {
eb48217dfa67ddb87b8fbd846de293d0636bd578Christian Maeder if (ch == '\015' || ch == '\012' || (ch & 0x80))
eb48217dfa67ddb87b8fbd846de293d0636bd578Christian Maeder#else /* APR_CHARSET_EBCDIC */
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder#endif /* APR_CHARSET_EBCDIC */
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder * Canonicalise ftp URLs.
4314e26a12954cb1c9be4dea10aa8103edac5bbbChristian Maederstatic int proxy_ftp_canon(request_rec *r, char *url)
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder char *user, *password, *host, *path, *parms, *strp, sport[7];
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly const char *err;
7857a35e3af533dfbd0f0e18638ebd211e6358a0Christian Maeder ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
7830e8fa7442fb7452af7ecdba102bc297ae367eChristian Maeder err = ap_proxy_canon_netloc(p, &url, &user, &password, &host, &port);
7857a35e3af533dfbd0f0e18638ebd211e6358a0Christian Maeder if (user != NULL && !ftp_check_string(user))
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder if (password != NULL && !ftp_check_string(password))
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder /* now parse path/parameters args, according to rfc1738 */
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder * N.B. if this isn't a true proxy request, then the URL path (but not
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder * query args) has already been decoded. This gives rise to the problem
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder * of a ; being decoded into the path.
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder parms = ap_proxy_canonenc(p, strp, strlen(strp), enc_parm, 0,
fa373bc327620e08861294716b4454be8d25669fChristian Maeder path = ap_proxy_canonenc(p, url, strlen(url), enc_path, 0, r->proxyreq);
fa373bc327620e08861294716b4454be8d25669fChristian Maeder strp = ap_proxy_canonenc(p, r->args, strlen(r->args), enc_parm, 1, r->proxyreq);
fa373bc327620e08861294716b4454be8d25669fChristian Maeder parms = apr_pstrcat(p, parms, "?", strp, NULL);
fa373bc327620e08861294716b4454be8d25669fChristian Maeder strp = ap_proxy_canonenc(p, r->args, strlen(r->args), enc_fpath, 1, r->proxyreq);
fa373bc327620e08861294716b4454be8d25669fChristian Maeder path = apr_pstrcat(p, path, "?", strp, NULL);
fa373bc327620e08861294716b4454be8d25669fChristian Maeder/* now, rebuild URL */
fa373bc327620e08861294716b4454be8d25669fChristian Maeder apr_snprintf(sport, sizeof(sport), ":%d", port);
fa373bc327620e08861294716b4454be8d25669fChristian Maeder if (ap_strchr_c(host, ':')) { /* if literal IPv6 address */
fa373bc327620e08861294716b4454be8d25669fChristian Maeder host = apr_pstrcat(p, "[", host, "]", NULL);
fa373bc327620e08861294716b4454be8d25669fChristian Maeder r->filename = apr_pstrcat(p, "proxy:ftp://", (user != NULL) ? user : "",
fa373bc327620e08861294716b4454be8d25669fChristian Maeder (user != NULL) ? "@" : "", host, sport, "/", path,
fa373bc327620e08861294716b4454be8d25669fChristian Maeder (parms[0] != '\0') ? ";" : "", parms, NULL);
7830e8fa7442fb7452af7ecdba102bc297ae367eChristian Maeder/* we chop lines longer than 80 characters */
7830e8fa7442fb7452af7ecdba102bc297ae367eChristian Maeder * Reads response lines, returns both the ftp status code and
7830e8fa7442fb7452af7ecdba102bc297ae367eChristian Maeder * remembers the response message in the supplied buffer
7830e8fa7442fb7452af7ecdba102bc297ae367eChristian Maederstatic int ftp_getrc_msg(conn_rec *ftp_ctrl, apr_bucket_brigade *bb, char *msgbuf, int msglen)
eb48217dfa67ddb87b8fbd846de293d0636bd578Christian Maeder if (APR_SUCCESS != (rv = ap_proxy_string_read(ftp_ctrl, bb, response, sizeof(response), &eos))) {
7830e8fa7442fb7452af7ecdba102bc297ae367eChristian Maeder ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, NULL,
7830e8fa7442fb7452af7ecdba102bc297ae367eChristian Maeder "proxy: <FTP: %s", response);
66bc8d6e69cde43f1ccbeb76104cf7b8038acd6cChristian Maeder if (!apr_isdigit(response[0]) || !apr_isdigit(response[1]) ||
66bc8d6e69cde43f1ccbeb76104cf7b8038acd6cChristian Maeder !apr_isdigit(response[2]) || (response[3] != ' ' && response[3] != '-'))
66bc8d6e69cde43f1ccbeb76104cf7b8038acd6cChristian Maeder status = 100 * response[0] + 10 * response[1] + response[2] - 111 * '0';
66bc8d6e69cde43f1ccbeb76104cf7b8038acd6cChristian Maeder mb = apr_cpystrn(mb, response + 4, me - mb);
7830e8fa7442fb7452af7ecdba102bc297ae367eChristian Maeder if (APR_SUCCESS != (rv = ap_proxy_string_read(ftp_ctrl, bb, response, sizeof(response), &eos))) {
7830e8fa7442fb7452af7ecdba102bc297ae367eChristian Maeder mb = apr_cpystrn(mb, response + (' ' == response[0] ? 1 : 4), me - mb);
8e9c3881fb6e710b1e08bf5ac8ff9d393df2e74eChristian Maeder/* this is a filter that turns a raw ASCII directory listing into pretty HTML */
7830e8fa7442fb7452af7ecdba102bc297ae367eChristian Maeder/* ideally, mod_proxy should simply send the raw directory list up the filter
7830e8fa7442fb7452af7ecdba102bc297ae367eChristian Maeder * stack to mod_autoindex, which in theory should turn the raw ascii into
7830e8fa7442fb7452af7ecdba102bc297ae367eChristian Maeder * pretty html along with all the bells and whistles it provides...
a00461fcf7432205a79a0f12dbe6c1ebc58bc000Christian Maeder * all in good time...! :)
a00461fcf7432205a79a0f12dbe6c1ebc58bc000Christian Maedertypedef struct {
a00461fcf7432205a79a0f12dbe6c1ebc58bc000Christian Maeder/* fallback regex for ls -s1; ($0..$2) == 3 */
a00461fcf7432205a79a0f12dbe6c1ebc58bc000Christian Maeder#define LS_REG_PATTERN "^ *([0-9]+) +([^ ]+)$"
bcd914850de931848b86d7728192a149f9c0108bChristian Maederstatic apr_status_t proxy_send_dir_filter(ap_filter_t *f,
a00461fcf7432205a79a0f12dbe6c1ebc58bc000Christian Maeder apr_bucket_brigade *out = apr_brigade_create(p, c->bucket_alloc);
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly register int n;
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly char *dir, *path, *reldir, *site, *str, *type;
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly const char *pwd = apr_table_get(r->notes, "Directory-PWD");
57221209d11b05aa0373cc3892d5df89ba96ebf9Christian Maeder const char *readme = apr_table_get(r->notes, "Directory-README");
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder f->ctx = ctx = apr_pcalloc(p, sizeof(*ctx));
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder ctx->in = apr_brigade_create(p, c->bucket_alloc);
bcd914850de931848b86d7728192a149f9c0108bChristian Maeder /* combine the stored and the new */
57221209d11b05aa0373cc3892d5df89ba96ebf9Christian Maeder /* basedir is either "", or "/%2f" for the "squid %2f hack" */
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly const char *basedir = ""; /* By default, path is relative to the $HOME dir */
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly * In the reverse proxy case we need to construct our site string
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly * via ap_construct_url. For non anonymous sites apr_uri_unparse would
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly * only supply us with 'username@' which leads to the construction of
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly * an invalid base href later on. Losing the username part of the URL
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly * is no problem in the reverse proxy case as the browser sents the
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly * credentials anyway once entered.
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly /* Save "scheme://site" prefix without password */
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly /* ... and path without query args */
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly path = apr_uri_unparse(p, &f->r->parsed_uri, APR_URI_UNP_OMITSITEPART | APR_URI_UNP_OMITQUERY);
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly /* If path began with /%2f, change the basedir */
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly /* Strip off a type qualifier. It is ignored for dir listings */
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly while (path[1] == '/') /* collapse multiple leading slashes to one */
57221209d11b05aa0373cc3892d5df89ba96ebf9Christian Maeder if (reldir != NULL && ftp_check_globbingchars(reldir)) {
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly reldir[0] = '\0'; /* strip off the wildcard suffix */
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly /* Copy path, strip (all except the last) trailing slashes */
9aeda2b3ae8ce0b018955521e4ca835a8ba8a27bLiam O'Reilly /* (the trailing slash is needed for the dir component loop below) */
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly for (n = strlen(path); n > 1 && path[n - 1] == '/' && path[n - 2] == '/'; --n)
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder /* Add a link to the root directory (if %2f hack was used) */
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly str = (basedir[0] != '\0') ? "<a href=\"/%2f/\">%2f</a>/" : "";
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly "<html>\n <head>\n <title>%s%s%s</title>\n"
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly "<base href=\"%s%s%s\">\n"
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly " <body>\n <h2>Directory of "
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder "<a href=\"/\">%s</a>/%s",
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly site, basedir, escpath, site, basedir, escpath, site, str);
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str),
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly for (dir = path+1; (dir = strchr(dir, '/')) != NULL; )
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly /* print "path/" component */
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder str = apr_psprintf(p, "<a href=\"%s%s/\">%s</a>/", basedir,
d5833d2ee7bafcbf2fdd2bdfd9a728c769b100c7Christian Maeder APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str,
9aeda2b3ae8ce0b018955521e4ca835a8ba8a27bLiam O'Reilly APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(wildcard,
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder /* If the caller has determined the current directory, and it differs */
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder /* from what the client requested, then show the real name */
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly if (pwd == NULL || strncmp(pwd, path, strlen(pwd)) == 0) {
33bdce26495121cdbce30331ef90a1969126a840Liam O'Reilly str = apr_psprintf(p, "</h2>\n\n <hr />\n\n<pre>");
bcd914850de931848b86d7728192a149f9c0108bChristian Maeder str = apr_psprintf(p, "</h2>\n\n(%s)\n\n <hr />\n\n<pre>",
bcd914850de931848b86d7728192a149f9c0108bChristian Maeder APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str, strlen(str),
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder /* print README */
57221209d11b05aa0373cc3892d5df89ba96ebf9Christian Maeder str = apr_psprintf(p, "%s\n</pre>\n\n<hr />\n\n<pre>\n",
57221209d11b05aa0373cc3892d5df89ba96ebf9Christian Maeder APR_BRIGADE_INSERT_TAIL(out, apr_bucket_pool_create(str,
9aeda2b3ae8ce0b018955521e4ca835a8ba8a27bLiam O'Reilly /* make sure page intro gets sent out */
57221209d11b05aa0373cc3892d5df89ba96ebf9Christian Maeder APR_BRIGADE_INSERT_TAIL(out, apr_bucket_flush_create(c->bucket_alloc));
648fe1220044aac847acbdfbc4155af5556063ebChristian Maeder if (APR_SUCCESS != (rv = ap_pass_brigade(f->next, out))) {
05cc55892e6c93bdd7b9c3f100ab1bb65fe6a21eLiam O'Reilly /* loop through each line of directory */
apr_bucket *e;
if (APR_BUCKET_IS_EOS(e)) {
return rv;
if (eos) {
if (!found) {
return APR_SUCCESS;
filename--;
else if (ctx->buffer[0] == 'd' || ctx->buffer[0] == '-' || ctx->buffer[0] == 'l' || apr_isdigit(ctx->buffer[0])) {
int searchidx = 0;
firstfile = 0;
filename = apr_pstrndup(p, &ctx->buffer[re_result[2].rm_so], re_result[2].rm_eo - re_result[2].rm_so);
c->bucket_alloc));
return rv;
c->bucket_alloc));
return rv;
return APR_SUCCESS;
char *crlf;
int rc;
return rc;
int rc;
return ret;
return cwd;
* ftp://user@host part of the reqest (sans password -if supplied but invalid-)
if (log_it)
return HTTP_UNAUTHORIZED;
return OK;
int dirlisting = 0;
int status;
if (proxyhost) {
return HTTP_NOT_IMPLEMENTED;
if (connectport == 0) {
return HTTP_INTERNAL_SERVER_ERROR;
if (!connect_addr)
connectport, 0,
if (!backend) {
if (backend) {
return status;
return HTTP_SERVICE_UNAVAILABLE;
return status;
while (*secs_str)
return ftp_unauthorized(r, 0);
++path;
* We could also have extended gen_test_char.c with a special T_ESCAPE_FTP_PATH
char *data_ip;
char *pstr;
char *tok_cntx;
if (pstr) {
if ((rv = apr_socket_create(&data_sock, connect_addr->family, SOCK_STREAM, 0, r->pool)) != APR_SUCCESS) {
return HTTP_INTERNAL_SERVER_ERROR;
if (!connect) {
char *pstr;
char *tok_cntx;
if ((rv = apr_socket_create(&data_sock, connect_addr->family, SOCK_STREAM, 0, r->pool)) != APR_SUCCESS) {
return HTTP_INTERNAL_SERVER_ERROR;
apr_sockaddr_info_get(&pasv_addr, apr_psprintf(p, "%d.%d.%d.%d", h3, h2, h1, h0), connect_addr->family, pasvport, 0, p);
if (!connect) {
char *local_ip;
if ((rv = apr_socket_create(&local_sock, connect_addr->family, SOCK_STREAM, 0, r->pool)) != APR_SUCCESS) {
return HTTP_INTERNAL_SERVER_ERROR;
!= APR_SUCCESS) {
return HTTP_INTERNAL_SERVER_ERROR;
return HTTP_INTERNAL_SERVER_ERROR;
return HTTP_INTERNAL_SERVER_ERROR;
/* from draft-ietf-ftpext-mlst-14.txt:
len = 0;
if (dirlisting) {
* queries like: ftp://user@host/apache/src/server/http_*.c
if (len != 0)
/* from draft-ietf-ftpext-mlst-14.txt:
* YYYYMMDDHHMMSS.sss
} time_val;
mtime = 0L;
/* rc is an intermediate response for the LIST command (125 transfer starting, 150 opening data connection) */
if (dirlisting) {
&proxy_module);
if (r->content_type) {
if (mtime != 0L) {
* @@@ FIXME (e.g., for ftp://user@host/file*.tar.gz,
if (use_port) {
return HTTP_BAD_GATEWAY;
if (!data) {
return HTTP_INTERNAL_SERVER_ERROR;
rc);
return rc;
if (dirlisting) {
if (!r->header_only) {
apr_bucket *e;
bb,
#if DEBUGGING
|| c->aborted) {
if (data_sock) {
return OK;