/*

Copyright (C) 2015-2019 Olaf Till <i7tiol@t-online.de>

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 3 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, see <http://www.gnu.org/licenses/>.

*/

#include "parallel-gnutls.h"

#define BUFLEN 2048

volatile sig_atomic_t octave_parallel_interrupted = 0;

bool octave_parallel_assert_maybe_inthandler (void)
{
  static octave_parallel_maybe_inthandler maybe_inthandler;

  return maybe_inthandler.good ();
}

void octave_parallel_inthandler (int signal)
{
  octave_parallel_interrupted = 1;
}

octave_parallel_socket_streambuf::
octave_parallel_socket_streambuf (int sid, bool server,
                                  encryption_type encrtype)
{
  fid = sid;

  if (fid == -1)
    {
      store_errno = -1;

      _p_error ("no valid socket");
    }
  else
    {
      readable = writeable = true;

      uint32_t info = 0;

      if (server)
        {
          info = encrtype << 16;

          info |= PROTOCOL_VERSION;

          network_send_4byteint (info, true);

          if (network_recv_4byteint (info))
            _p_error ("could not read client response to info");
          else
            {
              if (info != 0)
                {
                  store_errno = -1;

                  _p_error ("client rejected connection because its connection type is %u and its protocol version is %u",
                            info >> 16, info & 0xFFFF);
                }
            }
        }
      else // client
        {
          bool err = false;

          if (network_recv_4byteint (info))
            _p_error ("could not read server info");
          else
            {
              if (info >> 16 != encrtype)
                {
                  err = true;

                  _p_error ("server uses connection type %u, we use %u",
                            info >> 16, encrtype);
                }
              if ((info & 0xFFFF) != PROTOCOL_VERSION)
                {
                  err = true;

                  _p_error ("server uses protocol version %u, we use %u",
                            info & 0xFFFF, PROTOCOL_VERSION);
                }
            }

          if (err)
            {
              info = encrtype << 16;

              info |= PROTOCOL_VERSION;
            }
          else
            info = 0;

          network_send_4byteint (info, true);

          if (err)
            store_errno = -1;
        }
    }
}

std::streamsize octave_parallel_socket_streambuf::showmanyc (void)
{
  if (! good () || fid == -1)
    return -1;

  std::streamsize ret;

  if ((ret = gnulib_pollin_1_noblock (fid)) < 0)
    store_errno = -1;

  return ret;
}

#ifdef HAVE_LIBGNUTLS

// Username was given as argument to 'connect'. appasswd is optional.
octave_parallel_gnutls_srp_client_credentials::
octave_parallel_gnutls_srp_client_credentials (const char *user,
                                               char *apasswd)
: password (apasswd), pw_allocated (false)
{
  if (! password)
    password = gnulib_getpass ("password: ");

  if (! strlen (password))
    {
      _p_error ("password is empty");

      return;
    }

  if (! gnutls_srp_allocate_client_credentials
      ((gnutls_srp_client_credentials_t *) &cred))
    if (gnutls_srp_set_client_credentials
        ((gnutls_srp_client_credentials_t) cred, user, password))
      {
        free_credentials ();

        cred = NULL;
      }
}

// no username was given as argument to 'connect'
octave_parallel_gnutls_srp_client_credentials::
octave_parallel_gnutls_srp_client_credentials (std::string passwd_file)
  : password (NULL), pw_allocated (false)
{
  struct __bufguard
  {
    char *__b;
    __bufguard (char *__ib) : __b (__ib) { }
    ~__bufguard (void) { memset ((void *) __b, '\0', BUFLEN); delete[] __b; }
    char *__get (void) { return __b; }
  } __bg (new char[BUFLEN]);

  char *line = __bg.__get ();

  line[0] = '\0';

  octave_parallel_stream cpi
    (new octave_parallel_file_streambuf (passwd_file.c_str (), O_RDONLY, 0));

  if (! cpi.good ())
    {
      _p_error ("could not open password file");

      return;
    }

  cpi.get_istream ().getline (line, BUFLEN);

  int len = strlen (line);

  if (cpi.get_istream ().fail () && len == BUFLEN - 1)
    {
      _p_error ("line in password file too long");

      return;
    }

  char *p;

  if (! (p = strchr (line, ' ')))
    {
      _p_error ("invalid line in password file");

      return;
    }

  *p = '\0';

  if (! strlen (line))
    {
      _p_error ("no username in password file");

      return;
    }

  if (++p - line >= len)
    {
      _p_error ("no password in password file");

      return;
    }

  if (! gnutls_srp_allocate_client_credentials
      ((gnutls_srp_client_credentials_t *) &cred))
    {
      if (gnutls_srp_set_client_credentials
          ((gnutls_srp_client_credentials_t) cred, line, p))
        {
          free_credentials ();

          cred = NULL;
        }
      else
        {
          password = new char[strlen (p) + 1];

          pw_allocated = true;

          strcpy (password, p);
        }
    }
}

octave_parallel_gnutls_streambuf::octave_parallel_gnutls_streambuf
(int sid, octave_parallel_gnutls_srp_credentials *accred, bool server)
  : octave_parallel_socket_streambuf (sid, server, ENCR_TLS_SRP),
    ccred (accred), session (NULL)
{
  // check result of parent class constructor, reset if necessary
  if (! good ())
    return;
  else
    readable = writeable = false;

  dlprintf ("gnutls_steambuf ctor requests incr cred refcount\n");
  if (! (cred = ccred->get_ref ()))
    {
      store_errno = -1;

      _p_error ("no valid credentials");

      return;
    }

  if (gnutls_init (&session, server ? GNUTLS_SERVER : GNUTLS_CLIENT))
    {
      store_errno = -1;

      _p_error ("could not initialize gnutls session");

      return;
    }

  if (gnutls_priority_set_direct
      (session, "NORMAL:+SRP:+SRP-DSS:+SRP-RSA", NULL))
    {
      store_errno = -1;

      _p_error ("error in setting gnutls priorities");

      return;
    }

  if (gnutls_credentials_set (session, GNUTLS_CRD_SRP, cred))
    {
      store_errno = -1;

      _p_error ("could not set credentials");

      return;
    }

  gnutls_transport_set_ptr (session, (gnutls_transport_ptr_t) fid);

  int ret;
  ret = gnutls_handshake (session); // don't repeat interrupted handshake
  if (ret)
    {
      store_errno = -1;

      _p_error ("handshake failed");

      return;
    }

  readable = writeable = true;
}

#endif // HAVE_LIBGNUTLS
