/////////////////////////////////////////////////////////////////////////////// // // Copyright (c) 1997, Microsoft Corp. All rights reserved. // // FILE // // iasapi.cpp // // SYNOPSIS // // This file implements all the non-COM DLL exports for the IAS core. // // MODIFICATION HISTORY // // 09/02/1997 Original version. // 11/12/1997 Added IASInitialize and IASUnitialize. // Added debug support for small block heap. // Added IASUpdateRegistry // 12/04/1997 Added initialization of pAuditChannel to IASInitialize // 01/30/1998 Added IASAdler32. // 04/10/1998 Eliminate aliasing in IASSmallBlockFree. // 04/11/1998 Only update sb_count for non-NULL blocks. // 04/13/1998 Removed SystemMonitor coclass. // 05/11/1998 Lifecycle changes for auditors. // 06/04/1998 Remove lifecycle control of auditors. // 06/08/1998 Remove timer queue initialization and shutdown. // 06/16/1998 Added IASVariantChangeType. // 08/05/1998 Use task allocator for small block pool. // 08/10/1998 Remove obsolete API's. // 10/09/1998 Convert between VT_BSTR and VT_ARRAY | VT_UI1 // 05/21/1999 Remove old style trace. // 01/25/2000 Added IASGetHostByName. // 03/10/2000 IASGetHostByName can take a null hostname for localhost. // 04/14/2000 Added dictionary API. // 05/12/2000 Pass correct buffer size to LoadStringW. // /////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include #include #include /////////////////////////////////////////////////////////////////////////////// // // Audit Channel API // /////////////////////////////////////////////////////////////////////////////// // Global pointer to the audit channel. IAuditSink* pAuditChannel = NULL; HRESULT WINAPI IASReportEvent( DWORD dwEventID, DWORD dwNumStrings, DWORD dwDataSize, LPCWSTR* aszStrings, LPVOID pRawData ) { if (pAuditChannel == NULL) { return E_POINTER; } return pAuditChannel->AuditEvent(dwEventID, dwNumStrings, dwDataSize, (wchar_t**)aszStrings, (byte*)pRawData); } /////////////////////////////////////////////////////////////////////////////// // // Thread Pool API // /////////////////////////////////////////////////////////////////////////////// #include // The global dispatcher object. Dispatcher dispatcher; BOOL WINAPI IASRequestThread(PIAS_CALLBACK pOnStart) { return dispatcher.requestThread(pOnStart); } DWORD WINAPI IASSetMaxNumberOfThreads(DWORD dwNumberOfThreads) { return dispatcher.setMaxNumberOfThreads(dwNumberOfThreads); } DWORD WINAPI IASSetMaxThreadIdle(DWORD dwMilliseconds) { return dispatcher.setMaxThreadIdle(dwMilliseconds); } /////////////////////////////////////////////////////////////////////////////// // // FUNCTION // // IASRegisterComponent // // DESCRIPTION // // Updates the registry entries for the specified component. // /////////////////////////////////////////////////////////////////////////////// HRESULT WINAPI IASRegisterComponent( HINSTANCE hInstance, REFCLSID clsid, LPCWSTR szProgramName, LPCWSTR szComponent, DWORD dwRegFlags, REFGUID tlid, WORD wVerMajor, WORD wVerMinor, BOOL bRegister ) { ////////// // Create the registrar object. ////////// CComPtr p; RETURN_ERROR(CoCreateInstance(CLSID_Registrar, NULL, CLSCTX_INPROC_SERVER, IID_IRegistrar, (void**)&p)); ////////// // Get the module file name for the component. ////////// WCHAR szModule[MAX_PATH + 1]; if (!GetModuleFileNameW(hInstance, szModule, MAX_PATH + 1)) { DWORD error = GetLastError(); return HRESULT_FROM_WIN32(error); } ////////// // Get our module file name. ////////// WCHAR szOurModule[MAX_PATH + 1] = L""; if (!GetModuleFileNameW(_Module.GetModuleInstance(), szOurModule, MAX_PATH + 1)) { DWORD error = GetLastError(); return HRESULT_FROM_WIN32(error); } ////////// // Convert the GUID strings. ////////// WCHAR szClsID[40], szLibID[40]; RETURN_ERROR(StringFromGUID2( clsid, szClsID, sizeof(szClsID) / sizeof(WCHAR))); RETURN_ERROR(StringFromGUID2( tlid, szLibID, sizeof(szLibID) / sizeof(WCHAR))); ////////// // Convert the version to a string. ////////// WCHAR szMajor[7] = L""; wsprintfW(szMajor, L"%d", wVerMajor); WCHAR szMinor[7] = L""; wsprintfW(szMinor, L"%d", wVerMinor); ////////// // Parse the bit flags. ////////// PCWSTR szContext, szAttributes, szModel; if (dwRegFlags & IAS_REGISTRY_LOCAL) { szContext = L"LocalServer32"; } else { szContext = L"InprocServer32"; } if (dwRegFlags & IAS_REGISTRY_AUTO) { szAttributes = L"Programmable"; } else { szAttributes = L""; } if (dwRegFlags & IAS_REGISTRY_BOTH) { szModel = L"Both"; } else if (dwRegFlags & IAS_REGISTRY_APT) { szModel = L"Apartment"; } else { szModel = L"Free"; } ////////// // Add the replacement strings. ////////// RETURN_ERROR(p->AddReplacement(L"MODULE", szModule)); RETURN_ERROR(p->AddReplacement(L"CLSID", szClsID)); RETURN_ERROR(p->AddReplacement(L"PROGRAM", szProgramName)); RETURN_ERROR(p->AddReplacement(L"COMPONENT", szComponent)); RETURN_ERROR(p->AddReplacement(L"TYPENAME", L" ")); RETURN_ERROR(p->AddReplacement(L"LIBID", szLibID)); RETURN_ERROR(p->AddReplacement(L"MAJORVER", szMajor)); RETURN_ERROR(p->AddReplacement(L"MINORVER", szMinor)); RETURN_ERROR(p->AddReplacement(L"CONTEXT", szContext)); RETURN_ERROR(p->AddReplacement(L"ATTRIBUTES", szAttributes)); RETURN_ERROR(p->AddReplacement(L"MODEL", szModel)); ////////// // Now we either register or unregister the component based on the // bRegister flag. ////////// HRESULT hr; if (bRegister) { hr = p->ResourceRegister(szOurModule, IDR_IASCOM, L"REGISTRY"); } else { hr = p->ResourceUnregister(szOurModule, IDR_IASCOM, L"REGISTRY"); } return hr; } /////////////////////////////////////////////////////////////////////////////// // // FUNCTION // // IASAdler32 // // DESCRIPTION // // Computes the Adler-32 checksum of a buffer. // /////////////////////////////////////////////////////////////////////////////// DWORD WINAPI IASAdler32( CONST BYTE *pBuffer, DWORD nBufferLength ) { static const DWORD ADLER_BASE = 65521; DWORD s1 = 1; DWORD s2 = 0; while (nBufferLength--) { s1 = (s1 + *pBuffer++) % ADLER_BASE; s2 = (s2 + s1) % ADLER_BASE; } return (s2 << 16) + s1; } /////////////////////////////////////////////////////////////////////////////// // // FUNCTION // // IASAllocateUniqueID // // DESCRIPTION // // Allocates a 32-bit integer that's guaranteed to be unique process-wide. // /////////////////////////////////////////////////////////////////////////////// DWORD WINAPI IASAllocateUniqueID( VOID ) { static LONG nextID = 0; return (DWORD)InterlockedIncrement(&nextID); } ////////// // Convert a hex digit to the number it represents. ////////// inline BYTE digit2Num(WCHAR digit) throw () { return (digit >= L'0' && digit <= L'9') ? digit - L'0' : digit - (L'A' - 10); } ////////// // Convert a number to a hex representation. ////////// inline WCHAR num2Digit(BYTE num) throw () { return (num < 10) ? num + L'0' : num + (L'A' - 10); } /////////////////////////////////////////////////////////////////////////////// // // FUNCTION // // IASVariantChangeType // // DESCRIPTION // // Replacement for VariantChangeType (q.v.) to bypass creating a message // loop. // /////////////////////////////////////////////////////////////////////////////// HRESULT WINAPI IASVariantChangeType( VARIANT * pvargDest, VARIANT * pvarSrc, USHORT wFlags, VARTYPE vt ) { // Check the input arguments. if (pvargDest == NULL || pvarSrc == NULL) { return E_INVALIDARG; } // Is the source already the requested type? if (V_VT(pvarSrc) == vt) { return (pvargDest != pvarSrc) ? VariantCopy(pvargDest, pvarSrc) : S_OK; } VARIANT varTmp; VariantInit(&varTmp); switch (MAKELONG(vt, V_VT(pvarSrc))) { case MAKELONG(VT_BOOL, VT_BSTR): { if (V_BSTR(pvarSrc) == NULL) { return DISP_E_TYPEMISMATCH; } V_BOOL(&varTmp) = (VARIANT_BOOL) _wtol(V_BSTR(pvarSrc)) ? VARIANT_TRUE : VARIANT_FALSE; break; } case MAKELONG(VT_I4, VT_BSTR): { if (V_BSTR(pvarSrc) == NULL) { return DISP_E_TYPEMISMATCH; } V_I4(&varTmp) = _wtol(V_BSTR(pvarSrc)); break; } case MAKELONG((VT_UI1 | VT_ARRAY) , VT_BSTR): { // Extract the source string. PCWSTR src = V_BSTR(pvarSrc); if (src == NULL) { return DISP_E_TYPEMISMATCH; } LONG srclen = wcslen(src); // Compute the destination length. if (srclen & 1) { return DISP_E_TYPEMISMATCH; } LONG dstlen = srclen / 2; // Allocate a SAFEARRAY of bytes to hold the octets. CVariantVector vec(&varTmp, dstlen); PBYTE dst = vec.data(); // Loop through the source and convert. while (dstlen--) { *dst = digit2Num(*src++) << 4; *dst++ |= digit2Num(*src++); } break; } case MAKELONG(VT_BSTR, VT_BOOL): { V_BSTR(&varTmp) = SysAllocString(V_BOOL(pvarSrc) ? L"-1" : L"0"); if (V_BSTR(&varTmp) == NULL) { return E_OUTOFMEMORY; } break; } case MAKELONG(VT_BSTR, VT_I4): { WCHAR buffer[12]; V_BSTR(&varTmp) = SysAllocString(_ltow(V_I4(pvarSrc), buffer, 10)); if (V_BSTR(&varTmp) == NULL) { return E_OUTOFMEMORY; } break; } case MAKELONG(VT_BSTR, (VT_UI1 | VT_ARRAY)): { // Extract the source octets. CVariantVector vec(pvarSrc); CONST BYTE* src = vec.data(); LONG srclen = vec.size(); // Allocate space for the 'stringized' version. PWCHAR dst = SysAllocStringLen(NULL, srclen * 2); if (dst == NULL) { return E_OUTOFMEMORY; } V_BSTR(&varTmp) = dst; // Loop through and convert. while (srclen--) { *dst++ = num2Digit(*src >> 4); *dst++ = num2Digit(*src++ & 0xF); } // Add a null-terminator. *dst = L'\0'; break; } default: return DISP_E_TYPEMISMATCH; } // We successfully converted, so set the type. V_VT(&varTmp) = vt; // Free the destination. VariantClear(pvargDest); // Copy in the coerced variant. *pvargDest = varTmp; return S_OK; } /////////////////////////////////////////////////////////////////////////////// // // Routines to handle startup and shutdown. // /////////////////////////////////////////////////////////////////////////////// // Reference count for the IAS API. LONG refCount = 0; // Shared local dictionary. VARIANT theDictionaryStorage; BOOL WINAPI IASInitialize(VOID) { HRESULT hr; DWORD error; WSADATA wsaData; // Global lock to serialize access. std::_Lockit _Lk; // If we're already initialized, there's nothing to do. if (refCount > 0) { ++refCount; return TRUE; } // Initialize the audit channel. hr = CoCreateInstance(__uuidof(AuditChannel), NULL, CLSCTX_INPROC_SERVER, __uuidof(IAuditSink), (PVOID*)&pAuditChannel); if (FAILED(hr)) { SetLastError(hr); goto auditor_failed; } // Initialize winsock. error = WSAStartup(MAKEWORD(2, 0), &wsaData); if (error) { SetLastError(error); goto wsa_failed; } // Initialize the thread pool. if (!dispatcher.initialize()) { goto thrdpool_failed; } // Everything succeeded, so bump up the refCount. ++refCount; return TRUE; thrdpool_failed: WSACleanup(); wsa_failed: pAuditChannel->Release(); pAuditChannel = NULL; auditor_failed: return FALSE; } VOID WINAPI IASUninitialize( VOID) { std::_Lockit _Lk; _ASSERT(refCount != 0); if (--refCount == 0) { // Shutdown the thread pool. This blocks until all threads have exited. dispatcher.finalize(); // Shutdown winsock. WSACleanup(); // Shutdown the audit channel. pAuditChannel->Release(); pAuditChannel = NULL; // Shutdown the dictionary. VariantClear(&theDictionaryStorage); } } VOID WINAPI IASRadiusCrypt( BOOL encrypt, BOOL salted, const BYTE* secret, ULONG secretLen, const BYTE* reqAuth, PBYTE buf, ULONG buflen ) { MD5_CTX context; BYTE cipherText[MD5DIGESTLEN]; BYTE *p; const BYTE *end, *endBlock, *ct, *src; WORD salt; static LONG theNextSalt; // Use the Request-Authenticator as the first block of ciphertext. ct = reqAuth; // Compute the beginning and end of the data to be crypted. p = buf; end = buf + buflen; // Is the buffer salted ? if (salted) { if (encrypt) { // Get the next salt value. salt = (WORD)(++theNextSalt); // High bit must be set. salt |= 0x8000; // Store at the front of the buffer. IASInsertWORD(buf, salt); } // Skip past the salt. p += 2; } // Loop through the buffer. while (p < end) { // Compute the digest. MD5Init(&context); MD5Update(&context, secret, secretLen); MD5Update(&context, ct, MD5DIGESTLEN); if (salted) { MD5Update(&context, buf, 2); // Only use the salt on the first pass. salted = FALSE; } MD5Final(&context); // Find the end of the block to be decrypted. endBlock = p + MD5DIGESTLEN; if (endBlock >= end) { // We've reached the end of the buffer. endBlock = end; } else { // Save the ciphertext for the next pass. ct = encrypt ? p : (PBYTE)memcpy(cipherText, p, MD5DIGESTLEN); } // Crypt the block. for (src = context.digest; p < endBlock; ++p, ++src) { *p ^= *src; } } } ///////// // Unicode version of gethostbyname. The caller must free the returned hostent // struct by calling LocalFree. ///////// PHOSTENT WINAPI IASGetHostByName( IN PCWSTR name ) { // We put these at function scope, so we can clean them up on the way out. DWORD error = NO_ERROR; HANDLE lookup = NULL; union { WSAQUERYSETW querySet; BYTE buffer[512]; }; PWSAQUERYSETW result = NULL; PHOSTENT retval = NULL; do { if (!name) { // A NULL name means use the local host, so allocate a buffer ... DWORD size = 0; GetComputerNameEx( ComputerNamePhysicalDnsFullyQualified, NULL, &size ); PWSTR buf = (PWSTR)_alloca(size * sizeof(WCHAR)); // ... and get the local DNS name. if (!GetComputerNameEx( ComputerNamePhysicalDnsFullyQualified, buf, &size )) { error = GetLastError(); break; } name = buf; } ////////// // Create the query set ////////// GUID hostAddrByNameGuid = SVCID_INET_HOSTADDRBYNAME; AFPROTOCOLS protocols[2] = { { AF_INET, IPPROTO_UDP }, { AF_INET, IPPROTO_TCP } }; memset(&querySet, 0, sizeof(querySet)); querySet.dwSize = sizeof(querySet); querySet.lpszServiceInstanceName = (PWSTR)name; querySet.lpServiceClassId = &hostAddrByNameGuid; querySet.dwNameSpace = NS_ALL; querySet.dwNumberOfProtocols = 2; querySet.lpafpProtocols = protocols; ////////// // Execute the query. ////////// error = WSALookupServiceBeginW( &querySet, LUP_RETURN_ADDR, &lookup ); if (error) { error = WSAGetLastError(); break; } ////////// // How much space do we need for the result? ////////// DWORD length = sizeof(buffer); error = WSALookupServiceNextW( lookup, 0, &length, &querySet ); if (!error) { result = &querySet; } else { error = WSAGetLastError(); if (error != WSAEFAULT) { break; } ///////// // Allocate memory to hold the result. ///////// result = (PWSAQUERYSETW)LocalAlloc(0, length); if (!result) { error = WSA_NOT_ENOUGH_MEMORY; break; } ///////// // Get the result. ///////// error = WSALookupServiceNextW( lookup, 0, &length, result ); if (error) { error = WSAGetLastError(); break; } } if (result->dwNumberOfCsAddrs == 0) { error = WSANO_DATA; break; } /////// // Allocate memory to hold the hostent struct /////// DWORD naddr = result->dwNumberOfCsAddrs; SIZE_T nbyte = sizeof(hostent) + (naddr + 1) * sizeof(char*) + naddr * sizeof(in_addr); retval = (PHOSTENT)LocalAlloc(0, nbyte); if (!retval) { error = WSA_NOT_ENOUGH_MEMORY; break; } /////// // Initialize the hostent struct. /////// retval->h_name = NULL; retval->h_aliases = NULL; retval->h_addrtype = AF_INET; retval->h_length = sizeof(in_addr); retval->h_addr_list = (char**)(retval + 1); /////// // Store the addresses. /////// u_long* nextAddr = (u_long*)(retval->h_addr_list + naddr + 1); for (DWORD i = 0; i < naddr; ++i) { sockaddr_in* sin = (sockaddr_in*) result->lpcsaBuffer[i].RemoteAddr.lpSockaddr; retval->h_addr_list[i] = (char*)nextAddr; *nextAddr++ = sin->sin_addr.S_un.S_addr; } /////// // NULL terminate the address list. /////// retval->h_addr_list[i] = NULL; } while (FALSE); ////////// // Clean up and return. ////////// if (result && result != &querySet) { LocalFree(result); } if (lookup) { WSALookupServiceEnd(lookup); } if (error) { if (error == WSASERVICE_NOT_FOUND) { error = WSAHOST_NOT_FOUND; } WSASetLastError(error); } return retval; } ///////// // Fill in an IASTable struct from a VARIANT containing the table data. ///////// HRESULT ExtractTableFromVariant( IN VARIANT* var, OUT IASTable* table ) throw () { // Check the arguments. if (!var || !table) { return E_POINTER; } // Outer VARIANT must be an array of VARIANTs. if (V_VT(var) != (VT_ARRAY | VT_VARIANT)) { return E_INVALIDARG; } // Array must be 1D with exactly 3 elements. LPSAFEARRAY array = V_ARRAY(var); if (array->cDims != 1 || array->rgsabound[0].cElements != 3) { return E_INVALIDARG; } // tableData is an array of three variants: // (1) Column names // (2) Column types. // (3) Table data matrix. VARIANT* tableData = (VARIANT*)(array->pvData); // Process the column names. VARIANT* namesVariant = tableData + 0; // The VARIANT must be an array of BSTRs. if (V_VT(namesVariant) != (VT_ARRAY | VT_BSTR)) { return E_INVALIDARG; } // Array must be 1D. LPSAFEARRAY namesArray = V_ARRAY(namesVariant); if (namesArray->cDims != 1) { return E_INVALIDARG; } // Store the info in the IASTable. table->numColumns = namesArray->rgsabound[0].cElements; table->columnNames = (BSTR*)(namesArray->pvData); // Process the column types. VARIANT* typesVariant = tableData + 1; // The VARIANT must be an array of shorts. if (V_VT(typesVariant) != (VT_ARRAY | VT_UI2)) { return E_INVALIDARG; } // Array must be 1D with 1 element per column. LPSAFEARRAY typesArray = V_ARRAY(typesVariant); if (typesArray->cDims != 1 || typesArray->rgsabound[0].cElements != table->numColumns) { return E_INVALIDARG; } // Store the info in the IASTable. table->columnTypes = (VARTYPE*)(namesArray->pvData); // Process the table data matrix. VARIANT* tableVariant = tableData + 2; // The VARIANT must be an array of VARIANTs. if (V_VT(tableVariant) != (VT_ARRAY | VT_VARIANT)) { return E_INVALIDARG; } // Array must be 2D with 1st dim equal to number of columns. LPSAFEARRAY tableArray = V_ARRAY(tableVariant); if (tableArray->cDims != 2 || tableArray->rgsabound[0].cElements != table->numColumns) { return E_INVALIDARG; } // Store the info in the IASTable. table->numRows = tableArray->rgsabound[1].cElements; table->table = (VARIANT*)(tableArray->pvData); return S_OK; } HRESULT WINAPI IASGetDictionary( IN PCWSTR path, OUT IASTable* dnary, OUT VARIANT* storage ) { // Initialize the out parameters. VariantInit(storage); // Create the AttributeDictionary object. HRESULT hr; CComPtr dnaryObj; hr = CoCreateInstance( __uuidof(AttributeDictionary), NULL, CLSCTX_SERVER, __uuidof(IAttributeDictionary), (PVOID*)&dnaryObj ); if (FAILED(hr)) { return hr; } // We need to give the object permission to impersonate us. There's // no reason to abort if this fails; we'll just try with the // existing blanket. CoSetProxyBlanket( dnaryObj, RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT, COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_DEFAULT ); // Convert the path to a BSTR. CComBSTR bstrPath(path); if (!bstrPath) { return E_OUTOFMEMORY; } // Get the dictionary. hr = dnaryObj->GetDictionary(bstrPath, storage); if (FAILED(hr)) { return hr; } hr = ExtractTableFromVariant(storage, dnary); if (FAILED(hr)) { VariantClear(storage); } return hr; } ///////// // Get the path to the attribute dictionary on the local computer. ///////// LONG GetDictionaryPath(PWSTR buffer, PDWORD size) throw () { // Key ... const WCHAR POLICY_KEY[] = L"SYSTEM\\CurrentControlSet\\Services\\RemoteAccess\\Policy"; // ... and value where the path to the IAS directory is stored. const WCHAR PRODUCT_DIR_VALUE[] = L"ProductDir"; // String to append to ProductDir to form the path to the dictionary. const WCHAR DNARY_FILE[] = L"\\dnary.mdb"; // Number of extra characters needed to append DNARY_FILE to the path. const DWORD EXTRA_CHARS = sizeof(DNARY_FILE)/sizeof(WCHAR) - 1; // Check the arguments if (!buffer || !size) { return ERROR_INVALID_PARAMETER; } // Initialize the out parameter. DWORD inSize = *size; *size = 0; // Open the registry key. LONG result; HKEY hKey; result = RegOpenKeyExW( HKEY_LOCAL_MACHINE, POLICY_KEY, 0, KEY_READ, &hKey ); if (result != NO_ERROR) { return result; } // Read the ProductDir value. DWORD dwType; DWORD cbData = inSize * sizeof(WCHAR); result = RegQueryValueExW( hKey, PRODUCT_DIR_VALUE, NULL, &dwType, (PBYTE)buffer, &cbData ); // We're done with the registry key. RegCloseKey(hKey); // Compute the length of the full path in characters. DWORD outSize = cbData / sizeof(WCHAR) + EXTRA_CHARS; if (result != NO_ERROR) { // If we overflowed, return the needed size. if (result == ERROR_MORE_DATA) { *size = outSize; } return result; } // The registry value must contain a string. if (dwType != REG_SZ) { return REGDB_E_INVALIDVALUE; } // Do we have enough room to append the DNARY_FILE. if (outSize <= inSize) { wcscat(buffer, DNARY_FILE); } else { result = ERROR_MORE_DATA; } // Return the size (whether actual or needed). *size = outSize; return result; } const IASTable* WINAPI IASGetLocalDictionary( VOID ) { static IASTable theTable; // Global lock to serialize access. std::_Lockit _Lk; // Have we already gotten the local dictionary ? if (V_VT(&theDictionaryStorage) == VT_EMPTY) { HRESULT hr; // No, so determine the path ... WCHAR path[256]; DWORD size = sizeof(path)/sizeof(WCHAR); hr = GetDictionaryPath(path, &size); if (hr == NO_ERROR) { // ... and get the dictionary. hr = IASGetDictionary( path, &theTable, &theDictionaryStorage ); } else { hr = HRESULT_FROM_WIN32(hr); } if (FAILED(hr)) { SetLastError(hr); return NULL; } } return &theTable; }