/* od.c -- Open Directory routines
 *
 * Copyright (c) 2003 by Apple Computer, Inc., all rights reserved.
 *
 */



#include <string.h>
#include <unistd.h>
#include <sys/param.h>

#include <sys/time.h>
#include <sys/stat.h>
#include <sys/errno.h>

#include <Kerberos/Kerberos.h>
#include <Kerberos/gssapi_krb5.h>
#include <Kerberos/gssapi.h>

#include <syslog.h>

#include <DirectoryService/DirServices.h>

#include "od.h"
#include "prot.h"
#include "imap_err.h"
#include "xmalloc.h"

extern const char *config_getstring(const char *key, const char *def);
extern int config_getswitch(const char *key, int def);
extern char *auth_canonifyid(const char *identifier, size_t len);

int			gss_Init				( const char *inProtocol );
int			get_realm_form_creds	( char *inBuffer, int inSize );
int			get_principal_str		( char *inOutBuf, int inSize );
void		log_errors				( char *inName, OM_uint32 inMajor, OM_uint32 inMinor );
OM_uint32	export_and_print		( const gss_name_t principalName, char *outUserID );

int od_do_clear_text_auth ( const char *inUserID, const char *inPasswd )
{
	return( aodClearTextCrypt( inUserID, inPasswd ) );
} /* od_do_clear_text_auth */


static	gss_cred_id_t	stCredentials;

/* -----------------------------------------------------------
 * Do Open Directory Authentication
 * ----------------------------------------------------------- */

int od_do_auth( const char *inMethod,
				const char *inDigest,
				const char *inCont,
				const char *inProtocol,
				struct protstream *inStreamIn,
				struct protstream *inStreamOut,
				char **inOutCannonUser )
{
	int				r		= ODA_AUTH_FAILED;
	int				len		= 0;
	int				respLen	= 0;
	char		   *ptr		= NULL;
	char			user	[ MAX_USER_BUF_SIZE ];
	char			passwd	[ MAX_USER_BUF_SIZE ];
	char			chal	[ MAX_CHAL_BUF_SIZE ];
	char			resp	[ MAX_IO_BUF_SIZE ];
	char			buff	[ MAX_IO_BUF_SIZE ];
	char			hostname[ MAXHOSTNAMELEN + 1 ];
	gss_buffer_desc	in_token;
	gss_buffer_desc	out_token;
	OM_uint32		minStatus	= 0;
	OM_uint32		majStatus	= 0;
	gss_ctx_id_t	context		= GSS_C_NO_CONTEXT;
	OM_uint32		ret_flags	= 0;
	gss_name_t		clientName;
	unsigned long	maxsize		= htonl( 8192 );

	if ( strcasecmp( inMethod, "cram-md5" ) == 0 )
	{
		/* is CRAM-MD5 auth enabled */
		if ( !config_getswitch( "imap_auth_cram_md5", 0 ) )
		{
			return ( -1 );
		}

		/* create the challenge */
		gethostname( hostname, sizeof( hostname ) );
		sprintf( chal,"<%lu.%lu@%s>",(unsigned long) getpid(), (unsigned long)time(0), hostname );

		/* encode the challenge and send it */
		aodEncodeBase64( chal, strlen( chal ), buff, MAX_IO_BUF_SIZE );

		prot_printf( inStreamOut, "+ %s\r\n", buff );

		/* reset the buffer */
		memset( buff, 0, MAX_IO_BUF_SIZE );

		/* get the client response */
		if ( prot_fgets( buff, MAX_IO_BUF_SIZE, inStreamIn ) )
		{
			/* trim CRLF */
			ptr = buff + strlen( buff ) - 1;
			if ( (ptr >= buff) && (*ptr == '\n') )
			{
				*ptr-- = '\0';
			}
			if ( (ptr >= buff) && (*ptr == '\r') )
			{
				*ptr-- = '\0';
			}

			/* check if client cancelled */
			if ( buff[ 0 ] == '*' )
			{
				return( ODA_AUTH_CANCEL );
			}
		}

		/* reset the buffer */
		memset( resp, 0, MAX_IO_BUF_SIZE );

		/* decode the response */
		len = strlen( buff );
		aodDecodeBase64( buff, len, resp, MAX_IO_BUF_SIZE, &respLen );

		/* get the user name */
		ptr = strchr( resp, ' ' );
		if ( ptr != NULL )
		{
			len = ptr - resp;
			if ( len < MAX_USER_BUF_SIZE )
			{
				/* copy user name */
				strncpy( user, resp, len );

				/* move past the space */
				ptr++;
				if ( ptr != NULL )
				{
					/* validate the response */
					r = aodCRAM_MD5( user, chal, ptr );
					if ( r == ODA_NO_ERROR )
					{
						*inOutCannonUser = auth_canonifyid( user, 0 );
						return( ODA_NO_ERROR );
					}
				}
				else
				{
					return( ODA_PROTOCOL_ERROR );
				}
			}
			else
			{
				return( ODA_PROTOCOL_ERROR );
			}
		}
		else
		{
			return( ODA_PROTOCOL_ERROR );
		}
	}
	else if ( strcasecmp( inMethod, "PLAIN" ) == 0 )
	{
		/* is CRAM-MD5 auth enabled */
		if ( !config_getswitch( "imap_auth_plain", 0 ) )
		{
			return ( -1 );
		}

		prot_printf( inStreamOut, "%s\r\n", inCont );

		/* get the client response */
		if ( prot_fgets( buff, MAX_IO_BUF_SIZE, inStreamIn ) )
		{
			/* trim CRLF */
			ptr = buff + strlen( buff ) - 1;
			if ( (ptr >= buff) && (*ptr == '\n') )
			{
				*ptr-- = '\0';
			}
			if ( (ptr >= buff) && (*ptr == '\r') )
			{
				*ptr-- = '\0';
			}

			/* check if client cancelled */
			if ( buff[ 0 ] == '*' )
			{
				return( ODA_AUTH_CANCEL );
			}
		}

		/* reset the buffer */
		memset( resp, 0, MAX_IO_BUF_SIZE );

		/* decode the response */
		len = strlen( buff );
		aodDecodeBase64( buff, len, resp, MAX_IO_BUF_SIZE, &respLen );

		ptr = resp;
		if ( *ptr == NULL )
		{
			ptr++;
		}

		if ( ptr != NULL )
		{
			if ( strlen( ptr ) < MAX_USER_BUF_SIZE )
			{
				strcpy( user, ptr );

				ptr = ptr + (strlen( user ) + 1 );

				if ( ptr != NULL )
				{
					if ( strlen( ptr ) < MAX_USER_BUF_SIZE )
					{
						strcpy( passwd, ptr );

						/* do the auth */
						if ( aodClearTextCrypt( user, passwd ) == eAODNoErr )
						{
							*inOutCannonUser = auth_canonifyid( user, 0 );

							return( ODA_NO_ERROR );
						}
					}
				}
			}
		}
	}
	else if ( strcasecmp( inMethod, "LOGIN" ) == 0 )
	{
		/* is CRAM-MD5 auth enabled */
		if ( !config_getswitch( "imap_auth_login", 0 ) )
		{
			return ( -1 );
		}

		/* encode the user name prompt and send it */
		strcpy( chal, "Username:" );
		aodEncodeBase64( chal, strlen( chal ), buff, MAX_IO_BUF_SIZE );
		prot_printf( inStreamOut, "+ %s\r\n", buff );

		/* reset the buffer */
		memset( buff, 0, MAX_IO_BUF_SIZE );

		/* get the client response */
		if ( prot_fgets( buff, MAX_IO_BUF_SIZE, inStreamIn ) )
		{
			/* trim CRLF */
			ptr = buff + strlen( buff ) - 1;
			if ( (ptr >= buff) && (*ptr == '\n') )
			{
				*ptr-- = '\0';
			}
			if ( (ptr >= buff) && (*ptr == '\r') )
			{
				*ptr-- = '\0';
			}

			/* check if client cancelled */
			if ( buff[ 0 ] == '*' )
			{
				return( ODA_AUTH_CANCEL );
			}
		}

		memset( user, 0, MAX_USER_BUF_SIZE );

		len = strlen( buff );
		aodDecodeBase64( buff, len, user, MAX_USER_BUF_SIZE, &respLen );

		/* encode the password prompt and send it */
		strcpy( chal, "Password:" );
		aodEncodeBase64( chal, strlen( chal ), buff, MAX_IO_BUF_SIZE );
		prot_printf( inStreamOut, "+ %s\r\n", buff );

		/* get the client response */
		if ( prot_fgets( buff, MAX_IO_BUF_SIZE, inStreamIn ) )
		{
			/* trim CRLF */
			ptr = buff + strlen( buff ) - 1;
			if ( (ptr >= buff) && (*ptr == '\n') )
			{
				*ptr-- = '\0';
			}
			if ( (ptr >= buff) && (*ptr == '\r') )
			{
				*ptr-- = '\0';
			}

			/* check if client cancelled */
			if ( buff[ 0 ] == '*' )
			{
				return( ODA_AUTH_CANCEL );
			}
		}

		memset( passwd, 0, MAX_USER_BUF_SIZE );

		len = strlen( buff );
		aodDecodeBase64( buff, len, passwd, MAX_USER_BUF_SIZE, &respLen );

		/* do the auth */
		if ( aodClearTextCrypt( user, passwd ) == eAODNoErr )
		{
			*inOutCannonUser = auth_canonifyid( user, 0 );

			return( ODA_NO_ERROR );
		}
	}
	else if ( strcasecmp( inMethod, "GSSAPI" ) == 0 )
	{
		r = gss_Init( inProtocol );
		if ( r == GSS_S_COMPLETE )
		{
			r = ODA_AUTH_FAILED;
			prot_printf( inStreamOut, "+ \r\n" );
			if ( prot_fgets( buff, MAX_IO_BUF_SIZE, inStreamIn ) )
			{
				/* trim CRLF */
				ptr = buff + strlen( buff ) - 1;
				if ( (ptr >= buff) && (*ptr == '\n') )
				{
					*ptr-- = '\0';
				}
				if ( (ptr >= buff) && (*ptr == '\r') )
				{
					*ptr-- = '\0';
				}

				/* check if client cancelled */
				if ( buff[ 0 ] == '*' )
				{
					return( ODA_AUTH_CANCEL );
				}
			}

			/* clear response buffer */
			memset( resp, 0, MAX_IO_BUF_SIZE );

			len = strlen( buff );
			aodDecodeBase64( buff, len, resp, MAX_IO_BUF_SIZE, &respLen );

			in_token.value  = resp;
			in_token.length = respLen;

			do {
				// negotiate authentication
				majStatus = gss_accept_sec_context(	&minStatus,
													&context,
													stCredentials,
													&in_token,
													GSS_C_NO_CHANNEL_BINDINGS,
													&clientName,
													NULL, /* &mechTypes */
													&out_token,
													&ret_flags,
													NULL,	/* ignore time?*/
													NULL );

				switch ( majStatus )
				{
					case GSS_S_COMPLETE:		// successful
					case GSS_S_CONTINUE_NEEDED:
					{
						if ( out_token.value )
						{
							// Encode the challenge and send it
							aodEncodeBase64( (char *)out_token.value, out_token.length, buff, MAX_IO_BUF_SIZE );

							prot_printf( inStreamOut, "+ %s\r\n", buff );
							if ( prot_fgets( buff, MAX_IO_BUF_SIZE, inStreamIn ) )
							{
								/* trim CRLF */
								ptr = buff + strlen( buff ) - 1;
								if ( (ptr >= buff) && (*ptr == '\n') )
								{
									*ptr-- = '\0';
								}
								if ( (ptr >= buff) && (*ptr == '\r') )
								{
									*ptr-- = '\0';
								}

								/* check if client cancelled */
								if ( buff[ 0 ] == '*' )
								{
									return( ODA_AUTH_CANCEL );
								}
							}

							memset( resp, 0, MAX_IO_BUF_SIZE );

							// Decode the response
							len = strlen( buff );
							aodDecodeBase64( buff, len, resp, MAX_IO_BUF_SIZE, &respLen );

							in_token.value  = resp;
							in_token.length = respLen;

							gss_release_buffer( &minStatus, &out_token );
						}
						break;
					}

					default:
						log_errors( "gss_accept_sec_context", majStatus, minStatus );
						break;
				}
			} while ( in_token.value && in_token.length && (majStatus == GSS_S_CONTINUE_NEEDED) );

			if ( majStatus == GSS_S_COMPLETE )
			{
				gss_buffer_desc		inToken;
				gss_buffer_desc		outToken;

				memcpy( passwd, (void *)&maxsize, 4 );
				inToken.value	= passwd;
				inToken.length	= 4;

				passwd[ 0 ] = 1;

				majStatus = gss_wrap( &minStatus, context, 0, GSS_C_QOP_DEFAULT, &inToken, NULL, &outToken );
				if ( majStatus == GSS_S_COMPLETE )
				{
					// Encode the challenge and send it
					aodEncodeBase64( (char *)outToken.value, outToken.length, buff, MAX_IO_BUF_SIZE );

					prot_printf( inStreamOut, "+ %s\r\n", buff );
					memset( buff, 0, MAX_IO_BUF_SIZE );
					if ( prot_fgets( buff, MAX_IO_BUF_SIZE, inStreamIn ) )
					{
						/* trim CRLF */
						ptr = buff + strlen( buff ) - 1;
						if ( (ptr >= buff) && (*ptr == '\n') )
						{
							*ptr-- = '\0';
						}
						if ( (ptr >= buff) && (*ptr == '\r') )
						{
							*ptr-- = '\0';
						}

						/* check if client cancelled */
						if ( buff[ 0 ] == '*' )
						{
							return( ODA_AUTH_CANCEL );
						}
					}


					// Decode the response
					respLen = 0;
					memset( resp, 0, MAX_IO_BUF_SIZE );
					len = strlen( buff );

					aodDecodeBase64( buff, len, resp, MAX_IO_BUF_SIZE, &respLen );

					inToken.value  = resp;
					inToken.length = respLen;

					gss_release_buffer( &minStatus, &outToken );

					majStatus = gss_unwrap( &minStatus, context, &inToken, &outToken, NULL, NULL );
					if ( majStatus == GSS_S_COMPLETE )
					{
						if ( (outToken.value != NULL)		&&
							(outToken.length > 4)			&&
							(outToken.length < MAX_USER_BUF_SIZE) )
						{
							memcpy( user, outToken.value, outToken.length );
							if ( user[0] & 1 )
							{
								user[ outToken.length ] = '\0';
								*inOutCannonUser = auth_canonifyid( user + 4, 0 );
								r = kSGSSSuccess;
							}
						}
					}
					else
					{
						log_errors( "gss_unwrap", majStatus, minStatus );
					}
					gss_release_buffer( &minStatus, &outToken );
				}
				else
				{
					log_errors( "gss_wrap", majStatus, minStatus );
				}
			}
			else
			{
				log_errors( "gss_accept_sec_context", majStatus, minStatus );
			}
		}
	}
	else
	{
		r = ODA_PROTOCOL_ERROR;
	}

    return ( r );

} /* od_do_auth */


int gss_Init ( const char *inProtocol )
{
	int					iResult		= GSS_S_COMPLETE;
	char			   *pService	= NULL;
	gss_buffer_desc		nameToken;
	gss_name_t			principalName;
	OM_uint32			majStatus	= 0;
	OM_uint32			minStatus	= 0;

	pService = aodGetServerPrincipal( inProtocol );
	if ( pService == NULL )
	{
		syslog( LOG_ERR, "No service principal found for: %s", inProtocol );
		return( GSS_S_NO_CRED );
	}

	nameToken.value		= pService;
	nameToken.length	= strlen( pService );

	majStatus = gss_import_name( &minStatus, 
									&nameToken, 
									GSS_KRB5_NT_PRINCIPAL_NAME,	 //gss_nt_service_name
									&principalName );

	if ( majStatus != GSS_S_COMPLETE )
	{
		log_errors( "gss_import_name", majStatus, minStatus );
		iResult = kSGSSImportNameErr;
	}
	else
	{
		majStatus = gss_acquire_cred( &minStatus, 
										principalName, 
										GSS_C_INDEFINITE, 
										GSS_C_NO_OID_SET,
										GSS_C_ACCEPT,
									   &stCredentials,
										NULL, 
										NULL );

		if ( majStatus != GSS_S_COMPLETE )
		{
			log_errors( "gss_acquire_cred", majStatus, minStatus );
			iResult = kSGSSAquireCredErr;
		}
		(void)gss_release_name( &minStatus, &principalName );
	}

	free( pService );

	return( iResult );

} /* gss_Init */


/* -----------------------------------------------------------
 *	get_realm_form_creds ()
 * ----------------------------------------------------------- */

int get_realm_form_creds ( char *inBuffer, int inSize )
{
	int			iResult		= GSS_S_COMPLETE;
	char		buffer[256] = {0};
	char	   *token1		= NULL;

	iResult = get_principal_str( buffer, 256 );
	if ( iResult == 0 )
	{
		token1 = strrchr( buffer, '@' );
		if ( token1 != NULL )
		{
			++token1;
			if ( strlen( token1 ) > inSize - 1 )
			{
				iResult = kSGSSBufferSizeErr;
			}
			else
			{
				strncpy( inBuffer, token1, inSize - 1 );
				inBuffer[ strlen( token1 ) ] = 0;
			}
		}
		else
		{
			iResult = kUnknownErr;
		}
	}

	return( iResult );

} /* get_realm_form_creds */


/* -----------------------------------------------------------
 *	get_principal_str ()
 * ----------------------------------------------------------- */

int get_principal_str ( char *inOutBuf, int inSize )
{
	OM_uint32		minStatus	= 0;
	OM_uint32		majStatus	= 0;
	gss_name_t		principalName;
	gss_buffer_desc	token;
	gss_OID			id;

	majStatus = gss_inquire_cred(&minStatus, stCredentials, &principalName,  NULL, NULL, NULL);
	if ( majStatus != GSS_S_COMPLETE )
	{
		return( kSGSSInquireCredErr );
	}

	majStatus = gss_display_name( &minStatus, principalName, &token, &id );
	if ( majStatus != GSS_S_COMPLETE )
	{
		return( kSGSSInquireCredErr );
	}

	majStatus = gss_release_name( &minStatus, &principalName );
	if ( inSize - 1 < token.length )
	{
		return( kSGSSBufferSizeErr );
	}

	strncpy( inOutBuf, (char *)token.value, token.length );
	inOutBuf[ token.length ] = 0;

	(void)gss_release_buffer( &minStatus, &token );

	return( GSS_S_COMPLETE );

} /* get_principal_str */


/* -----------------------------------------------------------
 *	export_and_print ()
 * ----------------------------------------------------------- */

OM_uint32 export_and_print ( const gss_name_t principalName, char *outUserID )
{
	char		   *p			= NULL;
	OM_uint32		minStatus	= 0;
	OM_uint32		majStatus	= 0;
	gss_OID			mechid;
	gss_buffer_desc nameToken;

	majStatus = gss_display_name( &minStatus, principalName, &nameToken, &mechid );

	p = strstr( (char *)nameToken.value, "@" );
	if ( p != NULL )
	{
		strncpy( outUserID, (char *)nameToken.value, p - (char *)nameToken.value );
	}
	else
	{
		strncpy( outUserID, (char *)nameToken.value, nameToken.length );
	}

	(void)gss_release_buffer( &minStatus, &nameToken );

	return( majStatus );

} /* export_and_print */


/* -----------------------------------------------------------
	- odgetuseropts

		Get user options
 * ----------------------------------------------------------- */

int odgetuseropts ( const char *inUserID, struct od_user_opts *inOutOpts )
{
	int		r = 0;

	if ( inOutOpts != NULL )
	{
		memset( inOutOpts, 0, sizeof( struct od_user_opts ) );

		inOutOpts->fAcctState = eUnknownAcctState;
		inOutOpts->fIMAPLogin = eAcctDisabled;
		inOutOpts->fPOP3Login = eAcctDisabled;

		r = aodGetUserOptions( inUserID, inOutOpts );
	}
	return( r );

} /* odgetuseropts */


void log_errors ( char *inName, OM_uint32 inMajor, OM_uint32 inMinor )
{
	OM_uint32		msg_context = 0;
	OM_uint32		minStatus = 0;
	OM_uint32		majStatus = 0;
	gss_buffer_desc errBuf;
	int				count = 1;

	do {
		majStatus = gss_display_status( &minStatus, inMajor, GSS_C_GSS_CODE, GSS_C_NULL_OID, &msg_context, &errBuf );

		syslog( LOG_ERR, "  Major Error (%d): %s (%s)", count, (char *)errBuf.value, inName );

		majStatus = gss_release_buffer( &minStatus, &errBuf );
		++count;
	} while ( msg_context != 0 );

	count = 1;
	msg_context = 0;
	do {
		majStatus = gss_display_status( &minStatus, inMinor, GSS_C_MECH_CODE, GSS_C_NULL_OID, &msg_context, &errBuf );

		syslog( LOG_ERR, "  Minor Error (%d): %s (%s)", count, (char *)errBuf.value, inName );

		majStatus = gss_release_buffer( &minStatus, &errBuf );
		++count;

	} while ( msg_context != 0 );

} // LogErrors
