/* Copyright (C) 2010 Sergei Golubchik and Monty Program Ab

   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; version 2 of the License.

   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-1335 USA */

#include "feedback.h"

#ifdef HAVE_NETDB_H
#include <netdb.h>
#endif

#ifdef _WIN32
#include <ws2tcpip.h>
#define addrinfo ADDRINFOA
#endif

namespace feedback {

static const uint FOR_READING= 0;
static const uint FOR_WRITING= 1;

/**
  implementation of the Url class that sends the data via HTTP POST request.

  Both http:// and https:// protocols are supported.
*/
class Url_http: public Url {
  protected:
  const LEX_STRING host, port, path;
  LEX_STRING proxy_host, proxy_port;
  my_socket fd;
  bool ssl;

  bool use_proxy()
  {
    return proxy_host.length != 0;
  }

  Url_http(LEX_STRING &url_arg, LEX_STRING &host_arg,
          LEX_STRING &port_arg, LEX_STRING &path_arg, bool ssl_arg) :
    Url(url_arg), host(host_arg), port(port_arg), path(path_arg),
    fd(INVALID_SOCKET), ssl(ssl_arg)
    {
      proxy_host.length= 0;
    }
  ~Url_http() override
  {
    my_free(host.str);
    my_free(port.str);
    my_free(path.str);
    set_proxy(0,0);
  }

  public:
  void abort() override;
  int send(const char* data, size_t data_length) override;
  int set_proxy(const char *proxy, size_t proxy_len) override
  {
    if (use_proxy())
    {
      my_free(proxy_host.str);
      my_free(proxy_port.str);
    }

    return parse_proxy_server(proxy, proxy_len, &proxy_host, &proxy_port);
  }

  friend Url* http_create(const char *url, size_t url_length);
};

/**
  create a Url_http object out of the url, if possible.

  @note
  Arbitrary limitations here.

  The url must be http[s]://hostname[:port]/path
  No username:password@ or ?script=parameters are supported.

  But it's ok. This is not a generic purpose www browser - it only needs to be
  good enough to POST the data to mariadb.org.
*/
Url* http_create(const char *url, size_t url_length)
{
  const char *s;
  LEX_STRING full_url= {const_cast<char*>(url), url_length};
  LEX_STRING host, port, path;
  bool ssl= false;

  if (is_prefix(url, "http://"))
    s= url + 7;
#ifdef HAVE_OPENSSL
  else if (is_prefix(url, "https://"))
  {
    ssl= true;
    s= url + 8;
  }
#endif
  else
    return NULL;

  for (url= s; *s && *s != ':' && *s != '/'; s++) /* no-op */;
  host.str= const_cast<char*>(url);
  host.length= s-url;

  if (*s == ':')
  {
    for (url= ++s; *s && *s >= '0' && *s <= '9'; s++) /* no-op */;
    port.str= const_cast<char*>(url);
    port.length= s-url;
  }
  else
  {
    if (ssl)
    {
      port.str= const_cast<char*>("443");
      port.length=3;
    }
    else
    {
      port.str= const_cast<char*>("80");
      port.length=2;
    }
  }

  if (*s == 0)
  {
    path.str= const_cast<char*>("/");
    path.length= 1;
  }
  else
  {
    path.str= const_cast<char*>(s);
    path.length= strlen(s);
  }
  if (!host.length || !port.length || path.str[0] != '/')
    return NULL;

  host.str= my_strndup(PSI_INSTRUMENT_ME, host.str, host.length, MYF(MY_WME));
  port.str= my_strndup(PSI_INSTRUMENT_ME, port.str, port.length, MYF(MY_WME));
  path.str= my_strndup(PSI_INSTRUMENT_ME, path.str, path.length, MYF(MY_WME));

  if (!host.str || !port.str || !path.str)
  {
    my_free(host.str);
    my_free(port.str);
    my_free(path.str);
    return NULL;
  }

  return new Url_http(full_url, host, port, path, ssl);
}

void Url_http::abort()
{
  if (fd != INVALID_SOCKET)
    closesocket(fd); // interrupt I/O waits
}

/* do the vio_write and check that all data were sent ok */
#define write_check(VIO, DATA, LEN)             \
  (vio_write((VIO), (uchar*)(DATA), (LEN)) != (LEN))

int Url_http::send(const char* data, size_t data_length)
{
  char buf[1024];
  size_t len= 0;

  addrinfo *addrs, *addr, filter= {0, AF_UNSPEC, SOCK_STREAM, 6, 0, 0, 0, 0};
  int res= use_proxy() ?
    getaddrinfo(proxy_host.str, proxy_port.str, &filter, &addrs) :
    getaddrinfo(host.str, port.str, &filter, &addrs);

  if (res)
  {
    sql_print_error("feedback plugin: getaddrinfo() failed for url '%s': %s",
                    full_url.str, gai_strerror(res));
    return 1;
  }

  DBUG_ASSERT(fd == INVALID_SOCKET);
  for (addr= addrs; addr != NULL; addr= addr->ai_next)
  {
    fd= socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
    if (fd == INVALID_SOCKET)
      continue;

    if (connect(fd, addr->ai_addr, (int) addr->ai_addrlen) == 0)
      break;

    closesocket(fd);
    fd= INVALID_SOCKET;
  }

  freeaddrinfo(addrs);

  if (fd == INVALID_SOCKET)
  {
    sql_print_error("feedback plugin: could not connect for url '%s'",
                    full_url.str);
    return 1;
  }

  Vio *vio= vio_new(fd, VIO_TYPE_TCPIP, 0);
  if (!vio)
  {
    sql_print_error("feedback plugin: vio_new failed for url '%s'",
                    full_url.str);
    closesocket(fd);
    fd= INVALID_SOCKET;
    return 1;
  }

#ifdef HAVE_OPENSSL
  struct st_VioSSLFd *UNINIT_VAR(ssl_fd);
  if (ssl)
  {
    enum enum_ssl_init_error ssl_init_error= SSL_INITERR_NOERROR;
    ulong ssl_error= 0;
    if (!(ssl_fd= new_VioSSLConnectorFd(0, 0, 0, 0, 0, &ssl_init_error, 0, 0)) ||
        sslconnect(ssl_fd, vio, send_timeout, &ssl_error))
    {
      const char *err;
      if (ssl_init_error != SSL_INITERR_NOERROR)
        err= sslGetErrString(ssl_init_error);
      else
      {
        ERR_error_string_n(ssl_error, buf, sizeof(buf));
        buf[sizeof(buf)-1]= 0;
        err= buf;
      }

      sql_print_error("feedback plugin: ssl failed for url '%s' %s",
                      full_url.str, err);
      if (ssl_fd)
        free_vio_ssl_acceptor_fd(ssl_fd);
      closesocket(fd);
      vio_delete(vio);
      fd= INVALID_SOCKET;
      return 1;
    }
  }
#endif

  static const LEX_STRING boundary= 
  { C_STRING_WITH_LEN("----------------------------ba4f3696b39f") };
  static const LEX_STRING header=
  { C_STRING_WITH_LEN("\r\n"
      "Content-Disposition: form-data; name=\"data\"; filename=\"-\"\r\n"
      "Content-Type: application/octet-stream\r\n\r\n")
  };

  len= my_snprintf(buf, sizeof(buf),
                   use_proxy() ? "POST http://%s:%s/" : "POST ",
                   host.str, port.str);

  len+= my_snprintf(buf+len, sizeof(buf)-len,
                    "%s HTTP/1.0\r\n"
                    "User-Agent: MariaDB User Feedback Plugin\r\n"
                    "Host: %s:%s\r\n"
                    "Accept: */*\r\n"
                    "Content-Length: %u\r\n"
                    "Content-Type: multipart/form-data; boundary=%s\r\n"
                    "\r\n",
                    path.str, host.str, port.str,
                    (uint)(2*boundary.length + header.length + data_length + 4),
                    boundary.str + 2);

  vio_timeout(vio, FOR_READING, send_timeout);
  vio_timeout(vio, FOR_WRITING, send_timeout);
  res = write_check(vio, buf, len)
     || write_check(vio, boundary.str, boundary.length)
     || write_check(vio, header.str, header.length)
     || write_check(vio, data, data_length)
     || write_check(vio, boundary.str, boundary.length)
     || write_check(vio, "--\r\n", 4);

  if (res)
    sql_print_error("feedback plugin: failed to send report to '%s'",
                    full_url.str);
  else
  {
    sql_print_information("feedback plugin: report to '%s' was sent",
                          full_url.str);

    /*
      if the data were send successfully, read the reply.
      Extract the first string between <h1>...</h1> tags
      and put it as a server reply into the error log.
    */
    len= 0;
    for (;;)
    {
      size_t i= sizeof(buf) - len - 1;
      if (i)
        i= vio_read(vio, (uchar*)buf + len, i);
      if ((int)i <= 0)
        break;
      len+= i;
    }
    if (len)
    {
      char *from;

      buf[len]= 0; // safety

      if ((from= strstr(buf, "<h1>")))
      {
        from+= 4;
        char *to= strstr(from, "</h1>");
        if (to)
          *to= 0;
        else
          from= NULL;
      }
      if (from)
        sql_print_information("feedback plugin: server replied '%s'", from);
      else
        sql_print_warning("feedback plugin: failed to parse server reply");
    }
    else
    {
      res= 1;
      sql_print_error("feedback plugin: failed to read server reply");
    }
  }

  vio_delete(vio);

#ifdef HAVE_OPENSSL
  if (ssl)
  {
    SSL_CTX_free(ssl_fd->ssl_context);
    my_free(ssl_fd);
  }
#endif

  fd= INVALID_SOCKET;
  return res;
}

} // namespace feedback

