/*******************************************************************************
    OpenAirInterface
    Copyright(c) 1999 - 2014 Eurecom

    OpenAirInterface 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.


    OpenAirInterface 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 OpenAirInterface.The full GNU General Public License is
   included in this distribution in the file called "COPYING". If not,
   see <http://www.gnu.org/licenses/>.

  Contact Information
  OpenAirInterface Admin: openair_admin@eurecom.fr
  OpenAirInterface Tech : openair_tech@eurecom.fr
  OpenAirInterface Dev  : openair4g-devel@eurecom.fr

  Address      : Eurecom, Compus SophiaTech 450, route des chappes, 06451 Biot, France.

 *******************************************************************************/

/*! \file s6a_auth_info.c
 * \brief Handle an authentication information request procedure and generate the answer
 * \author Sebastien ROUX <sebastien.roux@eurecom.fr>
 * \date 2013
 * \version 0.1
 */

#include <freeDiameter/freeDiameter-host.h>
#include <freeDiameter/libfdproto.h>
#include <stdint.h>
#include <string.h>
#include <inttypes.h>
#include <pthread.h>

#include "hss_config.h"
#include "db_proto.h"
#include "s6a_proto.h"
#include "auc.h"
#include "access_restriction.h"

int s6a_auth_info_cb(struct msg **msg, struct avp *paramavp,
                     struct session *sess, void *opaque,
                     enum disp_action *act)
{
  struct msg *ans, *qry;
  struct avp *avp, *failed_avp = NULL;

  struct avp_hdr *hdr;
  union avp_value value;

  /* Database queries */
  mysql_auth_info_req_t  auth_info_req;
  mysql_auth_info_resp_t auth_info_resp;

  /* Authentication vector */
  auc_vector_t vector;

  int ret = 0;
  int result_code = ER_DIAMETER_SUCCESS;
  int experimental = 0;
  uint64_t imsi;
  uint8_t *sqn = NULL, *auts = NULL;

  if (msg == NULL) {
    return EINVAL;
  }

  /* Create answer header */
  qry = *msg;
  CHECK_FCT(fd_msg_new_answer_from_req(fd_g_config->cnf_dict, msg, 0));
  ans = *msg;

  /* Retrieving IMSI AVP: User-Name */
  CHECK_FCT(fd_msg_search_avp(qry, s6a_cnf.dataobj_s6a_imsi, &avp));

  if (avp) {
    CHECK_FCT(fd_msg_avp_hdr(avp, &hdr));

    if (hdr->avp_value->os.len > IMSI_LENGTH_MAX) {
      result_code = ER_DIAMETER_INVALID_AVP_VALUE;
      goto out;
    }

    sprintf(auth_info_req.imsi, "%*s", (int)hdr->avp_value->os.len,
            hdr->avp_value->os.data);
    sscanf(auth_info_req.imsi, "%"SCNu64, &imsi);
  } else {
    result_code = ER_DIAMETER_MISSING_AVP;
    goto out;
  }

  /* Retrieving Supported Features AVP. This is an optional AVP. */
  CHECK_FCT(fd_msg_search_avp(qry, s6a_cnf.dataobj_s6a_supported_features, &avp));

  if (avp) {
    CHECK_FCT(fd_msg_avp_hdr(avp, &hdr));
  }

  /* Retrieving the Requested-EUTRAN-Authentication-Info.
   * If this AVP is not present, we have to check for
   * Requested-GERAN-Authentication-Info AVP which will mean that the request
   * comes from RAT other than E-UTRAN, case not handled by this HSS
   * implementation.
   */
  CHECK_FCT(fd_msg_search_avp(qry, s6a_cnf.dataobj_s6a_req_e_utran_auth_info, &avp));

  if (avp) {
    struct avp *child_avp;

    /* Walk through childs avp */
    CHECK_FCT(fd_msg_browse(avp, MSG_BRW_FIRST_CHILD, &child_avp, NULL));

    while (child_avp) {
      /* Retrieve the header of the child avp */
      CHECK_FCT(fd_msg_avp_hdr(child_avp, &hdr));

      switch(hdr->avp_code) {
      case AVP_CODE_NUMBER_OF_REQ_VECTORS: {
        /* We allow only one vector request */
        if (hdr->avp_value->u32 != 1) {
          result_code = ER_DIAMETER_INVALID_AVP_VALUE;
          failed_avp = child_avp;
          goto out;
        }
      }
      break;

      case AVP_CODE_IMMEDIATE_RESP_PREF:
        /* We always respond immediately to the request */
        break;

      case AVP_CODE_RE_SYNCHRONIZATION_INFO:

        /* The resynchronization-info AVP is present.
         * AUTS = Conc(SQN MS ) || MAC-S
         */
        if (avp) {
          auts = hdr->avp_value->os.data;
        }

        break;

      default: {
        /* This AVP is not expected on s6a interface */
        result_code = ER_DIAMETER_AVP_UNSUPPORTED;
        failed_avp  = child_avp;
        goto out;
      }
      }

      /* Go to next AVP in the grouped AVP */
      CHECK_FCT(fd_msg_browse(child_avp, MSG_BRW_NEXT, &child_avp, NULL));
    }
  } else {
    CHECK_FCT(fd_msg_search_avp(qry, s6a_cnf.dataobj_s6a_req_geran_auth_info, &avp));

    if (avp) {
      result_code = DIAMETER_ERROR_RAT_NOT_ALLOWED;
      experimental = 1;
      goto out;
    } else {
      result_code = ER_DIAMETER_INVALID_AVP_VALUE;
      failed_avp = avp;
      goto out;
    }
  }

  /* Retrieving the Visited-PLMN-Id AVP */
  CHECK_FCT(fd_msg_search_avp(qry, s6a_cnf.dataobj_s6a_visited_plmn_id, &avp));

  if (avp) {
    /* TODO: check PLMN and allow/reject connectivity depending on roaming */
    CHECK_FCT(fd_msg_avp_hdr(avp, &hdr));

    if (hdr->avp_value->os.len == 3) {
      if (apply_access_restriction(auth_info_req.imsi, hdr->avp_value->os.data) != 0) {
        /* We found that user is roaming and has no right to do it ->
        * reject the connection
        */
        result_code = DIAMETER_ERROR_ROAMING_NOT_ALLOWED;
        experimental = 1;
        goto out;
      }
    } else {
      result_code = ER_DIAMETER_INVALID_AVP_VALUE;
      goto out;
    }
  } else {
    /* Mandatory AVP, raise an error if not present */
    result_code = ER_DIAMETER_MISSING_AVP;
    goto out;
  }

  /* Fetch User data */
  if (hss_mysql_auth_info(&auth_info_req, &auth_info_resp) != 0) {
    /* Database query failed... */
    result_code = DIAMETER_AUTHENTICATION_DATA_UNAVAILABLE;
    experimental = 1;
    goto out;
  }

  if (auts != NULL) {
    /* Try to derive SQN_MS from previous RAND */
    sqn = sqn_ms_derive(auth_info_resp.opc, auth_info_resp.key, auts, auth_info_resp.rand);

    if (sqn != NULL) {
      /* We succeeded to verify SQN_MS... */

      /* Pick a new RAND and store SQN_MS + RAND in the HSS */
      generate_random(vector.rand, RAND_LENGTH);
      hss_mysql_push_rand_sqn(auth_info_req.imsi, auth_info_resp.rand, sqn);

      hss_mysql_increment_sqn(auth_info_req.imsi);

      free(sqn);
    }

    /* Fetch new user data */
    if (hss_mysql_auth_info(&auth_info_req, &auth_info_resp) != 0) {
      /* Database query failed... */
      result_code = DIAMETER_AUTHENTICATION_DATA_UNAVAILABLE;
      experimental = 1;
      goto out;
    }

    sqn = auth_info_resp.sqn;

    memcpy(vector.rand, auth_info_resp.rand, RAND_LENGTH);
  } else {
    /* Pick a new RAND and store SQN_MS + RAND in the HSS */
    generate_random(vector.rand, RAND_LENGTH);
    sqn = auth_info_resp.sqn;
    hss_mysql_push_rand_sqn(auth_info_req.imsi, vector.rand, sqn);
  }

  hss_mysql_increment_sqn(auth_info_req.imsi);

  /* Generate authentication vector */
  generate_vector(auth_info_resp.opc, imsi, auth_info_resp.key,
                  hdr->avp_value->os.data, sqn, &vector);

  /* We add the vector */
  {
    struct avp *e_utran_vector, *child_avp;
    CHECK_FCT(fd_msg_avp_new(s6a_cnf.dataobj_s6a_authentication_info, 0, &avp));

    CHECK_FCT(fd_msg_avp_new(s6a_cnf.dataobj_s6a_e_utran_vector, 0, &e_utran_vector));

    CHECK_FCT(fd_msg_avp_new(s6a_cnf.dataobj_s6a_rand, 0, &child_avp));
    value.os.data = vector.rand;
    value.os.len = RAND_LENGTH_OCTETS;
    CHECK_FCT(fd_msg_avp_setvalue(child_avp, &value));
    CHECK_FCT(fd_msg_avp_add(e_utran_vector, MSG_BRW_LAST_CHILD, child_avp));

    CHECK_FCT(fd_msg_avp_new(s6a_cnf.dataobj_s6a_xres, 0, &child_avp));
    value.os.data = vector.xres;
    value.os.len = XRES_LENGTH_OCTETS;
    CHECK_FCT(fd_msg_avp_setvalue(child_avp, &value));
    CHECK_FCT(fd_msg_avp_add(e_utran_vector, MSG_BRW_LAST_CHILD, child_avp));

    CHECK_FCT(fd_msg_avp_new(s6a_cnf.dataobj_s6a_autn, 0, &child_avp));
    value.os.data = vector.autn;
    value.os.len = AUTN_LENGTH_OCTETS;
    CHECK_FCT(fd_msg_avp_setvalue(child_avp, &value));
    CHECK_FCT(fd_msg_avp_add(e_utran_vector, MSG_BRW_LAST_CHILD, child_avp));

    CHECK_FCT(fd_msg_avp_new(s6a_cnf.dataobj_s6a_kasme, 0, &child_avp));
    value.os.data = vector.kasme;
    value.os.len = KASME_LENGTH_OCTETS;
    CHECK_FCT(fd_msg_avp_setvalue(child_avp, &value));
    CHECK_FCT(fd_msg_avp_add(e_utran_vector, MSG_BRW_LAST_CHILD, child_avp));

    CHECK_FCT(fd_msg_avp_add(avp, MSG_BRW_LAST_CHILD, e_utran_vector));

    CHECK_FCT(fd_msg_avp_add(ans, MSG_BRW_LAST_CHILD, avp));
  }

out:
  /* Add the Auth-Session-State AVP */
  CHECK_FCT(fd_msg_search_avp(qry, s6a_cnf.dataobj_s6a_auth_session_state, &avp));
  CHECK_FCT(fd_msg_avp_hdr(avp, &hdr));

  CHECK_FCT(fd_msg_avp_new(s6a_cnf.dataobj_s6a_auth_session_state, 0, &avp));
  CHECK_FCT(fd_msg_avp_setvalue(avp, hdr->avp_value));
  CHECK_FCT(fd_msg_avp_add(ans, MSG_BRW_LAST_CHILD, avp));

  /* Append the result code to the answer */
  CHECK_FCT(s6a_add_result_code(ans, failed_avp, result_code, experimental));

  CHECK_FCT(fd_msg_send(msg, NULL, NULL ));
  return ret;
}