/* simple.c -- Security Protocol Module. */
/* Jeff Hostetler, Spyglass, Inc., 1994. */
/* Copyright (C) 1994, Spyglass, Inc.  All rights reserved. */

#include "module.h"

#define MAXFIELD        64
#define MAXHASHOUTPUT   33

//
// Must be the same length as same fields in HTSPM_H
//
extern unsigned char			gszMenuName[32];	/* short name for menu-command on menu (provided by SPM) */
extern unsigned char			gszStatusText[80];	/* long description (for status bar while menu highlighted) (provided by SPM) */

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

static unsigned char * x_GetSubValue(HTHeaderSVList * svl, unsigned char * name)
{
    /* return pointer to value buffer in list */
    
    while (svl)
    {
        if (strcasecomp(svl->name,name)==0)
            return svl->value;
        svl=svl->next;
    }

    return NULL;
}
/*****************************************************************/

static unsigned char * x_ComposeAuthResponse(F_UserInterface fpUI,
                                             void * pvOpaqueOS,
                                             unsigned char * szUserName,
                                             unsigned char * szRealm,
                                             unsigned char * szNonce,
                                             unsigned char * szOpaque,
                                             unsigned char * szPassword,
                                             unsigned char * szMethod,
                                             unsigned char * szURI)
{
    /* NOTE: caller must free the string we return. */
    
    unsigned char szResponseMD5[MAXHASHOUTPUT+1];
    unsigned char szHash1[MAXHASHOUTPUT+1];
    unsigned char szHash2[MAXHASHOUTPUT+1];
    unsigned char * szFormat;
    unsigned char * szTemp;
    int len;

#define BASE_STRING     "%s username=\"%s\", realm=\"%s\", nonce=\"%s\", response=\"%s\", uri=\"%s\""
#define OPAQUE_STRING   ", opaque=\"%s\""
    
    if (*szOpaque)
        szFormat = BASE_STRING OPAQUE_STRING;
    else
        szFormat = BASE_STRING;
    
    len = (  strlen(szFormat)
           + strlen(SCHEME_NAME) + strlen(szUserName) + strlen(szRealm)
           + strlen(szNonce) + MAXHASHOUTPUT + strlen(szURI)
           + strlen(szOpaque));

    szTemp = spm_calloc(fpUI,pvOpaqueOS,1,len);
    if (!szTemp)
        return NULL;

    sprintf(szTemp,"%s:%s:%s",szUserName,szRealm,szPassword);   /* generate "U:R:P" */
    md5(szTemp,szHash1);                                        /* compute "H(U:R:P)" */
    sprintf(szTemp,"%s:%s",szMethod,szURI);                     /* generate "M:U" */
    md5(szTemp,szHash2);                                        /* compute "H(M:U)" */
    sprintf(szTemp,"%s:%s:%s",szHash1,szNonce,szHash2);         /* generate "H(U:R:P):N:H(M:U)" */
    md5(szTemp,szResponseMD5);                                  /* compute "H(H(U:R:P):N:H(M:U))" */

    if (*szOpaque)
        sprintf(szTemp,
                szFormat,
                SCHEME_NAME,szUserName,szRealm,szNonce,szResponseMD5,szURI,szOpaque);
    else
        sprintf(szTemp,
                szFormat,
                SCHEME_NAME,szUserName,szRealm,szNonce,szResponseMD5,szURI);

    return szTemp;
}

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


static HTSPMStatusCode x_QueryUserForInfo(F_UserInterface fpUI,
                                          void * pvOpaqueOS,
                                          HTHeaderList * hlProtocol,
                                          HTHeader * hRequest,
                                          unsigned char * szRealm,
                                          unsigned char * szUserName,
                                          unsigned char * szPassword)
{
    HTSPMStatusCode hsc;

    szUserName[0] = szPassword[0] = 0;

    hsc = (Dialog_QueryUserForInfo(fpUI,pvOpaqueOS,hRequest,szRealm,szUserName,szPassword,MAXFIELD));
    if (hsc != HTSPM_STATUS_OK)
        return hsc;

#ifdef DEBUG
    {
        unsigned char buf[4*MAXFIELD];
        sprintf(buf,"Received [%s %s %s]",szRealm,szUserName,szPassword);
        (*fpUI)(pvOpaqueOS,UI_SERVICE_DEBUG_MESSAGE,buf,NULL);
    }
#endif /* DEBUG */

    return hsc;
}


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

static HTSPMStatusCode module__ListAbilities(F_UserInterface fpUI,
                                             void * pvOpaqueOS,
                                             HTSPM * htspm,
                                             HTHeader * hRequest)
{
    /* state our existence. */
    
    HTHeaderList * hlCur;
    unsigned char buf[256];

    sprintf(buf,EXTENSION_VALUE_SYNTAX,EXTENSION_CATEGORY,EXTENSION_CLASS);
    
    hlCur = HL_AppendNewNameValue(fpUI,pvOpaqueOS,hRequest,EXTENSION_HEADER,buf);

    return ( (hlCur != NULL) ? HTSPM_STATUS_OK : HTSPM_ERROR );
}

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

static HTSPMStatusCode module__ProcessResponse(F_UserInterface fpUI,
                                               void * pvOpaqueOS,
                                               HTSPM * htspm,
                                               HTHeaderList * hlProtocol,
                                               HTHeader * hRequest,
                                               HTHeader * hResponse,
                                               HTHeader ** phNewRequest,
                                               unsigned int bNonBlock)
{
    HTSPMStatusCode htsc;
    HTHeaderList * hlCur;

    unsigned char * szRealm;
    unsigned char * szNonce;
    unsigned char * szOpaque;
    unsigned char * szOldNonce;
    unsigned char szUserName[MAXFIELD];
    unsigned char szPassword[MAXFIELD];
    unsigned long bModelessDialogs = 0;
    unsigned char * szResponse = NULL;
    unsigned long bResult;
    PWCI * pwci;
    HTHeaderList * hlParsed;
    
    /* get important fields from server response */

    hlParsed = NULL;
    if ((*fpUI)(pvOpaqueOS,UI_SERVICE_PARSE_HEADER,hlProtocol,&hlParsed) != UI_SC_STATUS_OK)
    {
        bResult = FALSE;
        goto Finish;
    }
    
    szRealm = x_GetSubValue(hlParsed->sub_value,"realm");
    szNonce = x_GetSubValue(hlParsed->sub_value,"nonce");
    szOpaque = x_GetSubValue(hlParsed->sub_value,"opaque");
    szOldNonce = x_GetSubValue(hlParsed->sub_value,"stale");

    if (!szOpaque)                      /* opaque data from server is optional */
        szOpaque = "";
    
    /* see if there is already an Auth header in the request. */

    hlCur = HL_FindHeader(hRequest, "Authorization");

#ifdef UNIX
    {
        /* see if we already have a modeless dialog up for this request */
        extern void * Dialog_IsActive(HTHeader *);

        bModelessDialogs = (Dialog_IsActive(hRequest) != NULL);
    }
#endif /* UNIX */

    if (bModelessDialogs)
        goto QueryUser;

    /* see if we have a cache entry for this destination. */
    
    pwci = pwc_Lookup(fpUI,pvOpaqueOS,htspm->pvOpaque,
                      hRequest->host,hRequest->uri,
                      szRealm);

    /* see if cache is valid and if we can satisfy the request
     * without bothering the user.
     */
    
    if (pwci)
    {
        if (!hlCur)
        {
            /* we have a cache value and there was no auth in the
             * previous request, so we must assume that the cache
             * entry is OK.  so let's use it.
             */

#ifdef DEBUG
            {
                unsigned char msg[200];
                sprintf(msg,
                        "%s __ProcessResponse: Using cache value [%s %s %s]",
                        SCHEME_NAME,szRealm,pwci->szUserName,pwci->szPassword);
                (*fpUI)(pvOpaqueOS,UI_SERVICE_DEBUG_MESSAGE,msg,NULL);
            }
#endif /* DEBUG */

            szResponse = x_ComposeAuthResponse(fpUI,pvOpaqueOS,
                                               pwci->szUserName,
                                               szRealm,
                                               szNonce,
                                               szOpaque,
                                               pwci->szPassword,
                                               hRequest->command,
                                               hRequest->uri);
            if (!szResponse)
            {
                bResult = FALSE;
                goto Finish;
            }

            /* create new header for the auth */
            
            hlCur = HL_AppendNewNameValue(fpUI,pvOpaqueOS,hRequest,"Authorization",szResponse);
            spm_free(fpUI,pvOpaqueOS,szResponse);
            szResponse = NULL;
            bResult = (hlCur != NULL);
            goto Finish;
        }

        /* we have a cache value, but there was an auth header
         * in the request (that bounced).  since we're the only
         * ones that could have put in an auth header, we assume
         * that we did so in a previous call using the current
         * cache entry.  before we assume that the cached value
         * is stale, see if the server rejected it because of an
         * old nonce value.
         */

        if (szOldNonce && (*szOldNonce) && (strcasecomp(szOldNonce,"true")==0))
        {
            /* Server indicated that we should try again
             * using the new nonce value supplied.  (the
             * server is *not* asserting that the password
             * is valid -- just that it refuses to use old
             * nonce values.
             */
            
#ifdef DEBUG
            {
                unsigned char msg[200];
                sprintf(msg,
                        "%s __ProcessResponse: Server refused old nonce; retrying with cache value [%s %s %s]",
                        SCHEME_NAME,szRealm,pwci->szUserName,pwci->szPassword);
                (*fpUI)(pvOpaqueOS,UI_SERVICE_DEBUG_MESSAGE,msg,NULL);
            }
#endif /* DEBUG */

            szResponse = x_ComposeAuthResponse(fpUI,pvOpaqueOS,
                                               pwci->szUserName,
                                               szRealm,
                                               szNonce,
                                               szOpaque,
                                               pwci->szPassword,
                                               hRequest->command,
                                               hRequest->uri);
            if (!szResponse)
            {
                bResult = FALSE;
                goto Finish;
            }

            /* update existing auth header */
        
            bResult = spm_CloneString(fpUI,pvOpaqueOS,&hlCur->value,szResponse);
            spm_free(fpUI,pvOpaqueOS,szResponse);
            szResponse = NULL;
            goto Finish;
        }

        /* we must assume that our cache value is stale */
        
#ifdef DEBUG
        {
            unsigned char msg[200];
            sprintf(msg,
                    "%s __ProcessResponse: Assuming cache stale [%s %s %s]",
                    SCHEME_NAME,szRealm,pwci->szUserName,pwci->szPassword);
            (*fpUI)(pvOpaqueOS,UI_SERVICE_DEBUG_MESSAGE,msg,NULL);
        }
#endif /* DEBUG */
    }


#ifndef UNIX                            /* unix dialog start returns immediately */
    /* fall-thru, we must ask user for information */

    if (bNonBlock)
    {
        HL_Delete(fpUI,pvOpaqueOS,hlParsed);
        return HTSPM_STATUS_WOULD_BLOCK;
    }
#endif /* not UNIX */

 QueryUser:

    htsc = x_QueryUserForInfo(fpUI,pvOpaqueOS,hlProtocol,hRequest,szRealm,szUserName,szPassword);
    if (htsc != HTSPM_STATUS_OK)
    {
        HL_Delete(fpUI,pvOpaqueOS,hlParsed);
        return htsc;
    }
    
    /* update the cache */
    
    pwc_Store(fpUI,pvOpaqueOS,htspm->pvOpaque,
              hRequest->host,hRequest->uri,
              szUserName,szRealm,szNonce,szOpaque,szPassword);

    szResponse = x_ComposeAuthResponse(fpUI,pvOpaqueOS,
                                       szUserName,szRealm,
                                       szNonce,szOpaque,
                                       szPassword,
                                       hRequest->command,
                                       hRequest->uri);
    if (!szResponse)
    {
        bResult = FALSE;
        goto Finish;
    }

    if (hlCur)
    {
        /* update existing auth header */
        
        bResult = spm_CloneString(fpUI,pvOpaqueOS,&hlCur->value,szResponse);
    }
    else
    {
        /* create new header for the auth */
        
        hlCur = HL_AppendNewNameValue(fpUI,pvOpaqueOS,hRequest,"Authorization",szResponse);
        bResult = (hlCur != NULL);
    }
    spm_free(fpUI,pvOpaqueOS,szResponse);
    szResponse = NULL;

    
 Finish:

    /* For this SPM, we always just update the original header
     * and return.  Other SPM's may need to create a new one.
     */

    if (hlParsed)
    {
        HL_Delete(fpUI,pvOpaqueOS,hlParsed);
        hlParsed = NULL;
    }
    
    if (bResult)
        return HTSPM_STATUS_RESUBMIT_OLD;

    (void)(*fpUI)(pvOpaqueOS,UI_SERVICE_ERROR_MESSAGE,
                  "Unable to allocate memory to create authentication.",NULL);
    
    return HTSPM_ERROR;
}

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

static HTSPMStatusCode module__PreProcessRequest(F_UserInterface fpUI,
                                                 void * pvOpaqueOS,
                                                 HTSPM * htspm,
                                                 HTHeader * hRequest,
                                                 HTHeader ** phNewRequest)
{
    /* Take a guess at whether we can prevent a 401
     * server response, by pre-loading the request with
     * the necessary security information.
     */
    
    HTHeaderList * hlCur;
    unsigned char * szResponse = NULL;
    unsigned long bResult;
    PWCI * pwci;

    /* see if we have a cache entry for this destination
     * (ignoring the Realm).
     */
    
    pwci = pwc_Lookup(fpUI,pvOpaqueOS,htspm->pvOpaque,
                      hRequest->host,hRequest->uri,
                      NULL);

    if (!pwci)                      /* no matching item in the cache, we could */
        return HTSPM_ERROR;         /* not help, let someone else try. */

    /* let's make a guess and update the existing request */
    
#ifdef DEBUG
    {
        unsigned char msg[200];
        sprintf(msg,
                "%s __PreProcessResponse: Using cache value [%s %s %s]",
                SCHEME_NAME,pwci->szRealm,pwci->szUserName,pwci->szPassword);
        (*fpUI)(pvOpaqueOS,UI_SERVICE_DEBUG_MESSAGE,msg,NULL);
    }
#endif /* DEBUG */

    szResponse = x_ComposeAuthResponse(fpUI,pvOpaqueOS,
                                       pwci->szUserName,pwci->szRealm,
                                       pwci->szNonce,pwci->szOpaque,pwci->szPassword,
                                       hRequest->command,
                                       hRequest->uri);

    if (!szResponse)                    /* could not allocate memory, give */
        return HTSPM_ERROR;             /* up and let someone else try. */

    /* see if there is already an Auth header in the request. */

    hlCur = HL_FindHeader(hRequest, "Authorization");
    
    if (hlCur)
    {
        /* update existing auth header */
        
        bResult = spm_CloneString(fpUI,pvOpaqueOS,&hlCur->value,szResponse);
    }
    else
    {
        /* create new header for the auth */
        
        hlCur = HL_AppendNewNameValue(fpUI,pvOpaqueOS,hRequest,"Authorization",szResponse);
        bResult = (hlCur != NULL);
    }
    spm_free(fpUI,pvOpaqueOS,szResponse);
    szResponse = NULL;

    /* we updated the original request, so tell the client to send it. */
    
    return HTSPM_STATUS_RESUBMIT_OLD;
}

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

static HTSPMStatusCode module__Unload(F_UserInterface fpUI,
                                      void * pvOpaqueOS,
                                      HTSPM * htspm)
{
    pwc_Destroy(fpUI,pvOpaqueOS,htspm->pvOpaque);
    htspm->pvOpaque = NULL;
    
    return HTSPM_STATUS_OK;
}

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

static HTSPMStatusCode module__DownCall(HTSPM_ServiceId sid,            /* down-call service id */
                                        F_UserInterface fpUI,           /* common arg to all down calls */
                                        void * pvOpaqueOS,              /* common arg to all down calls */
                                        HTSPM * htspm,                  /* common arg to all down calls */
                                        void * pvMethodData)            /* per-method data */
{
#ifdef DEBUG
    {
        unsigned char msg[200];
        sprintf(msg,"Digest_DownCall: ServiceId [0x%x]\n",sid);
        (*fpUI)(pvOpaqueOS,UI_SERVICE_DEBUG_MESSAGE,msg,NULL);
    }
#endif /* DEBUG */

    switch (sid)
    {
    case HTSPM_SERVICE_UNLOAD:
        {
            return module__Unload(fpUI,pvOpaqueOS,htspm);
        }
    case HTSPM_SERVICE_MENUCOMMAND:
        {
            D_MenuCommand * pmc = pvMethodData;
            return Dialog_MenuCommand(fpUI,pvOpaqueOS,htspm,
                                      pmc->pszMoreInfo);
        }

    case HTSPM_SERVICE_LISTABILITIES:
        {
            D_ListAbilities * pla = pvMethodData;
            return module__ListAbilities(fpUI,pvOpaqueOS,htspm,
                                         pla->hRequest);
        }

    case HTSPM_SERVICE_PROCESSRESPONSE:
        {
            D_ProcessResponse * ppr = pvMethodData;
            return module__ProcessResponse(fpUI,pvOpaqueOS,htspm,
                                           ppr->hlProtocol,
                                           ppr->hRequest,
                                           ppr->hResponse,
                                           ppr->phNewRequest,
                                           ppr->bNonBlock);
        }

    case HTSPM_SERVICE_PREPROCESSREQUEST:
        {
            D_PreProcessRequest * pppr = pvMethodData;
            return module__PreProcessRequest(fpUI,pvOpaqueOS,htspm,
                                             pppr->hRequest,
                                             pppr->phNewRequest);
        }

    default:
        return HTSPM_ERROR_UNIMPLEMENTED;
    }
    /*NOTREACHED*/
}

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

/* WARNING: the name of this function is exported and used in the
 * WARNING: mosaic .ini file
 */

#ifdef WIN32
__declspec(dllexport) 
#endif
HTSPMStatusCode Digest_Load(F_UserInterface fpUI,
                            void * pvOpaqueOS,
                            HTSPM * htspm)
{
    if (htspm->ulStructureVersion != HTSPM_STRUCTURE_VERSION)
        return HTSPM_ERROR_WRONG_VERSION;

    /* we use the opaque field provided to us to store our password cache. */
    
    htspm->pvOpaque = pwc_Create(fpUI,pvOpaqueOS);

    htspm->f_downcall = module__DownCall;
    
    //strncpy(htspm->szMenuName,SCHEME_FULL_NAME,(sizeof(htspm->szMenuName)-1));
    //strncpy(htspm->szStatusText,SCHEME_ABOUT_NAME,(sizeof(htspm->szStatusText)-1));

    strncpy(htspm->szMenuName, gszMenuName, (sizeof(htspm->szMenuName)));
    strncpy(htspm->szStatusText, gszStatusText, (sizeof(htspm->szStatusText)));

    {
        UI_ProtocolId uid;

        uid.htspm           = htspm;
        uid.szIdentHeader   = "WWW-Authenticate";           /* note: no colon */
        uid.szIdentValue    = SCHEME_NAME;
        (*fpUI)(pvOpaqueOS,UI_SERVICE_REGISTER_PROTOCOL,&uid,NULL);
    }       
        
    return HTSPM_STATUS_OK;
}
