/* Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * Original Copyright (c) 2005 Covalent Technologies * * FTP Protocol module for Apache 2.0 */ #define CORE_PRIVATE #include "mod_ftp.h" #include "ftp_internal.h" /* Min # of bytes to allocate when reading a request line */ #define MIN_LINE_ALLOC 512 /* * ftp_read_line reads ahead one line from the control channel. * */ static apr_status_t ftp_read_line(char **result, apr_size_t *bytes_read, apr_pool_t *pool, apr_bucket_brigade *bb, ap_filter_t *input_filters, int block, ftp_connection *fc) { char *last_char = NULL; apr_status_t rv; apr_bucket_pool *pb; apr_bucket *pe; apr_bucket *e; char *pbuf; char *pos; /* * We manage a leading (intially empty) pool bucket that we will use to * concatinate the line over (possibly) multiple non-blocking invocations * of ftp_read_line() */ if (APR_BRIGADE_EMPTY(bb)) { pe = apr_bucket_pool_create(apr_palloc(pool, MIN_LINE_ALLOC), 0, pool, input_filters->c->bucket_alloc); pb = pe->data; pb->heap.alloc_len = MIN_LINE_ALLOC; pbuf = (char *) pb->base; #ifdef FTP_TRACE ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, fc->orig_server, "FTP frl: empty bb"); #endif } else { pe = APR_BRIGADE_FIRST(bb); pb = pe->data; if (APR_BUCKET_IS_POOL(pe) && pb->pool) pbuf = (char *) pb->base; else if (APR_BUCKET_IS_HEAP(pe) || APR_BUCKET_IS_POOL(pe)) pbuf = pb->heap.base; else return APR_EGENERAL; /* We hope to keep things simple! */ if (pe->start != 0) return APR_EGENERAL; /* Remove pe so we have a clean brigade for the loop below */ APR_BUCKET_REMOVE(pe); #ifdef FTP_TRACE ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, fc->orig_server, "FTP frl: using previous bb"); #endif } *result = NULL; do { for (;;) { apr_brigade_cleanup(bb); rv = ap_get_brigade(input_filters, bb, AP_MODE_GETLINE, block, 0); if (rv != APR_SUCCESS) { APR_BRIGADE_INSERT_HEAD(bb, pe); return rv; } if (APR_BRIGADE_EMPTY(bb)) { APR_BRIGADE_INSERT_HEAD(bb, pe); #ifdef FTP_TRACE ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, fc->orig_server, "FTP frl: got empty brigade"); #endif return (block == APR_BLOCK_READ) ? APR_EGENERAL : APR_EAGAIN; } while ((e = APR_BRIGADE_FIRST(bb)) != APR_BRIGADE_SENTINEL(bb)) { apr_bucket *e_next; const char *str; apr_size_t len; int mark; /* If we see an EOS, don't bother doing anything more. */ if (APR_BUCKET_IS_EOS(e)) { break; } rv = apr_bucket_read(e, &str, &len, block); /* * Upon discovering that the next bucket is a socket, * and that socket is at the OOB mark, we will dump * our current buffer and continue to read the priority * command beyond the OOB mark. * * XXX: Note that some bytes of the IAC IP IAC DM sequence * may fall back in band, or the leading 'A' of "ABOR" may * fall out of band, due to poor understanding of telnet * by many client authors. We need to take this a step * further and add logic to correct these cases. */ e_next = APR_BUCKET_NEXT(e); if (rv == APR_SUCCESS && APR_BUCKET_IS_SOCKET(e_next)) { apr_socket_t *sock = e_next->data; rv = apr_socket_atmark(sock, &mark); ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, fc->orig_server, "FTP frl: atmark: %x %d", (int) rv, mark); if (rv == APR_SUCCESS && mark) { pe->length = 0; ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, fc->orig_server, "FTP frl: Saw OOB"); continue; } } if (rv != APR_SUCCESS) { APR_BRIGADE_INSERT_HEAD(bb, pe); return rv; } if (len == 0) { /* * no use attempting a zero-byte alloc (hurts when using * --with-efence --enable-pool-debug) or doing any of the * other logic either */ apr_bucket_delete(e); continue; } /* Exceeding limits? If so, we'll die. */ if (pe->length + len > DEFAULT_LIMIT_REQUEST_LINE + 2) { APR_BRIGADE_INSERT_HEAD(bb, pe); return APR_ENOSPC; } /* If exceeding our limit, increase the buffer size */ if (pe->length + len > pb->heap.alloc_len) { apr_size_t new_size = pb->heap.alloc_len * 2; char *new_buffer; if (pe->length + len > new_size) { new_size = (pe->length + len) * 2; } if (new_size > DEFAULT_LIMIT_REQUEST_LINE + 2) { new_size = DEFAULT_LIMIT_REQUEST_LINE + 2; } if (pb->pool) { new_buffer = apr_palloc(pb->pool, new_size); memcpy(new_buffer, pb->base, pe->length); pb->base = new_buffer; pbuf = (char *) pb->base; } else { new_buffer = malloc(new_size); if (!new_buffer) return APR_ENOSPC; memcpy(new_buffer, pb->heap.base, pe->length); free(pb->heap.base); pb->heap.base = new_buffer; pbuf = (char *) pb->heap.base; } pb->heap.alloc_len = new_size; } /* Just copy the rest of the data to the end of the buffer. */ pos = pbuf + pe->length; memcpy(pos, str, len); pe->length += len; last_char = pos + len - 1; /* Destroy the now-consumed bucket */ apr_bucket_delete(e); } /* If we got a full line of input, stop reading */ if ((last_char > pbuf) && (*(last_char - 1) == APR_ASCII_CR) && (*last_char == APR_ASCII_LF)) { char *ssrc, *sdst; /* Since we want to remove the CRLF from the line, we'll go * ahead and NULL term the string; */ *(--last_char) = '\0'; /* * We ignore claims in draft-ietf-ftpext-utf-8-option-00 which * suggested RFC2640 is wrong with respect to RFC1123. RFC1123 * is quite clear in stating; * * The Telnet end-of-line sequence CR LF MUST be used to send * Telnet data that is not terminal-to-computer (e.g., for Server * Telnet sending output, or the Telnet protocol incorporated * another application protocol) * * ergo CR NUL is not a valid command completion sequence, as * FTP is not a terminal protocol, but an application protocol. * * Collapse to and to a single 0xFF * as documented in RFC854 and clarified by RFC2640 and RFC3659, * discarding all extranious sequences. As negotiation * is explicitly not allowed, we make no effort to catch such. */ for (ssrc = sdst = pbuf; ssrc < last_char; ++ssrc, ++sdst) { if ((ssrc[0] == '\xFF') || (ssrc[0] == APR_ASCII_CR && ssrc[1] == 0)) { if (ssrc[0] == '\xFF' && ssrc[1] != '\xFF') --sdst; break; } } /* Jumping from the parse-only loop above, into this parse w/copy */ for (ssrc += 2, ++sdst; ssrc < last_char; ++ssrc) { *(sdst++) = *ssrc; if ((ssrc[0] == '\xFF') || (ssrc[0] == APR_ASCII_CR && ssrc[1] == 0)) { ++ssrc; if (ssrc[0] == '\xFF' && ssrc[1] != '\xFF') --sdst; } } /* * Return the result string, and the actual bytes read from * the network (before we truncated characters) * * We may have moved from a pool to another pool, or to a heap * bucket. Reallocate from the current pool in these cases. */ if (pb->pool &&pb->pool == pool) { *result = pbuf; } else { *result = apr_palloc(pool, last_char - pbuf + 1); memcpy(*result, pbuf, last_char - pbuf + 1); } *bytes_read = pe->length; /* * Finally destroy the working bucket - if it is heap the * heap data will also be free()'d. */ apr_bucket_destroy(pe); #ifdef FTP_TRACE ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, fc->orig_server, "FTP frl: got full line"); #endif return APR_SUCCESS; } } } while (pe->length <= 0); #ifdef FTP_TRACE ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, fc->orig_server, "FTP frl: fall through success"); #endif return APR_SUCCESS; } /* * ftp_read_request_line: Read the request from the client, and set the * correct values in the request record. * * Arguments: r - The request to read from. * Arguments: bb - The brigade to retrieve. * * Returns: Returns 0 on success, 1 on error. */ static apr_status_t ftp_read_request_line(ftp_connection *fc, request_rec *r, apr_bucket_brigade *bb) { apr_size_t bytes_read; apr_status_t rv; const char *ll; if (fc->next_request && *fc->next_request) { r->the_request = apr_pstrdup(r->pool, fc->next_request); bytes_read = fc->next_reqsize; fc->next_request = NULL; fc->next_reqsize = 0; #ifdef FTP_TRACE ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, r->server, "FTP frrl: using read-ahead request"); #endif } else if ((rv = ftp_read_line(&r->the_request, &bytes_read, fc->connection->pool, bb, r->input_filters, APR_BLOCK_READ, fc)) != APR_SUCCESS) { return rv; } r->read_length = bytes_read; r->request_time = apr_time_now(); ll = r->the_request; #ifdef FTP_TRACE ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, r->server, "FTP frrl: raw command: %s", ll); #endif r->method = ftp_toupper(r->pool, ap_getword_white(r->pool, &ll)); r->method = ftp_get_cmd_alias(r->method); r->method_number = ap_method_number_of(r->method); return APR_SUCCESS; } apr_status_t ftp_read_ahead_request(ftp_connection *fc) { apr_status_t rv; const char *ll; const char *method; #ifdef FTP_TRACE ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, fc->orig_server, "FTP frar: entering"); #endif /* Review one command, only once */ if (fc->next_request && *fc->next_request) { #ifdef FTP_TRACE ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, fc->orig_server, "FTP frar: previously read-ahead"); #endif return APR_SUCCESS; } if (!fc->next_pool) { apr_pool_create(&fc->next_pool, fc->connection->pool); apr_pool_tag(fc->next_pool, "next_cmd"); fc->next_bb = apr_brigade_create(fc->next_pool, fc->connection->bucket_alloc); #ifdef FTP_TRACE ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, fc->orig_server, "FTP frar: created next_pool"); #endif } rv = ftp_read_line(&fc->next_request, &fc->next_reqsize, fc->next_pool, fc->next_bb, fc->connection->input_filters, APR_NONBLOCK_READ, fc); if (APR_STATUS_IS_EAGAIN(rv)) { #ifdef FTP_TRACE ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, fc->orig_server, "FTP frar: not ready - read again"); #endif /* We actually like some failures here */ return APR_SUCCESS; } else if (rv != APR_SUCCESS) { return rv; } /* The entire line is read - we no longer need this brigade */ apr_brigade_destroy(fc->next_bb); fc->next_bb = NULL; method = ftp_toupper(fc->next_pool, ap_getword_white(fc->next_pool, &ll)); #ifdef FTP_TRACE ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, fc->orig_server, "FTP frar: method: %s", method); #endif /* Can we ignore this command for a while? */ if (ftp_cmd_abort_data(method)) { ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, fc->orig_server, "FTP frar: I see ABOR"); return APR_ECONNRESET; } /* Wait to consider this command till the data transfer is complete */ return APR_SUCCESS; } /* * ftp_read_request: Called from the connection handler. Used to * read the request and fill out the request record. * * Arguments: fc - The ftp_connection rec associated with this request. * * Returns: Returns an initialized request_rec */ request_rec *ftp_read_request(ftp_connection *fc) { conn_rec *c = fc->connection; apr_status_t rv; request_rec *r; apr_pool_t *p; int access_status; apr_bucket_brigade *tmp_bb; ap_filter_t *f; apr_pool_create(&p, c->pool); apr_pool_tag(p, "request"); r = apr_pcalloc(p, sizeof(request_rec)); r->pool = p; r->connection = c; r->server = fc->orig_server; r->user = NULL; r->ap_auth_type = NULL; r->allowed_methods = ap_make_method_list(p, 2); r->headers_in = apr_table_make(r->pool, 50); r->subprocess_env = apr_table_make(r->pool, 50); r->headers_out = apr_table_make(r->pool, 12); r->err_headers_out = apr_table_make(r->pool, 5); r->notes = apr_table_make(r->pool, 5); r->request_config = ap_create_request_config(r->pool); /* Must be set before we run create request hook */ r->proto_output_filters = c->output_filters; r->output_filters = r->proto_output_filters; r->proto_input_filters = c->input_filters; r->input_filters = r->proto_input_filters; ap_run_create_request(r); /* * We now need to remove the NET_TIME filter to allow * use to control timeouts ourselves. */ for (f = c->input_filters; f; f = f->next) { if (strcasecmp(f->frec->name, "NET_TIME") == 0) { ap_remove_input_filter(f); break; } } for (f = r->input_filters; f; f = f->next) { if (strcasecmp(f->frec->name, "NET_TIME") == 0) { ap_remove_input_filter(f); break; } } for (f = r->proto_input_filters; f; f = f->next) { if (strcasecmp(f->frec->name, "NET_TIME") == 0) { ap_remove_input_filter(f); break; } } r->per_dir_config = r->server->lookup_defaults; r->sent_bodyct = 0; /* bytect isn't for body */ r->read_length = 0; r->read_body = REQUEST_NO_BODY; r->status = HTTP_OK; /* Until we get a request */ r->the_request = NULL; /* * Begin by presuming any module can make its own path_info assumptions, * until some module interjects and changes the value. */ r->used_path_info = AP_REQ_DEFAULT_PATH_INFO; r->protocol = "FTP"; r->method = NULL; /* * We don't use r->uri for every request, but some modules (SSL) require * r->uri to not be NULL in the post_read_request hook * * The canonical (http) form of "Any location" is *, e.g. the http OPTIONS * * request. It's not a bad pattern to keep with module author's * expectations. */ r->uri = "*"; /* Resume any partial request line from fc->next_bb */ if (fc->next_bb) { tmp_bb = fc->next_bb; #ifdef FTP_TRACE ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, r->server, "FTP frr: using next_bb"); #endif } else { tmp_bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); #ifdef FTP_TRACE ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, r->server, "FTP frr: using tmp_bb"); #endif } if ((rv = ftp_read_request_line(fc, r, tmp_bb)) != APR_SUCCESS) { apr_time_t timeout; apr_bucket_brigade *bb; apr_bucket *b; char *err; apr_size_t len; apr_brigade_destroy(tmp_bb); if (rv == APR_TIMEUP) { /* * Handle client timeouts here. The idle timeout for the * control connection is set in the process_connection * handler, if the timeout is reached, ftp_read_request_line * will return with an error. Here we send the client a * friendly error message, and close the connection. */ apr_socket_timeout_get(fc->cntlsock, &timeout); ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_INFO, 0, r->server, "User %s timed out after %d seconds", fc->user, (int) (timeout / APR_USEC_PER_SEC)); err = apr_psprintf(r->pool, "%d Idle Timeout (%d seconds): " "Closing control connection" CRLF, FTP_REPLY_SERVICE_NOT_AVAILABLE, (int) (timeout / APR_USEC_PER_SEC)); len = strlen(err); bb = apr_brigade_create(r->pool, c->bucket_alloc); rv = apr_brigade_write(bb, ap_filter_flush, c->output_filters, err, len); /* Flush the brigade down the filter chain */ b = apr_bucket_flush_create(c->bucket_alloc); APR_BRIGADE_INSERT_TAIL(bb, b); ap_pass_brigade(c->output_filters, bb); apr_brigade_destroy(bb); } else { /* * Remote client suddenly disconnected, don't bother sending an * error since the client is long gone. Just log the error. */ ap_log_error(APLOG_MARK, APLOG_INFO, rv, r->server, "User %s disconnected", fc->user); } /* * Return NULL to the connection handler, causing the connection to * be dropped. */ return NULL; } apr_brigade_destroy(tmp_bb); fc->next_bb = NULL; /* * ftp_read_line returns the_request always allocated from the correct * pool. If not, we have a bug. No need to clean up next_pool above in * the failure case, because it is allocated from the connection (soon to * be destroyed.) */ if (fc->next_pool) { apr_pool_destroy(fc->next_pool); fc->next_pool = NULL; #ifdef FTP_TRACE ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_DEBUG, 0, r->server, "FTP frr: clearing next_pool"); #endif } /* * PHP does initializations of important data structures in the * post_read_request phase. */ if ((access_status = ap_run_post_read_request(r))) { ap_log_error(APLOG_MARK, APLOG_NOERRNO | APLOG_INFO, 0, r->server, "Post read request failed, dropping " "client connection."); return NULL; } return r; } /* * ftp_reply: This function is used for sending command responses to the * client over the control connection. All responses should * be sent through this function. * * Arguments: out_filter - The output filter chain. * p - The pool to allocate from. * n - The ftp response code. * l - Flag that determines if this is a long response or not. Long * responses have the format %d-%s%s, short responses use a * space in place of the dash. * * 1 - this is long response. * 0 - this is a short (the last) response. * fmt - Format string to be sent to the client. * * Returns: apr_status_t */ apr_status_t ftp_reply(ftp_connection *fc, ap_filter_t *out_filter, apr_pool_t *p, int n, int l, const char *fmt,...) { char buf[BUFSIZ], reply[BUFSIZ]; int len; va_list ap; apr_bucket_brigade *bb; apr_bucket *b; va_start(ap, fmt); apr_vsnprintf(buf, sizeof(buf), fmt, ap); len = apr_snprintf(reply, sizeof(reply), "%d%s%s%s", n, l == 1 ? "-" : " ", buf, CRLF); va_end(ap); bb = apr_brigade_create(p, out_filter->c->bucket_alloc); b = apr_bucket_pool_create(reply, len, p, out_filter->c->bucket_alloc); APR_BRIGADE_INSERT_HEAD(bb, b); b = apr_bucket_flush_create(out_filter->c->bucket_alloc); APR_BRIGADE_INSERT_TAIL(bb, b); fc->traffic += len; return ap_pass_brigade(out_filter, bb); } /* ftp_show_file: Test if a file exists, and echo it to the control * connection if it exists. This is primarily for * displaying MOTD messages and .message files. * * Arguments: out_filter - The output filter chain. * p - The pool to allocate from. * code - Integer value representing the response code to use. * fc - The ftp connection. * file - Absolute path to the file to be displayed. * * Returns: apr_status_t */ apr_status_t ftp_show_file(ap_filter_t *out_filter, apr_pool_t *p, int code, ftp_connection *fc, const char *path) { apr_status_t rv; apr_file_t *file; char *pos; char buf[BUFSIZ]; char reply[BUFSIZ]; rv = apr_file_open(&file, path, APR_READ, APR_OS_DEFAULT, p); if (rv != APR_SUCCESS) { return rv; } while (apr_file_gets(buf, sizeof(buf), file) == APR_SUCCESS) { /* Strip off trailing space/cr/lf, ftp_reply does not expect them */ pos = buf + strlen(buf) - 1; while ((pos >= buf) && apr_isspace(*pos)) --pos; pos[1] = '\0'; ftp_message_generate(fc, buf, reply, sizeof(reply)); rv = ftp_reply(fc, out_filter, p, code, 1, "%s", reply); if (rv != APR_SUCCESS) { return rv; } } return apr_file_close(file); } /* ftp_send_response: Send response to the client based on the status code * These are currently listed in numerical order. * For responses such as 503's where the error message * can change, the caller is requried to fill out * the respose_notes in the ftp_connection structure. * * Arguments: r - The request. * status - FTP status code. * * Returns: nothing */ void ftp_send_response(request_rec *r, int status) { ftp_connection *fc = ftp_get_module_config(r->connection->conn_config); ftp_server_config *fsc; conn_rec *c = r->connection; /* * We are done checking subrequest values for r->status, so we can place * our ftp reply code there so it can be logged */ r->status = status; /* * In general, status codes below 400 will be considered success. * Specific exceptions are toggled, below. */ if (status >= 400) { apr_table_setn(r->subprocess_env, "ftp_transfer_ok", "0"); } switch (status) { case FTP_REPLY_SYSTEM_TYPE: ftp_reply(fc, c->output_filters, r->pool, FTP_REPLY_SYSTEM_TYPE, 0, apr_pstrcat(r->pool, "UNIX Type: L8 System: \"", #if AP_MODULE_MAGIC_AT_LEAST(20060905,0) ap_get_server_banner(), #else ap_get_server_version(), #endif "\"", NULL)); break; case FTP_REPLY_CONTROL_CLOSE: fsc = ftp_get_module_config(r->server->module_config); if (fsc->exit_message) { if (fsc->exit_message_isfile) { ftp_show_file(c->output_filters, r->pool, FTP_REPLY_CONTROL_CLOSE, fc, fsc->exit_message); } else { char reply[BUFSIZ]; ftp_message_generate(fc, fsc->exit_message, reply, sizeof(reply)); ftp_reply(fc, c->output_filters, r->pool, FTP_REPLY_CONTROL_CLOSE, 1, reply); } } ftp_reply(fc, c->output_filters, r->pool, FTP_REPLY_CONTROL_CLOSE, 0, "Goodbye."); break; case FTP_REPLY_DATA_CLOSE: ftp_reply(fc, c->output_filters, r->pool, FTP_REPLY_DATA_CLOSE, 0, "Transfer complete."); break; case FTP_REPLY_USER_LOGGED_IN: ftp_reply(fc, c->output_filters, r->pool, FTP_REPLY_USER_LOGGED_IN, 0, "User %s logged in", fc->user); break; case FTP_REPLY_SECURITY_EXCHANGE_DONE: ftp_reply(fc, c->output_filters, r->pool, FTP_REPLY_SECURITY_EXCHANGE_DONE, 0, "Security exchange completed"); break; case FTP_REPLY_COMPLETED: ftp_reply(fc, c->output_filters, r->pool, FTP_REPLY_COMPLETED, 0, "%s command successful.", r->method); break; case FTP_REPLY_CANNOT_OPEN_DATACONN: ftp_reply(fc, c->output_filters, r->pool, FTP_REPLY_CANNOT_OPEN_DATACONN, 0, "Cannot open data connection."); break; case FTP_REPLY_TRANSFER_ABORTED: ftp_reply(fc, c->output_filters, r->pool, FTP_REPLY_TRANSFER_ABORTED, 0, "Transfer aborted"); break; case FTP_REPLY_COMMAND_UNRECOGNIZED: ftp_reply(fc, c->output_filters, r->pool, FTP_REPLY_COMMAND_UNRECOGNIZED, 0, "%s: Command not recognized", r->method); break; case FTP_REPLY_SYNTAX_ERROR: ftp_reply(fc, c->output_filters, r->pool, FTP_REPLY_SYNTAX_ERROR, 0, "Syntax error in '%s'", r->the_request); break; case FTP_REPLY_BAD_SEQUENCE: ftp_reply(fc, c->output_filters, r->pool, FTP_REPLY_BAD_SEQUENCE, 0, "Bad sequence of commands"); break; case FTP_REPLY_BAD_PROTOCOL: /* * XXX This is really crufty, the given server may be configured to * support only an IPv4 or IPv6 binding */ ftp_reply(fc, c->output_filters, r->pool, FTP_REPLY_BAD_PROTOCOL, 0, #if APR_HAVE_IPV6 "Network protocol not supported, use (1,2)"); #else "Network protocol not supported, use (1)"); #endif break; case FTP_REPLY_PROT_NOT_SUPPORTED: ftp_reply(fc, c->output_filters, r->pool, FTP_REPLY_PROT_NOT_SUPPORTED, 0, "Requested PROT level not supported by mechanism"); break; /* Exception cases, failure codes that fall before the 400's: */ case FTP_REPLY_SERVICE_READY_IN_N_MIN: case FTP_REPLY_NOT_IMPLEMENTED: case FTP_REPLY_NEED_ACCOUNT: apr_table_setn(r->subprocess_env, "ftp_transfer_ok", "0"); /* * failure is flagged for status < 400 now fall through... */ default: ftp_reply(fc, c->output_filters, r->pool, status, 0, "%s", (fc->response_notes && *fc->response_notes) ? fc->response_notes : "Error (no message)"); } return; }