in modules/proxy/mod_proxy_ftp.c [970:2081]
static int proxy_ftp_handler(request_rec *r, proxy_worker *worker,
proxy_server_conf *conf, char *url,
const char *proxyhost, apr_port_t proxyport)
{
apr_pool_t *p = r->pool;
conn_rec *c = r->connection;
proxy_conn_rec *backend;
apr_socket_t *sock, *local_sock, *data_sock = NULL;
conn_rec *origin, *data = NULL;
apr_status_t err = APR_SUCCESS;
apr_bucket_brigade *bb;
char *buf, *connectname;
apr_port_t connectport;
char *ftpmessage = NULL;
char *path, *strp, *type_suffix, *cwd = NULL;
apr_uri_t uri;
char *user = NULL;
/* char *account = NULL; how to supply an account in a URL? */
const char *password = NULL;
int len, rc;
int one = 1;
char *size = NULL;
char xfer_type = 'A'; /* after ftp login, the default is ASCII */
int dirlisting = 0;
#if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
apr_time_t mtime = 0L;
#endif
proxy_ftp_dir_conf *fdconf = ap_get_module_config(r->per_dir_config,
&proxy_ftp_module);
/* stuff for PASV mode */
int connect = 0, use_port = 0;
char dates[APR_RFC822_DATE_LEN];
apr_status_t rv;
int status;
/* is this for us? */
if (proxyhost) {
ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r,
"declining URL %s - proxyhost %s specified:", url,
proxyhost);
return DECLINED; /* proxy connections are via HTTP */
}
if (ap_cstr_casecmpn(url, "ftp:", 4)) {
ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r,
"declining URL %s - not ftp:", url);
return DECLINED; /* only interested in FTP */
}
ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r, "serving URL %s", url);
/*
* I: Who Do I Connect To? -----------------------
*
* Break up the URL to determine the host to connect to
*/
/* we only support GET and HEAD */
if (r->method_number != M_GET)
return HTTP_NOT_IMPLEMENTED;
/* We break the URL into host, port, path-search */
if (r->parsed_uri.hostname == NULL) {
if (APR_SUCCESS != apr_uri_parse(p, url, &uri)) {
ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(10189)
"URI cannot be parsed: %s", url);
return ap_proxyerror(r, HTTP_BAD_REQUEST, "URI cannot be parsed");
}
connectname = uri.hostname;
connectport = uri.port;
path = apr_pstrdup(p, uri.path);
}
else {
connectname = r->parsed_uri.hostname;
connectport = r->parsed_uri.port;
path = apr_pstrdup(p, r->parsed_uri.path);
}
if (connectport == 0) {
connectport = apr_uri_port_of_scheme("ftp");
}
path = (path != NULL && path[0] != '\0') ? &path[1] : "";
type_suffix = strchr(path, ';');
if (type_suffix != NULL)
*(type_suffix++) = '\0';
if (type_suffix != NULL && strncmp(type_suffix, "type=", 5) == 0
&& apr_isalpha(type_suffix[5])) {
/* "type=d" forces a dir listing.
* The other types (i|a|e) are directly used for the ftp TYPE command
*/
if ( ! (dirlisting = (apr_tolower(type_suffix[5]) == 'd')))
xfer_type = apr_toupper(type_suffix[5]);
/* Check valid types, rather than ignoring invalid types silently: */
if (strchr("AEI", xfer_type) == NULL)
return ap_proxyerror(r, HTTP_BAD_REQUEST, apr_pstrcat(r->pool,
"ftp proxy supports only types 'a', 'i', or 'e': \"",
type_suffix, "\" is invalid.", NULL));
}
else {
/* make binary transfers the default */
xfer_type = 'I';
}
/*
* The "Authorization:" header must be checked first. We allow the user
* to "override" the URL-coded user [ & password ] in the Browsers'
* User&Password Dialog. NOTE that this is only marginally more secure
* than having the password travel in plain as part of the URL, because
* Basic Auth simply uuencodes the plain text password. But chances are
* still smaller that the URL is logged regularly.
*/
if ((password = apr_table_get(r->headers_in, "Authorization")) != NULL
&& ap_cstr_casecmp(ap_getword(r->pool, &password, ' '), "Basic") == 0
&& (password = ap_pbase64decode(r->pool, password))[0] != ':') {
/* Check the decoded string for special characters. */
if (!ftp_check_string(password)) {
return ap_proxyerror(r, HTTP_BAD_REQUEST,
"user credentials contained invalid character");
}
/*
* Note that this allocation has to be made from r->connection->pool
* because it has the lifetime of the connection. The other
* allocations are temporary and can be tossed away any time.
*/
user = ap_getword_nulls(r->connection->pool, &password, ':');
r->ap_auth_type = "Basic";
r->user = r->parsed_uri.user = user;
}
else if ((user = r->parsed_uri.user) != NULL) {
user = apr_pstrdup(p, user);
decodeenc(user);
if ((password = r->parsed_uri.password) != NULL) {
char *tmp = apr_pstrdup(p, password);
decodeenc(tmp);
password = tmp;
}
}
else {
user = "anonymous";
password = "apache-proxy@";
}
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01036)
"connecting %s to %s:%d", url, connectname, connectport);
/* create space for state information */
backend = ap_get_module_config(c->conn_config, &proxy_ftp_module);
if (!backend) {
status = ap_proxy_acquire_connection("FTP", &backend, worker, r->server);
if (status != OK) {
if (backend) {
backend->close = 1;
ap_proxy_release_connection("FTP", backend, r->server);
}
return status;
}
ap_set_module_config(c->conn_config, &proxy_ftp_module, backend);
}
/*
* get all the possible IP addresses for the destname and loop through
* them until we get a successful connection
*/
err = ap_proxy_determine_address("FTP", backend, connectname, connectport,
0, r, r->server);
if (APR_SUCCESS != err) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
"Error resolving backend address");
}
/* check if ProxyBlock directive on this host */
if (OK != ap_proxy_checkproxyblock(r, conf, connectname, backend->addr)) {
return ftp_proxyerror(r, backend, HTTP_FORBIDDEN,
"Connect to remote machine blocked");
}
/*
* II: Make the Connection -----------------------
*
* We have determined who to connect to. Now make the connection.
*/
if (ap_proxy_connect_backend("FTP", backend, worker, r->server)) {
proxy_ftp_cleanup(r, backend);
return HTTP_SERVICE_UNAVAILABLE;
}
status = ap_proxy_connection_create_ex("FTP", backend, r);
if (status != OK) {
proxy_ftp_cleanup(r, backend);
return status;
}
/* Use old naming */
origin = backend->connection;
sock = backend->sock;
ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
"control connection complete");
/*
* III: Send Control Request -------------------------
*
* Log into the ftp server, send the username & password, change to the
* correct directory...
*/
bb = apr_brigade_create(p, c->bucket_alloc);
/* possible results: */
/* 120 Service ready in nnn minutes. */
/* 220 Service ready for new user. */
/* 421 Service not available, closing control connection. */
rc = proxy_ftp_command(NULL, r, origin, bb, &ftpmessage);
if (rc == -1 || rc == 421) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, "Error reading from remote server");
}
if (rc == 120) {
/*
* RFC2616 states: 14.37 Retry-After
*
* The Retry-After response-header field can be used with a 503 (Service
* Unavailable) response to indicate how long the service is expected
* to be unavailable to the requesting client. [...] The value of
* this field can be either an HTTP-date or an integer number of
* seconds (in decimal) after the time of the response. Retry-After
* = "Retry-After" ":" ( HTTP-date | delta-seconds )
*/
char *secs_str = ftpmessage;
time_t secs;
/* Look for a number, preceded by whitespace */
while (*secs_str)
if ((secs_str==ftpmessage || apr_isspace(secs_str[-1])) &&
apr_isdigit(secs_str[0]))
break;
if (*secs_str != '\0') {
secs = atol(secs_str);
apr_table_addn(r->headers_out, "Retry-After",
apr_psprintf(p, "%lu", (unsigned long)(60 * secs)));
}
return ftp_proxyerror(r, backend, HTTP_SERVICE_UNAVAILABLE, ftpmessage);
}
if (rc != 220) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
}
rc = proxy_ftp_command(apr_pstrcat(p, "USER ", user, CRLF, NULL),
r, origin, bb, &ftpmessage);
/* possible results; 230, 331, 332, 421, 500, 501, 530 */
/* states: 1 - error, 2 - success; 3 - send password, 4,5 fail */
/* 230 User logged in, proceed. */
/* 331 User name okay, need password. */
/* 332 Need account for login. */
/* 421 Service not available, closing control connection. */
/* 500 Syntax error, command unrecognized. */
/* (This may include errors such as command line too long.) */
/* 501 Syntax error in parameters or arguments. */
/* 530 Not logged in. */
if (rc == -1 || rc == 421) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, "Error reading from remote server");
}
if (rc == 530) {
proxy_ftp_cleanup(r, backend);
return ftp_unauthorized(r, 1); /* log it: user name guessing
* attempt? */
}
if (rc != 230 && rc != 331) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
}
if (rc == 331) { /* send password */
if (password == NULL) {
proxy_ftp_cleanup(r, backend);
return ftp_unauthorized(r, 0);
}
rc = proxy_ftp_command(apr_pstrcat(p, "PASS ", password, CRLF, NULL),
r, origin, bb, &ftpmessage);
/* possible results 202, 230, 332, 421, 500, 501, 503, 530 */
/* 230 User logged in, proceed. */
/* 332 Need account for login. */
/* 421 Service not available, closing control connection. */
/* 500 Syntax error, command unrecognized. */
/* 501 Syntax error in parameters or arguments. */
/* 503 Bad sequence of commands. */
/* 530 Not logged in. */
if (rc == -1 || rc == 421) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
"Error reading from remote server");
}
if (rc == 332) {
return ftp_proxyerror(r, backend, HTTP_UNAUTHORIZED,
apr_pstrcat(p, "Need account for login: ", ftpmessage, NULL));
}
/* @@@ questionable -- we might as well return a 403 Forbidden here */
if (rc == 530) {
proxy_ftp_cleanup(r, backend);
return ftp_unauthorized(r, 1); /* log it: passwd guessing
* attempt? */
}
if (rc != 230 && rc != 202) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
}
}
apr_table_set(r->notes, "Directory-README", ftpmessage);
/* Special handling for leading "%2f": this enforces a "cwd /"
* out of the $HOME directory which was the starting point after login
*/
if (ap_cstr_casecmpn(path, "%2f", 3) == 0) {
path += 3;
while (*path == '/') /* skip leading '/' (after root %2f) */
++path;
rc = proxy_ftp_command("CWD /" CRLF, r, origin, bb, &ftpmessage);
if (rc == -1 || rc == 421)
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
"Error reading from remote server");
}
/*
* set the directory (walk directory component by component): this is
* what we must do if we don't know the OS type of the remote machine
*/
for (;;) {
strp = strchr(path, '/');
if (strp == NULL)
break;
*strp = '\0';
decodeenc(path); /* Note! This decodes a %2f -> "/" */
if (strchr(path, '/')) { /* are there now any '/' characters? */
return ftp_proxyerror(r, backend, HTTP_BAD_REQUEST,
"Use of /%2f is only allowed at the base directory");
}
/* NOTE: FTP servers do globbing on the path.
* So we need to escape the URI metacharacters.
* We use a special glob-escaping routine to escape globbing chars.
* We could also have extended gen_test_char.c with a special T_ESCAPE_FTP_PATH
*/
rc = proxy_ftp_command(apr_pstrcat(p, "CWD ",
ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
r, origin, bb, &ftpmessage);
*strp = '/';
/* responses: 250, 421, 500, 501, 502, 530, 550 */
/* 250 Requested file action okay, completed. */
/* 421 Service not available, closing control connection. */
/* 500 Syntax error, command unrecognized. */
/* 501 Syntax error in parameters or arguments. */
/* 502 Command not implemented. */
/* 530 Not logged in. */
/* 550 Requested action not taken. */
if (rc == -1 || rc == 421) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
"Error reading from remote server");
}
if (rc == 550) {
return ftp_proxyerror(r, backend, HTTP_NOT_FOUND, ftpmessage);
}
if (rc != 250) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
}
path = strp + 1;
}
/*
* IV: Make Data Connection? -------------------------
*
* Try EPSV, if that fails... try PASV, if that fails... try PORT.
*/
/* this temporarily switches off EPSV/PASV */
/*goto bypass;*/
/* set up data connection - EPSV */
{
apr_port_t data_port;
/*
* The EPSV command replaces PASV where both IPV4 and IPV6 is
* supported. Only the port is returned, the IP address is always the
* same as that on the control connection. Example: Entering Extended
* Passive Mode (|||6446|)
*/
rc = proxy_ftp_command("EPSV" CRLF,
r, origin, bb, &ftpmessage);
/* possible results: 227, 421, 500, 501, 502, 530 */
/* 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). */
/* 421 Service not available, closing control connection. */
/* 500 Syntax error, command unrecognized. */
/* 501 Syntax error in parameters or arguments. */
/* 502 Command not implemented. */
/* 530 Not logged in. */
if (rc == -1 || rc == 421) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
"Error reading from remote server");
}
if (rc != 229 && rc != 500 && rc != 501 && rc != 502) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
}
else if (rc == 229) {
/* Parse the port out of the EPSV reply. */
data_port = parse_epsv_reply(ftpmessage);
if (data_port) {
apr_sockaddr_t *remote_addr, epsv_addr;
ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
"EPSV contacting remote host on port %d", data_port);
/* Retrieve the client's address. */
rv = apr_socket_addr_get(&remote_addr, APR_REMOTE, sock);
if (rv == APR_SUCCESS) {
/* Take a shallow copy of the server address to
* modify; the _addr_get function gives back a
* pointer to the socket's internal structure.
* This is awkward given current APR network
* interfaces. */
epsv_addr = *remote_addr;
epsv_addr.port = data_port;
#if APR_HAVE_IPV6
if (epsv_addr.family == APR_INET6) {
epsv_addr.sa.sin6.sin6_port = htons(data_port);
}
else
#endif
{
epsv_addr.sa.sin.sin_port = htons(data_port);
}
rv = apr_socket_create(&data_sock, epsv_addr.family, SOCK_STREAM, 0, r->pool);
}
if (rv != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01040)
"could not establish socket for client data connection");
proxy_ftp_cleanup(r, backend);
return HTTP_INTERNAL_SERVER_ERROR;
}
if (conf->recv_buffer_size > 0
&& (rv = apr_socket_opt_set(data_sock, APR_SO_RCVBUF,
conf->recv_buffer_size))) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01041)
"apr_socket_opt_set(SO_RCVBUF): Failed to "
"set ProxyReceiveBufferSize, using default");
}
rv = apr_socket_opt_set(data_sock, APR_TCP_NODELAY, 1);
if (rv != APR_SUCCESS && rv != APR_ENOTIMPL) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01042)
"apr_socket_opt_set(APR_TCP_NODELAY): "
"Failed to set");
}
rv = apr_socket_connect(data_sock, &epsv_addr);
if (rv != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01043)
"EPSV attempt to connect to %pI failed - "
"Firewall/NAT?", &epsv_addr);
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, apr_psprintf(r->pool,
"EPSV attempt to connect to %pI failed - firewall/NAT?", &epsv_addr));
}
else {
ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
"connected data socket to %pI", &epsv_addr);
connect = 1;
}
}
}
}
/* set up data connection - PASV */
if (!connect) {
rc = proxy_ftp_command("PASV" CRLF,
r, origin, bb, &ftpmessage);
/* possible results: 227, 421, 500, 501, 502, 530 */
/* 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). */
/* 421 Service not available, closing control connection. */
/* 500 Syntax error, command unrecognized. */
/* 501 Syntax error in parameters or arguments. */
/* 502 Command not implemented. */
/* 530 Not logged in. */
if (rc == -1 || rc == 421) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
"Error reading from remote server");
}
if (rc != 227 && rc != 502) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
}
else if (rc == 227) {
unsigned int h0, h1, h2, h3, p0, p1;
char *pstr;
char *tok_cntx;
/* FIXME: Check PASV against RFC1123 */
pstr = ftpmessage;
pstr = apr_strtok(pstr, " ", &tok_cntx); /* separate result code */
if (pstr != NULL) {
if (*(pstr + strlen(pstr) + 1) == '=') {
pstr += strlen(pstr) + 2;
}
else {
pstr = apr_strtok(NULL, "(", &tok_cntx); /* separate address &
* port params */
if (pstr != NULL)
pstr = apr_strtok(NULL, ")", &tok_cntx);
}
}
/* FIXME: Only supports IPV4 - fix in RFC2428 */
if (pstr != NULL && (sscanf(pstr,
"%d,%d,%d,%d,%d,%d", &h3, &h2, &h1, &h0, &p1, &p0) == 6)) {
apr_sockaddr_t *pasv_addr;
apr_port_t pasvport = (p1 << 8) + p0;
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01044)
"PASV contacting host %d.%d.%d.%d:%d",
h3, h2, h1, h0, pasvport);
if ((rv = apr_socket_create(&data_sock, backend->addr->family,
SOCK_STREAM, 0, r->pool)) != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01045)
"error creating PASV socket");
proxy_ftp_cleanup(r, backend);
return HTTP_INTERNAL_SERVER_ERROR;
}
if (conf->recv_buffer_size > 0
&& (rv = apr_socket_opt_set(data_sock, APR_SO_RCVBUF,
conf->recv_buffer_size))) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01046)
"apr_socket_opt_set(SO_RCVBUF): Failed to set ProxyReceiveBufferSize, using default");
}
rv = apr_socket_opt_set(data_sock, APR_TCP_NODELAY, 1);
if (rv != APR_SUCCESS && rv != APR_ENOTIMPL) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01047)
"apr_socket_opt_set(APR_TCP_NODELAY): "
"Failed to set");
}
/* make the connection */
err = apr_sockaddr_info_get(&pasv_addr, apr_psprintf(p, "%d.%d.%d.%d",
h3, h2, h1, h0),
backend->addr->family, pasvport, 0, p);
if (APR_SUCCESS != err) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
apr_pstrcat(p, "DNS lookup failure for: ",
connectname, NULL));
}
rv = apr_socket_connect(data_sock, pasv_addr);
if (rv != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01048)
"PASV attempt to connect to %pI failed - Firewall/NAT?", pasv_addr);
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, apr_psprintf(r->pool,
"PASV attempt to connect to %pI failed - firewall/NAT?", pasv_addr));
}
else {
connect = 1;
}
}
}
}
/*bypass:*/
/* set up data connection - PORT */
if (!connect) {
apr_sockaddr_t *local_addr;
char *local_ip;
apr_port_t local_port;
unsigned int h0, h1, h2, h3, p0, p1;
if ((rv = apr_socket_create(&local_sock, backend->addr->family,
SOCK_STREAM, 0, r->pool)) != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01049)
"error creating local socket");
proxy_ftp_cleanup(r, backend);
return HTTP_INTERNAL_SERVER_ERROR;
}
apr_socket_addr_get(&local_addr, APR_LOCAL, sock);
local_port = local_addr->port;
apr_sockaddr_ip_get(&local_ip, local_addr);
if ((rv = apr_socket_opt_set(local_sock, APR_SO_REUSEADDR, one))
!= APR_SUCCESS) {
#ifndef _OSD_POSIX /* BS2000 has this option "always on" */
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01050)
"error setting reuseaddr option");
proxy_ftp_cleanup(r, backend);
return HTTP_INTERNAL_SERVER_ERROR;
#endif /* _OSD_POSIX */
}
err = apr_sockaddr_info_get(&local_addr, local_ip, APR_UNSPEC, local_port, 0, r->pool);
if (APR_SUCCESS != err) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
apr_pstrcat(p, "DNS lookup failure for: ",
connectname, NULL));
}
if ((rv = apr_socket_bind(local_sock, local_addr)) != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01051)
"error binding to ftp data socket %pI", local_addr);
proxy_ftp_cleanup(r, backend);
return HTTP_INTERNAL_SERVER_ERROR;
}
/* only need a short queue */
if ((rv = apr_socket_listen(local_sock, 2)) != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01052)
"error listening to ftp data socket %pI", local_addr);
proxy_ftp_cleanup(r, backend);
return HTTP_INTERNAL_SERVER_ERROR;
}
/* FIXME: Sent PORT here */
if (local_ip && (sscanf(local_ip,
"%d.%d.%d.%d", &h3, &h2, &h1, &h0) == 4)) {
p1 = (local_port >> 8);
p0 = (local_port & 0xFF);
rc = proxy_ftp_command(apr_psprintf(p, "PORT %d,%d,%d,%d,%d,%d" CRLF, h3, h2, h1, h0, p1, p0),
r, origin, bb, &ftpmessage);
/* possible results: 200, 421, 500, 501, 502, 530 */
/* 200 Command okay. */
/* 421 Service not available, closing control connection. */
/* 500 Syntax error, command unrecognized. */
/* 501 Syntax error in parameters or arguments. */
/* 502 Command not implemented. */
/* 530 Not logged in. */
if (rc == -1 || rc == 421) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
"Error reading from remote server");
}
if (rc != 200) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
}
/* signal that we must use the EPRT/PORT loop */
use_port = 1;
}
else {
/* IPV6 FIXME:
* The EPRT command replaces PORT where both IPV4 and IPV6 is supported. The first
* number (1,2) indicates the protocol type. Examples:
* EPRT |1|132.235.1.2|6275|
* EPRT |2|1080::8:800:200C:417A|5282|
*/
return ftp_proxyerror(r, backend, HTTP_NOT_IMPLEMENTED,
"Connect to IPV6 ftp server using EPRT not supported. Enable EPSV.");
}
}
/*
* V: Set The Headers -------------------
*
* Get the size of the request, set up the environment for HTTP.
*/
/* set request; "path" holds last path component */
len = decodeenc(path);
if (strchr(path, '/')) { /* are there now any '/' characters? */
return ftp_proxyerror(r, backend, HTTP_BAD_REQUEST,
"Use of /%2f is only allowed at the base directory");
}
/* If len == 0 then it must be a directory (you can't RETR nothing)
* Also, don't allow to RETR by wildcard. Instead, create a dirlisting,
* unless ProxyFtpListOnWildcard is off.
*/
if (len == 0 || (ftp_check_globbingchars(path) && fdconf->ftp_list_on_wildcard)) {
dirlisting = 1;
}
else {
/* (from FreeBSD ftpd):
* SIZE is not in RFC959, but Postel has blessed it and
* it will be in the updated RFC.
*
* Return size of file in a format suitable for
* using with RESTART (we just count bytes).
*/
/* from draft-ietf-ftpext-mlst-14.txt:
* This value will
* change depending on the current STRUcture, MODE and TYPE of the data
* connection, or a data connection which would be created were one
* created now. Thus, the result of the SIZE command is dependent on
* the currently established STRU, MODE and TYPE parameters.
*/
/* Therefore: switch to binary if the user did not specify ";type=a" */
ftp_set_TYPE(xfer_type, r, origin, bb, &ftpmessage);
rc = proxy_ftp_command(apr_pstrcat(p, "SIZE ",
ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
r, origin, bb, &ftpmessage);
if (rc == -1 || rc == 421) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
"Error reading from remote server");
}
else if (rc == 213) {/* Size command ok */
int j;
for (j = 0; apr_isdigit(ftpmessage[j]); j++)
;
ftpmessage[j] = '\0';
if (ftpmessage[0] != '\0')
size = ftpmessage; /* already pstrdup'ed: no copy necessary */
}
else if (rc == 550) { /* Not a regular file */
ap_log_rerror(APLOG_MARK, APLOG_TRACE4, 0, r,
"SIZE shows this is a directory");
dirlisting = 1;
rc = proxy_ftp_command(apr_pstrcat(p, "CWD ",
ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
r, origin, bb, &ftpmessage);
/* possible results: 250, 421, 500, 501, 502, 530, 550 */
/* 250 Requested file action okay, completed. */
/* 421 Service not available, closing control connection. */
/* 500 Syntax error, command unrecognized. */
/* 501 Syntax error in parameters or arguments. */
/* 502 Command not implemented. */
/* 530 Not logged in. */
/* 550 Requested action not taken. */
if (rc == -1 || rc == 421) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
"Error reading from remote server");
}
if (rc == 550) {
return ftp_proxyerror(r, backend, HTTP_NOT_FOUND, ftpmessage);
}
if (rc != 250) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
}
path = "";
len = 0;
}
}
cwd = ftp_get_PWD(r, origin, bb);
if (cwd != NULL) {
apr_table_set(r->notes, "Directory-PWD", cwd);
}
if (dirlisting) {
ftp_set_TYPE('A', r, origin, bb, NULL);
/* If the current directory contains no slash, we are talking to
* a non-unix ftp system. Try LIST instead of "LIST -lag", it
* should return a long listing anyway (unlike NLST).
* Some exotic FTP servers might choke on the "-lag" switch.
*/
/* Note that we do not escape the path here, to allow for
* queries like: ftp://user@host/apache/src/server/http_*.c
*/
if (len != 0)
buf = apr_pstrcat(p, "LIST ", path, CRLF, NULL);
else if (cwd == NULL || strchr(cwd, '/') != NULL)
buf = "LIST -lag" CRLF;
else
buf = "LIST" CRLF;
}
else {
/* switch to binary if the user did not specify ";type=a" */
ftp_set_TYPE(xfer_type, r, origin, bb, &ftpmessage);
#if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
/* from draft-ietf-ftpext-mlst-14.txt:
* The FTP command, MODIFICATION TIME (MDTM), can be used to determine
* when a file in the server NVFS was last modified. <..>
* The syntax of a time value is:
* time-val = 14DIGIT [ "." 1*DIGIT ] <..>
* Symbolically, a time-val may be viewed as
* YYYYMMDDHHMMSS.sss
* The "." and subsequent digits ("sss") are optional. <..>
* Time values are always represented in UTC (GMT)
*/
rc = proxy_ftp_command(apr_pstrcat(p, "MDTM ", ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
r, origin, bb, &ftpmessage);
/* then extract the Last-Modified time from it (YYYYMMDDhhmmss or YYYYMMDDhhmmss.xxx GMT). */
if (rc == 213) {
struct {
char YYYY[4+1];
char MM[2+1];
char DD[2+1];
char hh[2+1];
char mm[2+1];
char ss[2+1];
} time_val;
if (6 == sscanf(ftpmessage, "%4[0-9]%2[0-9]%2[0-9]%2[0-9]%2[0-9]%2[0-9]",
time_val.YYYY, time_val.MM, time_val.DD, time_val.hh, time_val.mm, time_val.ss)) {
struct tm tms;
memset (&tms, '\0', sizeof tms);
tms.tm_year = atoi(time_val.YYYY) - 1900;
tms.tm_mon = atoi(time_val.MM) - 1;
tms.tm_mday = atoi(time_val.DD);
tms.tm_hour = atoi(time_val.hh);
tms.tm_min = atoi(time_val.mm);
tms.tm_sec = atoi(time_val.ss);
#ifdef HAVE_TIMEGM /* Does system have timegm()? */
mtime = timegm(&tms);
mtime *= APR_USEC_PER_SEC;
#elif HAVE_GMTOFF /* does struct tm have a member tm_gmtoff? */
/* mktime will subtract the local timezone, which is not what we want.
* Add it again because the MDTM string is GMT
*/
mtime = mktime(&tms);
mtime += tms.tm_gmtoff;
mtime *= APR_USEC_PER_SEC;
#else
mtime = 0L;
#endif
}
}
#endif /* USE_MDTM */
/* FIXME: Handle range requests - send REST */
buf = apr_pstrcat(p, "RETR ", ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL);
}
rc = proxy_ftp_command(buf, r, origin, bb, &ftpmessage);
/* rc is an intermediate response for the LIST or RETR commands */
/*
* RETR: 110, 125, 150, 226, 250, 421, 425, 426, 450, 451, 500, 501, 530,
* 550 NLST: 125, 150, 226, 250, 421, 425, 426, 450, 451, 500, 501, 502,
* 530
*/
/* 110 Restart marker reply. */
/* 125 Data connection already open; transfer starting. */
/* 150 File status okay; about to open data connection. */
/* 226 Closing data connection. */
/* 250 Requested file action okay, completed. */
/* 421 Service not available, closing control connection. */
/* 425 Can't open data connection. */
/* 426 Connection closed; transfer aborted. */
/* 450 Requested file action not taken. */
/* 451 Requested action aborted. Local error in processing. */
/* 500 Syntax error, command unrecognized. */
/* 501 Syntax error in parameters or arguments. */
/* 530 Not logged in. */
/* 550 Requested action not taken. */
if (rc == -1 || rc == 421) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
"Error reading from remote server");
}
if (rc == 550) {
ap_log_rerror(APLOG_MARK, APLOG_TRACE4, 0, r,
"RETR failed, trying LIST instead");
/* Directory Listings should always be fetched in ASCII mode */
dirlisting = 1;
ftp_set_TYPE('A', r, origin, bb, NULL);
rc = proxy_ftp_command(apr_pstrcat(p, "CWD ",
ftp_escape_globbingchars(p, path, fdconf), CRLF, NULL),
r, origin, bb, &ftpmessage);
/* possible results: 250, 421, 500, 501, 502, 530, 550 */
/* 250 Requested file action okay, completed. */
/* 421 Service not available, closing control connection. */
/* 500 Syntax error, command unrecognized. */
/* 501 Syntax error in parameters or arguments. */
/* 502 Command not implemented. */
/* 530 Not logged in. */
/* 550 Requested action not taken. */
if (rc == -1 || rc == 421) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
"Error reading from remote server");
}
if (rc == 550) {
return ftp_proxyerror(r, backend, HTTP_NOT_FOUND, ftpmessage);
}
if (rc != 250) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
}
/* Update current directory after CWD */
cwd = ftp_get_PWD(r, origin, bb);
if (cwd != NULL) {
apr_table_set(r->notes, "Directory-PWD", cwd);
}
/* See above for the "LIST" vs. "LIST -lag" discussion. */
rc = proxy_ftp_command((cwd == NULL || strchr(cwd, '/') != NULL)
? "LIST -lag" CRLF : "LIST" CRLF,
r, origin, bb, &ftpmessage);
/* rc is an intermediate response for the LIST command (125 transfer starting, 150 opening data connection) */
if (rc == -1 || rc == 421)
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY,
"Error reading from remote server");
}
if (rc != 125 && rc != 150 && rc != 226 && rc != 250) {
return ftp_proxyerror(r, backend, HTTP_BAD_GATEWAY, ftpmessage);
}
r->status = HTTP_OK;
r->status_line = "200 OK";
apr_rfc822_date(dates, r->request_time);
apr_table_setn(r->headers_out, "Date", dates);
apr_table_setn(r->headers_out, "Server", ap_get_server_banner());
/* set content-type */
if (dirlisting) {
ap_set_content_type_ex(r, apr_pstrcat(p, "text/html;charset=",
fdconf->ftp_directory_charset ?
fdconf->ftp_directory_charset :
"ISO-8859-1", NULL), 1);
}
else {
if (xfer_type != 'A' && size != NULL) {
/* We "trust" the ftp server to really serve (size) bytes... */
apr_table_setn(r->headers_out, "Content-Length", size);
ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r,
"Content-Length set to %s", size);
}
}
if (r->content_type) {
apr_table_setn(r->headers_out, "Content-Type", r->content_type);
ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r,
"Content-Type set to %s", r->content_type);
}
#if defined(USE_MDTM) && (defined(HAVE_TIMEGM) || defined(HAVE_GMTOFF))
if (mtime != 0L) {
char datestr[APR_RFC822_DATE_LEN];
apr_rfc822_date(datestr, mtime);
apr_table_set(r->headers_out, "Last-Modified", datestr);
ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r,
"Last-Modified set to %s", datestr);
}
#endif /* USE_MDTM */
/* If an encoding has been set by mistake, delete it.
* @@@ FIXME (e.g., for ftp://user@host/file*.tar.gz,
* @@@ the encoding is currently set to x-gzip)
*/
if (dirlisting && r->content_encoding != NULL)
r->content_encoding = NULL;
/* set content-encoding (not for dir listings, they are uncompressed)*/
if (r->content_encoding != NULL && r->content_encoding[0] != '\0') {
ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r,
"Content-Encoding set to %s", r->content_encoding);
apr_table_setn(r->headers_out, "Content-Encoding", r->content_encoding);
}
/* wait for connection */
if (use_port) {
for (;;) {
rv = apr_socket_accept(&data_sock, local_sock, r->pool);
if (APR_STATUS_IS_EINTR(rv)) {
continue;
}
else if (rv == APR_SUCCESS) {
break;
}
else {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01053)
"failed to accept data connection");
proxy_ftp_cleanup(r, backend);
return HTTP_BAD_GATEWAY;
}
}
}
/* the transfer socket is now open, create a new connection */
data = ap_create_connection(p, r->server, data_sock, 0, NULL,
c->bucket_alloc, 1);
if (!data) {
/*
* the peer reset the connection already; ap_create_connection() closed
* the socket
*/
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01054)
"an error occurred creating the transfer connection");
proxy_ftp_cleanup(r, backend);
return HTTP_INTERNAL_SERVER_ERROR;
}
/*
* We do not do SSL over the data connection, even if the virtual host we
* are in might have SSL enabled
*/
ap_proxy_ssl_engine(data, r->per_dir_config, 0);
/* set up the connection filters */
rc = ap_run_pre_connection(data, data_sock);
if (rc != OK && rc != DONE) {
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01055)
"pre_connection setup failed (%d)", rc);
data->aborted = 1;
proxy_ftp_cleanup(r, backend);
return rc;
}
/*
* VI: Receive the Response ------------------------
*
* Get response from the remote ftp socket, and pass it up the filter chain.
*/
/* send response */
r->sent_bodyct = 1;
if (dirlisting) {
/* insert directory filter */
ap_add_output_filter("PROXY_SEND_DIR", NULL, r, r->connection);
}
/* send body */
if (!r->header_only) {
apr_bucket *e;
int finish = FALSE;
ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r, "start body send");
/* read the body, pass it to the output filters */
while (ap_get_brigade(data->input_filters,
bb,
AP_MODE_READBYTES,
APR_BLOCK_READ,
conf->io_buffer_size) == APR_SUCCESS) {
#if DEBUGGING
{
apr_off_t readbytes;
apr_brigade_length(bb, 0, &readbytes);
ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, APLOGNO(01056)
"proxy: readbytes: %#x", readbytes);
}
#endif
/* sanity check */
if (APR_BRIGADE_EMPTY(bb)) {
break;
}
/* found the last brigade? */
if (APR_BUCKET_IS_EOS(APR_BRIGADE_LAST(bb))) {
/* if this is the last brigade, cleanup the
* backend connection first to prevent the
* backend server from hanging around waiting
* for a slow client to eat these bytes
*/
ap_flush_conn(data);
if (data_sock) {
apr_socket_close(data_sock);
}
data_sock = NULL;
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01057)
"data connection closed");
/* signal that we must leave */
finish = TRUE;
}
/* if no EOS yet, then we must flush */
if (FALSE == finish) {
e = apr_bucket_flush_create(c->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(bb, e);
}
/* try send what we read */
if (ap_pass_brigade(r->output_filters, bb) != APR_SUCCESS
|| c->aborted) {
/* Ack! Phbtt! Die! User aborted! */
finish = TRUE;
}
/* make sure we always clean up after ourselves */
apr_brigade_cleanup(bb);
/* if we are done, leave */
if (TRUE == finish) {
break;
}
}
ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r, "end body send");
}
if (data_sock) {
ap_flush_conn(data);
apr_socket_close(data_sock);
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01058) "data connection closed");
}
/* Retrieve the final response for the RETR or LIST commands */
proxy_ftp_command(NULL, r, origin, bb, &ftpmessage);
apr_brigade_cleanup(bb);
/*
* VII: Clean Up -------------
*
* If there are no KeepAlives, or if the connection has been signalled to
* close, close the socket and clean up
*/
/* finish */
proxy_ftp_command("QUIT" CRLF, r, origin, bb, &ftpmessage);
/* responses: 221, 500 */
/* 221 Service closing control connection. */
/* 500 Syntax error, command unrecognized. */
ap_flush_conn(origin);
proxy_ftp_cleanup(r, backend);
apr_brigade_destroy(bb);
return OK;
}