/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 4 -*-
 * ex: set softtabstop=4 tabstop=8 expandtab shiftwidth=4: *
 * Editor Settings: expandtabs and use 4 spaces for indentation */

/*
 * Copyright Likewise Software
 * All rights reserved.
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 *
 * LIKEWISE SOFTWARE MAKES THIS SOFTWARE AVAILABLE UNDER OTHER LICENSING
 * TERMS AS WELL.  IF YOU HAVE ENTERED INTO A SEPARATE LICENSE AGREEMENT
 * WITH LIKEWISE SOFTWARE, THEN YOU MAY ELECT TO USE THE SOFTWARE UNDER THE
 * TERMS OF THAT SOFTWARE LICENSE AGREEMENT INSTEAD OF THE TERMS OF THE GNU
 * GENERAL PUBLIC LICENSE, NOTWITHSTANDING THE ABOVE NOTICE.  IF YOU
 * HAVE QUESTIONS, OR WISH TO REQUEST A COPY OF THE ALTERNATE LICENSING
 * TERMS OFFERED BY LIKEWISE SOFTWARE, PLEASE CONTACT LIKEWISE SOFTWARE AT
 * license@likewisesoftware.com
 */

/*
 * Copyright (C) Likewise Software. All rights reserved.
 *
 * Module Name:
 *
 *        durable.c
 *
 * Abstract:
 *
 *        Likewise IO (LWIO) - SRV
 *
 *        Protocols API - SMBV2
 *
 *        Durable handle table
 *
 * Authors: Brian Koropoff (bkoropoff@likewise.com)
 *
 */

#include "includes.h"

#define INITIAL_TABLE_SIZE 121

// LOCK ORDER: SRV_FILE_2 -> SRV_DURABLE_TABLE -> SRV_OPLOCK_STATE_SMB_V2

PCVOID
SrvSmb2DurableGetKey(
    PLW_HASHTABLE_NODE pNode,
    PVOID pUserData
    )
{
    PSRV_DURABLE_ENTRY pEntry = LW_STRUCT_FROM_FIELD(pNode, SRV_DURABLE_ENTRY, HashNode);

    return &pEntry->DurableId;
}

ULONG
SrvSmb2DurableDigest(
    PCVOID pKey,
    PVOID pUserData
    )
{
    return (ULONG) *(ULONG64 const *) pKey;
}

BOOLEAN
SrvSmb2DurableEqual(
    PCVOID pKey1,
    PCVOID pKey2,
    PVOID pUserData
    )
{
    return *(ULONG64 const*) pKey1 == *(ULONG64 const*) pKey2;
}

NTSTATUS
SrvSmb2DurableInit(
    PSRV_DURABLE_TABLE pTable
    )
{
    NTSTATUS status = STATUS_SUCCESS;

    status = LwErrnoToNtStatus(pthread_mutex_init(&pTable->Mutex, NULL));
    BAIL_ON_NT_STATUS(status);
    pTable->bDestroyMutex = TRUE;

    status = LwErrnoToNtStatus(pthread_cond_init(&pTable->Condition, NULL));
    BAIL_ON_NT_STATUS(status);
    pTable->bDestroyCondition = TRUE;

    pTable->bShuttingDown = FALSE;

    status = LwRtlCreateHashTable(
        &pTable->pTable,
        SrvSmb2DurableGetKey,
        SrvSmb2DurableDigest,
        SrvSmb2DurableEqual,
        pTable,
        INITIAL_TABLE_SIZE);
    BAIL_ON_NT_STATUS(status);

error:

    if (!NT_SUCCESS(status))
    {
        SrvSmb2DurableShutdown(pTable);
    }

    return status;
}

static
VOID
SrvSmb2FreeDurableEntry(
    PSRV_DURABLE_ENTRY pEntry
    )
{
    PSRV_DURABLE_TABLE pTable = pEntry->pTable;

    // Reclaim timer should have been dealt with at this point
    LWIO_ASSERT(pEntry->pReclaimTimer == NULL);

    LwRtlHashTableRemove(pTable->pTable, &pEntry->HashNode);

    if (pEntry->pToken)
    {
        RtlReleaseAccessToken(&pEntry->pToken);
    }

    if (pEntry->hOplock)
    {
        // Safe from lock recursion since the oplock has already
        // dropped its reference to this durable handle.
        SrvReleaseOplock(pEntry->hOplock);
    }

    if (pEntry->FileHandle)
    {
        pEntry->CloseAcb.Callback = LwRtlMemoryFree;
        pEntry->CloseAcb.CallbackContext = pEntry;
        if (IoAsyncCloseFile(
            pEntry->FileHandle,
            &pEntry->CloseAcb,
            &pEntry->CloseStatus) != STATUS_PENDING)
        {
            LwRtlMemoryFree(pEntry);
        }
    }
    else
    {
        LwRtlMemoryFree(pEntry);
    }

    if (pTable->bShuttingDown)
    {
        pthread_cond_signal(&pTable->Condition);
    }
}

static
VOID
SrvSmb2DurableAcquireInLock(
    PSRV_DURABLE_ENTRY pEntry
    )
{
    // An entry whose refernce has gone to zero is already free and
    // cannot be referenced again.
    LWIO_ASSERT(pEntry->RefCount > 0);
    ++pEntry->RefCount;
}

static
VOID
SrvSmb2DurableReleaseInLock(
    PSRV_DURABLE_ENTRY pEntry
    )
{
    LWIO_ASSERT(pEntry->RefCount > 0);
    if (--pEntry->RefCount == 0)
    {
        SrvSmb2FreeDurableEntry(pEntry);
    }
}

static
VOID
SrvSmb2DurableRelease(
    PSRV_DURABLE_ENTRY pEntry
    )
{
    BOOLEAN bInLock = FALSE;

    LWIO_LOCK_MUTEX(bInLock, &pEntry->pTable->Mutex);

    SrvSmb2DurableReleaseInLock(pEntry);

    LWIO_UNLOCK_MUTEX(bInLock, &pEntry->pTable->Mutex);
}

VOID
SrvSmb2DurableShutdown(
    IN OUT PSRV_DURABLE_TABLE pTable
    )
{
    PLW_HASHTABLE_NODE pNode = NULL;
    PSRV_DURABLE_ENTRY pEntry = NULL;
    PSRV_OPLOCK pOplock = NULL;
    LW_HASHTABLE_ITER iter = LW_HASHTABLE_ITER_INIT;
    BOOLEAN bInLock = FALSE;
    LW_LIST_ENTRY OplockList;
    PLW_LIST_ENTRY pLinks = NULL;

    LwListInit(&OplockList);

    if (pTable->pTable)
    {
        LWIO_LOCK_MUTEX(bInLock, &pTable->Mutex);
        while ((pNode = LwRtlHashTableIterate(pTable->pTable, &iter)))
        {
            pEntry = LW_STRUCT_FROM_FIELD(pNode, SRV_DURABLE_ENTRY, HashNode);

            pOplock = pEntry->hOplock;
            pEntry->hOplock = NULL;

            // Three things can hold a reference to a durable handle:
            // an active open (SrvFile2), a durable handle expiration
            // timer, and an oplock.  The first will be released by
            // file rundown processing.  The latter two are only possible
            // if the handle is orphaned and we address them here.
            //
            // The timer can be disposed of safely while holding the
            // table lock.  The oplock, however, needs to call back
            // into this module in order to drop its reference to
            // the durable handle.  Save the oplocks in a list
            // and cancel them after the table lock is dropped to
            // avoid lock recursion.
            if (pOplock)
            {
                // Reuse the LeaseRecord links for our purposes.
                // Since we're shutting down, there's no need to
                // preserve any lease association.
                SrvLeaseRecordRemoveOplock(pOplock);
                LwListInsertTail(&OplockList, &pOplock->LeaseRecordEntry);
            }

            if (pEntry->pReclaimTimer)
            {
                if (SrvTimerCancelRequest(pEntry->pReclaimTimer, NULL) ==
                    STATUS_SUCCESS)
                {
                    // Timer won't run.
                    SrvSmb2DurableReleaseInLock(pEntry);
                }
                SrvTimerRelease(&pEntry->pReclaimTimer);
            }
        }
        LWIO_UNLOCK_MUTEX(bInLock, &pTable->Mutex);

        while ((pLinks = LwListRemoveHead(&OplockList)) != NULL)
        {
            pOplock = LW_STRUCT_FROM_FIELD(
                            pLinks,
                            SRV_OPLOCK,
                            LeaseRecordEntry);

            SrvCancelOplock(pOplock);
            SrvReleaseOplock(pOplock);
        }

        // Wait for all handles to drain.
        LWIO_LOCK_MUTEX(bInLock, &pTable->Mutex);
        pTable->bShuttingDown = TRUE;
        while (LwRtlHashTableGetCount(pTable->pTable) != 0)
        {
            int error = pthread_cond_wait(&pTable->Condition, &pTable->Mutex);
            LWIO_ASSERT(!error || (error == EINTR));
        }
        LWIO_UNLOCK_MUTEX(bInLock, &pTable->Mutex);

        LwRtlFreeHashTable(&pTable->pTable);
    }

    if (pTable->bDestroyMutex)
    {
        pthread_mutex_destroy(&pTable->Mutex);
        pTable->bDestroyMutex = FALSE;
    }

    if (pTable->bDestroyCondition)
    {
        pthread_cond_destroy(&pTable->Condition);
        pTable->bDestroyCondition = FALSE;
    }
}

NTSTATUS
SrvSmb2DurableCreate(
    IN PSRV_DURABLE_TABLE pTable,
    OUT PSRV_DURABLE_ENTRY* ppEntry,
    IN ULONG64 DurableId,
    IN PACCESS_TOKEN pToken,
    IN PLWIO_SRV_OPEN_STATE_2 pState,
    IN PLWIO_SRV_FILE_2 pSrvFile,
    IN OPTIONAL PGUID ClientLeaseKey
    )
{
    NTSTATUS status = STATUS_SUCCESS;
    PSRV_DURABLE_ENTRY pEntry = NULL;
    BOOLEAN bInLock = FALSE;

    LWIO_LOCK_MUTEX(bInLock, &pTable->Mutex);

    if (LwRtlHashTableFindKey(pTable->pTable, NULL, &DurableId) == STATUS_SUCCESS)
    {
        // FIXME: better handling of collisions?
        status = STATUS_INVALID_PARAMETER;
        BAIL_ON_NT_STATUS(status);
    }

    status = LW_RTL_ALLOCATE_AUTO(&pEntry);
    BAIL_ON_NT_STATUS(status);

    RtlReferenceAccessToken(pToken);
    SrvFile2Acquire(pSrvFile);
    pEntry->pTable = pTable;
    pEntry->pToken = pToken;
    pEntry->DurableId = DurableId;
    pEntry->pSrvFile = pSrvFile;
    pEntry->RefCount = 1;
    pEntry->OpenState = *pState;
    pEntry->bValid = TRUE;

    if (ClientLeaseKey)
    {
        RtlCopyMemory(
            &pEntry->ClientLeaseKey,
            ClientLeaseKey,
            sizeof(pEntry->ClientLeaseKey));

        pEntry->pClientLeaseKey = &pEntry->ClientLeaseKey;
    }

    LwRtlHashTableResizeAndInsert(pTable->pTable, &pEntry->HashNode, NULL);

cleanup:

    LWIO_UNLOCK_MUTEX(bInLock, &pTable->Mutex);

    *ppEntry = pEntry;

    return status;

error:
    if (pEntry)
    {
        LwRtlMemoryFree(pEntry);
        pEntry = NULL;
    }

    goto cleanup;
}

static
VOID
SrvSmb2DurableTimeout(
    PSRV_TIMER_REQUEST pTimerRequest,
    PVOID              pUserData
    )
{
    PSRV_DURABLE_ENTRY pEntry = pUserData;
    PSRV_DURABLE_TABLE pTable = pEntry->pTable;
    HANDLE hOplock = NULL;
    BOOLEAN bInLock = FALSE;

    LWIO_LOCK_MUTEX(bInLock, &pTable->Mutex);

    if (pEntry->pReclaimTimer != pTimerRequest)
    {
        goto error;
    }

    // No longer eligible for reconnect.
    pEntry->bValid = FALSE;

    SrvTimerRelease(&pEntry->pReclaimTimer);

    hOplock = pEntry->hOplock;
    pEntry->hOplock = NULL;

error:

    // Remove timer reference
    SrvSmb2DurableReleaseInLock(pEntry);

    LWIO_UNLOCK_MUTEX(bInLock, &pTable->Mutex);

    if (hOplock)
    {
        // Still orphaned.
        // Cancel and release oplock which will trigger close processing.
        SrvCancelOplock(hOplock);
        SrvReleaseOplock(hOplock);
    }
}

VOID
SrvSmb2DurableClose(
    IN PSRV_DURABLE_TABLE pTable,
    IN PSRV_DURABLE_ENTRY pEntry,
    IN PLWIO_SRV_FILE_2 pSrvFile
    )
{
    BOOLEAN bInLock = FALSE;

    LWIO_LOCK_MUTEX(bInLock, &pTable->Mutex);

    // This test might be unnecessary
    if (pEntry->pSrvFile == pSrvFile)
    {
        // No longer eligible for reconnect.
        pEntry->bValid = FALSE;

        if (pEntry->pSrvFile)
        {
            SrvFile2Release(pEntry->pSrvFile);
            pEntry->pSrvFile = NULL;
        }

        if (pEntry->pReclaimTimer)
        {
            if (SrvTimerCancelRequest(pEntry->pReclaimTimer, NULL) ==
                STATUS_SUCCESS)
            {
                // Timer won't run.
                SrvSmb2DurableReleaseInLock(pEntry);
            }
            SrvTimerRelease(&pEntry->pReclaimTimer);
        }
    }

    SrvSmb2DurableReleaseInLock(pEntry);

    LWIO_UNLOCK_MUTEX(bInLock, &pTable->Mutex);
}

NTSTATUS
SrvSmb2DurableOrphan(
    IN PSRV_DURABLE_TABLE pTable,
    IN PSRV_DURABLE_ENTRY pEntry,
    IN IO_FILE_HANDLE FileHandle,
    IN OUT PHANDLE phOplock,
    IN ULONG Timeout
    )
{
    NTSTATUS status = STATUS_SUCCESS;
    HANDLE hOplock = *phOplock;
    BOOLEAN bInLock = FALSE;
    LONG64 expiry = 0;

    LWIO_LOCK_MUTEX(bInLock, &pTable->Mutex);

    // Transfer ownership of this durable entry (refcount) from the file
    // the oplock references to the oplock itself.
    status = SrvReparentOplock(hOplock, NULL, pEntry);
    BAIL_ON_NT_STATUS(status);

    pEntry->FileHandle = FileHandle;
    // Drop our reference to the file
    SrvFile2Release(pEntry->pSrvFile);
    pEntry->pSrvFile = NULL;

    // Take over file's refence to and callbacks for the oplock.
    pEntry->hOplock = hOplock;
    *phOplock = NULL;

    if (Timeout)
    {
        status = WireGetCurrentNTTime(&expiry);
        BAIL_ON_NT_STATUS(status);

        expiry += Timeout * WIRE_FACTOR_SECS_TO_HUNDREDS_OF_NANOSECS;

        // Add reference for timer
        SrvSmb2DurableAcquireInLock(pEntry);
        status = SrvTimerPostRequest(
            expiry,
            pEntry,
            SrvSmb2DurableTimeout,
            &pEntry->pReclaimTimer);
        if (status != STATUS_SUCCESS)
        {
            SrvSmb2DurableReleaseInLock(pEntry);
        }
        BAIL_ON_NT_STATUS(status);
    }

error:

    LWIO_UNLOCK_MUTEX(bInLock, &pTable->Mutex);

    return status;
}

static
NTSTATUS
SrvSmb2DurableVerifyTokenEquivalence(
    PACCESS_TOKEN pToken1,
    PACCESS_TOKEN pToken2
    )
{
    NTSTATUS status = STATUS_SUCCESS;
    union
    {
        TOKEN_USER User;
        BYTE Buffer[sizeof(TOKEN_USER) + SID_MAX_SIZE];
    } user1, user2;
    ULONG length = 0;

    status = RtlQueryAccessTokenInformation(
        pToken1,
        TokenUser,
        user1.Buffer,
        sizeof(user1.Buffer),
        &length);
    LWIO_ASSERT(NT_SUCCESS(status));

    status = RtlQueryAccessTokenInformation(
        pToken2,
        TokenUser,
        user2.Buffer,
        sizeof(user2.Buffer),
        &length);
    LWIO_ASSERT(NT_SUCCESS(status));

    if (!RtlEqualSid(user1.User.User.Sid, user2.User.User.Sid))
    {
        status = STATUS_ACCESS_DENIED;
        BAIL_ON_NT_STATUS(status);
    }

error:

    return status;
}

NTSTATUS
SrvSmb2DurableReconnect(
    IN PSRV_DURABLE_TABLE pTable,
    IN ULONG64 DurableId,
    IN PACCESS_TOKEN pToken,
    IN PLWIO_SRV_FILE_2 pSrvFile,
    IN OPTIONAL PGUID ClientLeaseKey
    )
{
    NTSTATUS status = STATUS_SUCCESS;
    PSRV_DURABLE_ENTRY pEntry = NULL;
    PLW_HASHTABLE_NODE pNode = NULL;
    BOOLEAN bInEntryLock = FALSE;
    BOOLEAN bInFileLock = FALSE;
    PLWIO_SRV_FILE_2 pOldFile = NULL;

    LWIO_LOCK_MUTEX(bInEntryLock, &pTable->Mutex);

    status = LwRtlHashTableFindKey(pTable->pTable, &pNode, &DurableId);
    if (status == STATUS_NOT_FOUND)
    {
        status = STATUS_OBJECT_NAME_NOT_FOUND;
    }
    BAIL_ON_NT_STATUS(status);
    pEntry = LW_STRUCT_FROM_FIELD(pNode, SRV_DURABLE_ENTRY, HashNode);

    if (!pEntry->bValid)
    {
        // Durable handle is being torn down.
        status = STATUS_OBJECT_NAME_NOT_FOUND;
        pEntry = NULL;
        BAIL_ON_NT_STATUS(status);
    }

    // Ensure entry persists across lock acquire and release calls
    // in this routine.
    SrvSmb2DurableAcquireInLock(pEntry);

    // Check that user has right to reconnect to file
    status = SrvSmb2DurableVerifyTokenEquivalence(pEntry->pToken, pToken);
    BAIL_ON_NT_STATUS(status);

    // If the original open is still around, we need to force orphan it here
    if (pEntry->pSrvFile)
    {
        // Take new reference because pEntry->pSrvFile can change when lock
        // is released.
        pOldFile = pEntry->pSrvFile;
        SrvFile2Acquire(pOldFile);

        // Avoid lock inversion
        LWIO_UNLOCK_MUTEX(bInEntryLock, &pTable->Mutex);
        LWIO_LOCK_RWMUTEX_EXCLUSIVE(bInFileLock, &pOldFile->mutex);

        // Check for race with the handle being orphaned externally.
        if (pOldFile->pDurable == pEntry)
        {
            status = SrvSmb2DurableOrphan(
                            pTable,
                            pEntry,
                            pOldFile->hFile,
                            &pOldFile->hOplock,
                            0);
            BAIL_ON_NT_STATUS(status);
            IoRetainFile(pOldFile->hFile);
            pOldFile->pDurable = NULL;
            pOldFile->bReleaseOnly = TRUE;
        }

        LWIO_UNLOCK_RWMUTEX(bInFileLock, &pOldFile->mutex);
        LWIO_LOCK_MUTEX(bInEntryLock, &pTable->Mutex);
    }

    // If we don't have a file handle at this point, someone beat us to it
    if (!pEntry->FileHandle)
    {
        status = STATUS_OBJECT_NAME_NOT_FOUND;
        BAIL_ON_NT_STATUS(status);
    }

    // Verify a matching LeaseKey if we have one
    if (!LW_IS_BOTH_OR_NEITHER(pEntry->pClientLeaseKey, ClientLeaseKey))
    {
        status = STATUS_OBJECT_NAME_NOT_FOUND;
        BAIL_ON_NT_STATUS(status);
    }
    else if (pEntry->pClientLeaseKey)
    {
        if (!SrvIsLeaseKeyEqual(pEntry->pClientLeaseKey, ClientLeaseKey))
        {
            status = STATUS_OBJECT_NAME_NOT_FOUND;
            BAIL_ON_NT_STATUS(status);
        }
    }

    if (pEntry->pReclaimTimer)
    {
        if (SrvTimerCancelRequest(pEntry->pReclaimTimer, NULL) ==
            STATUS_SUCCESS)
        {
            // Timer never ran, so remove its reference.
            SrvSmb2DurableReleaseInLock(pEntry);
        }
        SrvTimerRelease(&pEntry->pReclaimTimer);
        // If we didn't cancel it in time, timer will clean itself up
    }

    // Transfer ownership of this durable entry (refcount) from the oplock
    // to the new file.
    status = SrvReparentOplock(pEntry->hOplock, pSrvFile, NULL);
    if (status == STATUS_USER_SESSION_DELETED)
    {
        // Oplock was broken before we could reconnect it.  Someone else will
        // close the entry.
        status = STATUS_OBJECT_NAME_NOT_FOUND;
    }
    BAIL_ON_NT_STATUS(status);

    // Transfer file handle
    pSrvFile->hFile = pEntry->FileHandle;
    pEntry->FileHandle = NULL;

    // Transfer oplock state
    pSrvFile->hOplock = pEntry->hOplock;
    pEntry->hOplock = NULL;

    // Transfer open state
    pSrvFile->OpenState = pEntry->OpenState;

    // Take reference to new srv file object
    pEntry->pSrvFile = SrvFile2Acquire(pSrvFile);

    pSrvFile->pDurable = pEntry;

cleanup:

    LWIO_UNLOCK_MUTEX(bInEntryLock, &pTable->Mutex);
    LWIO_UNLOCK_RWMUTEX(bInFileLock, &pOldFile->mutex);

    if (pEntry)
    {
        // Drop extra reference
        SrvSmb2DurableRelease(pEntry);
    }

    if (pOldFile)
    {
        SrvFile2Release(pOldFile);
    }

    return status;

error:

    goto cleanup;
}

PSRV_DURABLE_TABLE
SrvGetDurableHandleTable(
    VOID
    )
{
    return &gSrvElements.DurableHandles;
}
