/* 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.
 */

#define CORE_PRIVATE
#include "mod_ftp.h"
#include "ftp_internal.h"

/* In httpd 2.4, we want to log by module */
#if AP_MODULE_MAGIC_AT_LEAST(20111130,0)
APLOG_USE_MODULE(ftp);
#endif

/* 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 <CR><NULL> to <CR> and <IAC><IAC> to a single 0xFF
                 * as documented in RFC854 and clarified by RFC2640 and RFC3659,
                 * discarding all extranious <IAC> 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;
    r->useragent_ip = c->client_ip;
    r->useragent_addr = c->client_addr;

#if ((MODULE_MAGIC_NUMBER_MAJOR == 20051115) \
              && (MODULE_MAGIC_NUMBER_MINOR >= 36)) \
       || AP_MODULE_MAGIC_AT_LEAST(20120211,37)
    r->trailers_in = apr_table_make(r->pool, 5);
    r->trailers_out = apr_table_make(r->pool, 5);
#endif

    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
    }

    /* Set Host/Auth headers prior to post_read_request, because SSL
     * uses this phase to validate the hostname.
     */
    ftp_set_authorization(r);

    /*
     * 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_NO_REPLY:
        /* Sending no response */
        break;

    case FTP_REPLY_SERVICE_READY:
        ftp_reply(fc, c->output_filters, r->pool, FTP_REPLY_SERVICE_READY, 0,
                  "%s FTP Server (%s) ready.",
                  fc->orig_server->server_hostname,
#if AP_MODULE_MAGIC_AT_LEAST(20060905,0)
                  ap_get_server_banner());
#else
                  ap_get_server_version());
#endif
        break;
    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;
}
