//+---------------------------------------------------------------------------
//
//  Microsoft Windows
//  Copyright (C) Microsoft Corporation, 2000.
//
//  File:       V R O O T S . C P P
//
//  Contents:   Implements the virtual root system for the HTTP server
//
//  Notes:
//
//  Author:     danielwe   2000/11/6
//
//----------------------------------------------------------------------------

#include "pch.h"
#pragma hdrstop

#include "httpd.h"
#include "ncreg.h"

// Used to split a URL and Path Translated apart for ISAPI/ASP scripts
inline void SetPathInfo(PSTR *ppszPathInfo,PSTR pszInputURL,int iURLLen)
{
    int  iLen = strlen(pszInputURL+iURLLen) + 2;


    // If we've mapped the virtual root "/" to a script, need an extra "/" for the path
    // (normally we use the origial trailing "/", but in this case the "/" is the URL
    // BUGBUG:  Probably should rewrite the Virtual roots parsing
    // mechanism so that it's cleaner.
    *ppszPathInfo = MySzAllocA((iURLLen == 1) ? iLen + 1 : iLen);
    if (! (*ppszPathInfo))
        goto done;

    if (iURLLen == 1)
    {
        (*ppszPathInfo)[0] = '/';
        memcpy( (*ppszPathInfo) +1, pszInputURL + iURLLen, iLen);
    }
    else
        memcpy(*ppszPathInfo, pszInputURL + iURLLen, iLen);


done:
    // URL shouldn't contain path info, break it apart
    pszInputURL[iURLLen] = 0;
}


PVROOTINFO CVRoots::MatchVRoot(PCSTR pszInputURL, int iInputLen)
{
    int i, iMatch;

    // If there was an error on setting up the vroots, m_pVRoots = NULL.
    if (!m_pVRoots)
        return NULL;

    for(i=0, iMatch=-1; i<m_nVRoots; i++)
    {
        int iLen = m_pVRoots[i].iURLLen;

        // If this root maps to physical path "\", special case.
        // In general we store pszURL without trailing "/", however we have
        // to store trailing "/" for root directory.

        if (m_pVRoots[i].bRootDir && iLen != 1)
            iLen--;

        if(iLen && iInputLen >= iLen)
        {
            if(0 == _memicmp(pszInputURL, m_pVRoots[i].pszURL, iLen))
            {
                // If it's not root dir, always matched.  Otherwise it's possible
                // there wasn't a match.  For root dirs, pszURL[iLen] is always "/"

                if (!m_pVRoots[i].bRootDir || m_pVRoots[i].iURLLen == 1 || pszInputURL[iLen] == '/' || pszInputURL[iLen] == '\0')
                {
                    TraceTag(ttidWebServer, "URL %s matched VRoot %s (path %S, perm=%d, auth=%d)",
                             pszInputURL, m_pVRoots[i].pszURL,
                             m_pVRoots[i].wszPath,
                             m_pVRoots[i].dwPermissions,
                             m_pVRoots[i].AuthLevel);
                    return &(m_pVRoots[i]);
                }
            }
        }
    }
    TraceTag(ttidWebServer, "URL %s did not matched any VRoot", pszInputURL);
    return NULL;
}

BOOL CVRoots::FillVRoot(PVROOTINFO pvr, LPWSTR wszURL, LPWSTR wszPath)
{
    int err  = 0;       //  err variable is used in non-Debug mode
    const char cszDLL[] = ".dll";
    const char cszASP[] = ".asp";

    CHAR pszURL[MAX_PATH+1];
    CHAR pszPath[MAX_PATH+1];
    // convert URL to MBCS
    int iLen = pvr->iURLLen = MyW2A(wszURL, pszURL, sizeof(pszURL));
    if(!iLen)
        { myleave(83); }

    pvr->iURLLen--; // -1 for null-term

    pvr->iPathLen = wcslen(wszPath);
    MyW2A(wszPath, pszPath, sizeof(pszPath));


    // check to see if Vroot ends in .dll or .asp, in this case we send
    // client not to the directory but to the script page.
    if  (pvr->iPathLen >= sizeof(cszDLL) &&
         0 == strcmpi(pszPath + pvr->iPathLen - sizeof(cszDLL) +1,cszDLL))
    {
        pvr->ScriptType = SCRIPT_TYPE_EXTENSION;
    }
    else if (pvr->iPathLen >= sizeof(cszASP) &&
         0 == strcmpi(pszPath + pvr->iPathLen - sizeof(cszASP) +1,cszASP))
    {
        pvr->ScriptType = SCRIPT_TYPE_ASP;
    }
    else
    {
        pvr->ScriptType = SCRIPT_TYPE_NONE;
    }

    // If one of URL or path ends in a slash, the other must too.
    // If either the URL ends in a "/" or when the path ends in "\", we remove
    // the extra symbol.  However, in the case where either URL or path is
    // root we don't do this.

    if (pvr->iURLLen != 1 && pszURL[pvr->iURLLen-1]=='/')
    {
        pszURL[pvr->iURLLen-1] = L'\0';
        pvr->iURLLen--;
    }
    else if (pvr->iURLLen == 1 && pszURL[0]=='/' && pvr->ScriptType == SCRIPT_TYPE_NONE)
    {
        // if it's the root URL, make sure correspinding path ends with "\"
        // (if it's a directory only, leave ASP + ISAPI's alone)
        if (wszPath[pvr->iPathLen-1] != L'\\')
        {
            wszPath[pvr->iPathLen] = L'\\';
            pvr->iPathLen++;
            wszPath[pvr->iPathLen] = L'\0';
        }
    }

    // If Path ends in "\" (and it's not the root path or root virtual root)
    // remove the "\"
    if (pvr->iURLLen != 1 && pvr->iPathLen != 1 && wszPath[pvr->iPathLen-1]==L'\\')
    {
        wszPath[pvr->iPathLen-1] = L'\0';
        pvr->iPathLen--;
    }
    else if (pvr->iPathLen == 1 && wszPath[0]==L'\\')
    {
        // Trailing "/" must match "\".  However, we need a slight HACK to make this work
        if (pszURL[pvr->iURLLen-1] != '/')
        {
            pszURL[pvr->iURLLen] = '/';
            pvr->iURLLen++;
            pszURL[pvr->iURLLen] = '\0';
        }
        pvr->bRootDir = TRUE;
    }

    pvr->pszURL = MySzDupA(pszURL);
    pvr->wszPath = MySzDupW(wszPath);

    // Fill in defaults for these
    pvr->wszUserList = NULL;
    pvr->dwPermissions = HTTP_DEFAULTP_PERMISSIONS;
    pvr->AuthLevel = AUTH_PUBLIC;

    TraceTag(ttidWebServer, "VROOT: (%s)=>(%s) perm=%d auth=%d ScriptType=%d",
             pvr->pszURL, pvr->wszPath, pvr->dwPermissions,
             pvr->AuthLevel,pvr->ScriptType);

done:
    if(err)
    {
        return FALSE;
    }
    return TRUE;
}

VOID CVRoots::Sort()
{
    BOOL fChange;
    int i=0;

    // We now want to sort the VRoots in descending order of URL-length so
    // that when we match we'll find the longest match first!!
    // Using a slow bubble-sort :-(
    do {
        fChange = FALSE;
        for(i=0; i<m_nVRoots-1; i++)
        {
            if(m_pVRoots[i].iURLLen < m_pVRoots[i+1].iURLLen)
            {
                // swap the 2 vroots
                VROOTINFO vtemp = m_pVRoots[i+1];
                m_pVRoots[i+1] = m_pVRoots[i];
                m_pVRoots[i] = vtemp;
                fChange = TRUE;
            }
        }
    } while(fChange);
}

static const WCHAR c_szPrefix[] = L"\\\\?\\";
static const int   c_cchPrefix = celems(c_szPrefix);

BOOL CVRoots::Init()
{
    int err  = 0;       //  err variable is used in non-Debug mode
    int i=0;

    InitializeCriticalSection(&m_csVroot);

    // Registry doesnt allow keynames longer than MAX_PATH so we won't map URL prefixes longer than MAX_PATH
    WCHAR wszURL[MAX_PATH+1];
    WCHAR wszPath[MAX_PATH+1];
    WCHAR   wszPathReal[MAX_PATH + 1] = {0};
    wszURL[0]=wszPath[0]=0;

    // open the VRoots key
    CReg topreg(HKEY_LOCAL_MACHINE, RK_HTTPDVROOTS);

    // allocate space for as many VRoots as we have subkeys
    m_nVRoots = topreg.NumSubkeys();
    if(!m_nVRoots)
        myleave(80);
    // Zero the memory so we know what to deallocate and what not to.
    if(!(m_pVRoots = MyRgAllocZ(VROOTINFO, m_nVRoots)))
        myleave(81);

    // enumerate all subkeys. Their names are URLs, their default value is the corresponding path
    // Note: EnumKey takes sizes in chars, not bytes!
    for(i=0; i<m_nVRoots && topreg.EnumKey(wszURL, CCHSIZEOF(wszURL)); i++)
    {
        CReg subreg(topreg, wszURL);

        // get the unnamed value. Again size is in chars, not bytes.
        if(!subreg.ValueSZ(NULL, wszPath, CCHSIZEOF(wszPath)))
        {
            // iURLLen and iPathLen set to 0 already, so no case of corruption in MatchVRoot
            subreg.Reset();
            continue;
        }
        else
        {
            // Prepend the \\?\ prefix
            //
            lstrcpy(wszPathReal, c_szPrefix);
            lstrcat(wszPathReal, wszPath);

            if (!FillVRoot(&m_pVRoots[i], wszURL, wszPathReal))
                myleave(121);

            m_pVRoots[i].wszUserList = MySzDupW( subreg.ValueSZ(RV_USERLIST));

            // default permissions is Read & Execute
            m_pVRoots[i].dwPermissions = subreg.ValueDW(RV_PERM, HTTP_DEFAULTP_PERMISSIONS);
            // default authentication is public
            m_pVRoots[i].AuthLevel = (AUTHLEVEL)subreg.ValueDW(RV_AUTH, (DWORD)AUTH_PUBLIC);

            // we don't fail if we can't load an extension map
            LoadExtensionMap (&m_pVRoots[i], subreg);
        }

        subreg.Reset();
    }

    Sort();

done:
    if(err)
    {
        TraceTag(ttidWebServer, "CVRoots::ctor FAILED due to err=%d GLE=%d "
                 "(num=%d i=%d pVRoots=0x%08x url=%s path=%s)",
                 err, GetLastError(), m_nVRoots, i, m_pVRoots, wszURL, wszPath);
        return FALSE;
    }
    return TRUE;
}

void CVRoots::Cleanup()
{
    if(!m_pVRoots)
        return;
    for(int i=0; i<m_nVRoots; i++)
    {
        MyFree(m_pVRoots[i].pszURL);
        MyFree(m_pVRoots[i].wszPath);
        MyFree(m_pVRoots[i].wszUserList);
        FreeExtensionMap (&m_pVRoots[i]);
    }
    MyFree(m_pVRoots);

    DeleteCriticalSection(&m_csVroot);
}

BOOL CVRoots::AddVRoot(LPWSTR szUrl, LPWSTR szPath)
{
    PVROOTINFO  pvrNew;
    int         err = 0;
    LPSTR       szaUrl = NULL;
    int         iInputLen;

    szaUrl = SzFromWsz(szUrl);
    if (!szaUrl)
    {
        // Can't use myleave since we don't have the critsec here
        //
        err = 400;
        goto err;
    }

    iInputLen = strlen(szaUrl);

    EnterCriticalSection(&m_csVroot);

    pvrNew = MatchVRoot(szaUrl, iInputLen);
    if(pvrNew)
    {
        TraceError("CVRoots::AddVRoot - already present", E_FAIL);
        myleave(10);
    }

    m_pVRoots = MyRgReAlloc(VROOTINFO, m_pVRoots, m_nVRoots, m_nVRoots + 1);
    if(!m_nVRoots)
        myleave(100);

    pvrNew = &m_pVRoots[m_nVRoots];

    if (!FillVRoot(pvrNew, szUrl, szPath))
    {
        myleave(101);
    }

    m_nVRoots++;

    HKEY    hkeyVroot;
    HKEY    hkeyNew;
    HRESULT hr;

    hr = HrRegOpenKeyEx(HKEY_LOCAL_MACHINE, RK_HTTPDVROOTS, KEY_ALL_ACCESS,
                        &hkeyVroot);
    if (SUCCEEDED(hr))
    {
        hr = HrRegCreateKeyEx(hkeyVroot, szUrl, 0, KEY_ALL_ACCESS, NULL,
                              &hkeyNew, NULL);
        if (SUCCEEDED(hr))
        {
            // Pass NULL to set default value
            //
            hr = HrRegSetSz(hkeyNew, NULL, szPath);

            RegCloseKey(hkeyNew);
        }
        RegCloseKey(hkeyVroot);
    }

    if (FAILED(hr))
    {
        TraceError("CVRoots::AddVRoot", hr);
        myleave(111);
    }
    else
    {
        Sort();
    }

done:

    delete [] szaUrl;

    LeaveCriticalSection(&m_csVroot);

err:
    if(err)
    {
        return FALSE;
    }
    return TRUE;
}

BOOL CVRoots::RemoveVRoot(LPWSTR szwUrl)
{
    int         ivr;
    LPSTR       szUrl = NULL;
    PVROOTINFO  pvr;
    int         iInputLen;
    int         err = 0;
    BOOL        fFound = FALSE;

    szUrl = SzFromWsz(szwUrl);
    if (!szUrl)
    {
        myleave(100);
    }

    iInputLen = strlen(szUrl);

    EnterCriticalSection(&m_csVroot);

    pvr = MatchVRoot(szUrl, iInputLen);
    if(!pvr)
    {
        myleave(140);
    }

    for (ivr = 0; ivr < m_nVRoots; ivr++)
    {
        if (&m_pVRoots[ivr] == pvr)
        {
            // Found the one to remove. So now let's shift the rest down by
            // one
            //
            MoveMemory(&m_pVRoots[ivr], &m_pVRoots[ivr + 1],
                       sizeof(VROOTINFO) * (m_nVRoots - ivr - 1));
            m_nVRoots--;
            fFound = TRUE;
            break;
        }
    }

    AssertSz(fFound, "How come it was there a minute ago??");

    HKEY    hkeyVroot;
    HKEY    hkeyNew;
    HRESULT hr;

    hr = HrRegOpenKeyEx(HKEY_LOCAL_MACHINE, RK_HTTPDVROOTS, KEY_ALL_ACCESS,
                        &hkeyVroot);
    if (SUCCEEDED(hr))
    {
        hr = HrRegDeleteKey(hkeyVroot, szwUrl);

        RegCloseKey(hkeyVroot);
    }

    if (FAILED(hr))
    {
        TraceError("CVRoots::RemoveVRoot", hr);
        myleave(111);
    }

    // No need to re-sort since we moved everything down by one

done:
    LeaveCriticalSection(&m_csVroot);

    delete [] szUrl;

    if(err)
    {
        return FALSE;
    }
    return TRUE;
}

PWSTR CVRoots::URLAtoPathW(PSTR pszInputURL, PDWORD pdwPerm /*=0*/,
                           AUTHLEVEL* pAuthLevel /* =0 */,
                           SCRIPT_TYPE *pScriptType /*=0 */,
                           PSTR *ppszPathInfo /* =0 */,
                           WCHAR **ppwszUserList /*=0 */)
{
    PSTR pszEndOfURL;
    WCHAR *wszTemp = NULL;
    int iInputLen = strlen(pszInputURL);

    EnterCriticalSection(&m_csVroot);

    PVROOTINFO pVRoot = MatchVRoot(pszInputURL, iInputLen);
    if(!pVRoot)
    {
        LeaveCriticalSection(&m_csVroot);
        return NULL;
    }

    // Do a lookup to see if the current URL contains and extension
    // that is in the extension map for the VROOT.  If so, this call
    // will truncate the URL after the extension.  Since the URL may
    // have changed, the length is re-obtained on a successfull call.

    if (MapExtToPath (pszInputURL, &pszEndOfURL))
    {
        if (ppszPathInfo && *ppszPathInfo == NULL)
            *ppszPathInfo = MySzDupA (pszInputURL);

        if (*pszEndOfURL != '\0')
        {
            *pszEndOfURL = 0;
            iInputLen = strlen(pszInputURL);
        }
    }

    // in computing the buffersize here we are assuming that an MBCS string of length N
    // cannot produce a unicode string of length greater than N
    int iOutLen = 1 + pVRoot->iPathLen + (iInputLen - pVRoot->iURLLen);
    PWSTR wszOutPath = MyRgAllocNZ(WCHAR, iOutLen);
    if (!wszOutPath)
    {
        LeaveCriticalSection(&m_csVroot);
        return NULL;
    }

    // assemble the path. First, the mapped base path
    memcpy(wszOutPath, pVRoot->wszPath, sizeof(WCHAR)*pVRoot->iPathLen);

    if(pdwPerm)
        *pdwPerm = pVRoot->dwPermissions;
    if(pAuthLevel)
        *pAuthLevel = pVRoot->AuthLevel;
    if (pScriptType)
        *pScriptType = pVRoot->ScriptType;
    if (ppwszUserList)
        *ppwszUserList = pVRoot->wszUserList;

    // If the vroot specifies an ISAPI dll or ASP page don't copy path info over.
    if (pVRoot->ScriptType != SCRIPT_TYPE_NONE)
    {
        if ( ppszPathInfo && pszInputURL[pVRoot->iURLLen] != 0)
        {
            SetPathInfo(ppszPathInfo,pszInputURL,pVRoot->iURLLen);
        }
        wszOutPath[pVRoot->iPathLen] = L'\0';
        LeaveCriticalSection(&m_csVroot);
        return wszOutPath;
    }

    // next the remainder of the URL, converted to wide
    if (iOutLen-pVRoot->iPathLen == 0)
    {
        wszOutPath[pVRoot->iPathLen] = L'\0';
    }
    else
    {
        int iRet = MyA2W(pszInputURL+pVRoot->iURLLen, wszOutPath+pVRoot->iPathLen, iOutLen-pVRoot->iPathLen);
        DEBUGCHK(iRet);
    }

    LeaveCriticalSection(&m_csVroot);

    // Flip forward slashes around to backslashes

    LPWSTR  pchFlip = wszOutPath;

    while (*pchFlip)
    {
        if (*pchFlip == '/')
        {
            *pchFlip = '\\';
        }

        pchFlip++;
    }

    return wszOutPath;
}

BOOL CVRoots::LoadExtensionMap(PVROOTINFO pvr, HKEY rootreg)
{
    BOOL bWildCard;
    WCHAR wszExt[MAX_PATH+1];
    WCHAR wszPath[MAX_PATH+1];

    CReg mapreg(rootreg, RV_EXTENSIONMAP);

    if (!mapreg.IsOK())
    {
        pvr->nExtensions = 0;
        return FALSE;
    }

    if (bWildCard = mapreg.ValueSZ(NULL, wszPath, CCHSIZEOF(wszPath)))
        pvr->nExtensions = 1;
    else
        pvr->nExtensions = mapreg.NumValues();

    if ((pvr->pExtMap = MyRgAllocZ(EXTMAP, pvr->nExtensions)) == NULL)
        return FALSE;

    if (bWildCard)
    {
        pvr->pExtMap[0].wszExt = MySzDupW(L"*");
        pvr->pExtMap[0].wszPath = MySzDupW(wszPath);
        return TRUE;
    }

    for (int i = 0; i < pvr->nExtensions; i++)
    {
        if (mapreg.EnumValue(wszExt, CCHSIZEOF(wszExt),
                             wszPath, CCHSIZEOF(wszPath)))
        {
            if (wszExt[0] != 0 && wszPath[0] != 0)
            {
                pvr->pExtMap[i].wszExt = MySzDupW(wszExt);
                pvr->pExtMap[i].wszPath = MySzDupW(wszPath);
            }
            else
            {
                pvr->pExtMap[i].wszExt = MySzDupW(L"");
                pvr->pExtMap[i].wszPath = MySzDupW(L"");
            }
        }
    }

    return TRUE;
}

void CVRoots::FreeExtensionMap (PVROOTINFO pvr)
{
    if (pvr->pExtMap == NULL) return;

    for (int i=0; i < pvr->nExtensions; i++)
    {
        MyFree(pvr->pExtMap[i].wszExt);
        MyFree(pvr->pExtMap[i].wszPath);
    }

    MyFree(pvr->pExtMap);
    pvr->pExtMap = NULL;
}

PWSTR CVRoots::MapExtToPath (PSTR pszInputURL, PSTR *ppszEndOfURL)
{
    PSTR pszTemp, pszStart, pszEnd;
    PWSTR wszPath = NULL;
    WCHAR wszExt[MAX_PATH+1];
    PVROOTINFO pvr;
    int iInputLen = strlen(pszInputURL);

    if (!FindExtInURL (pszInputURL, &pszStart, &pszEnd))
        return NULL;

    MyA2W (pszStart, wszExt, (int)((INT_PTR)(pszEnd - pszStart)));
    wszExt [pszEnd - pszStart] = L'\0';

    EnterCriticalSection(&m_csVroot);

    if (!(pvr = MatchVRoot(pszInputURL, iInputLen)))
    {
        LeaveCriticalSection(&m_csVroot);
        return NULL;
    }

    for (int i = 0; i < pvr->nExtensions; i++)
    {
        if ((i == 0 && pvr->pExtMap[i].wszExt[0] == L'*') ||
            0 == _wcsicmp (pvr->pExtMap[i].wszExt, wszExt))
        {
            wszPath = pvr->pExtMap[i].wszPath;

            if (ppszEndOfURL != NULL)
                *ppszEndOfURL = pszEnd;

            break;
        }
    }

    LeaveCriticalSection(&m_csVroot);
    return wszPath;
}

BOOL CVRoots::FindExtInURL (PSTR pszInputURL, PSTR *ppszStart, PSTR *ppszEnd)
{
    *ppszStart = strrchr (pszInputURL, '.');

    if ((*ppszEnd = *ppszStart) == NULL)
        return FALSE;

    while (**ppszEnd != '/' && **ppszEnd != '\0')
        *ppszEnd = *ppszEnd + 1;

    return TRUE;
}