// borfrras.cpp : Borf for Windows 2000 Routing and Remote Access.


#include "stdafx.h"
#include "borfrras.h"

CWinApp theApp;

using namespace std;


// *** GLOBALS ***

// The main loop condition.
BORF_RUNNING eRunning;

// Program options.
BORF_OPTIONS options;



void BorfEvent( WORD wType, DWORD dwIdm, DWORD dwVoid, LPVOID lpVoid, LPCTSTR lpwszList = NULL, ... )
{
/** 
 *  Sends an event to the logging service.
 *
 *    wType   [in]  The type of event.
 *    dwIdm   [in]  The sybolic message identifier from the 'borfevm.mc' resource.
 *    dwVoid  [in]  The length of the arbitrary data block.
 *    lpVoid  [in]  A pointer to the arbitrary data block.
 *    wsxList [in]  The substitution list for the dwIdm message.
 *
 *  The last argument must always be NULL or the program will segfault.
 *
 *
 *  The dwIdm parameter must be one of the following enumeration values:
 *
 *    EVENTLOG_SUCCESS
 *    EVENTLOG_ERROR_TYPE
 *    EVENTLOG_WARNING_TYPE
 *    EVENTLOG_INFORMATION_TYPE
 *    EVENTLOG_AUDIT_SUCCESS
 *    EVENTLOG_AUDIT_FAILURE
 *
 *
 *  The program must be installed as a service for the event viewer to
 *  properly parse events that are generated by this function.
 *
 */

	// The variable argument list.
	va_list vaList;

	// An error buffer.
	DWORD dwError;

	// A result buffer.
	DWORD dwResult;

	// A handle for the logging facility.
	HANDLE hLog;

	// A dynamic array of pointers.
	LPCTSTR* lpStrings;

	// The working index of lpStrings.
	DWORD dwStrings = 0;


	// Allocate the initial array element.
	lpStrings = (LPCTSTR*) malloc( sizeof( LPCTSTR* ) );

	// Set the initial element.
	lpStrings[0] = lpwszList;

	// Initialize the variable argument list.
	va_start( vaList, lpwszList );

	
	while( lpStrings[dwStrings++] != NULL )
	{
		// Allocate another array element.
		lpStrings = (LPCTSTR*) realloc( lpStrings, sizeof( LPCTSTR* ) * ( dwStrings + 1 ) );

		// Add a pointer for this argument to the array of pointers.
		lpStrings[ dwStrings ] = va_arg( vaList, LPCTSTR );

	} // for

	
	// Attach to the event logging service.
	hLog = RegisterEventSource( NULL, BORF_KNOB_SERVICE_NAME );

	if( hLog )
	{
		// Report the event.
		dwResult = ReportEvent(
			hLog,
			wType,
			0,
			dwIdm,
			NULL,
			dwStrings,
			dwVoid,
			lpStrings,
			lpVoid );

		if( ! dwResult )
		{
			// WARNING: Do not use BorfApiWarning here. You might loop the program.
			dwError = GetLastError();
			wcerr << L"BorfEvent: ReportEvent: " << dwError << endl;
		}
	}

	else
	{
		dwError = GetLastError();
		wcout << L"BorfADsEventWarning: RegisterEventSource: " << dwError << endl;
	}

	// Release the log handle.
	DeregisterEventSource( hLog );

	// Deallocate the array of pointers.
	free( lpStrings );

	// Demacroize the variable argument list.
	va_end( vaList );

} // BorfEvent



void BorfApiWarning( DWORD dwError, LPCTSTR lpwszSource )
{
/**
 *  Generates an event log warning for the given error.
 *
 *    dwError  [in]  The error code, usually from GetLastError().
 *    dwSource [in]  A decription about where the error happened.
 *
 */

	// A buffer for the system error.
	LPTSTR lpwszBuffer;
	
	// Get the error string.
	FormatMessage(
		FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER, 
		NULL,
		dwError,
		0,
		(LPTSTR)&lpwszBuffer,
		0,
		NULL );

	// DEBUG: Print the error.
	wcerr << lpwszSource << L": " << dwError << L": " << lpwszBuffer << endl;

	// Log the warning.
	BorfEvent(
		EVENTLOG_WARNING_TYPE,
		IDM_WARNING_API,
		sizeof( dwError ),
		&dwError,
		lpwszSource,
		lpwszBuffer,
		NULL );


	// Free the system buffer.
	LocalFree( lpwszBuffer );


} // BorfError


HRESULT BorfDomainDefault( CString& sDomainName )
{
/**
 *  Puts the default domain name of localhost into sDomainName.
 *
 *    sDomainName [out]  The domain name of this computer.
 *
 */

	// A buffer for API results.
	DWORD dwResult;

	// The maximum length of a fully qualified DNS name is 255 characters.
	ULONG ulSize = max( 255, UNCLEN +1 );

	// Allocate a fixed buffer for the domain name.
	LPTSTR lpszBuffer = sDomainName.GetBuffer( ulSize );
	
	// Get the domain name of this computer.  Note that this is a bool
	dwResult = GetComputerNameEx( ComputerNameDnsDomain, lpszBuffer, &ulSize );

	// Release the fixed domain name buffer.
	sDomainName.ReleaseBuffer();


	if( ! dwResult )
	{
		BorfApiWarning( dwResult, L"BorfDomainDefault: GetComputerNameEx" );
	}

	else if( ulSize < 1 )
	{
		// FIXME:  Log this.
		wcerr << L"BorfDomainDefault: GetComputerNameEx: This computer is not participating in a domain." << endl;
		return -1;
	}

	return S_OK;
	
} // BorfDomainDefault



void BorfUsage( int argc, TCHAR* argv[], TCHAR* envp[] )
{
/**
 *  Prints copyright and usage information.
 */

	wcout << endl;
	wcout << L"Borf for Windows 2000 Routing and Remote Access                      " << endl;
	wcout << L"Copyright Brant FreeNet. Copyright Darik Horn <darik@bfree.on.ca>.   " << endl;
	wcout << L"This software is provided \"as is\" without warranty.                " << endl;
	wcout << L"                                                                     " << endl;
	wcout << L"  Usage:                                                             " << endl;
	wcout << L"                                                                     " << endl;
	wcout << L"    " << argv[0] << L" [ --install | --uninstall ]                   " << endl;
	wcout << L"                                                                     " << endl;
	wcout << L"  Command line options:                                              " << endl;
	wcout << L"                                                                     " << endl;
	wcout << L"    --install    Install this program as a service.                  " << endl;
	wcout << L"    --uninstall  Uninstall this program as a service.                " << endl;
	wcout << L"                                                                     " << endl;
	wcout << L"  The event viewer will not understand borfrras events               " << endl;
	wcout << L"  unless the program has been installed as a service.                " << endl;
	wcout << L"                                                                     " << endl;
	wcout << endl;

} // BorfUsage



void BorfGetIni( void )
{
/**
 *  Parses the INI file and turns the appropriate knobs.
 */

	// The return value of GetPrivateProfileString().
	DWORD dwCount;

	// A scanning buffer.
	DWORD dwBuffer = 0;

	// A scanning buffer.
	FLOAT fBuffer = 0;

	// A data buffer. This size is the maximum size of an INI section.
	TCHAR wsBuffer [ 32767 ];

	// The INI file name.
	CString sFile;


	// Get the current working directory.
	GetCurrentDirectory( sizeof(wsBuffer) -1, (LPTSTR)wsBuffer );

	// Format the file name.  Do not quote the file name.
	sFile.Format( L"%s\\%s.ini", wsBuffer, BORF_KNOB_SERVICE_NAME );

	// DEBUG:
	// wcout << L"BorfGetIni: Loading \"" << (LPCTSTR)sFile << L"\"." << endl;

/*
	dwCount = GetPrivateProfileString(
		BORF_KNOB_SERVICE_NAME,  // Section.      "[foo]"
		L"domain",               // Key.          "foo="
		L"",                     // Return this value the key doesn't exist.
		(LPTSTR)&wsBuffer,       // Return value. "=foo"
		sizeof( wsBuffer ) -1,   // Available buffer size.
		(LPCTSTR)sFile );        // The INI file.

	if( dwCount > 0 )
	{
		wcout << L"BorfGetIni: Domain: " << wsBuffer << endl;
		options.sDomainName = wsBuffer;
	}
*/

	dwCount = GetPrivateProfileString(
		BORF_KNOB_SERVICE_NAME,
		L"PollingInterval",
		L"",
		(LPTSTR)&wsBuffer,
		sizeof( wsBuffer ) -1,
		(LPCTSTR)sFile );

	if( dwCount > 0 )
	{
		dwCount = swscanf( wsBuffer, L"%i", &dwBuffer );

		if( dwCount == 1 && dwBuffer >= 0 )
		{
			wcout << L"BorfGetIni: PollingInterval: " << wsBuffer << endl;
			options.dwPollingInterval = dwBuffer;
			options.dwPollingInterval *= 1000;
		}
	}


	dwCount = GetPrivateProfileString(
		BORF_KNOB_SERVICE_NAME,
		L"TimeServerGroup",
		L"",
		(LPTSTR)&wsBuffer,
		sizeof( wsBuffer ) -1,
		(LPCTSTR)sFile );

	if( dwCount > 0 )
	{
		wcout << L"BorfGetIni: TimeServerGroup: " << wsBuffer << endl;
		options.sTimeServerGroup = wsBuffer;
	}

	
	dwCount = GetPrivateProfileString(
		BORF_KNOB_SERVICE_NAME,
		L"TimeLockoutGroup",
		L"",
		(LPTSTR)&wsBuffer,
		sizeof( wsBuffer ) -1,
		(LPCTSTR)sFile );

	if( dwCount > 0 )
	{
		wcout << L"BorfGetIni: TimeLockoutGroup: " << wsBuffer << endl;
		options.sTimeLockoutGroup = wsBuffer;
	}

	
	dwCount = GetPrivateProfileString(
		BORF_KNOB_SERVICE_NAME,
		L"TimeBusyThreshold",
		L"",
		(LPTSTR)&wsBuffer,
		sizeof( wsBuffer ) -1,
		(LPCTSTR)sFile );

	if( dwCount > 0 )
	{
		dwCount = swscanf( wsBuffer, L"%f", &fBuffer );

		if( dwCount == 1 && fBuffer >= 0 && fBuffer <= 1 )
		{
			wcout << L"BorfGetIni: TimeBusyThreshold: " << fBuffer << endl;
			options.dTimeBusyThreshold = (DOUBLE)fBuffer;
		}
	}


	dwCount = GetPrivateProfileString(
		BORF_KNOB_SERVICE_NAME,
		L"TimeLimit",
		L"",
		(LPTSTR)&wsBuffer,
		sizeof( wsBuffer ) -1,
		(LPCTSTR)sFile );

	if( dwCount > 0 )
	{
		dwCount = swscanf( wsBuffer, L"%i", &dwBuffer );

		if( dwCount == 1 && dwBuffer >= 0 )
		{
			wcout << L"BorfGetIni: TimeLimit: " << wsBuffer << endl;
			options.dwTimeLimit = dwBuffer;
		}
	}


	dwCount = GetPrivateProfileString(
		BORF_KNOB_SERVICE_NAME,
		L"TimeLimitOff",
		L"",
		(LPTSTR)&wsBuffer,
		sizeof( wsBuffer ) -1,
		(LPCTSTR)sFile );

	if( dwCount > 0 )
	{
		dwCount = swscanf( wsBuffer, L"%i", &dwBuffer );

		if( dwCount == 1 && dwBuffer >= 0 && dwBuffer < 24 )
		{
			wcout << L"BorfGetIni: TimeLimitOff: " << wsBuffer << endl;
			options.dwTimeOff = dwBuffer;
		}
	}


	dwCount = GetPrivateProfileString(
		BORF_KNOB_SERVICE_NAME,
		L"TimeLimitOn",
		L"",
		(LPTSTR)&wsBuffer,
		sizeof( wsBuffer ) -1,
		(LPCTSTR)sFile );

	if( dwCount > 0 )
	{
		dwCount = swscanf( wsBuffer, L"%i", &dwBuffer );

		if( dwCount == 1 && dwBuffer >= 0 && dwBuffer < 24 )
		{
			wcout << L"BorfGetIni: TimeLimitOn: " << wsBuffer << endl;
			options.dwTimeOn = dwBuffer;
		}
	}


	dwCount = GetPrivateProfileString(
		BORF_KNOB_SERVICE_NAME,
		L"DuplicateServerGroup",
		L"",
		(LPTSTR)&wsBuffer,
		sizeof( wsBuffer ) -1,
		(LPCTSTR)sFile );

	if( dwCount > 0 )
	{
		wcout << L"BorfGetIni: DuplicateServerGroup: " << wsBuffer << endl;
		options.sDuplicateServerGroup = wsBuffer;
	}


	dwCount = GetPrivateProfileString(
		BORF_KNOB_SERVICE_NAME,
		L"DuplicateLockoutGroup",
		L"",
		(LPTSTR)&wsBuffer,
		sizeof( wsBuffer ) -1,
		(LPCTSTR)sFile );

	if( dwCount > 0 )
	{
		wcout << L"BorfGetIni: DuplicateLockoutGroup: " << wsBuffer << endl;
		options.sDuplicateLockoutGroup = wsBuffer;
	}


	dwCount = GetPrivateProfileString(
		BORF_KNOB_SERVICE_NAME,
		L"DuplicateLimit",
		L"",
		(LPTSTR)&wsBuffer,
		sizeof( wsBuffer ) -1,
		(LPCTSTR)sFile );

	if( dwCount > 0 )
	{
		dwCount = swscanf( wsBuffer, L"%i", &dwBuffer );

		if( dwCount == 1 && dwBuffer >= 0 )
		{
			wcout << L"BorfGetIni: DuplicateLimit: " << wsBuffer << endl;
			options.dwDuplicateLimit = dwBuffer;
		}
	}

} // BorfGetIni



void BorfGetOptions( int argc, TCHAR* argv[], TCHAR* envp[] )
{
/**
 *  Parses command line options and turns the appropriate knobs.
 */

	// Parse the command line for options.
	for( int i = 1 ; i < argc ; i++ )
	{

		if( wcscmp( argv[i], L"--install" ) == 0 )
		{
			// Install the service and exit.
			ExitProcess( BorfServiceInstall() );
		}


		if( wcscmp( argv[i], L"--uninstall" ) == 0 )
		{
			// Uninstall the service and exit.
			ExitProcess( BorfServiceUninstall() );
		}

		// Else this is an unknown option.
		BorfUsage( argc, argv, envp );

		// Fail and bail.
		ExitProcess( ERROR_BAD_ARGUMENTS );

	} // for argc

} // BorfGetOptions



int BorfMain( void )
{
/**
 *  This is the main processing loop.
 */

	// A buffer for COM results.
	HRESULT hResult;

	// A buffer for object names.
	CString sBuffer;

	// The server group path for 'time' rules.
	CString sADsTimePath;

	// The server group path for 'duplicate' rules.
	CString sADsDuplicatePath;

	// A list of server names.
	CStringList oServerNameList;

	// A list of MPR server handles.
	CListMprServerHandle oServerHandleList;

	// The list of BORF_CONNECTION structs.
	CListBorfConnection oConnectionList;

	// A position for iterating lists.
	POSITION borfPosition;

	// A timer.
	CTime oTime;

	// The number of connected ports over the busy threshold.
	DWORD dwServerOverload;

	// The number of servers in the previous time rule enumeration.
	DWORD dwTimeServerCount = -1;
	
	// The number of servers in the previous duplicate rule enumeration.
	DWORD dwDuplicateServerCount = -1;


	// Initialize the COM state.
	::CoInitialize( NULL );

	// Print the domain name.
	wcout << L"Connecting to domain: " << (LPCTSTR)options.sDomainName << endl;

	// Construct the ADSI path name of the server group to watch for duplicate logons.
	sADsDuplicatePath.Format( L"WinNT://%s/%s", options.sDomainName, options.sDuplicateServerGroup );

	// Construct the ADSI path name of the server group to watch for time restrictions.
	sADsTimePath.Format( L"WinNT://%s/%s", options.sDomainName, options.sTimeServerGroup );


	while( eRunning )
	{
	

		// *** DUPLICATE LOGON HANDLING *** 

		// Get the names of servers in the duplicate group.
		hResult = BorfADsMembers( sADsDuplicatePath, oServerNameList );

		// Strip the names of servers in the duplicate group.
		hResult = BorfStripWinntPathList( oServerNameList );

		// Check whether the number of servers has changed.
		if( oServerNameList.GetCount() != dwDuplicateServerCount )
		{
			// Set the new server count.
			dwDuplicateServerCount = oServerNameList.GetCount();

			if( oServerNameList.GetCount() == 0 )
			{
				// DEBUG:
				wcerr << L"Warning: The '" << (LPCTSTR)options.sDuplicateServerGroup << L"' group is empty." << endl;

				// Log that the server group is empty.
				BorfEvent(
					EVENTLOG_WARNING_TYPE,
					IDM_DUPLICATE_EMPTY,
					0,
					NULL,
					(LPCTSTR)options.sDuplicateServerGroup,
					NULL );
			}

			else
			{
				// Clear the name buffer.
				sBuffer.Empty();

				// Start at the top of the server list.
				borfPosition = oServerNameList.GetHeadPosition();

				// Add all server names to the list.
				while( borfPosition )
				{
					sBuffer += oServerNameList.GetNext( borfPosition ) + L" ";
				}

				wcout << endl;
				wcout << (LPCTSTR)options.sDuplicateServerGroup << L": " << (LPCTSTR)sBuffer;
				wcout << endl;

				// Log the new server list.
				BorfEvent(
					EVENTLOG_INFORMATION_TYPE,
					IDM_ADS_GROUPMEMBERS,
					0,
					NULL,
					(LPCTSTR)options.sDuplicateServerGroup,
					(LPCTSTR)sBuffer,
					NULL );

			} // else

		} // if the server count changed


		wcout << endl << L"BorfMain: Duplicate logon check." << endl;

		// Connect to all servers in the duplicate group.
		hResult = BorfServerConnect( oServerNameList, oServerHandleList );

		// Enumerate all connections in this group.
		hResult = BorfConnectionEnumerate( oServerHandleList, oConnectionList );

		// Borf all users with multiple connections.
		hResult = BorfDisconnectDuplicate( oConnectionList );

		// Free all connection buffers.
		hResult = BorfConnectionFree( oConnectionList );

		// Disconnect from the servers.
		hResult = BorfServerDisconnect( oServerHandleList );



		// Get the current time.
		oTime = CTime::GetCurrentTime();


		if( oTime.GetHour() < options.dwTimeOff || oTime.GetHour() >= options.dwTimeOn )
		{
			// *** TIME RESTRICTION HANDLING ***
			wcout << endl << L"BorfMain: Time restriction check." << endl;

			// Get the names of servers in the time group.
			hResult = BorfADsMembers( sADsTimePath, oServerNameList );

			// Strip the names of servers in the duplicate group.
			hResult = BorfStripWinntPathList( oServerNameList );


			// Check whether the number of servers has changed.
			if( oServerNameList.GetCount() != dwTimeServerCount )
			{
				// Set the new server count.
				dwTimeServerCount = oServerNameList.GetCount();

				if( oServerNameList.GetCount() == 0 )
				{
					// DEBUG:
					wcerr << L"Warning: The '" << (LPCTSTR)options.sTimeServerGroup << L"' group is empty." << endl;

					// Log that the server group is empty.
					BorfEvent(
						EVENTLOG_WARNING_TYPE,
						IDM_DUPLICATE_EMPTY,
						0,
						NULL,
						(LPCTSTR)options.sTimeServerGroup,
						NULL );
				}

				else
				{
					// Clear the name buffer.
					sBuffer.Empty();

					// Start at the top of the server list.
					borfPosition = oServerNameList.GetHeadPosition();

					// Add all server names to the list.
					while( borfPosition )
					{
						sBuffer += oServerNameList.GetNext( borfPosition ) + L" ";
					}

					wcout << endl;
					wcout << (LPCTSTR)options.sTimeServerGroup << L": " << (LPCTSTR)sBuffer;
					wcout << endl;

					// Log the new server list.
					BorfEvent(
						EVENTLOG_INFORMATION_TYPE,
						IDM_ADS_GROUPMEMBERS,
						0,
						NULL,
						(LPCTSTR)options.sTimeServerGroup,
						(LPCTSTR)sBuffer,
						NULL );

				} // else

			} // if the server count changed

			// Connect to all servers in the time group.
			hResult = BorfServerConnect( oServerNameList, oServerHandleList );

			// Check whether the servers are busy.
			hResult = BorfServerIsBusy( oServerHandleList, dwServerOverload = 0 );

			// DEBUG:
			wcout << L"BorfServerIsBusy: A maximum of " << dwServerOverload << L" users will be disconnected." << endl;

			// Force full borfing by setting dwServerOverload negative.
			// dwServerOverload = -1;

			if( dwServerOverload )
			{
				// Enumerate all connections in this group.
				hResult = BorfConnectionEnumerate( oServerHandleList, oConnectionList );

				// Enforce time restrictions.
				hResult = BorfDisconnectTime( oConnectionList, dwServerOverload );

				// Free all connection buffers.
				hResult = BorfConnectionFree( oConnectionList );

			}

			// Disconnect from the servers.
			hResult = BorfServerDisconnect( oServerHandleList );

		} // if time


		// Doze for the polling interval.
		Sleep( options.dwPollingInterval ); 

	} // while main

	
	return S_OK;


} // borfMain



int _tmain( int argc, TCHAR* argv[], TCHAR* envp[] )
{
/**
 *  The main function that is called regardless of whether this program
 *  is being started as as service or as a console program
 */

	// An error buffer.
	DWORD dwError;

	// Initialize the Microsoft Foundation Classes.
	if( ! AfxWinInit( ::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0 ) )
	{
		wcerr << L"AfxWinInit: MFC initialization failed" << endl;
		return -1;
	}

	// Load the default domain name.
	BorfDomainDefault( options.sDomainName );

	// Set option defaults.
	options.dwPollingInterval      = BORF_KNOB_POLLING_INTERVAL;
	options.dTimeBusyThreshold     = BORF_KNOB_TIME_BUSY_THRESHOLD;
	options.dwTimeLimit            = BORF_KNOB_TIME_LIMIT;
	options.dwTimeOff              = BORF_KNOB_TIME_OFF;
	options.dwTimeOn               = BORF_KNOB_TIME_OFF;
	options.sTimeServerGroup       = BORF_KNOB_TIME_SERVER_GROUP;
	options.sTimeLockoutGroup      = BORF_KNOB_TIME_LOCKOUT_GROUP;
	options.dwDuplicateLimit       = BORF_KNOB_DUPLICATE_LIMIT;
	options.sDuplicateServerGroup  = BORF_KNOB_DUPLICATE_SERVER_GROUP;
	options.sDuplicateLockoutGroup = BORF_KNOB_DUPLICATE_LOCKOUT_GROUP;


	// Load options from the INI file.
	BorfGetIni();

	// Load options from the command line.
	BorfGetOptions( argc, argv, envp );

	// This function table is used by the SCM to start the service thread.
	SERVICE_TABLE_ENTRY borfServiceTable [] =
	{
		{ BORF_KNOB_SERVICE_NAME, BorfServiceMain }, // The service name and entry function.
		{ NULL, NULL }                               // Padding.
	}; 

	// DEBUG:
	wcout << L"Trying to start as a service. Please wait..." << endl;

	// Attempt to start as a service.
	if( StartServiceCtrlDispatcher( borfServiceTable ) )
	{
		// The SCM successfully started a service thread.
		// TODO: Log this.

		// This thread must terminate.
		return 0;
	}

	// Get the error code.
	dwError = GetLastError();

	switch( dwError )
	{

		case ERROR_FAILED_SERVICE_CONTROLLER_CONNECT:

			// DEBUG:
			wcout << L"StartServiceCtrlDispatcher: ERROR_FAILED_SERVICE_CONTROLLER_CONNECT" << endl;
			wcout << L"Okay, running as a console program instead." << endl;

			// Initialize the termination condition.
			eRunning = BORF_RUNNING_CONSOLE;

			// Begin operations as a console program.
			return BorfMain();

		case ERROR_INVALID_DATA:

			// Bork!
			wcerr << L"StartServiceCtrlDispatches: ERROR_INVALID_DATA" << endl;
			return dwError;

		case ERROR_SERVICE_ALREADY_RUNNING:

			// Doh!
			wcerr << L"StartServiceCtrlDispatches: ERROR_SERVICE_ALREADY_RUNNING" << endl;
			return dwError;

		default:

			// Uh-oh!
			wcerr << L"StartServiceCtrlDispatches: " << dwError << endl;
			return dwError;

	} // switch
	
} // _tmain

// eof