/* Copyright(C) 2007-2014 VoIP objects (voipobjects.com)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "ICEAuthTransaction.h"
#include "ICEStunAttributes.h"
#include "ICEMD5.h"
#include "ICELog.h"

using namespace ice;

#define LOG_SUBSYSTEM "ice"

AuthTransaction::AuthTransaction()
:Transaction(), mActive(false), mComposed(false), mConformsToKeepaliveSchedule(true),
mCredentialsEncoded(false), mErrorCode(0)
{
}

AuthTransaction::~AuthTransaction()
{
}

void AuthTransaction::init()
{
  mConformsToKeepaliveSchedule = false;
  if (mRealm.size() && mNonce.size())
    buildAuthenticatedMsg();
  else
  {
    auto msg = std::make_shared<StunMessage>();
    msg->setTransactionId(mTransactionID);
    setInitialRequest(*msg);
    mComposed = false;
    mOutgoingMsgQueue.push_back(msg);
  }
}

void AuthTransaction::buildAuthenticatedMsg()
{
  // Setup key for MessageIntegrity
  std::string key = mUsername + ":" + mRealm + ":" + mPassword;
  md5Bin(key.c_str(), key.size(), mKey);

  // Create new authenticated message
  auto newMsg = std::make_shared<StunMessage>();

  // Optional - generate new transaction ID
  // mTransactionID = StunMessage::TransactionID::GenerateNew();

  newMsg->setTransactionId( mTransactionID );

  // Add USERNAME
  newMsg->usernameAttr().setValue( mUsername );
  newMsg->realmAttr().setValue( mRealm );
  newMsg->nonceAttr().setValue( mNonce );      

  // Adjust message
  setAuthenticatedRequest( *newMsg );

  // Ensure MessageIntegrity exists
  newMsg->messageIntegrityAttr().setValue( NULL );

  // Enqueue msg
  mComposed = false;

  mOutgoingMsgQueue.clear();
  mOutgoingMsgQueue.push_back( newMsg );
}

bool AuthTransaction::processData(StunMessage& msg, NetworkAddress& address)
{
  if (msg.transactionId() != mTransactionID)
    return false;
  
  // Check for 401 error code
  if (msg.hasAttribute(StunAttribute::ErrorCode))
  {
    ErrorCode& ec = msg.errorCodeAttr();
    
    // Get realm value - it must be long term credentials
    if (ec.errorCode() == 401 && (!mCredentialsEncoded || !mLongTerm))
    {
      if (!msg.hasAttribute(StunAttribute::Realm) || !msg.hasAttribute(StunAttribute::Nonce))
        return false;
      
      Realm& realm = msg.realmAttr();
      
      // Extract realm and nonce 
      mRealm = realm.value();
      mNonce = msg.nonceAttr().value();
      
      // Change transaction id
      if (mLongTerm)
      {
        mCredentialsEncoded = true;
        generateId();
      }
      ICELogDebug(<< "Server requested long term credentials for realm " << mRealm);
      buildAuthenticatedMsg();
    }
    else
    if (ec.errorCode() == 438 && (msg.hasAttribute(StunAttribute::Realm) || msg.hasAttribute(StunAttribute::Nonce)))
    {
      if (msg.hasAttribute(StunAttribute::Realm))
        mRealm = msg.realmAttr().value();
      if (msg.hasAttribute(StunAttribute::Nonce))
        mNonce = msg.nonceAttr().value();
      
      ICELogDebug(<< "Returned error 438, using new nonce");
      if (msg.hasAttribute(StunAttribute::Realm) && msg.hasAttribute(StunAttribute::Nonce))
      {
        if (mLongTerm)
        {
          mCredentialsEncoded = true;
          generateId();
        }
      }
      buildAuthenticatedMsg();
    }  
    else
    {
      mErrorCode = ec.errorCode();
      mErrorResponse = ec.errorPhrase();
      mState = Transaction::Failed;
      processError();
      
      ICELogError(<<"Stack ID " << mStackID << ". Got error code " << mErrorCode << " for STUN transaction. Error message: " << ec.errorPhrase());
    }
  }
  else
  if (msg.messageClass() == StunMessage::ErrorClass)
  {
    mErrorCode = 0;
    mState = Transaction::Failed;
    processError();
    ICELogError(<<"Stack ID " << mStackID << ". Got ErrorClass response.");
  }
  else
  {
    ICELogDebug(<< "Process STUN success message");
    processSuccessMessage(msg, address);
    mState = Transaction::Success;
  }

  return true;
}

ByteBuffer* AuthTransaction::generateData(bool force)
{
  if (!mActive)
  {
    init();
    mActive = true;
  }

  if (!mComposed)
  {
    // Restart retransmission timer as new message will be built
    mRTOTimer.stop();
    mRTOTimer.start();
    
    // Get message from outgoing queue
    if (!mOutgoingMsgQueue.empty())
    {
      // Clear buffer for raw data
      mOutgoingData.clear();

      // Encode next message
      StunMessage& msg = *mOutgoingMsgQueue.front();
      
      //ICELogDebug(<< "Stack ID " << mStackID << ". Build message " << msg.GetTransactionID().ToString() << " from transaction " << this->GetComment());
      
      if (mShortTerm)
        msg.buildPacket(mOutgoingData, mPassword);
      else
        msg.buildPacket(mOutgoingData, std::string((char*)mKey, 16));
      
      // Remove encoded message from msg queue
      mOutgoingMsgQueue.pop_front();
    }
    else
      ICELogDebug(<< "No outgoing message in queue");
    
    mComposed = true;
  }

  return Transaction::generateData(force);
}

int AuthTransaction::errorCode()
{
  return mErrorCode;
}

std::string AuthTransaction::errorResponse()
{
  return mErrorResponse;
}

void AuthTransaction::restart()
{
  // Clear outgoing data
  mOutgoingData.clear();
  mOutgoingMsgQueue.clear();

  mState = Transaction::Running;

  // Mark "message is not built yet"
  mComposed = false;

  // Mark "first request is not sent yet"
  mActive = false;

  // Reset retransmission timer
  mRTOTimer.stop();
  mRTOTimer.start();
  mCredentialsEncoded = false;
  mConformsToKeepaliveSchedule = true;
}

bool AuthTransaction::hasToRunNow()
{
  bool result = Transaction::hasToRunNow();
  if (!result && keepalive())
  {
    result |= mState == /*TransactionState::*/Running && !mConformsToKeepaliveSchedule;
  }
  return result;
}

std::string AuthTransaction::realm() const
{
  return mRealm;
}

void AuthTransaction::setRealm(std::string realm)
{
  mRealm = realm;
}

std::string AuthTransaction::nonce() const
{
  return mNonce;
}

void AuthTransaction::setNonce(std::string nonce)
{
  mNonce = nonce;
}
