/*
 * Copyright 2000, International Business Machines Corporation and others.
 * All Rights Reserved.
 *
 * This software has been released under the terms of the IBM Public
 * License.  For details, see the LICENSE file in the top-level source
 * directory or online at http://www.openafs.org/dl/license10.html
 */

#include <afsconfig.h>
#include "afs/param.h"


#if !defined(AFS_NONFSTRANS) || defined(AFS_AIX_IAUTH_ENV)
#include "afs/sysincludes.h"	/* Standard vendor system headers */
#include "afsincludes.h"	/* Afs-based standard headers */
#include "afs/afs_stats.h"	/* statistics */
#include "afs/nfsclient.h"
#include "rx/rx_globals.h"
#include "afs/pagcb.h"

void afs_nfsclient_hold(), afs_PutNfsClientPag(), afs_nfsclient_GC();
static void afs_nfsclient_getcreds();
int afs_nfsclient_sysname(), afs_nfsclient_stats(), afs_nfsclient_checkhost();
afs_uint32 afs_nfsclient_gethost();
#ifdef AFS_AIX_IAUTH_ENV
int afs_allnfsreqs, afs_nfscalls;
#endif

/* routines exported to the "AFS exporter" layer */
struct exporterops nfs_exportops = {
    afs_nfsclient_reqhandler,
    afs_nfsclient_hold,
    afs_PutNfsClientPag,	/* Used to be afs_nfsclient_rele */
    afs_nfsclient_sysname,
    afs_nfsclient_GC,
    afs_nfsclient_stats,
    afs_nfsclient_checkhost,
    afs_nfsclient_gethost
};


struct nfsclientpag *afs_nfspags[NNFSCLIENTS];
afs_lock_t afs_xnfspag /*, afs_xnfsreq */ ;
extern struct afs_exporter *afs_nfsexporter;

/* Creates an nfsclientpag structure for the (uid, host) pair if one doesn't
 * exist. RefCount is incremented and it's time stamped. */
static struct nfsclientpag *
afs_GetNfsClientPag(afs_int32 uid, afs_uint32 host)
{
    struct nfsclientpag *np;
    afs_int32 i, now;

#if defined(AFS_SGIMP_ENV)
    osi_Assert(ISAFS_GLOCK());
#endif
    AFS_STATCNT(afs_GetNfsClientPag);
    i = NHash(host);
    now = osi_Time();
    ObtainWriteLock(&afs_xnfspag, 314);
    for (np = afs_nfspags[i]; np; np = np->next) {
	if (np->uid == uid && np->host == host) {
	    np->refCount++;
	    np->lastcall = now;
	    ReleaseWriteLock(&afs_xnfspag);
	    return np;
	}
    }
    /* next try looking for NOPAG dude, if we didn't find an exact match */
    for (np = afs_nfspags[i]; np; np = np->next) {
	if (np->uid == NOPAG && np->host == host) {
	    np->refCount++;
	    np->lastcall = now;
	    ReleaseWriteLock(&afs_xnfspag);
	    return np;
	}
    }
    np = afs_osi_Alloc(sizeof(struct nfsclientpag));
    osi_Assert(np != NULL);
    memset(np, 0, sizeof(struct nfsclientpag));
    /* Copy the necessary afs_exporter fields */
    memcpy((char *)np, (char *)afs_nfsexporter, sizeof(struct afs_exporter));
    np->next = afs_nfspags[i];
    afs_nfspags[i] = np;
    np->uid = uid;
    np->host = host;
    np->refCount = 1;
    np->lastcall = now;
    ReleaseWriteLock(&afs_xnfspag);
    return np;
}


/* Decrement refCount; must always match a previous afs_FindNfsClientPag/afs_GetNfsClientPag call .
It's also called whenever a unixuser structure belonging to the remote user associated with the nfsclientpag structure, np, is garbage collected. */
void
afs_PutNfsClientPag(np)
     struct nfsclientpag *np;
{
#if defined(AFS_SGIMP_ENV)
    osi_Assert(ISAFS_GLOCK());
#endif
    AFS_STATCNT(afs_PutNfsClientPag);
    --np->refCount;
}


/* Return the nfsclientpag structure associated with the (uid, host) or
 * {pag, host} pair, if pag is nonzero. RefCount is incremented and it's
 * time stamped. */
static struct nfsclientpag *
afs_FindNfsClientPag(afs_int32 uid, afs_uint32 host, afs_int32 pag)
{
    struct nfsclientpag *np;
    afs_int32 i;

#if defined(AFS_SGIMP_ENV)
    osi_Assert(ISAFS_GLOCK());
#endif
    AFS_STATCNT(afs_FindNfsClientPag);
    i = NHash(host);
    ObtainWriteLock(&afs_xnfspag, 315);
    for (np = afs_nfspags[i]; np; np = np->next) {
	if (np->host == host) {
	    if ((pag && pag == np->pag) || (!pag && (uid == np->uid))) {
		np->refCount++;
		np->lastcall = osi_Time();
		ReleaseWriteLock(&afs_xnfspag);
		return np;
	    }
	}
    }
    /* still not there, try looking for a wildcard dude */
    for (np = afs_nfspags[i]; np; np = np->next) {
	if (np->host == host) {
	    if (np->uid == NOPAG) {
		np->refCount++;
		np->lastcall = osi_Time();
		ReleaseWriteLock(&afs_xnfspag);
		return np;
	    }
	}
    }
    ReleaseWriteLock(&afs_xnfspag);
    return NULL;
}


/* routine to initialize the exporter, made global so we can call it
 * from pioctl calls.
 */
struct afs_exporter *afs_nfsexported = 0;
static afs_int32 init_nfsexporter = 0;

void
afs_nfsclient_init(void)
{
#if defined(AFS_SGIMP_ENV)
    osi_Assert(ISAFS_GLOCK());
#endif
    if (!init_nfsexporter) {

	init_nfsexporter = 1;
	LOCK_INIT(&afs_xnfspag, "afs_xnfspag");
	afs_nfsexported =
	    exporter_add(0, &nfs_exportops, EXP_EXPORTED, EXP_NFS, NULL);
    }
}


/* Main handler routine for the NFS exporter. It's called in the early
 * phases of any remote call (via the NFS server or pioctl).
 */
int
afs_nfsclient_reqhandler(struct afs_exporter *exporter,
			 afs_ucred_t **cred,
			 afs_uint32 host, afs_int32 *pagparam,
			 struct afs_exporter **outexporter)
{
    struct nfsclientpag *np, *tnp;
    extern struct unixuser *afs_FindUser(), *afs_GetUser();
    struct unixuser *au = 0;
    afs_int32 uid, pag, code = 0;

    AFS_ASSERT_GLOCK();
    AFS_STATCNT(afs_nfsclient_reqhandler);
    if (!afs_nfsexporter)
	afs_nfsexporter = afs_nfsexported;

    afs_nfsexporter->exp_stats.calls++;
    if (!(afs_nfsexporter->exp_states & EXP_EXPORTED)) {
	/* No afs requests accepted as long as EXPORTED flag is turned 'off'.
	 * Set/Reset via a pioctl call (fs exportafs). Note that this is on
	 * top of the /etc/exports nfs requirement (i.e. /afs must be
	 * exported to all or whomever there too!)
	 */
	afs_nfsexporter->exp_stats.rejectedcalls++;
	return EINVAL;
    }
/*    ObtainWriteLock(&afs_xnfsreq); */
    pag = PagInCred(*cred);
#if defined(AFS_SUN510_ENV)
    uid = crgetuid(*cred);
#else
    uid = afs_cr_uid(*cred);
#endif
    /* Do this early, so pag management knows */
    afs_set_cr_rgid(*cred, NFSXLATOR_CRED);	/* Identify it as nfs xlator call */
    if ((afs_nfsexporter->exp_states & EXP_CLIPAGS) && pag != NOPAG) {
	uid = pag;
    } else if (pag != NOPAG) {
	/* Do some minimal pag verification */
	if (pag > getpag()) {
	    pag = NOPAG;	/* treat it as not paged since couldn't be good  */
	} else {
	    if ((au = afs_FindUser(pag, -1, READ_LOCK))) {
		if (!au->exporter) {
		    pag = NOPAG;
		    afs_PutUser(au, READ_LOCK);
		    au = NULL;
		}
	    } else
		pag = NOPAG;	/*  No unixuser struct so pag not trusted  */
	}
    }
    np = afs_FindNfsClientPag(uid, host, 0);
    afs_Trace4(afs_iclSetp, CM_TRACE_NFSREQH, ICL_TYPE_INT32, pag,
	       ICL_TYPE_LONG, afs_cr_uid(*cred), ICL_TYPE_INT32, host,
	       ICL_TYPE_POINTER, np);
    /* If remote-pags are enabled, we are no longer interested in what PAG
     * they claimed, and from here on we should behave as if they claimed
     * none at all, which is to say we use the (local) pag named in the
     * nfsclientpag structure (if any).  This is deferred until here so
     * that we can log the PAG they claimed.
     */
    if ((afs_nfsexporter->exp_states & EXP_CLIPAGS))
       	pag = NOPAG;
    if (!np) {
	/* Even if there is a "good" pag coming in we don't accept it if no
	 * nfsclientpag struct exists for the user since that would mean
	 * that the translator rebooted and therefore we ignore all older
	 * pag values
	 */
	if ((code = setpag(cred, -1, &pag, 0))) {
	    if (au)
		afs_PutUser(au, READ_LOCK);
/*	    ReleaseWriteLock(&afs_xnfsreq);		*/
#if defined(KERNEL_HAVE_UERROR)
	    setuerror(code);
#endif
	    return (code);
	}
	np = afs_GetNfsClientPag(uid, host);
	np->pag = pag;
	np->client_uid = afs_cr_uid(*cred);
    } else {
	if (pag == NOPAG) {
	    if ((code = setpag(cred, np->pag, &pag, 0))) {
		afs_PutNfsClientPag(np);
/*		ReleaseWriteLock(&afs_xnfsreq);	*/
#if defined(KERNEL_HAVE_UERROR)
		setuerror(code);
#endif
		return (code);
	    }
	} else if (au->exporter
		   && ((struct afs_exporter *)np != au->exporter)) {
	    tnp = (struct nfsclientpag *)au->exporter;
	    if (tnp->uid && (tnp->uid != (afs_int32) - 2)) {	/* allow "root" initiators */
		/* Pag doesn't belong to caller; treat it as an unpaged call too */
		if ((code = setpag(cred, np->pag, &pag, 0))) {
		    afs_PutNfsClientPag(np);
		    afs_PutUser(au, READ_LOCK);
		    /*      ReleaseWriteLock(&afs_xnfsreq);     */
#if defined(KERNEL_HAVE_UERROR)
		    setuerror(code);
#endif
		    return (code);
		}
		afs_nfsexporter->exp_stats.invalidpag++;
	    }
	}
    }
    if (au)
	afs_PutUser(au, READ_LOCK);
    /* do not get a lock on au; afs_nfsclient_getcreds may write-lock the
     * same unixuser */
    au = afs_GetUser(pag, -1, 0);
    if (!(au->exporter)) {	/* Created new unixuser struct */
	np->refCount++;		/* so it won't disappear */
	au->exporter = (struct afs_exporter *)np;
	if ((afs_nfsexporter->exp_states & EXP_CALLBACK))
	    afs_nfsclient_getcreds(au);
    } else while (au->states & UNFSGetCreds) {
	afs_osi_Sleep((void *)au);
    }
    *pagparam = pag;
    *outexporter = (struct afs_exporter *)np;
    afs_PutUser(au, 0);
/*    ReleaseWriteLock(&afs_xnfsreq);	*/
    return 0;
}

void
afs_nfsclient_getcreds(struct unixuser *au)
{
    struct nfsclientpag *np = (struct nfsclientpag *)(au->exporter);
    struct rx_securityClass *csec;
    struct rx_connection *tconn;
    union tokenUnion *tokenPtr;
    struct rxkadToken *token;
    SysNameList tsysnames;
    CredInfos tcreds;
    CredInfo *tcred;
    struct unixuser *tu;
    struct cell *tcell;
    int code, i, cellnum;

    au->states |= UNFSGetCreds;
    memset(&tcreds, 0, sizeof(tcreds));
    memset(&tsysnames, 0, sizeof(tsysnames));

    /* Get a connection */
    /* This sucks a little.  We should cache the connections or something.
     * But at this point I don't yet think it's worth the effort.
     */
    csec = rxnull_NewClientSecurityObject();
    AFS_GUNLOCK();
    tconn = rx_NewConnection(np->host, htons(7001), PAGCB_SERVICEID, csec, 0);
    AFS_GLOCK();

    /* Get the sysname, if needed */
    if (!np->sysnamecount) {
	AFS_GUNLOCK();
	code = PAGCB_GetSysName(tconn, np->uid, &tsysnames);
	AFS_GLOCK();
	if (code ||
	    tsysnames.SysNameList_len <= 0 ||
	    tsysnames.SysNameList_len > MAXNUMSYSNAMES)
	    goto done;

	for(i = 0; i < np->sysnamecount; i++)
	    afs_osi_Free(np->sysname[i], MAXSYSNAME);

	np->sysnamecount = tsysnames.SysNameList_len;
	for(i = 0; i < np->sysnamecount; i++)
	    np->sysname[i] = tsysnames.SysNameList_val[i].sysname;
        afs_osi_Free(tsysnames.SysNameList_val,
                     tsysnames.SysNameList_len * sizeof(SysNameEnt));
    }

    /* Get credentials */
    AFS_GUNLOCK();
    code = PAGCB_GetCreds(tconn, np->uid, &tcreds);
    AFS_GLOCK();
    if (code)
	goto done;

    /* Now, set the credentials they gave us... */
    for (i = 0; i < tcreds.CredInfos_len; i++) {
	tcred = &tcreds.CredInfos_val[i];

	/* Find the cell.  If it is unknown to us, punt this entry. */
	tcell = afs_GetCellByName(tcred->cellname, READ_LOCK);
	afs_osi_Free(tcred->cellname, strlen(tcred->cellname) + 1);
	if (!tcell) {
	    memset(tcred->ct.HandShakeKey, 0, 8);
	    memset(tcred->st.st_val, 0, tcred->st.st_len);
	    afs_osi_Free(tcred->st.st_val, tcred->st.st_len);
	    continue;
	}
	cellnum = tcell->cellNum;
	afs_PutCell(tcell, READ_LOCK);

	/* Find the appropriate unixuser.  This might be the same as
	 * the one we were passed (au), but that's OK.
	 */
	tu = afs_GetUser(np->pag, cellnum, WRITE_LOCK);
	if (!(tu->exporter)) {	/* Created new unixuser struct */
	    np->refCount++;		/* so it won't disappear */
	    tu->exporter = (struct afs_exporter *)np;
	}

	afs_FreeTokens(&tu->tokens);

	/* Add a new rxkad token. Using the afs_AddRxkadToken interface
	 * would require another copy, so we do this the hard way */
	tokenPtr = afs_AddToken(&tu->tokens, 2);
	token = &tokenPtr->rxkad;
	token->ticket = tcred->st.st_val;
	token->ticketLen = tcred->st.st_len;

	/* copy the clear token */
	memset(&token->clearToken, 0, sizeof(token->clearToken));
	memcpy(token->clearToken.HandShakeKey, tcred->ct.HandShakeKey, 8);
	memset(tcred->ct.HandShakeKey, 0, 8);
	token->clearToken.AuthHandle     = tcred->ct.AuthHandle;
	token->clearToken.ViceId         = tcred->ct.ViceId;
	token->clearToken.BeginTimestamp = tcred->ct.BeginTimestamp;
	token->clearToken.EndTimestamp   = tcred->ct.EndTimestamp;

	/* Set everything else, reset connections, and move on. */
	tu->viceId = tcred->vid;
	tu->states |= UHasTokens;
	tu->states &= ~UTokensBad;
	afs_SetPrimary(tu, !!(tcred->states & UPrimary));
	tu->tokenTime = osi_Time();
	afs_ResetUserConns(tu);
	afs_PutUser(tu, WRITE_LOCK);
    }
    afs_osi_Free(tcreds.CredInfos_val, tcreds.CredInfos_len * sizeof(CredInfo));

done:
    AFS_GUNLOCK();
    rx_DestroyConnection(tconn);
    AFS_GLOCK();
    au->states &= ~UNFSGetCreds;
    afs_osi_Wakeup((void *)au);
}


/* It's called whenever a new unixuser structure is created for the remote
 * user associated with the nfsclientpag structure, np */
void
afs_nfsclient_hold(struct nfsclientpag *np)
{
#if defined(AFS_SGIMP_ENV)
    osi_Assert(ISAFS_GLOCK());
#endif
    AFS_STATCNT(afs_nfsclient_hold);
    np->refCount++;
}


/* check if this exporter corresponds to the specified host */
int
afs_nfsclient_checkhost(struct nfsclientpag *np, afs_uint32 host)
{
    if (np->type != EXP_NFS)
	return 0;
    return np->host == host;
}


/* get the host for this exporter, or 0 if there is an error */
afs_uint32
afs_nfsclient_gethost(struct nfsclientpag *np)
{
    if (np->type != EXP_NFS)
	return 0;
    return np->host;
}


/* if inname is non-null, a new system name value is set for the remote
 * user (inname contains the new sysname). In all cases, outname returns
 * the current sysname value for this remote user */
int
afs_nfsclient_sysname(struct nfsclientpag *np, char *inname,
		      char ***outname, int *num, int allpags)
{
    struct nfsclientpag *tnp;
    afs_int32 i;
    char *cp;
    int count, t;
#if defined(AFS_SGIMP_ENV)
    osi_Assert(ISAFS_GLOCK());
#endif
    AFS_STATCNT(afs_nfsclient_sysname);
    if (allpags > 0) {
	/* update every client, not just the one making the request */
	i = NHash(np->host);
	ObtainWriteLock(&afs_xnfspag, 315);
	for (tnp = afs_nfspags[i]; tnp; tnp = tnp->next) {
	    if (tnp != np && tnp->host == np->host)
		afs_nfsclient_sysname(tnp, inname, outname, num, -1);
	}
	ReleaseWriteLock(&afs_xnfspag);
    }
    if (inname) {
	    for(count=0; count < np->sysnamecount;++count) {
		afs_osi_Free(np->sysname[count], MAXSYSNAME);
		np->sysname[count] = NULL;
	    }
	for(count=0; count < *num;++count) {
	    np->sysname[count]= afs_osi_Alloc(MAXSYSNAME);
	    osi_Assert(np->sysname[count] != NULL);
	}
	cp = inname;
	for(count=0; count < *num;++count) {
	    t = strlen(cp);
	    memcpy(np->sysname[count], cp, t+1); /* include null */
	    cp += t+1;
	}
	np->sysnamecount = *num;
    }
    if (allpags >= 0) {
	/* Don't touch our arguments when called recursively */
	*outname = np->sysname;
	*num = np->sysnamecount;
	if (!np->sysname[0])
	    return ENODEV; /* XXX */
    }
    return 0;
}


/* Garbage collect routine for the nfs exporter. When pag is -1 then all
 * entries are removed (used by the nfsclient_shutdown routine); else if
 * it's non zero then only the entry with that pag is removed, else all
 * "timedout" entries are removed. TimedOut entries are those who have no
 * "unixuser" structures associated with them (i.e. unixusercnt == 0) and
 * they haven't had any activity the last NFSCLIENTGC seconds */
void
afs_nfsclient_GC(struct afs_exporter *exporter,
		 afs_int32 pag)
{
    struct nfsclientpag *np, **tnp, *nnp;
    afs_int32 i, delflag;
	int count;

#if defined(AFS_SGIMP_ENV)
    osi_Assert(ISAFS_GLOCK());
#endif
    AFS_STATCNT(afs_nfsclient_GC);
    ObtainWriteLock(&afs_xnfspag, 316);
    for (i = 0; i < NNFSCLIENTS; i++) {
	for (tnp = &afs_nfspags[i], np = *tnp; np; np = nnp) {
	    nnp = np->next;
	    delflag = 0;
	    if (np->refCount == 0 && np->lastcall < osi_Time() - NFSCLIENTGC)
		delflag = 1;
	    if ((pag == -1) || (!pag && delflag)
		|| (pag && (np->refCount == 0) && (np->pag == pag))) {
		*tnp = np->next;
		for(count=0; count < np->sysnamecount;++count) {
			afs_osi_Free(np->sysname[count], MAXSYSNAME);
		}
		afs_osi_Free(np, sizeof(struct nfsclientpag));
	    } else {
		tnp = &np->next;
	    }
	}
    }
    ReleaseWriteLock(&afs_xnfspag);
}


int
afs_nfsclient_stats(struct afs_exporter *export)
{
    /* Nothing much to do here yet since most important stats are collected
     * directly in the afs_exporter structure itself */
    AFS_STATCNT(afs_nfsclient_stats);
    return 0;
}

#ifdef AFS_AIX41_ENV
/* This is exposed so that vop_fid can test it, even if iauth is not
 *  installed.
 */
extern int afs_iauth_initd;
#endif

#ifdef AFS_AIX_IAUTH_ENV
char *afs_nfs_id = "AFSNFSTRANS";
/* afs_iauth_verify is the AFS authenticator for NFS.
 *
 * always returns 0.
 */
int
afs_iauth_verify(long id, fsid_t * fsidp, long host, int uid,
		 afs_ucred_t *credp, struct exportinfo *exp)
{
    int code;
    struct nfsclientpag *nfs_pag;
    afs_int32 dummypag;
    struct afs_exporter *outexporter = 0;


    /* Still needs basic test to see if exporter is on. And need to check the
     * whole no submounts bit.
     */

    if (id != (long)id)
	return 0;		/* not us. */

    /* Only care if it's AFS */
    if ((fsidp->val[0] != AFS_VFSMAGIC) || (fsidp->val[1] != AFS_VFSFSID)) {
	return 0;
    }

    AFS_GLOCK();
    code =
	afs_nfsclient_reqhandler((struct afs_exporter *)0, &credp, host,
				 &dummypag, &outexporter);
    if (!code && outexporter)
	EXP_RELE(outexporter);

    if (code) {
	/* ensure anonymous cred. */
	afs_set_cr_uid(credp, (uid_t) -2;	/* anonymous */
	afs_set_cr_ruid(credp, (uid_t) -2;
    }

    /* Mark this thread as an NFS translator thread. */
    afs_set_cr_rgid(credp, NFSXLATOR_CRED);

    AFS_GUNLOCK();
    return 0;
}

/* afs_iauth_register - register the iauth verify routine. Returns 0 on success
 * and -1 on failure. Can fail because DFS has already registered.
 */
int
afs_iauth_register(void)
{
    if (nfs_iauth_register((unsigned long)afs_nfs_id, afs_iauth_verify))
	return -1;
    else {
	afs_iauth_initd = 1;
	return 0;
    }
}

/* afs_iauth_unregister - unregister the iauth verify routine. Called on shutdown.
 */
void
afs_iauth_unregister(void)
{
    if (afs_iauth_initd)
	nfs_iauth_unregister((unsigned long)afs_nfs_id);
    afs_iauth_initd = 0;
}
#endif /* AFS_AIX_IAUTH_ENV */



void
shutdown_nfsclnt(void)
{
#if defined(AFS_SGIMP_ENV)
    osi_Assert(ISAFS_GLOCK());
#endif
    AFS_STATCNT(afs_nfsclient_shutdown);
#ifdef AFS_AIX_IAUTH_ENV
    afs_iauth_register();
#endif
    afs_nfsclient_GC(afs_nfsexporter, -1);
    init_nfsexporter = 0;
}
#endif /* AFS_NONFSTRANS */
