//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012-2017 OpenVPN Technologies, Inc.
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License Version 3
//    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 in the COPYING file.
//    If not, see <http://www.gnu.org/licenses/>.

// Encapsulate the state of a static or dynamic authentication challenge.

#ifndef OPENVPN_AUTH_CR_H
#define OPENVPN_AUTH_CR_H

#include <string>
#include <sstream>
#include <vector>

#include <openvpn/common/exception.hpp>
#include <openvpn/common/base64.hpp>
#include <openvpn/common/split.hpp>
#include <openvpn/common/rc.hpp>
#include <openvpn/common/string.hpp>

// Static Challenge response:
//   SCRV1:<BASE64_PASSWORD>:<BASE64_RESPONSE>
//
// Dynamic Challenge:
//   CRV1:<FLAGS>:<STATE_ID>:<BASE64_USERNAME>:<CHALLENGE_TEXT>
//   FLAGS is a comma-separated list of options:
//     E -- echo
//     R -- response required
//
// Dynamic Challenge response:
//   Username: [username decoded from username_base64]
//   Password: CRV1::<STATE_ID>::<RESPONSE_TEXT>

namespace openvpn {
  class ChallengeResponse : public RC<thread_unsafe_refcount> {
  public:
    typedef RCPtr<ChallengeResponse> Ptr;

    OPENVPN_SIMPLE_EXCEPTION(dynamic_challenge_parse_error);
    OPENVPN_SIMPLE_EXCEPTION(static_challenge_parse_error);

    ChallengeResponse()
      : echo(false), response_required(false)
    {
    }

    explicit ChallengeResponse(const std::string& cookie)
      : echo(false), response_required(false)
    {
      init(cookie);
    }

    ChallengeResponse(const std::string& cookie, const std::string& user)
      : echo(false), response_required(false)
    {
      if (!is_dynamic(cookie) && cookie.find_first_of(':') == std::string::npos)
	{
	  state_id = cookie;
	  username = user;
	}
      else
	init(cookie);
    }

    void init(const std::string& cookie)
    {
      typedef std::vector<std::string> StringList;
      StringList sl;
      sl.reserve(5);
      Split::by_char_void<StringList, NullLex, Split::NullLimit>(sl, cookie, ':', 0, 4);
      if (sl.size() != 5)
	throw dynamic_challenge_parse_error();
      if (sl[0] != "CRV1")
	throw dynamic_challenge_parse_error();

      // parse options
      {
	StringList opt;
	opt.reserve(2);
	Split::by_char_void<StringList, NullLex, Split::NullLimit>(opt, sl[1], ',');
	for (StringList::const_iterator i = opt.begin(); i != opt.end(); ++i)
	  {
	    if (*i == "E")
	      echo = true;
	    else if (*i == "R")
	      response_required = true;
	  }
      }

      // save state ID
      state_id = sl[2];

      // save username
      try {
	username = base64->decode(sl[3]);
      }
      catch (const Base64::base64_decode_error&)
	{
	  throw dynamic_challenge_parse_error();
	}

      // save challenge
      challenge_text = sl[4];
    }

    static bool is_dynamic(const std::string& s)
    {
      return string::starts_with(s, "CRV1:");
    }

    static bool is_static(const std::string& s)
    {
      return string::starts_with(s, "SCRV1:");
    }

    static void validate_dynamic(const std::string& cookie)
    {
      ChallengeResponse cr(cookie);
    }

    std::string construct_dynamic_password(const std::string& response) const
    {
      std::ostringstream os;
      os << "CRV1::" << state_id << "::" << response;
      return os.str();
    }

    static std::string construct_static_password(const std::string& password,
						 const std::string& response)
    {
      std::ostringstream os;
      os << "SCRV1:" << base64->encode(password) << ':' << base64->encode(response);
      return os.str();
    }

    static void parse_static_cookie(const std::string& cookie,
				    std::string& password,
				    std::string& response)
    {
      typedef std::vector<std::string> StringList;
      StringList sl;
      sl.reserve(3);
      Split::by_char_void<StringList, NullLex, Split::NullLimit>(sl, cookie, ':');
      if (sl.size() != 3)
	throw static_challenge_parse_error();
      if (sl[0] != "SCRV1")
	throw static_challenge_parse_error();

      // get password
      try {
	password = base64->decode(sl[1]);
      }
      catch (const Base64::base64_decode_error&)
	{
	  throw static_challenge_parse_error();
	}

      // get response
      try {
	response = base64->decode(sl[2]);
      }
      catch (const Base64::base64_decode_error&)
	{
	  throw static_challenge_parse_error();
	}
    }

    static std::string generate_dynamic_challenge(const std::string& session_token,
						  const std::string& username,
						  const std::string& challenge,
						  const bool echo,
						  const bool response_required)
    {
      std::ostringstream os;
      bool comma = false;
      os << "CRV1:";
      if (echo)
	{
	  if (comma)
	    os << ",";
	  os << "E";
	  comma = true;
	}
      if (response_required)
	{
	  if (comma)
	    os << ",";
	  os << "R";
	  comma = true;
	}
      os << ':' << session_token;
      os << ':' << base64->encode(username);
      os << ':' << challenge;
      return os.str();
    }

    const std::string& get_state_id() const { return state_id; }
    const std::string& get_username() const { return username; }
    bool get_echo() const { return echo; }
    bool get_response_required() const { return response_required; }
    const std::string& get_challenge_text() const { return challenge_text; }

  private:
    bool echo;
    bool response_required;
    std::string state_id;
    std::string username;
    std::string challenge_text;
  };
}

#endif
