< Name Pipe Implementation Details | Name Pipes Main | Name Pipes APIs & Client Program Example >


 

 

Named Pipes 15 Part 2

 

 

What do we have in this chapter 15 part 2?

  1. Building Null DACLs

  2. Advanced Server

  3. Threads

  4. Overlapped I/O

  5. Security Impersonation

  6. Client Details

 

Building Null DACLs (this is a security breach actually!)

 

When applications create securable objects such as files and named pipes on the Windows NT platform using Windows API functions, the operating system grants the applications the ability to set up access control rights by specifying a SECURITY_ATTRIBUTES structure, defined as follows:

 

typedef struct _SECURITY_ATTRIBUTES {

    DWORD   nLength;

    LPVOID  lpSecurityDescriptor;

    BOOL    bInheritHandle

} SECURITY_ATTRIBUTES;

 

The lpSecurityDescriptor field defines the access rights for an object in a SECURITY_DESCRIPTOR structure. A SECURITY_DESCRIPTOR structure contains a DACL field that defines which users and groups can access the object. If you set this field to NULL, any user or group can access your resource.

Applications cannot directly access a SECURITY_DESCRIPTOR structure and must use Windows security API functions to do so. If you want to assign a null DACL to a SECURITY_DESCRIPTOR structure, you must do the following:

 

  1. Create and initialize a SECURITY_DESCRIPTOR structure by calling the InitializeSecurityDescriptor() API function.
  2. Assign a null DACL to the SECURITY_DESCRIPTOR structure by calling the SetSecurityDescriptorDacl() API function.

 

After you successfully build a new SECURITY_DESCRIPTOR structure, you must assign it to the SECURITY_ATTRIBUTES structure. Now you are ready to begin calling Windows functions such as CreateNamedPipe() with your new SECURITY_ATTRIBUTES structure, which contains a null DACL. The following code fragment demonstrates how to call the security API functions needed to accomplish this:

 

// Create new SECURITY_ATTRIBUTES and SECURITY_DESCRIPTOR structure objects

SECURITY_ATTRIBUTES sa;

SECURITY_DESCRIPTOR sd;

 

// Initialize the new SECURITY_DESCRIPTOR object to empty values

if (InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION) == 0)

{

    printf("InitializeSecurityDescriptor failed with error %d\n", GetLastError());

    return;

}

 

// Set the DACL field in the SECURITY_DESCRIPTOR object to NULL

if (SetSecurityDescriptorDacl(&sd, TRUE, NULL, FALSE) == 0)

{

    printf("SetSecurityDescriptorDacl failed with error %d\n", GetLastError());

    return;

}

 

// Assign the new SECURITY_DESCRIPTOR object to the SECURITY_ATTRIBUTES object

sa.nLength = sizeof(SECURITY_ATTRIBUTES);

sa.lpSecurityDescriptor = &sd;

sa.bInheritHandle = TRUE;

 

Advanced Server

 

The previous sample demonstrates how to develop a named pipe server application that handles only a single pipe instance. All of the API calls operate in a synchronous mode in which each call waits until an I/O request is complete. A named pipe server is also capable of having multiple pipe instances so that clients can form two or more connections to the server; the number of pipe instances is limited by the number specified in the nMaxInstances parameter of the CreateNamedPipe() API call. To handle more than one pipe instance, a server must consider using multiple threads or asynchronous Windows I/O mechanisms, such as overlapped I/O and completion ports, to service each pipe instance. Asynchronous I/O mechanisms allow a server to service all pipe instances simultaneously from a single application thread. Our discussion demonstrates how to develop advanced servers using threads and overlapped I/O.

 

Threads

 

Developing an advanced server that can support more than one pipe instance using threads is simple. All you need to do is create one thread for each pipe instance and service each instance using the techniques we described earlier for the simple server. The following sample demonstrates a server that is capable of serving five pipe instances. The application is an echo server that reads data from a client and echoes the data back.

 

// Threads sample

#include <windows.h>

#include <stdio.h>

#include <conio.h>

 

#define NUM_PIPES 5

 

DWORD WINAPI PipeInstanceProc(LPVOID lpParameter);

 

void main(void)

{

    HANDLE ThreadHandle;

    INT i;

    DWORD ThreadId;

 

    for(i = 0; i < NUM_PIPES; i++)

    {

        // Create a thread to serve each pipe instance

        if ((ThreadHandle = CreateThread(NULL, 0, PipeInstanceProc, NULL, 0, &ThreadId)) == NULL)

        {

            printf("CreateThread failed with error %\n", GetLastError());

            return;

        }

        CloseHandle(ThreadHandle);

    }

 

    printf("Press a key to stop the server\n");

    _getch();

}

 

// Function: PipeInstanceProc

// Description:  This function handles the communication details of a single named pipe instance

DWORD WINAPI PipeInstanceProc(LPVOID lpParameter)

{

    HANDLE PipeHandle;

    DWORD BytesRead;

    DWORD BytesWritten;

    CHAR Buffer[256];

 

    if ((PipeHandle = CreateNamedPipe("\\\\.\\PIPE\\jim", PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE │ PIPE_READMODE_BYTE,

        NUM_PIPES, 0, 0, 1000, NULL)) == INVALID_HANDLE_VALUE)

    {

        printf("CreateNamedPipe failed with error %d\n", GetLastError());

        return 0;

    }

 

    // Serve client connections forever

    while(1)

    {

        if (ConnectNamedPipe(PipeHandle, NULL) == 0)

        {

            printf("ConnectNamedPipe failed with error %d\n",  GetLastError());

            break;

        }

 

        // Read data from and echo data to the client until the client is ready to stop

        while(ReadFile(PipeHandle, Buffer, sizeof(Buffer), &BytesRead,  NULL) > 0)

        {

            printf("Echo %d bytes to client\n", BytesRead);

 

            if (WriteFile(PipeHandle, Buffer, BytesRead,  &BytesWritten, NULL) == 0)

            {

                printf("WriteFile failed with error %d\n", GetLastError());

                break;

            }

        }

 

        if (DisconnectNamedPipe(PipeHandle) == 0)

        {

            printf("DisconnectNamedPipe failed with error %d\n", GetLastError());

            break;

        }

    }

 

    CloseHandle(PipeHandle);

    return 0;

}

 

To develop your server to handle five pipe instances, start by calling the CreateThread() API function. CreateThread() starts five execution threads, all of which execute the PipeInstanceProc() function simultaneously. The PipeInstanceProc() function operates exactly like the basic server application (the previous sample) except that it reuses a named pipe handle by calling the DisconnectNamedPipe() API function, which closes a client's session to the server. Once an application calls DisconnectNamedPipe(), it is free to service another client by calling the ConnectNamedPipe() function with the same pipe instance handle.

 

Overlapped I/O

 

Overlapped I/O is a mechanism that allows Windows API functions such as ReadFile() and WriteFile() to operate asynchronously when I/O requests are made. This is accomplished by passing an OVERLAPPED structure to these API functions and later retrieving the results of an I/O request through the original OVERLAPPED structure using the GetOverlappedResult() API function. When a Windows API function is invoked with an overlapped structure, the call returns immediately.

To develop an advanced named pipe server that can manage more than one named pipe instance using overlapped I/O, you need to call CreateNamedPipe() with the nMaxInstances parameter set to a value greater than 1. You also must set the dwOpenMode flag to FILE_FLAG_OVERLAPPED. The next sample demonstrates how to develop this advanced named pipe server. The application is an echo server that reads data from a client and writes the data back.

 

// Overlap sample

#include <windows.h>

#include <stdio.h>

 

#define NUM_PIPES 5

#define BUFFER_SIZE 256

 

void main(void)

{

    HANDLE PipeHandles[NUM_PIPES];

    DWORD BytesTransferred;

    CHAR Buffer[NUM_PIPES][BUFFER_SIZE];

    INT i;

    OVERLAPPED Ovlap[NUM_PIPES];

    HANDLE Event[NUM_PIPES];

 

    // For each pipe handle instance, the code must maintain the

    // pipes' current state, which determines if a ReadFile or

    // WriteFile is posted on the named pipe. This is done using

    // the DataRead variable array. By knowing each pipe's

    // current state, the code can determine what the next I/O operation should be.

    BOOL DataRead[NUM_PIPES];

    DWORD Ret;

    DWORD Pipe;

 

    for(i = 0; i < NUM_PIPES; i++)

    {

        // Create a named pipe instance

        if ((PipeHandles[i] = CreateNamedPipe("\\\\.\\PIPE\\jim",

            PIPE_ACCESS_DUPLEX │ FILE_FLAG_OVERLAPPED,

            PIPE_TYPE_BYTE │ PIPE_READMODE_BYTE, NUM_PIPES,

            0, 0, 1000, NULL)) == INVALID_HANDLE_VALUE)

        {

            printf("CreateNamedPipe for pipe %d failed with error %d\n", i, GetLastError());

            return;

        }

 

        // Create an event handle for each pipe instance. This

        // will be used to monitor overlapped I/O activity on each pipe.

        if ((Event[i] = CreateEvent(NULL, TRUE, FALSE, NULL)) == NULL)

        {

            printf("CreateEvent for pipe %d failed with error %d\n", i, GetLastError());

            continue;

        }

 

        // Maintain a state flag for each pipe to determine when data is to be read from or written to the pipe

        DataRead[i] = FALSE;

 

        ZeroMemory(&Ovlap[i], sizeof(OVERLAPPED));

        Ovlap[i].hEvent = Event[i];

        // Listen for client connections using ConnectNamedPipe()

        if (ConnectNamedPipe(PipeHandles[i], &Ovlap[i]) == 0)

        {

            if (GetLastError() != ERROR_IO_PENDING)

            {

                printf("ConnectNamedPipe for pipe %d failed with error %d\n", i, GetLastError());

                CloseHandle(PipeHandles[i]);

                return;

            }

        }

    }

 

    printf("Server is now running\n");

    // Read and echo data back to Named Pipe clients forever

    while(1)

    {

        if ((Ret = WaitForMultipleObjects(NUM_PIPES, Event, FALSE, INFINITE)) == WAIT_FAILED)

        {

            printf("WaitForMultipleObjects failed with error %d\n", GetLastError());

            return;

        }

 

        Pipe = Ret - WAIT_OBJECT_0;

        ResetEvent(Event[Pipe]);

 

        // Check overlapped results, and if they fail, reestablish

        // communication for a new client; otherwise, process read and write operations with the client

        if (GetOverlappedResult(PipeHandles[Pipe], &Ovlap[Pipe], &BytesTransferred, TRUE) == 0)

        {

            printf("GetOverlapped result failed %d start over\n", GetLastError());

 

            if (DisconnectNamedPipe(PipeHandles[Pipe]) == 0)

            {

                printf("DisconnectNamedPipe failed with error %d\n", GetLastError());

                return;

            }

 

            if (ConnectNamedPipe(PipeHandles[Pipe], &Ovlap[Pipe]) == 0)

            {

                if (GetLastError() != ERROR_IO_PENDING)

                {

                    // Severe error on pipe. Close this handle forever.

                    printf("ConnectNamedPipe for pipe %d failed with error %d\n", i, GetLastError());

                    CloseHandle(PipeHandles[Pipe]);

                }

            }

            DataRead[Pipe] = FALSE;

        }

        else

        {

            // Check the state of the pipe. If DataRead equals

            // FALSE, post a read on the pipe for incoming data.

            // If DataRead equals TRUE, then prepare to echo data back to the client

            if (DataRead[Pipe] == FALSE)

            {

                // Prepare to read data from a client by posting a ReadFile operation

                ZeroMemory(&Ovlap[Pipe], sizeof(OVERLAPPED));

                Ovlap[Pipe].hEvent = Event[Pipe];

 

                if (ReadFile(PipeHandles[Pipe], Buffer[Pipe], BUFFER_SIZE, NULL, &Ovlap[Pipe]) == 0)

                {

                    if (GetLastError() != ERROR_IO_PENDING)

                    {

                        printf("ReadFile failed with error %d\n", GetLastError());

                    }

                }

 

                DataRead[Pipe] = TRUE;

            }

            else

            {

                // Write received data back to the client by posting a WriteFile operation

                printf("Received %d bytes, echo bytes back\n", BytesTransferred);

 

                ZeroMemory(&Ovlap[Pipe], sizeof(OVERLAPPED));

                Ovlap[Pipe].hEvent = Event[Pipe];

 

                if (WriteFile(PipeHandles[Pipe], Buffer[Pipe], BytesTransferred, NULL, &Ovlap[Pipe]) == 0)

                 {

                    if (GetLastError() != ERROR_IO_PENDING)

                    {

                        printf("WriteFile failed with error %d\n", GetLastError());

                    }

                }

                DataRead[Pipe] = FALSE;

            }

        }

    }

}

 

For the server application to service five pipe instances at a time, it must call CreateNamedPipe() five times to retrieve an instance handle for each pipe. After the server retrieves all the instance handles, it begins to listen for clients by calling ConnectNamedPipe() asynchronously five times using an overlapped I/O structure for each pipe. As clients form connections to the server, all I/O is processed asynchronously. When clients disconnect, the server reuses each pipe instance handle by calling DisconnectNamedPipe() and reissuing a ConnectNamedPipe() call.

 

Security Impersonation

 

One of the best reasons for using named pipes as a network programming solution is that they rely on Windows NT platform security features to control access when clients attempt to form communication to a server. Windows NT security offers security impersonation, which allows a named pipe server application to execute in the security context of a client. When a named pipe server executes, it normally operates at the security context permission level of the process that starts the application. For example, if a person with administrator privileges starts up a named pipe server, the server has the ability to access almost every resource on a Windows NT system. Such security access for a named pipe server is bad if the SECURITY_DESCRIPTOR structure specified in CreateNamedPipe() allows all users to access your named pipe.

When a server accepts a client connection using the ConnectNamedPipe() function, it can make its execution thread operate in the security context of the client by calling the ImpersonateNamedPipeClient() API function, which is defined as follows:

BOOL ImpersonateNamedPipeClient(HANDLE hNamedPipe);

The hNamedPipe parameter represents the pipe instance handle that is returned from CreateNamedPipe(). When this function is called, the operating system changes the thread security context of the server to the security context of the client. This is quite handy: If your server is designed to access resources such as files, it will do so using the client's access rights, thereby allowing your server to preserve access control to resources regardless of who started the process.

When a server thread executes in a client's security context, it does so through a security impersonation level. There are four basic impersonation levels: anonymous, identification, impersonation, and delegation. Security impersonation levels govern the degree to which a server can act on behalf of a client. We discuss these impersonation levels in greater detail when we develop a client application later in this chapter. After the server finishes processing a client's session, it should call RevertToSelf() to return to its original thread execution security context. The RevertToSelfAPI() function is defined as follows:

BOOL RevertToSelf(VOID);

This function does not have any parameters.

 

Client Details

 

Implementing a named pipe client requires developing an application that forms a connection to a named pipe server. Clients cannot create named pipe instances. However, clients do open handles to preexisting instances from a server. The following steps describe how to write a basic client application:

 

  1. Wait for a named pipe instance to become available using the WaitNamedPipe() API function.
  2. Connect to the named pipe using the CreateFile() API function.
  3. Send data to and receive data from the server using the WriteFile() and ReadFile() API functions.
  4. Close the named pipe session using the CloseHandle() API function.

 

Before forming a connection, clients need to check for the existence of a named pipe instance using the WaitNamedPipe() function, which is defined as follows:

BOOL WaitNamedPipe(LPCTSTR lpNamedPipeName, DWORD nTimeOut);

The lpNamedPipeName parameter represents the named pipe you are trying to connect to. The nTimeOut parameter represents how long a client is willing to wait for a pipe's server process to have a pending ConnectNamedPipe() operation on the pipe.

After WaitNamedPipe() successfully completes, the client needs to open a handle to the server's named pipe instance using the CreateFile() API function. CreateFile() is defined as follows:

 

HANDLE CreateFile(

    LPCTSTR lpFileName,

    DWORD dwDesiredAccess,

    DWORD dwShareMode,

    LPSECURITY_ATTRIBUTES lpSecurityAttributes,

    DWORD dwCreationDisposition,

    DWORD dwFlagsAndAttributes,

    HANDLE hTemplateFile

);

 

The lpFileName parameter is the name of the pipe you are trying to open; the name must conform to the named pipe naming conventions mentioned earlier in this chapter.

The dwDesiredAccess parameter defines the access mode and should be set to GENERIC_READ for reading data off the pipe and GENERIC_WRITE for writing data to the pipe. These flags can also be specified together by ORing both flags. The access mode must be compatible with how the pipe was created in the server. Match the mode specified in the dwOpenMode parameter of CreateNamedPipe(), as described earlier. For example, if the server creates a pipe with PIPE_ACCESS_INBOUND, the client should specify GENERIC_WRITE.

The dwShareMode parameter should be set to 0 because only one client is capable of accessing a pipe instance at a time. The lpSecurityAttributes parameter should be set to NULL unless you need a child process to inherit the client's handle. This parameter is incapable of specifying security controls because CreateFile() is not capable of creating named pipe instances. The dwCreationDisposition parameter should be set to OPEN_EXISTING, which means that the CreateFile() function will fail if the named pipe does not exist.

The dwFlagsAndAttributes parameter should always be set to FILE_ATTRIBUTE_NORMAL. Optionally, you can specify the FILE_FLAG_WRITE_THROUGH, FILE_FLAG_OVERLAPPED, and SECURITY_SQOS_PRESENT flags by ORing them with the FILE_ATTRIBUTE_NORMAL flag. The FILE_FLAG_WRITE_THROUGH and FILE_FLAG_OVERLAPPED flags behave like the server's mode flags described earlier in this chapter. The SECURITY_SQOS_PRESENT flag controls client impersonation security levels in a named pipe server. Security impersonation levels govern the degree to which a server process can act on behalf of a client process. A client can specify this information when it connects to a server. When the client specifies the SECURITY_SQOS_PRESENT flag, it must use one or more of the following security flags:

 

SECURITY_DELEGATION works only if the server process is running on Windows 2000 and Windows XP. Windows NT 4.0 does not implement security delegation.

 

Named pipe security impersonation is described earlier in this chapter in the section entitled “Server Details.”

The final parameter of CreateFile(), hTemplateFile(), does not apply to named pipes and should be specified as NULL. If CreateFile() completes without an error, the client application can begin to send and receive data on the named pipe using the ReadFile() and WriteFile() functions. Once the application is finished processing data, it can close down the connection using the CloseHandle() function.

The next program listing is a simple named pipe client that demonstrates the API calls needed to successfully develop a basic named pipe client application. When this application successfully connects to a named pipe, it writes the message “This is a test” to the server.

 

// Client sample

#include <windows.h>

#include <stdio.h>

 

#define PIPE_NAME "\\\\.\\Pipe\\jim"

 

void main(void)

{

    HANDLE PipeHandle;

    DWORD BytesWritten;

 

    if (WaitNamedPipe(PIPE_NAME, NMPWAIT_WAIT_FOREVER) == 0)

    {

        printf("WaitNamedPipe failed with error %d\n", GetLastError());

        return;

    }

 

    // Create the named pipe file handle

    if ((PipeHandle = CreateFile(PIPE_NAME,

        GENERIC_READ │ GENERIC_WRITE, 0,

        (LPSECURITY_ATTRIBUTES) NULL, OPEN_EXISTING,

        FILE_ATTRIBUTE_NORMAL, (HANDLE) NULL)) == INVALID_HANDLE_VALUE)

    {

        printf("CreateFile failed with error %d\n", GetLastError());

        return;

    }

 

    if (WriteFile(PipeHandle, "This is a test", 14, &BytesWritten, NULL) == 0)

    {

        printf("WriteFile failed with error %d\n", GetLastError());

        CloseHandle(PipeHandle);

        return;

    }

 

    printf("Wrote %d bytes", BytesWritten);

    CloseHandle(PipeHandle);

}

 

 

 


< Name Pipe Implementation Details | Name Pipes Main | Name Pipes APIs & Client Program Example >