/*
* Copyright (C) 2010-2011 Mamadou Diop.
*
* Contact: Mamadou Diop <diopmamadou(at)doubango[dot]org>
*	
* This file is part of Open Source Doubango Framework.
*
* DOUBANGO 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.
*	
* DOUBANGO 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 DOUBANGO.
*
*/

/**@file thttp_session.c
 * @brief HTTP/HTTPS session.
 *
 * @author Mamadou Diop <diopmamadou(at)doubango[dot]org>
 *

 */
#include "tinyhttp/thttp_session.h"

#include "thttp.h"
#include "tinyhttp/thttp_action.h"

#include "tinyhttp/headers/thttp_header_Dummy.h"
#include "tinyhttp/headers/thttp_header_WWW_Authenticate.h"

#include "tsk_debug.h"

/**@defgroup thttp_session_group HTTP Session
*/

int thttp_session_signal(thttp_session_t *self, thttp_action_type_t atype);

/**Sets parameters.
*/
int __thttp_session_set(thttp_session_t *self, va_list* app)
{
	thttp_session_param_type_t curr;

	if(!self){
		return -1;
	}

	while((curr=va_arg(*app, thttp_session_param_type_t)) != httpp_null){
		switch(curr){
			case httpp_option:
				{	/* (thttp_session_option_t)ID_ENUM, (const char*)VALUE_STR */
					thttp_session_option_t id = va_arg(*app, thttp_session_option_t);
					const char* value = va_arg(*app, const char *);
					tsk_options_add_option(&self->options, id, value);
					break;
				}

			case httpp_header:
				{	/* (const char*)NAME_STR, (const char*)VALUE_STR */
					const char* name = va_arg(*app, const char *);
					const char* value = va_arg(*app, const char *);
					if(value == ((const char*)-1)){ /* UNSET */
						tsk_params_remove_param(self->headers, name);
					}
					else{ /* SET */
						tsk_params_add_param(&self->headers, name, value);
					}
					break;
				}
			
			case httpp_cred:
				{	/* (const char*)USERNAME_STR, (const char*)PASSWORD_STR */
					tsk_strupdate(&self->cred.usename, va_arg(*app, const char *));
					tsk_strupdate(&self->cred.password, va_arg(*app, const char *));
					break;
				}

			case httpp_userdata:
				{	/* (const void*)USERDATA_PTR */
					self->userdata = va_arg(*app, const void *);
					break;
				}

			default:
				{	/* va_list will be unsafe => exit */
					TSK_DEBUG_ERROR("NOT SUPPORTED.");
					goto bail;
				}
		} /* sxitch */
	} /* while */
	return 0;

bail:
	return -2;
}

/**@ingroup thttp_session_group
* Creates new session.
* @param stack The HTTP/HTTPS @a stack to use. The @a stack shall be created using @ref thttp_stack_create.
* @param ... Any @b THTTP_SESSION_SET_*() macros. MUST ends with @ref THTTP_SESSION_SET_NULL().
* @retval A pointer to the newly created session.
* A session is a well-defined object.
*
* @code
thttp_session_handle_t * session = thttp_session_create(stack,
	// session-level parameters
	THTTP_SESSION_SET_PARAM("timeout", "6000"),

	// session-level headers
	THTTP_SESSION_SET_HEADER("Pragma", "No-Cache"),
	THTTP_SESSION_SET_HEADER("Connection", "Keep-Alive"),
	THTTP_SESSION_SET_HEADER("User-Agent", "doubango 1.0"),
			
	THTTP_SESSION_SET_NULL());
* @endcode
*
* @sa @ref thttp_session_set
*/
thttp_session_handle_t* thttp_session_create(const thttp_stack_handle_t* stack, ...)
{
	thttp_session_handle_t* ret = tsk_null;

	if((ret = tsk_object_new(thttp_session_def_t, stack))){
		va_list ap;
		va_start(ap, stack);
		if(__thttp_session_set(ret, &ap)){
			TSK_OBJECT_SAFE_FREE(ret);
		}
		va_end(ap);
	}
	else{
		TSK_DEBUG_ERROR("failed to create new HTTP/HTTPS session.");
	}
	return ret;
}

/**@ingroup thttp_session_group
* Updates the session parameters.
* @param self The session to update. The session shall be created using @ref thttp_session_create().
* @param ... Any @b THTTP_SESSION_SET_*() macros. MUST ends with @ref THTTP_SESSION_SET_NULL().
* @retval Zero if succeed and non zero error code otherwise.
*
* @code
int ret = thttp_session_set(session,
	// session-level parameters
	THTTP_SESSION_SET_OPTION(THTTP_SESSION_OPTION_TIMEOUT, "6000"),

	// session-level headers
	THTTP_SESSION_SET_HEADER("Pragma", "No-Cache"),
	THTTP_SESSION_SET_HEADER("Connection", "Keep-Alive"),
	THTTP_SESSION_SET_HEADER("User-Agent", "doubango 1.0"),
			
	THTTP_SESSION_SET_NULL());
* @endcode
*
* @sa @ref thttp_session_create
*/
int thttp_session_set(thttp_session_handle_t *self, ...)
{
	if(self){
		int ret;
		va_list ap;

		thttp_session_t *session = self;

		if(session->id == THTTP_SESSION_INVALID_ID){
			TSK_DEBUG_ERROR("Using invalid session.");
			return -2;
		}
		
		va_start(ap, self);
		ret = __thttp_session_set(session, &ap);
		va_end(ap);
		return ret;
	}

	return -1;
}

/**@ingroup thttp_session_group
* Gets the session id.
* @param self The session for which to get the id.
* @retval The id of the session.
*/
thttp_session_id_t thttp_session_get_id(const thttp_session_handle_t *self)
{
	const thttp_session_t *session = self;
	if(session){
		return session->id;
	}
	return THTTP_SESSION_INVALID_ID;
}

/**@ingroup thttp_session_group
* Gets the user context (user/application data).
* @param self A pointer to the session from which to get the context.
* @retval A pointer to the context. Previously defined by using @ref THTTP_SESSION_SET_USERDATA() macro.
* @sa @ref THTTP_SESSION_SET_USERDATA()
*/
const void* thttp_session_get_userdata(const thttp_session_handle_t *self)
{
	if(self){
		return ((const thttp_session_t*)self)->userdata;
	}
	return tsk_null;
}

int thttp_session_closefd(thttp_session_handle_t *_self)
{
	int ret = 0;
	thttp_session_t* self = _self;

	if(self->fd != TNET_INVALID_FD){
		if((ret = tnet_transport_remove_socket(self->stack->transport, &self->fd))){
			ret = tnet_sockfd_close(&self->fd);
		}
	}
	
	return ret;
}

/** Updates authentications headers.
*/
int thttp_session_update_challenges(thttp_session_t *self, const thttp_response_t* response, tsk_bool_t answered)
{
	int ret = 0;
	tsk_size_t i;

	tsk_list_item_t *item;

	thttp_challenge_t *challenge;
	
	const thttp_header_WWW_Authenticate_t *WWW_Authenticate;
	const thttp_header_Proxy_Authenticate_t *Proxy_Authenticate;

	tsk_safeobj_lock(self);

	/* RFC 2617 - Digest Operation

	*	(A) The client response to a WWW-Authenticate challenge for a protection
		space starts an authentication session with that protection space.
		The authentication session lasts until the client receives another
		WWW-Authenticate challenge from any server in the protection space.

		(B) The server may return a 401 response with a new nonce value, causing the client
		to retry the request; by specifying stale=TRUE with this response,
		the server tells the client to retry with the new nonce, but without
		prompting for a new username and password.
	*/
	/* RFC 2617 - 1.2 Access Authentication Framework
		The realm directive (case-insensitive) is required for all authentication schemes that issue a challenge.
	*/

	/* FIXME: As we perform the same task ==> Use only one loop.
	*/

	for(i =0; (WWW_Authenticate = (const thttp_header_WWW_Authenticate_t*)thttp_message_get_headerAt(response, thttp_htype_WWW_Authenticate, i)); i++)
	{
		tsk_bool_t isnew = tsk_true;

		tsk_list_foreach(item, self->challenges)
		{
			challenge = item->data;
			if(challenge->isproxy){
				continue;
			}
			
			if(tsk_striequals(challenge->realm, WWW_Authenticate->realm) && (WWW_Authenticate->stale || !answered)){
				/*== (B) ==*/
				if((ret = thttp_challenge_update(challenge, 
					WWW_Authenticate->scheme,
					WWW_Authenticate->realm, 
					WWW_Authenticate->nonce, 
					WWW_Authenticate->opaque, 
					WWW_Authenticate->algorithm, 
					WWW_Authenticate->qop))){
					return ret;
				}
				else{
					isnew = tsk_false;
					continue;
				}
			}
			else{
				ret = -1;
				goto bail;
			}
		}

		if(isnew){
			if((challenge = thttp_challenge_create(tsk_false, /* Not proxy */
					WWW_Authenticate->scheme, 
					WWW_Authenticate->realm, 
					WWW_Authenticate->nonce, 
					WWW_Authenticate->opaque, 
					WWW_Authenticate->algorithm, 
					WWW_Authenticate->qop))){
				tsk_list_push_back_data(self->challenges, (void**)&challenge);
			}
			else{
				ret = -1;
				goto bail;
			}
		}
	}
	
	for(i=0; (Proxy_Authenticate = (const thttp_header_Proxy_Authenticate_t*)thttp_message_get_headerAt(response, thttp_htype_Proxy_Authenticate, i)); i++)
	{
		tsk_bool_t isnew = tsk_true;

		tsk_list_foreach(item, self->challenges){
			challenge = item->data;
			if(!challenge->isproxy){
				continue;
			}
			
			if(tsk_striequals(challenge->realm, Proxy_Authenticate->realm) && (Proxy_Authenticate->stale || !answered)){
				/*== (B) ==*/
				if((ret = thttp_challenge_update(challenge, 
					Proxy_Authenticate->scheme, 
					Proxy_Authenticate->realm, 
					Proxy_Authenticate->nonce, 
					Proxy_Authenticate->opaque, 
					Proxy_Authenticate->algorithm, 
					Proxy_Authenticate->qop)))
				{
					goto bail;
				}
				else{
					isnew = tsk_false;
					continue;
				}
			}
			else{
				ret = -1;
				goto bail;
			}
		}

		if(isnew){
			if((challenge = thttp_challenge_create(tsk_true, /* Proxy */
					Proxy_Authenticate->scheme, 
					Proxy_Authenticate->realm, 
					Proxy_Authenticate->nonce,
					Proxy_Authenticate->opaque,
					Proxy_Authenticate->algorithm, 
					Proxy_Authenticate->qop)))
			{
				tsk_list_push_back_data(self->challenges, (void**)&challenge);
			}
			else{
				ret = -1;
				goto bail;
			}
		}
	}

bail:
	tsk_safeobj_unlock(self);
	return ret;

}

/* internal function */
int thttp_session_signal(thttp_session_t *self, thttp_action_type_t atype)
{
	tsk_list_item_t *item;
	
	if(!self){
		TSK_DEBUG_ERROR("Invalid parameter");
		return -1;
	}
	
	tsk_safeobj_lock(self);
again:
	tsk_list_foreach(item, self->dialogs){
		item = tsk_object_ref(item);
		thttp_dialog_fsm_act((thttp_dialog_t*)item->data, atype, tsk_null, tsk_null);
		/* As the above action could terminate the dialog (which means change the content of self->dialogs) 
		* => list becomes unsafe */
		if(!(item = tsk_object_unref(item))){
			goto again;
		}
	}

	switch(atype){
		case thttp_thttp_atype_closed:
			self->fd = TNET_INVALID_FD;
			break;
		default:
			break;
	}
	
	tsk_safeobj_unlock(self);

	return 0;
}

/** Signals to all dialogs that the connection have been closed. */
int thttp_session_signal_closed(thttp_session_t *self)
{
	return thttp_session_signal(self, thttp_thttp_atype_closed);
}

/** Signals to all dialogss that we got an error */
int thttp_session_signal_error(thttp_session_t *self)
{
	return thttp_session_signal(self, thttp_atype_error);
}


/** Retrieves a session by fd */
thttp_session_t* thttp_session_get_by_fd(thttp_sessions_L_t* sessions, tnet_fd_t fd)
{
	thttp_session_t* ret = tsk_null;
	const tsk_list_item_t *item;
	
	if(!sessions){
		goto bail;
	}

	tsk_list_foreach(item, sessions){
		if(((thttp_session_t*)item->data)->fd == fd){
			ret = tsk_object_ref(item->data);
			goto bail;
		}
	}

bail:
	return ret;
}




//========================================================
//	HTTP SESSION object definition
//
static tsk_object_t* _thttp_session_create(tsk_object_t * self, va_list * app)
{
	thttp_session_t *session = self;
	static thttp_session_id_t unique_id = THTTP_SESSION_INVALID_ID;
	if(session){
		tsk_safeobj_init(session);

		session->stack = va_arg(*app, const thttp_stack_handle_t*);
		session->options = tsk_list_create();
		session->headers = tsk_list_create();
		session->challenges = tsk_list_create();
		session->dialogs = tsk_list_create();
		session->fd = TNET_INVALID_FD;
		
		session->id = THTTP_SESSION_INVALID_ID;

		/* add the session to the stack */
		if(session->stack){
			session->id = ++unique_id;
			tsk_list_push_back_data(session->stack->sessions, (void**)&session);
		}
	}

	return self;
}

static tsk_object_t* thttp_session_destroy(tsk_object_t * self)
{ 
	thttp_session_t *session = self;
	if(session){
		TSK_DEBUG_INFO("*** HTTP/HTTPS Session destroyed ***");

		/* remove from the stack */
		if(session->stack){
			tsk_list_remove_item_by_data(session->stack->sessions, session);
		}
		
		TSK_OBJECT_SAFE_FREE(session->options);
		TSK_OBJECT_SAFE_FREE(session->headers);
		TSK_OBJECT_SAFE_FREE(session->challenges);
		TSK_OBJECT_SAFE_FREE(session->dialogs);
		
		// cred
		TSK_FREE(session->cred.usename);
		TSK_FREE(session->cred.password);
		
		// fd
		if(session->fd != TNET_INVALID_FD){
			if(tnet_transport_remove_socket(session->stack->transport, &session->fd)){
				tnet_sockfd_close(&session->fd);
			}
		}

		tsk_safeobj_deinit(session);
	}
	return self;
}

static int thttp_session_cmp(const tsk_object_t *_session1, const tsk_object_t *_session2)
{
	const thttp_session_t *session1 = _session1;
	const thttp_session_t *session2 = _session2;

	if(session1 && session2){
		return (int)(session1->id-session2->id);
	}
	return -1;
}

static const tsk_object_def_t thttp_session_def_s = 
{
	sizeof(thttp_session_t),
	_thttp_session_create, 
	thttp_session_destroy,
	thttp_session_cmp, 
};
const tsk_object_def_t *thttp_session_def_t = &thttp_session_def_s;
