/*
 *  OpenVPN -- An application to securely tunnel IP networks
 *             over a single TCP/UDP port, with support for SSL/TLS-based
 *             session authentication and key exchange,
 *             packet encryption, packet authentication, and
 *             packet compression.
 *
 *  Copyright (C) 2002-2010 OpenVPN Technologies, Inc. <sales@openvpn.net>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2
 *  as published by the Free Software Foundation.
 *
 *  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 (see the file COPYING included with this
 *  distribution); if not, write to the Free Software Foundation, Inc.,
 *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#elif defined(_MSC_VER)
#include "config-msvc.h"
#endif

#include "syshead.h"

#include "push.h"
#include "options.h"
#include "ssl.h"
#include "ssl_verify.h"
#include "manage.h"

#include "memdbg.h"

#if P2MP

/**
 * Add an option to the push list by providing a format string.
 *
 * The string added to the push options is allocated in o->gc, so the caller
 * does not have to preserve anything.
 *
 * @param o		The current connection's options
 * @param msglevel	The message level to use when printing errors
 * @param fmt		Format string for the option
 * @param ...		Format string arguments
 *
 * @return true on success, false on failure.
 */
static bool push_option_fmt(struct options *o, int msglevel,
    const char *fmt, ...)
#ifdef __GNUC__
#if __USE_MINGW_ANSI_STDIO
    __attribute__ ((format (gnu_printf, 3, 4)))
#else
    __attribute__ ((format (__printf__, 3, 4)))
#endif
#endif
    ;

/*
 * Auth username/password
 *
 * Client received an authentication failed message from server.
 * Runs on client.
 */
void
receive_auth_failed (struct context *c, const struct buffer *buffer)
{
  msg (M_VERB0, "AUTH: Received control message: %s", BSTR(buffer));
  c->options.no_advance=true;

  if (c->options.pull)
    {
      switch (auth_retry_get ())
	{
	case AR_NONE:
	  c->sig->signal_received = SIGTERM; /* SOFT-SIGTERM -- Auth failure error */
	  break;
	case AR_INTERACT:
	  ssl_purge_auth (false);
	case AR_NOINTERACT:
	  c->sig->signal_received = SIGUSR1; /* SOFT-SIGUSR1 -- Auth failure error */
	  break;
	default:
	  ASSERT (0);
	}
      c->sig->signal_text = "auth-failure";
#ifdef ENABLE_MANAGEMENT
      if (management)
	{
	  const char *reason = NULL;
	  struct buffer buf = *buffer;
	  if (buf_string_compare_advance (&buf, "AUTH_FAILED,") && BLEN (&buf))
	    reason = BSTR (&buf);
	  management_auth_failure (management, UP_TYPE_AUTH, reason);
	}
#endif
      /*
       * Save the dynamic-challenge text even when management is defined
       */
	{
#ifdef ENABLE_CLIENT_CR
	  struct buffer buf = *buffer;
	  if (buf_string_match_head_str (&buf, "AUTH_FAILED,CRV1:") && BLEN (&buf))
	    {
	      buf_advance (&buf, 12); /* Length of "AUTH_FAILED," substring */
	      ssl_put_auth_challenge (BSTR (&buf));
	    }
#endif
	}
    }
}

/*
 * Act on received restart message from server
 */
void
server_pushed_signal (struct context *c, const struct buffer *buffer, const bool restart, const int adv)
{
  if (c->options.pull)
    {
      struct buffer buf = *buffer;
      const char *m = "";
      if (buf_advance (&buf, adv) && buf_read_u8 (&buf) == ',' && BLEN (&buf))
	m = BSTR (&buf);

      /* preserve cached passwords? */
      /* advance to next server? */
      {
	bool purge = true;

	if (m[0] == '[')
	  {
	    int i;
	    for (i = 1; m[i] != '\0' && m[i] != ']'; ++i)
	      {
		if (m[i] == 'P')
		  purge = false;
		else if (m[i] == 'N')
		  {
		    /* next server? */
		    c->options.no_advance = false;
		  }
	      }
	  }
	if (purge)
	  ssl_purge_auth (true);
      }

      if (restart)
	{
	  msg (D_STREAM_ERRORS, "Connection reset command was pushed by server ('%s')", m);
	  c->sig->signal_received = SIGUSR1; /* SOFT-SIGUSR1 -- server-pushed connection reset */
	  c->sig->signal_text = "server-pushed-connection-reset";
	}
      else
	{
	  msg (D_STREAM_ERRORS, "Halt command was pushed by server ('%s')", m);
	  c->sig->signal_received = SIGTERM; /* SOFT-SIGTERM -- server-pushed halt */
	  c->sig->signal_text = "server-pushed-halt";
	}
#ifdef ENABLE_MANAGEMENT
      if (management)
	management_notify (management, "info", c->sig->signal_text, m);
#endif
    }
}

#if P2MP_SERVER

/*
 * Send auth failed message from server to client.
 */
void
send_auth_failed (struct context *c, const char *client_reason)
{
  struct gc_arena gc = gc_new ();
  static const char auth_failed[] = "AUTH_FAILED";
  size_t len;

  schedule_exit (c, c->options.scheduled_exit_interval, SIGTERM);

  len = (client_reason ? strlen(client_reason)+1 : 0) + sizeof(auth_failed);
  if (len > PUSH_BUNDLE_SIZE)
    len = PUSH_BUNDLE_SIZE;

  {
    struct buffer buf = alloc_buf_gc (len, &gc);
    buf_printf (&buf, auth_failed);
    if (client_reason)
      buf_printf (&buf, ",%s", client_reason);
    send_control_channel_string (c, BSTR (&buf), D_PUSH);
  }

  gc_free (&gc);
}

/*
 * Send restart message from server to client.
 */
void
send_restart (struct context *c, const char *kill_msg)
{
  schedule_exit (c, c->options.scheduled_exit_interval, SIGTERM);
  send_control_channel_string (c, kill_msg ? kill_msg : "RESTART", D_PUSH);
}

#endif

/*
 * Push/Pull
 */

void
incoming_push_message (struct context *c, const struct buffer *buffer)
{
  struct gc_arena gc = gc_new ();
  unsigned int option_types_found = 0;
  int status;

  msg (D_PUSH, "PUSH: Received control message: '%s'", sanitize_control_message(BSTR(buffer), &gc));

  status = process_incoming_push_msg (c,
				      buffer,
				      c->options.pull,
				      pull_permission_mask (c),
				      &option_types_found);

  if (status == PUSH_MSG_ERROR)
    msg (D_PUSH_ERRORS, "WARNING: Received bad push/pull message: %s", sanitize_control_message(BSTR(buffer), &gc));
  else if (status == PUSH_MSG_REPLY || status == PUSH_MSG_CONTINUATION)
    {
      c->options.push_option_types_found |= option_types_found;

      /* delay bringing tun/tap up until --push parms received from remote */
      if (status == PUSH_MSG_REPLY)
	{
	  if (!do_up (c, true, c->options.push_option_types_found))
	    {
	      msg (D_PUSH_ERRORS, "Failed to open tun/tap interface");
	      goto error;
	    }
	}
      event_timeout_clear (&c->c2.push_request_interval);
    }
  else if (status == PUSH_MSG_REQUEST)
    {
      if (c->options.mode == MODE_SERVER)
	{
	  struct tls_session *session = &c->c2.tls_multi->session[TM_ACTIVE];
	  /* Do not regenerate keys if client send a second push request */
	  if (!session->key[KS_PRIMARY].crypto_options.key_ctx_bi.initialized &&
	      !tls_session_update_crypto_params (session, &c->options,
		  &c->c2.frame))
	    {
	      msg (D_TLS_ERRORS, "TLS Error: server generate_key_expansion failed");
	      goto error;
	    }
	}
    }

  goto cleanup;
error:
  register_signal (c, SIGUSR1, "process-push-msg-failed");
cleanup:
  gc_free (&gc);
}

bool
send_push_request (struct context *c)
{
  const int max_push_requests = c->options.handshake_window / PUSH_REQUEST_INTERVAL;
  if (++c->c2.n_sent_push_requests <= max_push_requests)
    {
      return send_control_channel_string (c, "PUSH_REQUEST", D_PUSH);
    }
  else
    {
      msg (D_STREAM_ERRORS, "No reply from server after sending %d push requests", max_push_requests);
      c->sig->signal_received = SIGUSR1; /* SOFT-SIGUSR1 -- server-pushed connection reset */
      c->sig->signal_text = "no-push-reply";
      return false;
    }
}

#if P2MP_SERVER

/**
 * Prepare push options, based on local options and available peer info.
 *
 * @param options	Connection options
 * @param tls_multi	TLS state structure for the current tunnel
 *
 * @return true on success, false on failure.
 */
static bool
prepare_push_reply (struct options *o, struct tls_multi *tls_multi)
{
  const char *optstr = NULL;
  const char * const peer_info = tls_multi->peer_info;

  /* Send peer-id if client supports it */
  optstr = peer_info ? strstr(peer_info, "IV_PROTO=") : NULL;
  if (optstr)
    {
      int proto = 0;
      int r = sscanf(optstr, "IV_PROTO=%d", &proto);
      if ((r == 1) && (proto >= 2))
	{
	  push_remove_option(o, "peer-id");
	  push_option_fmt(o, M_USAGE, "peer-id %d", tls_multi->peer_id);
	}
    }

  /* Push cipher if client supports Negotiable Crypto Parameters */
  if (tls_peer_info_ncp_ver (peer_info) >= 2 && o->ncp_enabled)
    {
      /* if we have already created our key, we cannot change our own
       * cipher, so disable NCP and warn = explain why
       */
      struct tls_session *session = &tls_multi->session[TM_ACTIVE];
      if ( session->key[KS_PRIMARY].crypto_options.key_ctx_bi.initialized )
	{
	   msg( M_INFO, "PUSH: client wants to negotiate cipher (NCP), but "
			"server has already generated data channel keys, "
			"ignoring client request" );
	}
      else
	{
	  /* Push the first cipher from --ncp-ciphers to the client.
	   * TODO: actual negotiation, instead of server dictatorship. */
	  char *push_cipher = string_alloc(o->ncp_ciphers, &o->gc);
	  o->ciphername = strtok (push_cipher, ":");
	  push_remove_option(o, "cipher");
	  push_option_fmt(o, M_USAGE, "cipher %s", o->ciphername);
	}
    }
  return true;
}

static bool
send_push_reply (struct context *c)
{
  struct gc_arena gc = gc_new ();
  struct buffer buf = alloc_buf_gc (PUSH_BUNDLE_SIZE, &gc);
  struct push_entry *e = c->options.push_list.head;
  bool multi_push = false;
  static char cmd[] = "PUSH_REPLY";
  const int extra = 84; /* extra space for possible trailing ifconfig and push-continuation */
  const int safe_cap = BCAP (&buf) - extra;
  bool push_sent = false;

  msg( M_INFO, "send_push_reply(): safe_cap=%d", safe_cap );

  buf_printf (&buf, "%s", cmd);

  if ( c->c2.push_ifconfig_ipv6_defined &&
          !c->options.push_ifconfig_ipv6_blocked )
    {
      /* IPv6 is put into buffer first, could be lengthy */
      buf_printf( &buf, ",ifconfig-ipv6 %s/%d %s",
		    print_in6_addr( c->c2.push_ifconfig_ipv6_local, 0, &gc),
		    c->c2.push_ifconfig_ipv6_netbits,
		    print_in6_addr( c->c2.push_ifconfig_ipv6_remote, 0, &gc) );
      if (BLEN (&buf) >= safe_cap)
	{
	  msg (M_WARN, "--push ifconfig-ipv6 option is too long");
	  goto fail;
	}
    }

  while (e)
    {
      if (e->enable)
	{
	  const int l = strlen (e->option);
	  if (BLEN (&buf) + l >= safe_cap)
	    {
	      buf_printf (&buf, ",push-continuation 2");
	      {
		const bool status = send_control_channel_string (c, BSTR (&buf), D_PUSH);
		if (!status)
		  goto fail;
		push_sent = true;
		multi_push = true;
		buf_reset_len (&buf);
		buf_printf (&buf, "%s", cmd);
	      }
	    }
	  if (BLEN (&buf) + l >= safe_cap)
	    {
	      msg (M_WARN, "--push option is too long");
	      goto fail;
	    }
	  buf_printf (&buf, ",%s", e->option);
	}
      e = e->next;
    }

  if (c->c2.push_ifconfig_defined && c->c2.push_ifconfig_local && c->c2.push_ifconfig_remote_netmask)
    {
      in_addr_t ifconfig_local = c->c2.push_ifconfig_local;
      if (c->c2.push_ifconfig_local_alias)
	ifconfig_local = c->c2.push_ifconfig_local_alias;
      buf_printf (&buf, ",ifconfig %s %s",
		  print_in_addr_t (ifconfig_local, 0, &gc),
		  print_in_addr_t (c->c2.push_ifconfig_remote_netmask, 0, &gc));
    }
  if (multi_push)
    buf_printf (&buf, ",push-continuation 1");

  if (BLEN (&buf) > sizeof(cmd)-1)
    {
      const bool status = send_control_channel_string (c, BSTR (&buf), D_PUSH);
      if (!status)
        goto fail;
      push_sent = true;
    }

  /* If nothing have been pushed, send an empty push,
   * as the client is expecting a response
   */
  if (!push_sent)
    {
      bool status = false;

      buf_reset_len (&buf);
      buf_printf (&buf, "%s", cmd);
      status = send_control_channel_string (c, BSTR(&buf), D_PUSH);
      if (!status)
	goto fail;
    }

  gc_free (&gc);
  return true;

 fail:
  gc_free (&gc);
  return false;
}

static void
push_option_ex (struct options *o, const char *opt, bool enable, int msglevel)
{
  if (!string_class (opt, CC_ANY, CC_COMMA))
    {
      msg (msglevel, "PUSH OPTION FAILED (illegal comma (',') in string): '%s'", opt);
    }
  else
    {
      struct push_entry *e;
      ALLOC_OBJ_CLEAR_GC (e, struct push_entry, &o->gc);
      e->enable = true;
      e->option = opt;
      if (o->push_list.head)
	{
	  ASSERT(o->push_list.tail);
	  o->push_list.tail->next = e;
	  o->push_list.tail = e;
	}
      else
	{
	  ASSERT(!o->push_list.tail);
	  o->push_list.head = e;
	  o->push_list.tail = e;
	}
    }
}

void
push_option (struct options *o, const char *opt, int msglevel)
{
  push_option_ex (o, opt, true, msglevel);
}

void
clone_push_list (struct options *o)
{
  if (o->push_list.head)
    {
      const struct push_entry *e = o->push_list.head;
      push_reset (o);
      while (e)
	{
	  push_option_ex (o, string_alloc (e->option, &o->gc), true, M_FATAL);
	  e = e->next;
	}
    }
}

void
push_options (struct options *o, char **p, int msglevel, struct gc_arena *gc)
{
  const char **argv = make_extended_arg_array (p, gc);
  char *opt = print_argv (argv, gc, 0);
  push_option (o, opt, msglevel);
}

static bool push_option_fmt(struct options *o, int msglevel,
    const char *format, ...)
{
  va_list arglist;
  char tmp[256] = {0};
  int len = -1;
  va_start (arglist, format);
  len = vsnprintf (tmp, sizeof(tmp), format, arglist);
  va_end (arglist);
  if (len > sizeof(tmp)-1)
    return false;
  push_option (o, string_alloc (tmp, &o->gc), msglevel);
  return true;
}

void
push_reset (struct options *o)
{
  CLEAR (o->push_list);
}

void
push_remove_option (struct options *o, const char *p)
{
  msg (D_PUSH_DEBUG, "PUSH_REMOVE searching for: '%s'", p);

  /* ifconfig-ipv6 is special, as not part of the push list */
  if ( streq( p, "ifconfig-ipv6" ))
    {
      o->push_ifconfig_ipv6_blocked = true;
      return;
    }

  if (o && o->push_list.head )
    {
      struct push_entry *e = o->push_list.head;

      /* cycle through the push list */
      while (e)
	{
	  if ( e->enable &&
               strncmp( e->option, p, strlen(p) ) == 0 )
	    {
	      msg (D_PUSH_DEBUG, "PUSH_REMOVE removing: '%s'", e->option);
	      e->enable = false;
	    }

	  e = e->next;
	}
    }
}
#endif

#if P2MP_SERVER
int
process_incoming_push_request (struct context *c)
{
  int ret = PUSH_MSG_ERROR;

#ifdef ENABLE_ASYNC_PUSH
  c->c2.push_request_received = true;
#endif
  if (tls_authentication_status (c->c2.tls_multi, 0) == TLS_AUTHENTICATION_FAILED || c->c2.context_auth == CAS_FAILED)
    {
      const char *client_reason = tls_client_reason (c->c2.tls_multi);
      send_auth_failed (c, client_reason);
      ret = PUSH_MSG_AUTH_FAILURE;
    }
  else if (!c->c2.push_reply_deferred && c->c2.context_auth == CAS_SUCCEEDED)
    {
      time_t now;

      openvpn_time (&now);
      if (c->c2.sent_push_reply_expiry > now)
	{
	  ret = PUSH_MSG_ALREADY_REPLIED;
	}
      else
	{
	  if (prepare_push_reply(&c->options, c->c2.tls_multi) &&
	      send_push_reply (c))
	    {
	      ret = PUSH_MSG_REQUEST;
	      c->c2.sent_push_reply_expiry = now + 30;
	    }
	}
    }
  else
    {
      ret = PUSH_MSG_REQUEST_DEFERRED;
    }

  return ret;
}
#endif

static void
push_update_digest(md_ctx_t *ctx, struct buffer *buf)
{
  char line[OPTION_PARM_SIZE];
  while (buf_parse (buf, ',', line, sizeof (line)))
    {
      /* peer-id might change on restart and this should not trigger reopening tun */
      if (strstr (line, "peer-id ") != line)
	{
	  md_ctx_update (ctx, (const uint8_t *) line, strlen(line));
	}
    }
}

int
process_incoming_push_msg (struct context *c,
			   const struct buffer *buffer,
			   bool honor_received_options,
			   unsigned int permission_mask,
			   unsigned int *option_types_found)
{
  int ret = PUSH_MSG_ERROR;
  struct buffer buf = *buffer;

#if P2MP_SERVER
  if (buf_string_compare_advance (&buf, "PUSH_REQUEST"))
    {
      ret = process_incoming_push_request(c);
    }
  else
#endif

  if (honor_received_options && buf_string_compare_advance (&buf, "PUSH_REPLY"))
    {
      const uint8_t ch = buf_read_u8 (&buf);
      if (ch == ',')
	{
	  struct buffer buf_orig = buf;
	  if (!c->c2.pulled_options_md5_init_done)
	    {
	      md_ctx_init(&c->c2.pulled_options_state, md_kt_get("MD5"));
	      c->c2.pulled_options_md5_init_done = true;
	    }
	  if (!c->c2.did_pre_pull_restore)
	    {
	      pre_pull_restore (&c->options, &c->c2.gc);
	      c->c2.did_pre_pull_restore = true;
	    }
	  if (apply_push_options (&c->options,
				  &buf,
				  permission_mask,
				  option_types_found,
				  c->c2.es))
	    {
	      push_update_digest (&c->c2.pulled_options_state, &buf_orig);
	      switch (c->options.push_continuation)
		{
		  case 0:
		  case 1:
		    md_ctx_final (&c->c2.pulled_options_state, c->c2.pulled_options_digest.digest);
		    md_ctx_cleanup (&c->c2.pulled_options_state);
		    c->c2.pulled_options_md5_init_done = false;
		    ret = PUSH_MSG_REPLY;
		    break;
		  case 2:
		    ret = PUSH_MSG_CONTINUATION;
		    break;
		}
	    }
	}
      else if (ch == '\0')
	{
	  ret = PUSH_MSG_REPLY;
	}
      /* show_settings (&c->options); */
    }
  return ret;
}

#if P2MP_SERVER

/*
 * Remove iroutes from the push_list.
 */
void
remove_iroutes_from_push_route_list (struct options *o)
{
  if (o && o->push_list.head && o->iroutes)
    {
      struct gc_arena gc = gc_new ();
      struct push_entry *e = o->push_list.head;

      /* cycle through the push list */
      while (e)
	{
	  char *p[MAX_PARMS];
	  bool enable = true;

	  /* parse the push item */
	  CLEAR (p);
	  if ( e->enable &&
               parse_line (e->option, p, SIZE (p), "[PUSH_ROUTE_REMOVE]", 1, D_ROUTE_DEBUG, &gc))
	    {
	      /* is the push item a route directive? */
	      if (p[0] && !strcmp (p[0], "route") && !p[3])
		{
		  /* get route parameters */
		  bool status1, status2;
		  const in_addr_t network = getaddr (GETADDR_HOST_ORDER, p[1], 0, &status1, NULL);
		  const in_addr_t netmask = getaddr (GETADDR_HOST_ORDER, p[2] ? p[2] : "255.255.255.255", 0, &status2, NULL);

		  /* did route parameters parse correctly? */
		  if (status1 && status2)
		    {
		      const struct iroute *ir;

		      /* does route match an iroute? */
		      for (ir = o->iroutes; ir != NULL; ir = ir->next)
			{
			  if (network == ir->network && netmask == netbits_to_netmask (ir->netbits >= 0 ? ir->netbits : 32))
			    {
			      enable = false;
			      break;
			    }
			}
		    }
		}

	      /* should we copy the push item? */
	      e->enable = enable;
	      if (!enable)
		msg (D_PUSH, "REMOVE PUSH ROUTE: '%s'", e->option);
	    }

	  e = e->next;
	}

      gc_free (&gc);
    }
}

#endif

#endif