/*
 *
 * @APPLE_LICENSE_HEADER_START@
 *
 * Copyright (c) 1999-2008 Apple Inc.  All Rights Reserved.
 *
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 * 
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 * 
 * @APPLE_LICENSE_HEADER_END@
 *
 */
/*
    File:       QTSSHomeDirectoryModule.cpp

    Contains:   Module that expands ~ in request URLs to home directories
	    
 */
    
#include <stdio.h>      
#include <stdlib.h>
#include "SafeStdLib.h"
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include "OSMemory.h"
#include "StringParser.h"
#include "ResizeableStringFormatter.h"
#include "QTSSModuleUtils.h"
#include "OSArrayObjectDeleter.h"
#include "DirectoryInfo.h"
#include "QTSSMemoryDeleter.h"

#include "QTSSHomeDirectoryModule.h"

#define	HOME_DIRECTORY_MODULE_DEBUGGING 0

// STATIC DATA
static QTSS_ServerObject		sServer						= NULL;
static QTSS_ModuleObject        sModule						= NULL;
static QTSS_ModulePrefsObject	sPrefs						= NULL;

// Attributes
static char*					sIsFirstRequestName			= "QTSSHomeDirectoryModuleIsFirstRequest";
static QTSS_AttributeID         sIsFirstRequestAttr			= qtssIllegalAttrID;
static char*					sRequestHomeDirAttrName			= "QTSSHomeDirectoryModuleHomeDir";
static QTSS_AttributeID         sRequestHomeDirAttr				= qtssIllegalAttrID;
static char*					sSessionHomeDirAttrName			= "QTSSHomeDirectoryModuleHomeDir";
static QTSS_AttributeID         sSessionHomeDirAttr				= qtssIllegalAttrID;

// Module description and version
static char*					sDescription				= "Provides support for streaming from users' home directories";
static UInt32                   sVersion 					= 0x00010000;

// Module preferences and their defaults
static Bool16					sEnabled					= false;
static Bool16					kDefaultEnabled				= false;
static char*					sMoviesDirectory			= NULL; // Streaming dir in user's home dir
static char*					kDefaultMoviesDirectory		= "/Sites/Streaming/";
static UInt32                   sMaxNumConnsPerHomeDir		= 0; //Max conns. allowed per home directory
static UInt32                   kDefaultMaxNumConnsPerHomeDir	= 0; // 0 => no limit enforced
static UInt32                   sMaxBWInKbpsPerHomeDir			= 0; // Max BW allowed per home directory
static UInt32                   kDefaultMaxBWInKbpsPerHomeDir	= 0; // 0 => no limit enforced

enum { kDefaultHomeDirSize = 64 };
static Bool16 					sTrue						= true;

static OSRefTable*	sDirectoryInfoMap = NULL;

// FUNCTIONS
static QTSS_Error	QTSSHomeDirectoryDispatch(QTSS_Role inRole, QTSS_RoleParamPtr inParams);
static QTSS_Error	Register(QTSS_Register_Params* inParams);
static QTSS_Error	Initialize(QTSS_Initialize_Params* inParams);
static QTSS_Error	RereadPrefs();
static QTSS_Error	RewriteRequestFilePathAndRootDir(QTSS_StandardRTSP_Params* inParams);
static void			RewriteRootDir(QTSS_StandardRTSP_Params* inParams, StrPtrLen* inHomeDir);
static QTSS_Error	AuthorizeRequest(QTSS_StandardRTSP_Params* inParams);
static QTSS_Error	RemoveClientSessionFromMap(QTSS_ClientSessionClosing_Params* inParams);

// Helper functions
static Bool16		ExpandPath(const StrPtrLen *inPathPtr, StrPtrLen *outPathPtr);

// FUNCTION IMPLEMENTATIONS
QTSS_Error QTSSHomeDirectoryModule_Main(void* inPrivateArgs)
{
    return _stublibrary_main(inPrivateArgs, QTSSHomeDirectoryDispatch);
}


QTSS_Error QTSSHomeDirectoryDispatch(QTSS_Role inRole, QTSS_RoleParamPtr inParams)
{
    switch (inRole)
    {
        case QTSS_Register_Role:
            return Register(&inParams->regParams);
        case QTSS_Initialize_Role:
            return Initialize(&inParams->initParams);
        case QTSS_RereadPrefs_Role:
            return RereadPrefs();
        case QTSS_RTSPRoute_Role:
			{
				if (!sEnabled) 
					break;
				return RewriteRequestFilePathAndRootDir(&inParams->rtspRouteParams);
			} 
		case QTSS_RTSPAuthorize_Role:
			{
				if (!sEnabled) 
					break;
				return AuthorizeRequest(&inParams->rtspAuthParams);
			}
		case QTSS_ClientSessionClosing_Role:
			{
				if (!sEnabled) 
					break;
				return RemoveClientSessionFromMap(&inParams->clientSessionClosingParams);
			}
    }
    return QTSS_NoErr;
}


QTSS_Error Register(QTSS_Register_Params* inParams)
{
    // Do role & attribute setup
    (void)QTSS_AddRole(QTSS_Initialize_Role);
    (void)QTSS_AddRole(QTSS_RereadPrefs_Role);
	(void)QTSS_AddRole(QTSS_RTSPRoute_Role);
    (void)QTSS_AddRole(QTSS_RTSPAuthorize_Role);
	(void)QTSS_AddRole(QTSS_ClientSessionClosing_Role);
    
	// Add an RTSP session attribute to track if the request is the first request of the session
	// so that the bandwidth/# of connections quota check can be done only if it is the first request
	(void)QTSS_AddStaticAttribute(qtssRTSPSessionObjectType, sIsFirstRequestName, NULL, qtssAttrDataTypeBool16);
	(void)QTSS_IDForAttr(qtssRTSPSessionObjectType, sIsFirstRequestName, &sIsFirstRequestAttr);
		
	// Add an RTSP request object attribute for tracking the home directory it is being served from
    (void)QTSS_AddStaticAttribute(qtssRTSPRequestObjectType, sRequestHomeDirAttrName, NULL, qtssAttrDataTypeCharArray);
    (void)QTSS_IDForAttr(qtssRTSPRequestObjectType, sRequestHomeDirAttrName, &sRequestHomeDirAttr);
	
	// Add an RTP session object attribute for tracking the home directory it is being served from
    (void)QTSS_AddStaticAttribute(qtssClientSessionObjectType, sSessionHomeDirAttrName, NULL, qtssAttrDataTypeCharArray);
    (void)QTSS_IDForAttr(qtssClientSessionObjectType, sSessionHomeDirAttrName, &sSessionHomeDirAttr);


    // Tell the server our name!
    static char* sModuleName = "QTSSHomeDirectoryModule";
    ::strcpy(inParams->outModuleName, sModuleName);

    return QTSS_NoErr;
}


QTSS_Error Initialize(QTSS_Initialize_Params* inParams)
{
    // Setup module utils
    QTSSModuleUtils::Initialize(inParams->inMessages, inParams->inServer, inParams->inErrorLogStream);
	
	// Get the server object
	sServer = inParams->inServer;
    
	// Get our prefs object
	sModule = inParams->inModule;
    sPrefs = QTSSModuleUtils::GetModulePrefsObject(sModule);
	
	// Set our version and description
	(void)QTSS_SetValue(sModule, qtssModDesc, 0, sDescription, ::strlen(sDescription));   
    (void)QTSS_SetValue(sModule, qtssModVersion, 0, &sVersion, sizeof(sVersion)); 
	
    RereadPrefs();
	
	sDirectoryInfoMap = NEW OSRefTable();
	
	return QTSS_NoErr;
}


QTSS_Error RereadPrefs()
{
	QTSSModuleUtils::GetAttribute(sPrefs, "enabled", qtssAttrDataTypeBool16, 
								&sEnabled, &kDefaultEnabled, sizeof(sEnabled));
    delete [] sMoviesDirectory;
    sMoviesDirectory = QTSSModuleUtils::GetStringAttribute(sPrefs, "movies_directory", kDefaultMoviesDirectory);
	QTSSModuleUtils::GetAttribute(sPrefs, "max_num_conns_per_home_directory", qtssAttrDataTypeUInt32, 
								&sMaxNumConnsPerHomeDir, &kDefaultMaxNumConnsPerHomeDir, sizeof(sMaxNumConnsPerHomeDir));
	QTSSModuleUtils::GetAttribute(sPrefs, "max_bandwidth_kbps_per_home_directory", qtssAttrDataTypeUInt32, 
								&sMaxBWInKbpsPerHomeDir, &kDefaultMaxBWInKbpsPerHomeDir, sizeof(sMaxBWInKbpsPerHomeDir));
	return QTSS_NoErr;
}


QTSS_Error RewriteRequestFilePathAndRootDir(QTSS_StandardRTSP_Params* inParams)
{
	QTSS_RTSPRequestObject theRequest = inParams->inRTSPRequest;
	
	char* requestPathStr;
	(void)QTSS_GetValueAsString(theRequest, qtssRTSPReqFilePath, 0, &requestPathStr);
    QTSSCharArrayDeleter requestPathStrDeleter(requestPathStr);
	StrPtrLen theRequestPath(requestPathStr);
	StringParser theRequestPathParser(&theRequestPath);
	
	// request path begins with a '/' for ex. /mysample.mov or /~joe/mysample.mov
	theRequestPathParser.Expect('/');
	
	if (theRequestPathParser.PeekFast() == '~')
	{	
		theRequestPathParser.Expect('~');
		
		StrPtrLen theFirstPath;
		theRequestPathParser.ConsumeUntil(&theFirstPath, '/');
		if (theFirstPath.Len != 0) // if the URI is /~/mysample.mov --> do nothing
		{
			StrPtrLen theHomeDir;
			// ExpandPath allocates memory, so delete it before exiting
			if (ExpandPath(&theFirstPath, &theHomeDir))
			{
				// Rewrite the qtssRTSPReqRootDir attribute
				RewriteRootDir(inParams, &theHomeDir);
				
				StrPtrLen theStrippedRequestPath;
				theRequestPathParser.ConsumeLength(&theStrippedRequestPath, theRequestPathParser.GetDataRemaining());
				(void) QTSS_SetValue(theRequest, qtssRTSPReqFilePath, 0, theStrippedRequestPath.Ptr, theStrippedRequestPath.Len);
			}
			theHomeDir.Delete();
		}
	}

	return QTSS_NoErr;
}

void RewriteRootDir(QTSS_StandardRTSP_Params* inParams, StrPtrLen* inHomeDir)
{
    QTSS_RTSPRequestObject theRequest = inParams->inRTSPRequest;
	QTSS_ClientSessionObject theSession = inParams->inClientSession;
	QTSS_Error theErr = QTSS_NoErr;
	
	Assert(inHomeDir->Len != 0);
	if (inHomeDir->Len == 0)
		return;
		
	char homeDirFormatterBuf[kDefaultHomeDirSize] = "";
	ResizeableStringFormatter theHomeDirFormatter(homeDirFormatterBuf, kDefaultHomeDirSize);
	theHomeDirFormatter.Put(inHomeDir->Ptr, inHomeDir->Len);
	// Append a path separator if the movies directory doesn't begin with it
	if ((::strlen(sMoviesDirectory) != 0) && (sMoviesDirectory[0] != '/'))
		theHomeDirFormatter.PutChar('/');
	// Append the movies_directory
	theHomeDirFormatter.Put(sMoviesDirectory);
	
	StrPtrLen theRootDir(theHomeDirFormatter.GetBufPtr(), theHomeDirFormatter.GetBytesWritten());
	
	// Set qtssRTSPReqRootDir
	(void)QTSS_SetValue(theRequest, qtssRTSPReqRootDir, 0, theRootDir.Ptr, theRootDir.Len);
	
	// Set the client session's home dir attribute
	(void)QTSS_SetValue(theSession, sSessionHomeDirAttr, 0, theRootDir.Ptr, theRootDir.Len);

	// If the request is a PLAY, then add this to the session list for the home directory
	QTSS_RTSPMethod *theMethod = NULL;
	UInt32 theLen = 0;
	(void)QTSS_GetValuePtr(theRequest, qtssRTSPReqMethod, 0, (void **)&theMethod, &theLen);
	if (*theMethod == qtssPlayMethod)
	{
		OSRef* theDirectoryInfoRef = sDirectoryInfoMap->Resolve(&theRootDir);
		if (theDirectoryInfoRef == NULL)
		{
			DirectoryInfo* newDirInfo = NEW DirectoryInfo(&theRootDir);
			theErr = sDirectoryInfoMap->Register(newDirInfo->GetRef());
			Assert(theErr == QTSS_NoErr);
			theDirectoryInfoRef = sDirectoryInfoMap->Resolve(&theRootDir);
			Assert (theDirectoryInfoRef == newDirInfo->GetRef());
		}
		
		DirectoryInfo* theDirInfo = (DirectoryInfo *)theDirectoryInfoRef->GetObject();
		theDirInfo->AddSession(&theSession);
		sDirectoryInfoMap->Release(theDirectoryInfoRef);
	} 

#if HOME_DIRECTORY_MODULE_DEBUGGING
	char* newRequestPath = NULL;
	theErr = QTSS_GetValueAsString (theRequest, qtssRTSPReqFilePath, 0, &newRequestPath);
	qtss_printf("QTSSHomeDirectoryModule::RewriteRootDir qtssRTSPReqFilePath = %s\n", newRequestPath);
	QTSS_Delete(newRequestPath);

	char* newRequestRootDir = NULL;
	theErr = QTSS_GetValueAsString (theRequest, qtssRTSPReqRootDir, 0, &newRequestRootDir);
	qtss_printf("QTSSHomeDirectoryModule::RewriteRootDir qtssRTSPReqRootDir = %s\n", newRequestRootDir);
	QTSS_Delete(newRequestRootDir);
#endif

}


QTSS_Error AuthorizeRequest(QTSS_StandardRTSP_Params* inParams)
{
    // Only do anything if this is the first request	
	Bool16 *isFirstRequest	= NULL;
	UInt32 theLen = sizeof(isFirstRequest);
    (void)QTSS_GetValuePtr(inParams->inRTSPSession, sIsFirstRequestAttr, 0, (void**)&isFirstRequest, &theLen);
    if (isFirstRequest != NULL)
        return QTSS_NoErr;
	
	OSRef *theDirectoryInfoRef = NULL;
	DirectoryInfo *theDirInfo = NULL;
	StrPtrLen theHomeDir;
	
	// if no limits are imposed, do nothing
	if ((sMaxNumConnsPerHomeDir == 0) && (sMaxBWInKbpsPerHomeDir == 0))
		goto end_authorize;
		
	// Get this client session's home dir
	(void)QTSS_GetValuePtr(inParams->inClientSession, sSessionHomeDirAttr, 0, (void**)&theHomeDir.Ptr, &theHomeDir.Len);
	if ((theHomeDir.Ptr == NULL) || (theHomeDir.Len == 0)) // If it's NULL it's not served out of the home dir
		goto end_authorize;
	
	// Get the sessions for this home dir
	theDirectoryInfoRef = sDirectoryInfoMap->Resolve(&theHomeDir); 
	if (theDirectoryInfoRef == NULL) // No sessions exist yet for this homeDir
		goto end_authorize;
	
	theDirInfo = (DirectoryInfo *)(theDirectoryInfoRef->GetObject());

	if ((sMaxNumConnsPerHomeDir != 0) && ((theDirInfo->NumSessions()) >= sMaxNumConnsPerHomeDir))
		QTSSModuleUtils::SendErrorResponse(inParams->inRTSPRequest, qtssClientNotEnoughBandwidth, qtssMsgTooManyClients);
	else if((sMaxBWInKbpsPerHomeDir != 0) && ((theDirInfo->CurrentTotalBandwidthInKbps()) >= sMaxBWInKbpsPerHomeDir))
		QTSSModuleUtils::SendErrorResponse(inParams->inRTSPRequest, qtssClientNotEnoughBandwidth, qtssMsgTooMuchThruput);
		
	sDirectoryInfoMap->Release(theDirectoryInfoRef);
	
end_authorize:	
	// Mark the request so we'll know subsequent ones aren't the first.
    (void)QTSS_SetValue(inParams->inRTSPSession, sIsFirstRequestAttr, 0, &sTrue, sizeof(sTrue));
	
	return QTSS_NoErr;
}

QTSS_Error RemoveClientSessionFromMap(QTSS_ClientSessionClosing_Params* inParams)
{
	QTSS_ClientSessionObject theSession = inParams->inClientSession;
	
	StrPtrLen theHomeDir;    
    (void)QTSS_GetValuePtr (theSession, sSessionHomeDirAttr, 0, (void**)&theHomeDir.Ptr, &theHomeDir.Len);
    if ((theHomeDir.Ptr != NULL) && (theHomeDir.Len != 0))
	{
		OSRef* theDirectoryInfoRef = sDirectoryInfoMap->Resolve(&theHomeDir);
		if (theDirectoryInfoRef != NULL)
		{
			DirectoryInfo *theDirInfo = (DirectoryInfo *)theDirectoryInfoRef->GetObject();
			theDirInfo->RemoveSession(&theSession);
			sDirectoryInfoMap->Release(theDirectoryInfoRef);
			(void)sDirectoryInfoMap->TryUnRegister(theDirectoryInfoRef);
		}
	}
	
	return QTSS_NoErr;
}

// Expand path expands the ~ or ~username to the home directory of the user
// It allocates memory for the outPathPtr->Ptr, so the caller should delete it
// inPathPtr has the tilde stripped off, so inPathPtr->Len == 0 is valid
Bool16 ExpandPath(const StrPtrLen *inPathPtr, StrPtrLen *outPathPtr)
{    
    Assert(inPathPtr != NULL);
    Assert(outPathPtr != NULL);

	char *newPath = NULL;
   
	if (inPathPtr->Len != 0)
	{
        char* inPathStr = inPathPtr->GetAsCString();
        struct passwd *passwdStruct = getpwnam(inPathStr);
        delete [] inPathStr;
        if (passwdStruct != NULL)
            newPath = passwdStruct->pw_dir;
    }


/*
	if (inPathPtr->Len == 0)
	{
		newPath = getenv("HOME");
        
		if (newPath == NULL)
        {
            struct passwd *passwdStruct = getpwuid(getuid());
            if (passwdStruct != NULL)
                newPath = passwdStruct->pw_dir;
        }
	}
*/

    if (newPath == NULL)
        return false;

    UInt32 newPathLen = ::strlen(newPath);
    char* newPathStr = NEW char[newPathLen + 1];
    ::strcpy(newPathStr, newPath);
    
    outPathPtr->Ptr = newPathStr;
    outPathPtr->Len = newPathLen;
    
    return true;
}
