Microsoft KB Archive/813414

= How to create an anonymous pipe that gives access to everyone =

Article ID: 813414

Article Last Modified on 1/10/2007

-

APPLIES TO


 * Microsoft Platform Software Development Kit-January 2000 Edition

-



SUMMARY
''This article describes how to create an anonymous pipe that gives access to everyone. This article contains a sample application that creates an anonymous session pipe that is used for communication between the server and the client. This article describes the following in detail:''

 

Creating a server that writes the information at one end of the pipe.

 

Registering the name of the pipe in the registry.

 

Handling the Windows security when communicating over the pipe.

 

Creating a client that reads the information from the other end of the pipe.





INTRODUCTION
This article describes how to create a pipe server that sends and receives data across domains without requiring user names and passwords. When a client connects to the pipe server, the server makes an access check, and then requests the client to authenticate. Depending on how the permissions of the pipe are defined, the access may be granted or denied. If the client user has an access token, the client can connect to the server. If the client does not have an access token, the client application must perform a logon to the server. However, sometimes you may have to give access to everyone. Therefore, you must create an anonymous, or null, session pipe. Because you are granting permissions to everyone, your pipe must not give access to any sensible information to the users.



MORE INFORMATION
The sample application in this article is a simple client/server application that uses an anonymous named pipe. The server incrementally writes numbers to the pipe that are read and printed by the client. Because the pipe is anonymous, the client may connect from other domains through an anonymous logon. The sample application is explained as follows.

Server
The server creates the anonymous pipes, and then listens for clients. To create an anonymous pipe, make sure that you do the following:
 * 1) Add the name of the pipe to the registry.
 * 2) Define the LPSECURITY_ATTRIBUTES attributes that are passed to the CreateNamedPipe function.

Registry
In the registry of the server computer, the pipe name is added to the following registry subkey:

The pipe name that is added to this registry entry is the name after the last backslash (\) character in the string that is used to open the pipe.

For example, for the \\ \pipe\AnonymousPipe pipe, you must add AnonymousPipe to the NullSessionPipes registry entry on the server computer. You can do this by using the RegisterNullSession function and the UnregisterNullSession function in the sample application.

Note  is a placeholder for the name of your server.

Security
Many programmers use NULL Discretionary Access Control List (DACL) to give anonymous access to shared resources. However, there are no restrictions that are defined to access the shared resource. You must create a security identifier (SID) that gives the anonymous logon and the read-only access to the pipe. The SID that you create in the sample is known by every installation of Microsoft Windows, and is named AnonymousLogin. This SID is described as S-1-5-7. For more information about these numbers, see the Winnt.h file. When the pipe client connects to the server, an anonymous logon is performed by making a call to the WNetAddConnection2 function, and the AnonymousLogin SID will grant the access. In the sample application, a small class that is named SEC is created to handle the complex access structures. This behavior eases the freeing of allocated memory. The BuildSecurityAttributes function in this class builds the SID and sets the permissions. The permissions for the anonymous logon are set as read-only because the pipe is a read-only pipe. You do this by making a call to the AddAccessAllowedAce function. If you want to create a pipe that has both read and write permissions, you must change the permissions with the AddAccessAllowedAce function.

The other SID must be defined by the owner who owns the named pipe, and this makes sure that the application has access to the named pipe. The owner SID is the user SID that is running the thread or the process. The GetUserSid function extracts the SID from the token of the user. The token is created when you log on and you specify your name and password that is used for all access checks.

With the attributes defined, the server calls the CreateNamedPipe function, and then waits for incoming client calls.

Client
The client opens the pipe with a call to the CreateFile function. If an error occurs, the client checks if it is a logon failure error, an access denied error, or a bad password error. If an error occurs, perform an anonymous logon by calling the WNetAddConnection2 function and passing an empty string as user name and password. When the null session is established, the client calls the CreateFile function again. After the client is connected, it prints a number each second.

Sample application
To create an application that communicates between the processes by using an anonymous pipe that gives access to everyone, follow these steps:  Start Microsoft Visual C++ 6.0. On the File menu, click New.</li> On the Projects tab, click Win32 Console Application, type MyApp in the Project name box, and then click OK.</li> In the Win32 Console Application - Step 1 of 1 dialog box, click Finish.</li> In the New Project Information window, click OK.</li> On the File menu, click New. The New dialog box appears.</li> Click C++ Source File. In the right pane, type MyApp in the File name box, and then click OK.</li> In the left pane, click the FileView tab.</li> Expand MyApp files, and then expand Source Files.</li> Under Source Files, right-click MyApp.cpp, and then click Open. In the right pane, the code view of the MyApp.cpp file opens.</li>  Add the following code to the MyApp.cpp file:
 * 1) define UNICODE
 * 2) include   <windows.h>
 * 3) include   <stdio.h>

// Types

class SEC {   // Class to handle security attributes public: SEC; ~SEC; BOOL BuildSecurityAttributes( SECURITY_ATTRIBUTES* psa ); private: BOOL GetUserSid( PSID* ppSidUser );

BOOL   allocated; PSECURITY_DESCRIPTOR   psd; PACL   pACL; PTOKEN_USER pTokenUser; };

// Constants static const WCHAR* PipeName = L&quot;AnonymousPipe&quot;;

// Functions static void Client( const WCHAR* server ); static void Server( void ); static void WriteToPipe( HANDLE hPipe ); static void ReadFromPipe( HANDLE hPipe ); static void DisplayError( WCHAR *pszAPI); static BOOL WINAPI ConsoleCtrlHandler( DWORD CtrlType );

static BOOL EstablishNullSession( const WCHAR* computerName );

static BOOL RegisterNullSession( const WCHAR* pipeName ); static BOOL UnregisterNullSession( const WCHAR* pipeName );

static LONG RegInsertToMultiString( HKEY hKey, const WCHAR* valueName, const WCHAR* str ); static LONG RegRemoveFromMultiString( HKEY hKey, const WCHAR* valueName, const WCHAR* str ); static BOOL FindEntryInMultiString( const WCHAR* data, const WCHAR* entry, DWORD* offset ); void wmain( int argc, WCHAR* argv[] ) {   if( argc < 2 ) {       wprintf( L&quot;Usage: pipe.exe <server|client [server name for client]>\n&quot; ); exit(1); }

if( !SetConsoleCtrlHandler( ConsoleCtrlHandler, TRUE ) ) DisplayError( L&quot;SetConsoleCtrlHandler&quot; );

if( wcsicmp( argv[1], L&quot;client&quot;) == 0 ) {       WCHAR    server[512]; if( argc == 3 ) {           wcscpy( server, argv[2] ); }       else {           wcscpy( server, L&quot;.&quot; ); }       Client( server ); }   else if( wcsicmp( argv[1], L&quot;server&quot; ) == 0 ) {       Server; }   else {       wprintf( L&quot;Usage: pipe.exe <server|client [server name for client]>\n&quot; ); exit(1); } }

//Runs the server void Server( void ) {   SEC sec; HANDLE hPipe; BOOL   isConnected; SECURITY_ATTRIBUTES    sa; WCHAR  server[512];

swprintf( server, L&quot;\\\\.\\pipe\\%s&quot;, PipeName );

if( !RegisterNullSession( PipeName ) ) {       wprintf( L&quot;WARNING: could not register %s for Null Session, server will not be accessible from outside domain.\n&quot;, server ); wprintf( L&quot;WARNING: only administrators can register a Null Session Pipe.\n&quot; ); }

sec.BuildSecurityAttributes( &sa );

wprintf( L&quot;INFORM: to stop, press Ctrl-C or Ctrl-Break.\n&quot; ); while( 1 ) {       hPipe = CreateNamedPipe(                     server,                    PIPE_ACCESS_OUTBOUND,       // read-only pipe                    PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,                    PIPE_UNLIMITED_INSTANCES,                     sizeof(DWORD),                     0,                     NMPWAIT_USE_DEFAULT_WAIT,                             &sa );

if( hPipe == INVALID_HANDLE_VALUE ) DisplayError( L&quot;CreatePipe&quot; ); // Wait for the client to connect. wprintf( L&quot;INFORM: listening at %s, waiting for client to connect\n&quot;, server ); isConnected = ConnectNamedPipe( hPipe, NULL ) ? TRUE : (GetLastError == ERROR_PIPE_CONNECTED); if( isConnected ) {           wprintf( L&quot;INFORM: client connected.\n&quot; ); WriteToPipe( hPipe ); }       else // The client could not connect. Therefore, close the pipe. CloseHandle(hPipe); } }

//Runs the client void Client( const WCHAR* server ) {   HANDLE hPipe; WCHAR fullName[512]; BOOL  triedLogon = FALSE;

swprintf( fullName, L&quot;\\\\%s\\pipe\\%s&quot;, server, PipeName );

wprintf( L&quot;INFORM: connecting to %s\n&quot;, fullName );

while( 1 ) {        hPipe = CreateFile(                     fullName,                    GENERIC_READ,                    FILE_SHARE_READ,                    NULL,                    OPEN_EXISTING,                    SECURITY_ANONYMOUS,                    NULL); if( hPipe == INVALID_HANDLE_VALUE ) {           DWORD lastError = GetLastError;            // An error ocurred, try to analyse it            if( (lastError == ERROR_LOGON_FAILURE) ||                (lastError == ERROR_ACCESS_DENIED) ||                (lastError == ERROR_INVALID_PASSWORD))  //Add other access errors here if it is required {               if( triedLogon ) {                   DisplayError( L&quot;CreateFile&quot; ); }               else if( EstablishNullSession( server ) ) {                   wprintf( L&quot;INFORM: established Null session.\n&quot; ); }               else {                   wprintf( L&quot;WARNING: could not establish Null Session.\n&quot; ); }               triedLogon = TRUE; }           else if( lastError == ERROR_PIPE_BUSY ) {               // All pipe instances are busy. Therefore, wait for 20 seconds. wprintf( L&quot;INFORM: pipe busy, trying for 20secs to reconnect.\n&quot; ); if( !WaitNamedPipe( fullName, 20000) ) DisplayError( L&quot;WaitNamedPipe&quot; ); }           else {               DisplayError( L&quot;CreateFile&quot; ); }       }        else {           ReadFromPipe( hPipe ); }   } }

/** Tries to establish a NULL session to the remote computer. A NULL session allows access to NullSessionPipes of the server. Input parameters: computerName: name of the server to connect to Output parameters: TRUE | FALSE

BOOL EstablishNullSession( const WCHAR* computerName ) {   NETRESOURCE nr; WCHAR server[MAX_PATH]; DWORD ret;

if( wcscmp( computerName, L&quot;.&quot; ) == 0 ) {       wcscpy( server, computerName ); }   else {       wcscpy( server, L&quot;\\\\&quot; ); wcscat( server, computerName ); }   ZeroMemory( &nr, sizeof(nr) ); nr.dwType      = RESOURCETYPE_ANY; nr.lpRemoteName = server;

ret = WNetAddConnection2( &nr, L&quot;&quot;, L&quot;&quot;, 0); if( ret != ERROR_SUCCESS ) {       DisplayError( L&quot;WNetAddConnection2&quot; ); return FALSE; }   return TRUE; }

//Called when the user presses Ctrl-C or Ctrl-Break in the console window.

BOOL WINAPI ConsoleCtrlHandler( DWORD CtrlType ) {   // clean up the registry UnregisterNullSession( PipeName ); return FALSE; }

/** Writes to the pipe Input parameters: hPipe: handle to an opened pipe void WriteToPipe( HANDLE hPipe ) {   DWORD i = 0; DWORD cbWrite, cbWritten;

cbWrite = sizeof(DWORD); while( 1 ) {         if( !WriteFile( hPipe, &i, cbWrite,  &cbWritten,  NULL) ) {            if( cbWrite != cbWritten) break; }        i++; Sleep( 1000 ); }   // Flush the pipe to allow the client to read the contents of the pipe // before disconnecting. Disconnect the pipe, and then close the // handle to this pipe instance. FlushFileBuffers( hPipe ); DisconnectNamedPipe( hPipe ); CloseHandle( hPipe );

wprintf( L&quot;INFORM: client disconnected.\n&quot; ); }

/** Reads from pipe Input parameters: hPipe: handle to opened pipe. */

void ReadFromPipe( HANDLE hPipe ) {   DWORD i, cbRead; while( 1 ) {        if( !ReadFile( hPipe, &i, sizeof(DWORD), &cbRead, NULL) ) {           if( GetLastError != ERROR_MORE_DATA ) {               CloseHandle( hPipe ); DisplayError( L&quot;ReadFile&quot; ); }       }        wprintf( L&quot; %li &quot;, i ); }; }

/** Displays an error and exits the process Input parameters: pszAPI: name of the Win32 function that returned an error. */

void DisplayError( WCHAR* pszAPI ) {   LPVOID lpvMessageBuffer;

FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM,                    NULL, GetLastError,                     MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),                    (LPTSTR)&lpvMessageBuffer, 0, NULL);

//... now display this string wprintf( L&quot;ERROR: API       = %s.\n&quot;, pszAPI); wprintf( L&quot;      error code = %d.\n&quot;, GetLastError); wprintf( L&quot;      message    = %s.\n&quot;, (char *)lpvMessageBuffer);

// Free the buffer allocated by the system LocalFree( lpvMessageBuffer );

ExitProcess( GetLastError ); }

/** Constructor */ SEC:: SEC {    allocated = FALSE; psd = NULL; pACL = NULL; pTokenUser = NULL; }

/** Destructor */ SEC:: ~SEC {    if( allocated ) {            if( psd ) HeapFree( GetProcessHeap, 0, psd ); if( pACL ) HeapFree( GetProcessHeap, 0, pACL ); if( pTokenUser ) HeapFree( GetProcessHeap, 0, pTokenUser ); allocated = FALSE; } }

/** Builds security attributes that allows read-only access to everyone Input parameters: psa: security attributes to build Output parameters: TRUE | FALSE */ BOOL SEC:: BuildSecurityAttributes( SECURITY_ATTRIBUTES* psa ) {   DWORD dwAclSize; PSID pSidAnonymous = NULL; // Well-known AnonymousLogin SID PSID pSidOwner = NULL; if( allocated ) return FALSE;

SID_IDENTIFIER_AUTHORITY siaAnonymous = SECURITY_NT_AUTHORITY; SID_IDENTIFIER_AUTHORITY siaOwner = SECURITY_NT_AUTHORITY; do   { psd = (PSECURITY_DESCRIPTOR) HeapAlloc( GetProcessHeap,                                               HEAP_ZERO_MEMORY,                                                SECURITY_DESCRIPTOR_MIN_LENGTH); if( psd == NULL ) {           DisplayError( L&quot;HeapAlloc&quot; ); break; }

if( !InitializeSecurityDescriptor( psd, SECURITY_DESCRIPTOR_REVISION) ) {           DisplayError( L&quot;InitializeSecurityDescriptor&quot; ); break; }

// Build anonymous SID AllocateAndInitializeSid( &siaAnonymous, 1,                                  SECURITY_ANONYMOUS_LOGON_RID,                                   0,0,0,0,0,0,0,                                  &pSidAnonymous                                );

if( !GetUserSid( &pSidOwner ) ) {           return FALSE; }

// Compute size of ACL dwAclSize = sizeof(ACL) + 2 * ( sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD) ) + GetLengthSid( pSidAnonymous ) + GetLengthSid( pSidOwner ); pACL = (PACL)HeapAlloc( GetProcessHeap, HEAP_ZERO_MEMORY, dwAclSize ); if( pACL == NULL ) {           DisplayError( L&quot;HeapAlloc&quot; ); break; }       InitializeAcl( pACL, dwAclSize, ACL_REVISION); if( !AddAccessAllowedAce( pACL, ACL_REVISION, GENERIC_ALL, pSidOwner ))        {            DisplayError( L&quot;AddAccessAllowedAce&quot; ); break; }       if( !AddAccessAllowedAce( pACL, ACL_REVISION, FILE_GENERIC_READ, //GENERIC_READ | GENERIC_WRITE, pSidAnonymous ) )        {            DisplayError( L&quot;AddAccessAllowedAce&quot; ); break; }       if( !SetSecurityDescriptorDacl( psd, TRUE, pACL, FALSE) ) {           DisplayError( L&quot;SetSecurityDescriptorDacl&quot; ); break; }       psa->nLength = sizeof(SECURITY_ATTRIBUTES); psa->bInheritHandle = TRUE; psa->lpSecurityDescriptor = psd;

allocated = TRUE; }while(0);

if( pSidAnonymous )  FreeSid( pSidAnonymous ); if( pSidOwner )      FreeSid( pSidOwner ); if( !allocated ) {       if( psd ) HeapFree( GetProcessHeap, 0, psd ); if( pACL ) HeapFree( GetProcessHeap, 0, pACL ); }

return allocated; }

/** Obtains the SID of the user running this thread or process. Output parameters: ppSidUser: the SID of the current user, TRUE  | FALSE: could not obtain the user SID */ BOOL SEC:: GetUserSid( PSID* ppSidUser ) {   HANDLE      hToken; DWORD      dwLength; DWORD      cbName = 250; DWORD      cbDomainName = 250; if( !OpenThreadToken( GetCurrentThread, TOKEN_QUERY, TRUE, &hToken) ) {       if( GetLastError == ERROR_NO_TOKEN ) {           if( !OpenProcessToken( GetCurrentProcess, TOKEN_QUERY, &hToken) ) {               return FALSE; }       }        else {           return FALSE; }   }

if( !GetTokenInformation( hToken,      // handle of the access token TokenUser,   // type of information to retrieve pTokenUser,  // address of retrieved information 0,           // size of the information buffer &dwLength    // address of required buffer size ))   {        if( GetLastError == ERROR_INSUFFICIENT_BUFFER ) {           pTokenUser = (PTOKEN_USER) HeapAlloc( GetProcessHeap, HEAP_ZERO_MEMORY, dwLength ); if( pTokenUser == NULL ) {               return FALSE; }       }        else {           return FALSE; }   }

if( !GetTokenInformation(  hToken,     // handle of the access token TokenUser, // type of information to retrieve pTokenUser, // address of retrieved information dwLength,  // size of the information buffer &dwLength  // address of required buffer size ))   {        HeapFree( GetProcessHeap, 0, pTokenUser ); pTokenUser = NULL;

return FALSE; }

*ppSidUser = pTokenUser->User.Sid; return TRUE; }

/** Registers a pipe as a NULL SESSION pipe that may be accessed by unauthenticated users. Input parameters: serverName: pipe to register Output parameters: TRUE: pipe registered | FALSE: could no register pipe. */

BOOL RegisterNullSession( const WCHAR* pipeName ) {   LONG status; HKEY hKey;

// Opens key status = RegCreateKeyEx( HKEY_LOCAL_MACHINE,                             L&quot;SYSTEM\\CurrentControlSet\\Services\\lanmanserver\\parameters&quot;,                              0,                              NULL,                              REG_OPTION_NON_VOLATILE,                              KEY_QUERY_VALUE | KEY_SET_VALUE,                              NULL,                              &hKey,                              NULL);

if( status != ERROR_SUCCESS ) {       return FALSE; }

status = RegInsertToMultiString( hKey, L&quot;NullSessionPipes&quot;, pipeName );

RegCloseKey( hKey );

if( status != ERROR_SUCCESS ) {       return FALSE; }

return TRUE; }

/** Unregisters a pipe from the NULL SESSION pipe list. Input parameters: pipeName: pipe to unregister Output parameters: TRUE: pipe unregistered or pipe was not registered | FALSE: could not unregister pipe

BOOL UnregisterNullSession( const WCHAR* pipeName ) {   LONG status; HKEY hKey;

// Opens key status = RegCreateKeyEx( HKEY_LOCAL_MACHINE,                             L&quot;SYSTEM\\CurrentControlSet\\Services\\lanmanserver\\parameters&quot;,                              0,                              NULL,                              REG_OPTION_NON_VOLATILE,                              KEY_QUERY_VALUE | KEY_SET_VALUE,                              NULL,                              &hKey,                              NULL);

if( status != ERROR_SUCCESS ) {       return FALSE; }   status = RegRemoveFromMultiString( hKey, L&quot;NullSessionPipes&quot;, pipeName ); RegCloseKey( hKey );

if( (status != ERROR_SUCCESS) && (status != ERROR_INVALID_NAME) ) {       return FALSE; }

return TRUE; }

/** Inserts a string to a MULTI_SZ value. If the string is already present, nothing will be added. This avoids duplicated strings. Input parameters: hKey: handle to open key, valueName: name of the multistring value to which the str value will be appended, str: string to append <-- ERROR_SUCCESS | any error code returned by RegQueryValueEx and RegSetValueEx */

LONG RegInsertToMultiString( HKEY hKey, const WCHAR* valueName, const WCHAR* str ) {   LONG status; BYTE* data; DWORD newSize, oldSize, strSize;

// Obtains the current data size status = RegQueryValueEx( hKey,                              valueName,                               NULL,                               NULL,                               NULL,                               &oldSize );

if( status != ERROR_SUCCESS ) {       return status; }

// Allocates memory to hold all the data strSize = (wcslen(str) + 1) * sizeof(WCHAR); newSize = oldSize + strSize; data = (BYTE*)HeapAlloc( GetProcessHeap, 0, newSize ); if( data == NULL ) {       return ERROR_OUTOFMEMORY; }

// Obtains the current data status = RegQueryValueEx( hKey,                              valueName,                               NULL,                               NULL,                               data,                               &oldSize ); if( status != ERROR_SUCCESS ) {       HeapFree( GetProcessHeap, 0, data ); return status; }

// Looks if the data is already there if( FindEntryInMultiString( (WCHAR*)data, str, NULL ) ) {       HeapFree( GetProcessHeap, 0, data ); return ERROR_SUCCESS; }

// Appends our entry CopyMemory( data+oldSize-2, str, strSize );

// Append another NULL terminator data[newSize-2] = 0; data[newSize-1] = 0;

// Sets the new data status = RegSetValueEx( hKey,                            valueName,                             0,                             REG_MULTI_SZ,                             data,                             newSize );

HeapFree( GetProcessHeap, 0, data );

return status; }

/** Removes a string from a MULTI_SZ value. Input parameters: hKey: handle to open key, valueName: name of the multistring value to which the str value will be appended, str: string to append <-- ERROR_SUCCESS | ERROR_INVALID_NAME: str was not found | any error code returned by RegQueryValueEx and RegSetValueEx */

LONG RegRemoveFromMultiString( HKEY hKey, const WCHAR* valueName, const WCHAR* str ) {   LONG status; BYTE* data; DWORD size, offset, strSize; BYTE* p;   BOOL  found = FALSE;

// Obtains the current data size status = RegQueryValueEx( hKey,                              valueName,                               NULL,                               NULL,                               NULL,                               &size ); if( status != ERROR_SUCCESS ) {       return status; }

// Allocates memory to hold all the data strSize = (wcslen(str) + 1) * sizeof(WCHAR); data = (BYTE*)HeapAlloc( GetProcessHeap, 0, size ); if( data == NULL ) {       return ERROR_OUTOFMEMORY; }

// Obtains the current data status = RegQueryValueEx( hKey,                              valueName,                               NULL,                               NULL,                               data,                               &size ); if( status != ERROR_SUCCESS ) {       HeapFree( GetProcessHeap, 0, data ); return status; }

// Searches the entry to be removed found = FindEntryInMultiString( (WCHAR*)data, str, &offset ); if( !found ) {       HeapFree( GetProcessHeap, 0, data ); return ERROR_INVALID_NAME; }

// Writes over the entry, and then puts it back in the registry p = data + offset;

CopyMemory( p, p + strSize, size - strSize - offset );

status = RegSetValueEx( hKey,                            valueName,                             0,                             REG_MULTI_SZ,                             data,                             size - strSize );

HeapFree( GetProcessHeap, 0, data );

return status; }

/** Input parameters: data: multi string in which the entry is to be found, entry: to find offset: if not NULL holds the number of bytes from the beginning of data where entry starts Output parameters: TRUE: found entry | FALSE: did not find entry */

BOOL FindEntryInMultiString( const WCHAR* data, const WCHAR* entry, DWORD* offset ) {   BYTE* p;    DWORD read, tmp; BOOL found = FALSE;

p = (BYTE*)data; read = 0; while( !( (*p == 0 ) && ((*(p+1)) == 0 ) ) ) {       if( wcscmp( (WCHAR*)p, entry ) == 0 ) {           found = TRUE; break; }

tmp = (wcslen( (WCHAR*)p ) + 1) * sizeof(WCHAR); read += tmp ; p += tmp; }

if( offset ) {       if( found ) {           *offset = read; }       else {           *offset = 0; }   }    return found; } </li> To build the application, you must add the Mpr.lib file to your project. To do this, follow these steps: <ol style="list-style-type: lower-alpha;"> On the Project menu, click Settings. The Project Settings dialog box appears.</li> Click the Link tab, and then append Mpr.lib to the string in the Object/library modules box.</li> Click OK.</li></ol> </li> On the Build menu, click Build MyApp.exe.</li> To run the application, follow these steps: <ol style="list-style-type: lower-alpha;"> <li>Click Start, click Run, type cmd in the Open box, and then click OK.</li> <li>At the command prompt, run the following command:

\MyAPP server

The server waits for the client to connect.

Note  is a placeholder for the path where the MyApp.exe file is saved.</li> <li>Repeat step a, and then run the following command at the command prompt:

\MyAPP client

The client connects to the server, and then prints the numbers sequentially that are read from the pipe.

Note  is a placeholder for the name of the client computer.</li></ol> </li></ol>

<div class="references_section">