/***************************************************************************
 *
 * Copyright (c) 2000-2015 BalaBit IT Ltd, Budapest, Hungary
 * Copyright (c) 2015-2018 BalaSys IT Ltd, Budapest, Hungary
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 *
 ***************************************************************************/

#include "http.h"

#include <zorpll/log.h>
#include <zorp/attach.h>

/* NOTE: cross site scripting
 *
 * we may not add anything unescaped to the HTML output as otherwise cross
 * site scripting would become possible.  This includes the HTML page and
 * HTTP headers.
 */

enum
  {
    HTTP_FTP_FETCH_DIR = 0,
    HTTP_FTP_FETCH_FILE = 1
  };

/**
 * http_ftp_fetch_response:
 * @self: HttpProxy instance
 * @status: returned FTP status code
 * @msg: returned FTP message
 * @msglen: size of the @msg buffer
 *
 * This function is called to fetch a response from the FTP server. Line
 * continuations are supported however only the first line will be returned
 * in @msg.
 **/
static gboolean
http_ftp_fetch_response(HttpProxy *self, gint *status, gchar *msg, gsize msglen)
{
  gchar *line;
  gsize length;
  gboolean continuation = TRUE, first = TRUE;
  gint mul, value, i;

  msg[0] = 0;

  while (continuation)
    {
      if (z_stream_line_get(self->super.endpoints[EP_SERVER], &line, &length, NULL) != G_IO_STATUS_NORMAL)
        return FALSE;

      if (length < 4)
        {
          /*LOG
            This message indicates that the response given by the FTP server
            was too short, not even the mandatory status code was included.
          */
          z_proxy_log(self, HTTP_VIOLATION, 2, "Invalid FTP response, line too short; line='%.*s'", (gint)length, line);
          return FALSE;
        }

      value = 0;
      mul = 100;

      for (i = 0; i < 3; i++)
        {
          if (!isdigit(line[i]))
            {
              /*LOG
                This message indicates that the FTP server gave an invalid
                response, the status code returned by the server was not
                numeric.
              */
              z_proxy_log(self, HTTP_VIOLATION, 2, "Invalid FTP response, response code not numeric; line='%.*s'", (gint)length, line);
              return FALSE;
            }

          value = value + mul * (line[i] - '0');
          mul = mul / 10;
        }

      if (first)
        {
          gint copy = MIN(msglen - 1, length - 4 + 1);

          *status = value;
          memcpy(msg, &line[4], copy);
          msg[copy] = 0;
        }
      else if (*status != value)
        {
          /*LOG
            This message indicates that the FTP server gave an invalid response as the
            status code changed from the one which was present on the first line.
          */
          z_proxy_log(self, HTTP_VIOLATION, 2, "Invalid FTP response, continuation line contains different status code; ftp_status='%d', line='%.*s'", *status, (gint)length, line);
          return FALSE;
        }

      continuation = line[3] == '-';

    }

  return TRUE;
}

/**
 * http_ftp_send_command:
 * @self: HttpProxy instance
 * @cmd: FTP command to send
 * @param: parameter to @cmd
 *
 * Send a command to the FTP server.
 **/
static gboolean
http_ftp_send_command(HttpProxy *self, const gchar *cmd, const gchar *param)
{
  gchar request_msg[1024];
  gsize bw;

  if (param)
    g_snprintf(request_msg, sizeof(request_msg), "%s %s\r\n", cmd, param);
  else
    g_snprintf(request_msg, sizeof(request_msg), "%s\r\n", cmd);

  if (z_stream_write(self->super.endpoints[EP_SERVER], request_msg, strlen(request_msg), &bw, NULL) != G_IO_STATUS_NORMAL)
    return FALSE;

  return TRUE;
}

/**
 * http_ftp_communicate:
 * @self: HttpProxy instance
 * @cmd: FTP command to send
 * @param: parameter to @cmd
 * @status: FTP status is returned here
 * @response_msg: FTP response is returned here
 * @response_msglen: size of @response_msg
 *
 * Send a command and wait for a response from the FTP server.
 **/
static gboolean
http_ftp_communicate(HttpProxy *self, const gchar *cmd, const gchar *param, gint *status, gchar *response_msg, gsize response_msglen)
{
  if (!http_ftp_send_command(self, cmd, param))
    return FALSE;

  if (!http_ftp_fetch_response(self, status, response_msg, response_msglen))
    return FALSE;

  return TRUE;
}

/**
 * http_ftp_login:
 * @self: HttpProxy instance
 * @user: username to log in with
 * @passwd: password for @user
 *
 * Log in to the FTP server using credentials in @user and @passwd.
 **/
static gboolean
http_ftp_login(HttpProxy *self, const gchar *user, const gchar *passwd)
{
  gchar response_msg[1024];
  gint status;

  if (!http_ftp_communicate(self, "USER", user, &status, response_msg, sizeof(response_msg)))
    return FALSE;

  if (status == 230)
    {
      /* no password required */
      return TRUE;
    }
  else if (status != 331)
    {
      /* no password required */
      g_string_sprintf(self->error_info, "Unhandled status code returned to login request (%d, %s)", status, response_msg);
      /*LOG
        This message indicates that the FTP server returned an unknown
        status code in response to our login request.
      */
      z_proxy_log(self, HTTP_ERROR, 4, "FTP server returned an unhandled status code for the login request; ftp_status='%d'", status);
      return FALSE;
    }

  if (!http_ftp_communicate(self, "PASS", passwd, &status, response_msg, sizeof(response_msg)))
    return FALSE;

  if (status == 230)
    {
      /* logged in */
      return TRUE;
    }
  else
    {
      g_string_sprintf(self->error_info, "Error logging in (%d, %s)", status, response_msg);
      /*LOG
        This message indicates that the FTP refused our authentication
        attempt.
      */
      z_proxy_log(self, HTTP_ERROR, 4, "Error logging in; user='%s', ftp_status='%d', ftp_response='%s'", user, status, response_msg);
      return FALSE;
    }
}

/**
 * http_ftp_setcwd:
 * @self: HttpProxy instance
 * @cwd: directory to change into
 *
 * Set the current working directory on the FTP server to @cwd.
 **/
static gboolean
http_ftp_setcwd(HttpProxy *self, const gchar *cwd)
{
  gchar response_msg[1024];
  gint status;

  if (!http_ftp_communicate(self, "CWD", cwd, &status, response_msg, sizeof(response_msg)))
    return FALSE;

  if (status != 250)
    {
      return FALSE;
    }

  return TRUE;
}

/**
 * http_ftp_set_type:
 * @self: HttpProxy instance
 * @type: transfer type
 *
 * Set the transfer type to ascii (A) or binary (I).
 **/
static gboolean
http_ftp_set_type(HttpProxy *self, const gchar *type)
{
  gchar response_msg[1024];
  gint status;

  if (!http_ftp_communicate(self, "TYPE", type, &status, response_msg, sizeof(response_msg)))
    return FALSE;

  if (status != 200)
    {
      return FALSE;
    }

  return TRUE;
}

/**
 * http_ftp_initiate_passive_data:
 * @self: HttpProxy instance
 *
 * This function is called to initiate a passive data connection to the FTP
 * server. If it returns FALSE an active connection might still be
 * attempted. (however it is currently unimplemented).
 *
 * It currently creates the ZAttach object only does not start the actual
 * connection, it is done somewhat later in the http_ftp_complete_data()
 * function.
 **/
static gboolean
http_ftp_initiate_passive_data(HttpProxy *self)
{
  gchar response_msg[1024];
  gint status;
  gchar *start, *end;
  gint i;
  gint ftp_pasv_endpoint[6];
  gchar ip[16];
  ZSockAddr *peer;
  ZAttachParams params;

  if (!http_ftp_communicate(self, "PASV", NULL, &status, response_msg, sizeof(response_msg)))
    return FALSE;

  if (status != 227)
    return FALSE;

  start = strchr(response_msg, '(');

  if (!start)
    {
      /* hmm no '(' in PASV response */
      return FALSE;
    }

  start++;

  for (i = 0; i < 6; i++)
    {
      ftp_pasv_endpoint[i] = strtol(start, &end, 10);

      if ((i < 5 && *end != ',') ||
          (i == 5 && *end != ')'))
        {
          g_string_sprintf(self->error_info, "Response to PASV is invalid; response='%s'", response_msg);
          return FALSE;
        }

      start = end + 1;
    }

  g_snprintf(ip, sizeof(ip), "%d.%d.%d.%d", ftp_pasv_endpoint[0], ftp_pasv_endpoint[1], ftp_pasv_endpoint[2], ftp_pasv_endpoint[3]);
  peer = z_sockaddr_inet_new(ip, 256 * ftp_pasv_endpoint[4] + ftp_pasv_endpoint[5]);

  memset(&params, 0, sizeof(params));
  params.timeout = 30000;

  self->ftp_data_attach = z_attach_new(&self->super, ZD_PROTO_TCP, NULL, peer, &params, NULL, NULL, NULL);
  z_sockaddr_unref(peer);
  /* attach not started yet */

  return TRUE;
}

/**
 * http_ftp_initiate_passive_data:
 * @self: HttpProxy instance
 *
 * This function is called to initiate an active data connection to the FTP
 * server.
 *
 * This function is only a placeholder as active-mode data transfer is not
 * yet implemented.
 **/
static gboolean
http_ftp_initiate_active_data(HttpProxy * /* self */)
{
  return FALSE;
}

/**
 * http_ftp_cleanup_data:
 * @self: HttpProxy instance
 *
 * This function is called to clean up the FTP data state, e.g. dispose
 * self->ftp_data_attach.
 **/
static inline void
http_ftp_cleanup_data(HttpProxy *self)
{
  z_attach_free(self->ftp_data_attach);
  self->ftp_data_attach = NULL;
}

/**
 * http_ftp_complete_data:
 * @self: HttpProxy
 * @data_stream: the resulting data stream
 *
 * This function is called to complete the data connection setup and should
 * return with the negotiated data channel in @data_stream. It currently
 * supports passive mode only.
 **/
static gboolean
http_ftp_complete_data(HttpProxy *self, ZStream **data_stream)
{
  ZConnection *conn;
  gboolean success;

  /* FIXME: supports PASV only */
  success = z_attach_start_block(self->ftp_data_attach, &conn);
  http_ftp_cleanup_data(self);

  if (!success || !conn)
    {
      *data_stream = NULL;
      return FALSE;
    }
  else
    {
      *data_stream = z_stream_ref(conn->stream);
      z_connection_destroy(conn, FALSE);
      return TRUE;
    }
}

/**
 * http_ftp_format_response:
 * @self: HttpProxy instance
 * @stacked: whether headers towards the stacked proxy need to be formatted
 * @response: resulting HTTP response
 *
 * This function is passed as a function pointer to HttpTransfer whenever it
 * needs to format the preamble to be sent prior to the first data byte.
 **/
static gboolean
http_ftp_format_response(HttpProxy *self, gboolean stacked, GString *response)
{
  if (self->proto_version[EP_CLIENT] >= 0x0100)
    {
      http_flat_headers(&self->headers[EP_SERVER]);
      g_string_sprintf(response, "Content-Type: application/octet-stream\r\n"
                       "%s\r\n", self->headers[EP_SERVER].flat->str);

      if (!stacked)
        {
          g_string_prepend(response, "HTTP/1.0 200 OK\r\n");
        }
    }
  else
    g_string_truncate(response, 0);

  return TRUE;
}

#define SKIP_SPACES                             \
  do                                            \
    {                                           \
      while (left && *src == ' ')               \
        {                                       \
          src++;                                \
          left--;                               \
        }                                       \
    }                                           \
  while (0)

#define HTTP_FTP_MAX_FILES 16384
#define HTTP_FTP_FILE_WIDTH 32

/**
 * http_ftp_htmlize_listing:
 * @self: HttpProxy instance
 * @data_stream: stream to fetch LIST output from
 * @http_response: GString to store HTML output into
 *
 * This function reads the incoming LIST output from the FTP server and
 * reformats it in HTML so that browsers can simply display it.  NOTE:
 * maximum HTTP_FTP_MAX_FILES of lines are processed as the HTTP response
 * and body is formatted in memory.
 *
 * Returns: TRUE for success, FALSE otherwise
 **/
static gboolean
http_ftp_htmlize_listing(HttpProxy *self, ZStream *data_stream, GString *http_response)
{
  ZStream *list;
  gchar *line;
  gsize length;
  gchar perm[16];
  gchar owner[32], group[32], size[16], date[13], *filename;
  gsize fill;
  gint file_count = 0, filelen, i;
  gchar spaces[HTTP_FTP_FILE_WIDTH + 3 + 1];

  z_stream_set_timeout(data_stream, self->timeout);
  memset(spaces, ' ', sizeof(spaces));
  spaces[HTTP_FTP_FILE_WIDTH] = 0;

  for (i = sizeof(spaces) - 1; i > 0; i -= 3)
    spaces[i] = '.';

  /* request_url is canonicalized, thus it might only contain US-ASCII
   * characters, anything else will be escaped */

  g_string_sprintfa(http_response,
                    "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n"
                    "<!-- HTML listing generated by Zorp %s -->\n"
                    "<HTML><HEAD><TITLE>FTP directory %s</TITLE>\n"
                    "<STYLE type=\"text/css\"><!--BODY{background-color:#ffffff;font-family:verdana,sans-serif}--></STYLE>\n"
                    "</HEAD><BODY>\n"
                    "<H2>FTP directory <A HREF=\"%s\">%s</A></H2>\n"
                    "<PRE>\n", VERSION, self->request_url->str, self->request_url->str, self->request_url->str);

  g_string_sprintfa(http_response,
                    "<A HREF=\"../\">Parent directory</A>\n");
  list = z_stream_line_new(data_stream, 4096, ZRL_EOL_NL | ZRL_TRUNCATE);

  while (file_count < HTTP_FTP_MAX_FILES && z_stream_line_get(list, &line, &length, NULL) == G_IO_STATUS_NORMAL)
    {
      gchar *src = line;
      gint left = length;

      fill = 0;

      while (left && *src && *src != ' ' && fill < sizeof(perm) - 1)
        {
          perm[fill] = *src;
          src++;
          left--;
          fill++;
        }

      perm[fill] = 0;

      /* link count */
      SKIP_SPACES;

      while (left && *src && *src != ' ')
        {
          src++;
          left--;
        }

      /* owner */
      SKIP_SPACES;
      fill = 0;

      while (left && *src && *src != ' ' && fill < sizeof(owner) - 1)
        {
          owner[fill] = *src;
          src++;
          left--;
          fill++;
        }

      owner[fill] = 0;

      /* group */
      SKIP_SPACES;
      fill = 0;

      while (left && *src && *src != ' ' && fill < sizeof(group) - 1)
        {
          group[fill] = *src;
          src++;
          left--;
          fill++;
        }

      group[fill] = 0;

      /* size */
      SKIP_SPACES;
      fill = 0;

      while (left && *src && *src != ' ' && fill < sizeof(size) - 1)
        {
          size[fill] = *src;
          src++;
          left--;
          fill++;
        }

      size[fill] = 0;

      /* date */
      SKIP_SPACES;
      fill = 0;

      while (left && *src && fill < sizeof(date) - 1)
        {
          date[fill] = *src;
          src++;
          left--;
          fill++;
        }

      date[fill] = 0;

      SKIP_SPACES;
      filelen = 0;

      filename = src;

      while (left && *src && *src != ' ' && *src != '\n' && *src != '\r')
        {
          src++;
          filelen++;
          left--;
        }

      if (filelen > 0)
        {
          if (perm[0] == 'd')
            {
              filename[filelen] = '/';
              filelen++;
            }

          /* FIXME: escape HTML output properly */
          g_string_sprintfa(http_response,
                            "<A HREF=\"%.*s\">%.*s</A>   %.*s  %s  %12s bytes\n",
                            filelen, filename,
                            MIN(filelen, HTTP_FTP_FILE_WIDTH), filename,
                            MAX(HTTP_FTP_FILE_WIDTH - filelen, 0), spaces + filelen % 3,
                            date, size);
        }

      file_count++;
    }

  if (file_count >= HTTP_FTP_MAX_FILES)
    {
      g_string_sprintfa(http_response, "Too many files, listing truncated (limit is %d)\n", HTTP_FTP_MAX_FILES);
    }

  g_string_sprintfa(http_response, "</PRE></BODY></HTML>");

  z_stream_unref(list);
  return TRUE;
}

#undef  SKIP_SPACES

/**
 * http_handle_ftp_request:
 * @self: HttpProxy instance
 *
 * Handle an incoming non-transparent FTP request if permit_ftp_over_http is
 * enabled. Connects to the upstream server using the FTP protocol.
 **/
gboolean
http_handle_ftp_request(HttpProxy *self)
{
  gint status;
  gchar response_msg[1024];
  const gchar *user, *passwd, *fetch_fname = NULL;
  gchar *slash;
  gint fetch = HTTP_FTP_FETCH_DIR;
  ZStream *data_stream;
  gboolean res = TRUE;

  http_clear_headers(&self->headers[EP_SERVER]);
  self->connection_hdr = http_add_header(&self->headers[EP_SERVER], "Proxy-Connection", 16, "close", 5);
  self->connection_hdr->present = TRUE;

  if (!http_connect_server(self))
    {
      return FALSE;
    }

  if (!http_ftp_fetch_response(self, &status, response_msg, sizeof(response_msg)))
    {
      return FALSE;
    }

  if (status != 220)
    {
      /*LOG
        This message indicates that the FTP server greeted Zorp with a
        non-220 response code.
      */
      z_proxy_log(self, HTTP_ERROR, 3, "Error in FTP greeting, expecting 220; ftp_status='%d'", status);
    }

  if (self->request_url_parts.user->len)
    {
      user = self->request_url_parts.user->str;
      passwd = self->request_url_parts.passwd->str;
    }
  else
    {
      user = "anonymous";
      passwd = "ftp@";
    }

  if (!http_ftp_login(self, user, passwd))
    {
      return FALSE;
    }

  /* check whether request_url_parts.file ends with '/'  => directory                      */
  /* try to cd into request_url_parts.file, if successful => directory                     */
  /* try to cd into path of request_url_parts.file, if successful => file, if not => error */
  /* if file => fetch file, if directory => fetch and format dirlist */

  if (self->request_url_parts.file->str[self->request_url_parts.file->len - 1] == '/')
    {
      fetch = HTTP_FTP_FETCH_DIR;
      slash = &self->request_url_parts.file->str[self->request_url_parts.file->len - 1];

      if (slash != self->request_url_parts.file->str)
        *slash = '\0';

      if (!http_ftp_setcwd(self, self->request_url_parts.file->str))
        {
          *slash = '/';
          g_string_sprintf(self->error_info, "Directory not found on server. (%s)", self->request_url_parts.file->str);
          /*LOG
            This message indicates that the directory specified in the URL
            was not accepted by the FTP server.
          */
          z_proxy_log(self, HTTP_ERROR, 4, "Error changing directory on server; dir='%s'", self->request_url_parts.file->str);
          return FALSE;
        }

      *slash = '/';
    }
  else
    {
      if (http_ftp_setcwd(self, self->request_url_parts.file->str))
        {
          const gchar *reason;
          GString *url = g_string_sized_new(0);

          g_string_append_c(self->request_url_parts.file, '/');

          if (!http_format_url(&self->request_url_parts, url, TRUE, FALSE, TRUE, &reason))
            {
              g_string_assign(url, "Error formatting URL");
            }

          self->error_code = HTTP_MSG_REDIRECT;
          self->error_status = 301;
          g_string_assign(self->error_info, url->str);
          g_string_sprintf(self->error_msg, "Redirect");

          /* NOTE: url is properly escaped, as it was canonicalized by
           * http_format_url above */

          g_string_sprintfa(self->error_headers, "Location: %s\r\n", url->str);
          g_string_free(url, TRUE);
          return FALSE;
        }
      else
        {
          slash = strrchr(self->request_url_parts.file->str, '/');

          if (!slash)
            {
              /* no backslash (or only first backslash present), we already tried to CD into this */
              g_string_sprintf(self->error_info, "Directory not found on server. (%s)", self->request_url_parts.file->str);
              /*LOG
                This message indicates that the directory specified in the URL
                was not accepted by the FTP server.
              */
              z_proxy_log(self, HTTP_ERROR, 4, "Error changing directory on server; dir='%s'", self->request_url_parts.file->str);
              return FALSE;
            }

          if (slash != self->request_url_parts.file->str)
            {
              *slash = '\0';

              if (!http_ftp_setcwd(self, self->request_url_parts.file->str))
                {
                  g_string_sprintf(self->error_info, "Directory not found on server. (%s)", self->request_url_parts.file->str);
                  /*LOG
                    This message indicates that the directory specified in the URL
                    was not accepted by the FTP server.
                  */
                  z_proxy_log(self, HTTP_ERROR, 4, "Error changing directory on server; dir='%s'", self->request_url_parts.file->str);
                  *slash = '/';
                  return FALSE;
                }

              *slash = '/';
            }

          fetch = HTTP_FTP_FETCH_FILE;
          fetch_fname = slash + 1;
        }
    }

  if (!http_ftp_set_type(self, (fetch == HTTP_FTP_FETCH_FILE) ? "I" : "A"))
    {
      z_proxy_log(self, HTTP_ERROR, 4, "Error setting FTP transfer type;");
      g_string_assign(self->error_info, "Error setting FTP transfer type.");
      return FALSE;
    }

  /* ok, everything ok, set up data channel */

  if (!http_ftp_initiate_passive_data(self) &&
      !http_ftp_initiate_active_data(self))
    {
      return FALSE;
    }

  g_string_sprintf(self->error_info, "Error downloading file %s", self->request_url->str);

  /* fetch file pointed to by fetch_fname */
  if (!http_ftp_send_command(self, fetch == HTTP_FTP_FETCH_FILE ? "RETR" : "LIST", fetch == HTTP_FTP_FETCH_FILE ? fetch_fname : NULL))
    {
      /*LOG
        This message indicates that an I/O error occurred while sending our
        RETR or LIST request.
      */
      z_proxy_log(self, HTTP_ERROR, 4, "Error sending RETR or LIST command to FTP server;");
      http_ftp_cleanup_data(self);
      return FALSE;
    }

  if (!http_ftp_complete_data(self, &data_stream))
    {
      /*LOG
        This message indicates that an I/O error occurred while trying to
        establish the data connection with the FTP server.
      */
      z_proxy_log(self, HTTP_ERROR, 4, "Error establishing data connection to FTP server;");
      return FALSE;
    }

  if (!http_ftp_fetch_response(self, &status, response_msg, sizeof(response_msg)) || (status != 150 && status != 125))
    {
      /* file was successfully downloaded, finished */
      z_stream_shutdown(data_stream, SHUT_RDWR, NULL);
      z_stream_close(data_stream, NULL);
      z_stream_unref(data_stream);
      g_string_sprintf(self->error_info, "FTP error: %d %s", status, response_msg);
      /*LOG
        This message indicates that the FTP server did not react with a
        150 response after the data connection was established.
      */
      z_proxy_log(self, HTTP_ERROR, 4, "Error reading data channel confirmation from FTP server; status='%d', response='%s'", status, response_msg);
      return FALSE;
    }

  if (fetch == HTTP_FTP_FETCH_FILE)
    {
      if (!http_data_transfer(self, HTTP_TRANSFER_NORMAL, EP_SERVER, data_stream, EP_CLIENT, self->super.endpoints[EP_CLIENT], TRUE, FALSE, http_ftp_format_response))
        {
          /* file was not successfully downloaded, finished */
          res = FALSE;
        }
    }
  else
    {
      GString *http_response = g_string_sized_new(128);

      g_string_sprintf(http_response, "HTTP/1.0 200 OK\r\n"
                       "Content-Type: text/html\r\n"
                       "Proxy-Connection: close\r\n\r\n");

      if (!http_ftp_htmlize_listing(self, data_stream, http_response))
        {
          res = FALSE;
        }
      else
        {
          if (!http_write(self, EP_CLIENT, http_response->str, http_response->len))
            res = FALSE;
        }

      g_string_free(http_response, TRUE);
    }

  /* file was successfully downloaded, finished */
  z_stream_shutdown(data_stream, SHUT_RDWR, NULL);
  z_stream_close(data_stream, NULL);
  z_stream_unref(data_stream);

  if (res)
    {
      status = 999;

      if (!http_ftp_fetch_response(self, &status, response_msg, sizeof(response_msg)) || status != 226)
        {
          /*LOG
            This message indicates that the FTP server did not return with
            an appropriate status code at the end of the data transfer.
          */
          z_proxy_log(self, HTTP_ERROR, 4, "Error reading 226 from FTP server; status='%d', response='%s'", status, response_msg);
          /* we already sent the page, thus we may not return FALSE here */
        }
    }

  return res;
}
