/*
    Copyright (C) 2002-2005  Thomas Ries <tries@gmx.net>

    This file is part of Siproxd.
    
    Siproxd 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 2 of the License, or
    (at your option) any later version.
    
    Siproxd 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 Siproxd; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
*/

#include "config.h"

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>

#include <sys/time.h>

#include <netinet/in.h>

#include <osipparser2/osip_parser.h>
#include <osipparser2/osip_md5.h>

#include "digcalc.h"

#include "siproxd.h"
#include "log.h"

static char const ident[]="$Id: auth.c,v 1.18 2005/01/08 10:05:12 hb9xar Exp $";

/* configuration storage */
extern struct siproxd_config configuration;

/* Global File instance on pw file */
extern FILE *siproxd_passwordfile;

/* local protorypes */
static char *auth_generate_nonce(void);
static int auth_check(osip_proxy_authorization_t *proxy_auth);
static char *auth_getpwd(char *username);

/*
 * perform proxy authentication
 *
 * RETURNS
 *	STS_SUCCESS : authentication ok / not needed
 *	STS_FAILURE : authentication failed
 *	STS_NEEDAUTH: authentication needed
 */
int authenticate_proxy(sip_ticket_t *ticket) {
   osip_proxy_authorization_t *proxy_auth=NULL;
   
   /* required by config? */
   if (configuration.proxy_auth_realm == NULL) {
      return STS_SUCCESS;
   }
   
   /* supplied by UA? */
   osip_message_get_proxy_authorization(ticket->sipmsg, 0, &proxy_auth);
   if (proxy_auth == NULL) {
      DEBUGC(DBCLASS_AUTH,"proxy-auth required, not supplied by UA");
      return STS_NEED_AUTH;
   }

   /* verify supplied authentication */
   if (auth_check(proxy_auth) == STS_SUCCESS) {
      DEBUGC(DBCLASS_AUTH,"proxy-auth succeeded");
      return STS_SUCCESS;
   }

   /* authentication failed */
   WARN("authenticate_proxy failed");
   return STS_FAILURE;
}

/*
 * includes proxy authentication header in SIP message
 *
 * RETURNS
 *	STS_SUCCESS
 *	STS_FAILURE
 */
int auth_include_authrq(sip_ticket_t *ticket) {
   osip_proxy_authenticate_t *p_auth;
   char *realm=NULL;

   if (osip_proxy_authenticate_init(&p_auth) != 0) {
      ERROR("proxy_authenticate_init failed");
      return STS_FAILURE;
   }

   osip_proxy_authenticate_set_auth_type(p_auth, osip_strdup("Digest"));
   osip_proxy_authenticate_set_nonce(p_auth, osip_strdup(auth_generate_nonce()));
   realm=malloc(strlen(configuration.proxy_auth_realm)+3); /* add 2x" and \0 */
   if (realm) {
      sprintf(realm,"\"%s\"",configuration.proxy_auth_realm);
      osip_proxy_authenticate_set_realm(p_auth, realm);
   } else {
      ERROR("unable to malloc() %i bytes for authentication realm",
            strlen(configuration.proxy_auth_realm)+3);
      return STS_FAILURE;
   }

   osip_list_add (ticket->sipmsg->proxy_authenticates, p_auth, -1);

   DEBUGC(DBCLASS_AUTH,"added authentication header");

   return STS_SUCCESS;
}

/*
 * generates a nonce string
 *
 * RETURNS nonce string
 */
static char *auth_generate_nonce() {
   static char nonce[40];
   struct timeval tv;
   
   gettimeofday (&tv, NULL);

/* yeah, I know... should be a better algorithm */
/* enclose it in double quotes, as libosip does *not* do it (2.0.6) */
   sprintf(nonce, "\"%8.8lx%8.8lx%8.8x%8.8x\"",
           (long)tv.tv_sec, (long)tv.tv_usec, rand(), rand() );

   DEBUGC(DBCLASS_AUTH,"created nonce=\"%s\"",nonce);
   return nonce;
}


/*
 * verify the supplied authentication information from UA
 *
 * RETURNS
 *	STS_SUCCESS if succeeded
 *	STS_FAILURE if failed
 */
static int auth_check(osip_proxy_authorization_t *proxy_auth) {
   char *password=NULL;
   int sts;

   HASHHEX HA1;
   HASHHEX HA2 = "";
   HASHHEX Lcl_Response;
 
   char *Username   = NULL;
   char *Realm      = NULL;
   char *Nonce      = NULL;
   char *CNonce     = NULL;
   char *NonceCount = NULL;
   char *Qpop	    = NULL;
   char *Uri	    = NULL;
   char *Response   = NULL;

   /* if item exists, allocate& copy string without quotes */
   if (proxy_auth->username)
      Username=osip_strdup_without_quote(proxy_auth->username);

   if (proxy_auth->realm)
      Realm=osip_strdup_without_quote(proxy_auth->realm);

   if (proxy_auth->nonce)
      Nonce=osip_strdup_without_quote(proxy_auth->nonce);

   if (proxy_auth->cnonce)
      CNonce=osip_strdup_without_quote(proxy_auth->cnonce);

   if (proxy_auth->nonce_count)
      NonceCount=osip_strdup_without_quote(proxy_auth->nonce_count);

   if (proxy_auth->message_qop)
      Qpop=osip_strdup_without_quote(proxy_auth->message_qop);

   if (proxy_auth->uri) 
      Uri=osip_strdup_without_quote(proxy_auth->uri);

   if (proxy_auth->response)
      Response=osip_strdup_without_quote(proxy_auth->response);

   /* get password */
   if (configuration.proxy_auth_pwfile) {
      /* check in passwd file */
      password=auth_getpwd(Username);
   } else if (configuration.proxy_auth_passwd) {
      /* get password from configuration */
      password=configuration.proxy_auth_passwd;
   }

   if (password == NULL) password="";

   DEBUGC(DBCLASS_BABBLE," username=\"%s\"",Username  );
   DEBUGC(DBCLASS_BABBLE," realm   =\"%s\"",Realm     );
   DEBUGC(DBCLASS_BABBLE," nonce   =\"%s\"",Nonce     );
   DEBUGC(DBCLASS_BABBLE," cnonce  =\"%s\"",CNonce    );
   DEBUGC(DBCLASS_BABBLE," nonce_nc=\"%s\"",NonceCount);
   DEBUGC(DBCLASS_BABBLE," qpop    =\"%s\"",Qpop      );
   DEBUGC(DBCLASS_BABBLE," uri     =\"%s\"",Uri	    );
   DEBUGC(DBCLASS_BABBLE," response=\"%s\"",Response  );

   /* calculate the MD5 digest (heavily inspired from linphone code) */
   DigestCalcHA1("MD5", Username, Realm, password, Nonce, CNonce, HA1);
   DigestCalcResponse(HA1, Nonce, NonceCount, CNonce, Qpop,
		      "REGISTER", Uri, HA2, Lcl_Response);

   DEBUGC(DBCLASS_BABBLE,"calculated Response=\"%s\"", Lcl_Response);

   if (strcmp(Lcl_Response, Response)==0) {
      DEBUGC(DBCLASS_AUTH,"Authentication succeeded");
      sts = STS_SUCCESS;
   } else {
      DEBUGC(DBCLASS_AUTH,"Authentication failed");
      sts = STS_FAILURE;
   }

   /* free allocated memory from above */
   if (Username)   free(Username);
   if (Realm)      free(Realm);
   if (Nonce)      free(Nonce);
   if (CNonce)     free(CNonce);
   if (NonceCount) free(NonceCount);
   if (Qpop)       free(Qpop);
   if (Uri)        free(Uri);
   if (Response)   free(Response);

   return sts;
}


/*
 * lookup in the password file and return
 * the user specific password for 'username'
 *
 * RETURNS
 *	password for user or NULL if not found
 */
static char *auth_getpwd(char *username) {
   typedef struct {
      char username[USERNAME_SIZE];
      char password[PASSWORD_SIZE];
   } auth_cache_t;

   char buff[128];
   int i;
   static auth_cache_t *auth_cache=NULL;
   void *tmpptr;
   static int auth_cache_size=0;
   static int auth_cache_count=0;

   if (auth_cache==NULL) {
      DEBUGC(DBCLASS_AUTH,"initialize password cache");

      /* config file not found or unable to open for read */
      if (siproxd_passwordfile==NULL) {
         ERROR ("could not open password file: %s", strerror(errno));
         return NULL;
      }
      
      rewind(siproxd_passwordfile);

      while (fgets(buff,sizeof(buff),siproxd_passwordfile) != NULL) {
         /* life insurance */
         buff[sizeof(buff)-1]='\0';

	 /* strip newline if present */
	 if (buff[strlen(buff)-1]=='\n') buff[strlen(buff)-1]='\0';

	 /* strip emty lines */
	 if (strlen(buff) == 0) continue;

	 /* strip comments and line with only whitespaces */
	 for (i=0;i<strlen(buff);i++) {
            if ((buff[i] == ' ') && (buff[i] == '\t')) continue;
            if (buff[i] =='#') i=strlen(buff);
            break;
	 }
	 if (i == strlen(buff)) continue;

         /* allocate space whenever needed */
	 if (auth_cache_count >= auth_cache_size) {
	    auth_cache_size+=10;
	    tmpptr=realloc(auth_cache, auth_cache_size*sizeof(auth_cache_t));
            if (tmpptr != NULL) {
               auth_cache= (auth_cache_t *)tmpptr;
	    } else {
               ERROR("realloc failed! this is not good");
	       auth_cache_size-=10;
	       return NULL;
	    }
         } /* cnt > size */

         i=sscanf(buff,"%s %s",auth_cache[auth_cache_count].username,
	                       auth_cache[auth_cache_count].password);
         /* if I got username & passwd, make it valid and increment counter */
         if (i == 2) auth_cache_count++;
      }

   } /* initialize cache */

   /* search cache for user */
   DEBUGC(DBCLASS_AUTH,"searching password entry for user %s",username);
   for (i=0; i< auth_cache_count;i++) {
      if (strcmp(username, auth_cache[i].username)==0) {
         DEBUGC(DBCLASS_AUTH,"found password entry for user %s",username);
         return auth_cache[i].password;
      }
   }

   DEBUGC(DBCLASS_AUTH,"no password entry found for user %s",username);
   return NULL;
}


/*-------------------------------------------------------------------------
  -------------------------------------------------------------------------
  The routines below have been taken from linphone
  (osipua/src/authentication.c)
  -------------------------------------------------------------------------
  -------------------------------------------------------------------------*/

void CvtHex(
	    IN HASH Bin,
	    OUT HASHHEX Hex
	    )
{
  unsigned short i;
  unsigned char j;
  
  for (i = 0; i < HASHLEN; i++) {
    j = (Bin[i] >> 4) & 0xf;
    if (j <= 9)
      Hex[i*2] = (j + '0');
    else
      Hex[i*2] = (j + 'a' - 10);
    j = Bin[i] & 0xf;
    if (j <= 9)
      Hex[i*2+1] = (j + '0');
    else
      Hex[i*2+1] = (j + 'a' - 10);
  };
  Hex[HASHHEXLEN] = '\0';
}

/* calculate H(A1) as per spec */
void DigestCalcHA1(
		   IN char * pszAlg,
		   IN char * pszUserName,
		   IN char * pszRealm,
		   IN char * pszPassword,
		   IN char * pszNonce,
		   IN char * pszCNonce,
		   OUT HASHHEX SessionKey
		   )
{
  MD5_CTX Md5Ctx;
  HASH HA1;
  
  MD5Init(&Md5Ctx);
  if (pszUserName) MD5Update(&Md5Ctx, pszUserName, strlen(pszUserName));
  MD5Update(&Md5Ctx, ":", 1);
  if (pszRealm)    MD5Update(&Md5Ctx, pszRealm, strlen(pszRealm));
  MD5Update(&Md5Ctx, ":", 1);
  if (pszPassword) MD5Update(&Md5Ctx, pszPassword, strlen(pszPassword));
  MD5Final(HA1, &Md5Ctx);

  if ((pszAlg!=NULL) && (osip_strcasecmp(pszAlg, "md5-sess") == 0)) {
    MD5Init(&Md5Ctx);
    MD5Update(&Md5Ctx, HA1, HASHLEN);
    MD5Update(&Md5Ctx, ":", 1);
    if (pszNonce)  MD5Update(&Md5Ctx, pszNonce, strlen(pszNonce));
    MD5Update(&Md5Ctx, ":", 1);
    if (pszCNonce) MD5Update(&Md5Ctx, pszCNonce, strlen(pszCNonce));
    MD5Final(HA1, &Md5Ctx);
  };
  CvtHex(HA1, SessionKey);
}

/* calculate request-digest/response-digest as per HTTP Digest spec */
void DigestCalcResponse(
			IN HASHHEX HA1,         /* H(A1) */
			IN char * pszNonce,     /* nonce from server */
			IN char * pszNonceCount,  /* 8 hex digits */
			IN char * pszCNonce,    /* client nonce */
			IN char * pszQop,       /* qop-value: "", "auth", "auth-int" */
			IN char * pszMethod,    /* method from the request */
			IN char * pszDigestUri, /* requested URL */
			IN HASHHEX HEntity,     /* H(entity body) if qop="auth-int" */
			OUT HASHHEX Response    /* request-digest or response-digest */
			)
{
  MD5_CTX Md5Ctx;
  HASH HA2;
  HASH RespHash;
  HASHHEX HA2Hex;
  
  /* calculate H(A2) */
  MD5Init(&Md5Ctx);
  if (pszMethod)   MD5Update(&Md5Ctx, pszMethod, strlen(pszMethod));
  MD5Update(&Md5Ctx, ":", 1);
  if (pszDigestUri)MD5Update(&Md5Ctx, pszDigestUri, strlen(pszDigestUri));
  
  if (pszQop!=NULL) {
      goto auth_withqop;
  };
  
  /* auth_withoutqop: */
  MD5Final(HA2, &Md5Ctx);
  CvtHex(HA2, HA2Hex);

  /* calculate response */
  MD5Init(&Md5Ctx);
  MD5Update(&Md5Ctx, HA1, HASHHEXLEN);
  MD5Update(&Md5Ctx, ":", 1);
  if (pszNonce)    MD5Update(&Md5Ctx, pszNonce, strlen(pszNonce));
  MD5Update(&Md5Ctx, ":", 1);

  goto end;

 auth_withqop:

  MD5Update(&Md5Ctx, ":", 1);
  MD5Update(&Md5Ctx, HEntity, HASHHEXLEN);
  MD5Final(HA2, &Md5Ctx);
  CvtHex(HA2, HA2Hex);

  /* calculate response */
  MD5Init(&Md5Ctx);
  MD5Update(&Md5Ctx, HA1, HASHHEXLEN);
  MD5Update(&Md5Ctx, ":", 1);
  if (pszNonce)    MD5Update(&Md5Ctx, pszNonce, strlen(pszNonce));
  MD5Update(&Md5Ctx, ":", 1);
  if (pszNonceCount)MD5Update(&Md5Ctx, pszNonceCount, strlen(pszNonceCount));
  MD5Update(&Md5Ctx, ":", 1);
  if (pszCNonce)   MD5Update(&Md5Ctx, pszCNonce, strlen(pszCNonce));
  MD5Update(&Md5Ctx, ":", 1);
  if (pszQop)      MD5Update(&Md5Ctx, pszQop, strlen(pszQop));
  MD5Update(&Md5Ctx, ":", 1);

 end:
  MD5Update(&Md5Ctx, HA2Hex, HASHHEXLEN);
  MD5Final(RespHash, &Md5Ctx);
  CvtHex(RespHash, Response);
}


