Dr. Dobb's Journal October 1997
In the November 1996 issue of Dr. Dobb's Journal, I presented a method of issuing NetWare Core Protocol (NCP) requests by directly sending file-system control requests to Windows NT's built-in NetWare redirector driver, \Device\NwRdr (see "Undocumented NT and the NetWare Core Protocol"). In that article, I touched upon the undocumented Windows 95/NT API NwlibMakeNcp() (exported from NWAPI32.dll), which comes with Microsoft's "Client Services for NetWare."
At the outset, it sounds like excellent news that Windows NT and Windows 95 export a uniform NCP API. However, a closer look reveals some small but important incompatibilities. First, both platforms use different representations of NetWare connection handles. Second, they use different internal control codes to identify NCP requests. These problems are minor and can be coped with quite easily. But in addition, Windows 95 supports only a small subset of NCP calls. So, while NT allows you to send arbitrary NCP requests, Windows 95's NCP interface is heavily crippled, lacking important functionality.
Essentially, NCP is a remote-procedure-call mechanism, allowing a NetWare client program to instruct a NetWare server to execute some fairly complex service routines. NCP requests and replies are transported inside IPX packets and consist of a header and data area. While the size of the header is fixed, the data area may vary in size. NCP is a powerful protocol, since it is simple on the client's side, and leaves the hard work to the server.
One problem with the NetWare clients coming with Windows NT and Windows 95 is that they lack a consistent client API. In fact, the original NetWare API as defined by Novell, with its hundreds of functions, is a no-no under Microsoft's NetWare client. This has long since been a drawback for NetWare utility and systems software developers, forcing many companies to install Novell's client instead, although the operating system has built-in NetWare support.
A small and not particularly useful subset of NetWare APIs is exported by Microsoft's NWAPI32.dll. Interestingly, this DLL also exports NwlibMakeNcp(), which is not part of Novell's API specification, and opens a back door to the NetWare redirector by allowing NCP requests to be sent to a NetWare server. Although NCP isn't NetWare and there are many tasks that can't be accomplished using NCP alone, it's a place to start. Hopefully, further research will reveal more undocumented NetWare functionality that fits into the picture.
NwlibMakeNcp() communicates with the underlying kernel-mode driver or VxD through file-system control (NT) or device I/O control (95) calls. While my previous article focused more on the lower end of this communication, I'll examine the higher levels here. NWAPI32.dll can be called from any application running in user-mode and is available under both Windows NT and Windows 95. Windows 95 users do have to install NetWare Directory Services (NDS) support, available as a separate add-on from Microsoft (ftp://ftp.microsoft.com/Softlib/MSLFILES/MSNDS.EXE).
Example 1 shows the NwlibMakeNcp() prototype. NwlibMakeNcp() accepts five required and an arbitrary number of optional arguments, depending on which NCP request is sent. The first argument, hDevice, is an internal device handle that is needed to communicate with the redirector device. dIoControlCode is an I/O control code, telling the driver what kind of NCP request it should send. dInBufferSize and dOutBufferSize specify the sizes of the NCP request and reply packets, respectively. And finally, psFragmentControl consists of a string of characters indicating how many and what kind of arguments are following.
It's important to note that NwlibMakeNcp() doesn't return NetWare completion codes. Instead, its return value is the status code returned by the redirector device driver. If you are interested in details on them, look at NTSTATUS.h, which comes with the Windows NT or Windows 95 Device Driver Kits (DDK). Since Windows NT and Windows 95 feature quite different driver architectures, NwlibMakeNcp() returns different error codes on these platforms. The only thing you can rely on is a return code of zero, indicating success.
The device handle identifies the server connection you are referring to. Under Windows 95, this is simply the connection handle returned by NWAttachToFileServer(), a standard NetWare API function exported from NWAPI32.dll. Under Windows NT, the device handle is part of a structure I've named NWCONN_HANDLE_DATA, a pointer to which is returned by NWAttachToFileServer(). Example 2 is the prototype of this API.
The first argument, psServerName, is the name of the NetWare server you send NCP requests to. Note that it is not specified in UNC notation; for instance, it should not start with two leading backslashes (although Windows 95 is pretty forgiving if you include the backslashes anyway). The second argument is reserved and should be set to 0. phConnection points to a NWCONN_HANDLE variable that will receive the connection handle. Under Windows NT, this handle is defined as Example 3(a). As you see, it's just a pointer to a NWCONN_HANDLE_DATA structure, where NWCONN_ HANDLE_ DATA looks like Example 3(b).
The hServer member is the hDevice handle you need to stuff into NT's version of NwlibMakeNcp(). The ServerName member contains the UNC name of the corresponding server, represented as a UNICODE_STRING structure; see Example 3(c).
The third function taking part in the game is NWDetachFromFileServer(). It does exactly the opposite of NWAttachToFileServer(); for example, it closes the server connection and the corresponding device handle. This, too, is a standard NetWare API function exported from NWAPI32.dll, and its prototype is extraordinarily simple, as Example 3(d) shows.
Windows NT and Windows 95 use similar mechanisms to implement NCP functionality. Calls to NT's NwlibMakeNcp() are directly transformed to file-system control requests, using the undocumented NtFsControlFile() API from NTDLL.dll. Windows 95 takes a much more twisted approach. First, NwlibMakeNcp() rearranges its request parameters and calls into yet another NetWare interface DLL, named NWNET32.dll. This DLL does not export symbolic APIs, so magic ordinal numbers are used. NCP requests are handled by functions NWAPI32.1 and NWAPI32.2. NwlibMakeNcp() calls the latter to do its job. NWAPI32.2 rearranges the request parameters again and finally calls into the NetWare redirector VxD, using device I/O control requests.
Both Windows NT and Windows 95 use control codes to identify what the redirector is supposed to do. The codes expected by NwlibMakeNcp() are undocumented. My previous article on NCP gave a thorough explanation of how valid NCP file-system control codes are formed under Windows NT. In short, you have to use 00141003h as a base value, adding the desired NCP function code -- multiplied by four -- to it. For instance, the control code for "Get File Server Date And Time" (NCP function 14h) is 00141053h. The value of 00141003h is composed of the device type (FILE_ DEVICE_NETWORK_FILE_SYSTEM =0014h, bits 16..31) the access type (FILE_ANY_ACCESS = 0, bits 14..15), the buffering method (METHOD_ NEITHER = 3, bits 0..1), and the NCP ID code (always 4, bits 10..13).
Under Windows 95, NCP requests have to pass several stages of preprocessing, finally ending as a device-I/O request. Thus, the control codes accepted by Windows 95's variant of NwlibMakeNcp() are quite different from NT's. While NT's control codes are true file-system control codes, the control codes used by Windows 95 undergo several mappings before eventually constituting a device I/O request code. This results in NwlibMakeNcp() using only the lowest eight bits of its dIoControlCode argument. Moreover, the value range reserved for NCP starts at no less than 0xE0. This design makes it impossible to have control codes for all valid NCP request codes. Therefore, Windows 95 supports just a small subset of NCP calls.
A control code of 0xE0 maps to NCP function 0x14 ("Get File Server Date and Time"). Therefore, you cannot use any NCP functions below that limit. This is bad news, since one of the most vital functions, "Get Station Number" (0x13), is not included. This means that you cannot use any of the NCP requests that involve a server connection number. There are quite a few of these--including some of the most popular ones, such as "Get Station's Logged Information" (0x17, subfunction 0x1C), which is needed to retrieve the user name of a logged-in user.
In all file-system control and device-I/O control calls, the target driver receives input and output buffers to exchange data with the caller. NwlibMakeNcp()'s dInBufferSize and dOutBufferSize arguments specify the respective sizes of these buffers. Interestingly, NwlibMakeNcp() does not expect any arguments for the buffer addresses. Instead, it allocates and deallocates the memory for them transparently.
Note that NWAPI32.dll always specifies two extra bytes for the NCP request and reply packets under both Windows NT and Windows 95. It appears that these are unnecessary under Windows NT. In fact, some NetWare 4 functions -- "Get UTC Time" (0x72, subfunction 0x01), for example -- may even fail if the I/O buffer sizes are not specified exactly. However, Windows 95 seems to require extra bytes in some cases. For instance, I've seen "Get File Server Date and Time" (0x14) fail if the extra bytes are missing.
The remaining arguments supplied to NwlibMakeNcp() constitute parameters that vary according to the type of request involved. The fifth argument to NwlibMakeNcp() is a control string, containing type information about the parameters to follow. Refer to my NCP article in the November 1996 issue of Dr. Dobb's Journal for an in-depth description of these.
In short, the request and reply packets are broken into fragments, like bytes, words, double words, strings, buffers, and so on. For every fragment, there's a corresponding (and somewhat mnemonic) control character in the psFragmentControl string (for example, "b" for byte, "w" for word, "p" for Pascal-type strings, and so forth). The scalar data types included in a request packet are typically passed as immediate data. Strings, buffers, and UNICODE_STRING structures are referenced by pointers. Reply fragments, whether scalar or not, are always referenced by pointers, because NwlibMakeNcp() needs some target address in order to copy the data included in the reply packet received from the remote server.
To show how to use NwlibMakeNcp() to implement standard NetWare APIs, I've written a sample DLL, named CALWIN32 .dll after the corresponding Novell API DLL. One important feature of this DLL is that it accounts for the slight NT versus Win95 incompatibilities in the implementation of NwlibMakeNcp() mentioned previously. CALWIN32.dll detects the platform it runs on at startup, setting the global flag fWinNT for later use. Listing One shows the platform-dependent code of CALWIN32.c, handling the API inconsistencies. In Listing Two, an excerpt from CALWIN32.c implements some standard NetWare APIs in terms of NCP requests. The full source code and all accompanying project files of CALWIN32.c are available electronically; see "Availability," page 3.
In Table 1, the APIs exported from CALWIN32 are listed. The list includes a couple of standard NetWare APIs not included in Microsoft's NWAPI32.dll, as well as some auxiliary functions. For transparency reasons, NWAttachToFileServer() and NWDetachFromFileServer() are included, too, although these are simply forwarded to NWAPI32.dll. This eliminates the need to dynamically link to NWAPI32.dll in addition to CALWIN32.dll from your applications.
Before you can compile CALWIN32.c, some preparatory work needs to be done. Since CALWIN32.c uses APIs exported from NWAPI32.dll, you need the import library NWAPI32.lib to allow the linker to emit an appropriate import section into the executable. Unfortunately, neither Visual C++ 4.x nor the Win32 SDK come with this file, nor do they contain a utility to extract a .lib file directly from a DLL's export section.
There's more than one solution to this problem. One of them, proposed by Claus Brod in the "Tech Tips" column of Windows Developer's Journal (November 1996) uses Microsoft's Library Manager to create a .lib file from a .def file built on the fly. The approach used here involves writing a dummy DLL, containing just enough code to yield a .lib file matching the original DLL. This requires a little more work, but you're getting a nice test DLL (for free), which you can use for debugging purposes. The NWAPI32 project files also are available electronically.
To test CALWIN32.dll, I've written CALTST32.exe, a console-mode application that displays some data about the NetWare server specified on the command line. Listing Three is a simplified excerpt (lacking the error-handling code) from CALTST32.c, showing how to call some of the NetWare APIs implemented in CALWIN32.dll.
Running CALTST32 under Windows 95 clearly shows the limited NCP capabilities of Windows 95. While CALTST32 executes without errors under Windows NT (except for the call to NWGetConnectionInformation() that fails if you are not logged into the target server), you'll get an error code of 0x00000001 (ERROR_INVALID_FUNCTION) for functions NWGetConnectionNumber() and NWGetFileServerUTCTime() under Windows 95. The former function is not supported because it involves an NCP request number of 0x13 -- just below the lower limit of supported NCP requests (which is 0x14). Contrary to this, the latter uses an NCP request number of 0x72, well exceeding the supported range.
Please note that CALTST32.exe does not use the standard VC++4 startup and run-time library (RTL) code. Instead, CALTST32.exe includes CuiStart.h (available electronically), which contains my own startup code WinStartup(), as well as a cloned version of printf(), which relies on the Win32 APIs wvsprintf() and WriteFile(). Getting rid of the standard startup and RTL code results in much leaner executables. To force VC++4 to use the alternative startup code, simply open the project settings dialog and enter WinStartup into the "Entry-point symbol" field of the linker output tab. In addition, make sure you don't forget to include CALWIN32.lib in the list of library modules.
Similarly, CALWIN32.dll and our dummy DLL NWAPI32.dll don't use any of Microsoft's startup and RTL routines either. DLLs don't require special startup treatment, so you can simply specify DllMain as the entry-point symbol. Since CALWIN32.dll imports symbols from NWAPI32.dll, you have to include NWAPI32.lib in the linker's library module list of CALWIN32's project settings.
The primary advantage of Microsoft's NetWare clients for Windows NT and Windows 95 is that they are a well-integrated part of the operating system. However, their APIs are rudimentary, making it almost impossible for NetWare client-software developers to write nontrivial applications. Luckily, both operating systems offer a useful -- although undocumented -- NCP interface, allowing client/server communication through the back door. But because Windows 95 supports only a small subset of NCP requests, Windows NT is a superior target platform for NetWare client software.
// =================================================================// PLATFORM-DEPENDENT SERVICES
// =================================================================
BOOL WINAPI
NwlibIsWinNT (void)
{
return fWinNT;
}
// -----------------------------------------------------------------
PWSTR WINAPI
NwlibGetServerName (NWCONN_HANDLE hConnection)
{
return (fWinNT ? hConnection->ServerName.sBuffer : L"");
}
// -----------------------------------------------------------------
HANDLE WINAPI
NwlibGetServerHandle (NWCONN_HANDLE hConnection)
{
return (fWinNT ? hConnection->hServer : hConnection);
}
// -----------------------------------------------------------------
BYTE WINAPI
NwlibGetNcpFunction (DWORD dNcpFunctionId)
{
return (BYTE) (dNcpFunctionId >> 8);
}
// -----------------------------------------------------------------
BYTE WINAPI
NwlibGetNcpSubFunction (DWORD dNcpFunctionId)
{
return (BYTE) dNcpFunctionId;
}
// -----------------------------------------------------------------
DWORD WINAPI
NwlibGetNcpControlCode (DWORD dNcpFunctionId)
{
DWORD dNcpFunction = NwlibGetNcpFunction (dNcpFunctionId);
return (fWinNT ? 0x00141003 | (dNcpFunction << 2)
: 0x000000CC + (dNcpFunction));
}
// =================================================================// NETWARE API FUNCTIONS
// =================================================================
DWORD WINAPI
NWGetConnectionNumber (NWCONN_HANDLE hConnection,
PDWORD pdConnection)
{
WORD wReserved;
DWORD dStatus;
*pdConnection = 0;
dStatus =
NwlibMakeNcp
(NwlibGetServerHandle (hConnection),
NwlibGetNcpControlCode (_GET_STATION_NUMBER),
RqSize (0),
RpSize (WORD_+BYTE_),
"|wb",
&wReserved,
pdConnection);
return dStatus;
}
// -----------------------------------------------------------------
DWORD WINAPI
NWGetConnectionInformation (NWCONN_HANDLE hConnection,
DWORD dConnection,
NWOBJECT_NAME ObjectName,
PWORD pwObjectType,
PDWORD pdObjectId,
PNWDATE_TIME pLoginTime)
{
DWORD dStatus;
dStatus =
NwlibMakeNcp
(NwlibGetServerHandle (hConnection),
NwlibGetNcpControlCode (_GET_STATION_LOGGED_INFORMATION),
RqSize (BYTE_+DWORD_),
RpSize (DWORD_+WORD_+NWOBJECT_NAME_+NWDATE_TIME_+BYTE_),
"br|dwrr",
NwlibGetNcpSubFunction (_GET_STATION_LOGGED_INFORMATION),
&dConnection, DWORD_,
pdObjectId,
pwObjectType,
ObjectName, NWOBJECT_NAME_,
pLoginTime, NWDATE_TIME_);
return dStatus;
}
// =================================================================// MAIN ROUTINES
// =================================================================
void DisplayVolumeNames (NWCONN_HANDLE hConnection)
{
BYTE bVolume;
NWVOLUME_NAME VolumeName;
DWORD dStatus;
printf ("\n\nEnumerating volumes ...");
for (bVolume = 0; bVolume < NW_MAX_VOLUMES; bVolume++)
{
dStatus = NWGetVolumeName (hConnection, bVolume, VolumeName);
if (!dStatus)
{
if (VolumeName [0])
{
printf ("\n Volume #%02lu = \"%s\"",
bVolume+1,
VolumeName);
}
}
}
return;
}
// -----------------------------------------------------------------
void DisplayConnectionData (NWCONN_HANDLE hConnection)
{
DWORD dConnection;
NWOBJECT_NAME ObjectName;
WORD wObjectType;
DWORD dObjectId;
NWDATE_TIME LoginTime;
DWORD dStatus;
printf ("\n\nRetrieving the connection number ...");
dStatus = NWGetConnectionNumber (hConnection, &dConnection);
if (!dStatus)
{
printf ("\n Connection number = %lu", dConnection);
printf ("\n\nRetrieving connection information ...");
dStatus = NWGetConnectionInformation (hConnection, dConnection,
ObjectName, &wObjectType,
&dObjectId, &LoginTime);
if (!dStatus)
{
printf ("\n Object name = \"%s\"", ObjectName);
printf ("\n Object type = 0x%04X", wObjectType);
printf ("\n Object ID = 0x%08X", dObjectId);
}
}
return;
}
DDJ