Sunny Ahuwanya's Blog

Mostly notes on .NET and C#

Probing the ASP.NET State Service Part II

In my previous post, I took a first look at the ASP.NET state service communication protocol. In this post I'll describe the techniques I'm using to piece out and understand the protocol.

I needed to monitor the traffic between the web server and the state service so I installed WinDump, a Windows version of tcpdump.
With WinDump, I could capture the low level tcp traffic between the state service and the web server. The only caveat was that I had to monitor a state service on a different computer because WinDump doesn’t work with the loopback device.

After a while I decided WinDump was too low level for the task at hand. What I really needed was a tcp relay server.

A tcp relay server is a more involved version of an echo server that sits between a server and a client and relays transmitted data to and fro. With a tcp relay server, not only can I capture the transmitted data, I can also modify it.



To have the greatest flexibility, I decided to write one. My relay server is a console application. Here's the code:

using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace RelayServer
{
    //State object
    public class StateObject
    {
        // Client  socket.
        public Socket WorkSocket = null;
        // Size of receive buffer.
        public const int BufferSize = 1024;
        // Receive buffers.
        public byte[] Buffer = new byte[BufferSize];
        // Socket that connects to other party
        public Socket HandoffSocket = null;
    }

    class Program
    {
        static int relayPort = 24242; //Port to listen on for client connection
        static string serviceHost = "localhost"; //State service host
        static int servicePort = 42424; //State service port
        static StateObject pollStateObj = new StateObject(); //State object polled for disconnection

        static void Main(string[] args)
        {
            IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, relayPort);
            Socket clientListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            clientListener.Bind(localEndPoint);
            clientListener.Listen(100);
            clientListener.BeginAccept(new AsyncCallback(AcceptCallback), clientListener);

            while (!Console.KeyAvailable)
            {
                Thread.Sleep(200); //check every 200 milliseconds

                //is the state object instantiated and connected?
                if (pollStateObj.WorkSocket != null && 
                     (pollStateObj.WorkSocket.Connected || pollStateObj.HandoffSocket.Connected))
                {                    
                    //check if work (client) socket is disconnected
                    try
                    {
                        //Test for disconnection
                        bool isDisconnected = false;
                        lock (pollStateObj.WorkSocket)
                        {
                            isDisconnected = pollStateObj.WorkSocket.Available == 0 && 
                                pollStateObj.WorkSocket.Poll(1, SelectMode.SelectRead);
                        }

                        if (isDisconnected)
                        {
                            Console.ForegroundColor = ConsoleColor.Red;
                            Console.WriteLine("Client disconnected.");
                            DisconnectSockets(pollStateObj);
                            continue;
                        }
                    }
                    catch (ObjectDisposedException ex)
                    {
                        //do nothing
                        continue;
                    }
                    catch (SocketException ex)
                    {
                        //Was Socket remotely disconnected?
                        if (ex.ErrorCode == 10054)
                        {
                            Console.ForegroundColor = ConsoleColor.Red;
                            Console.WriteLine("Client disconnected.");
                        }
                        else if (ex.ErrorCode == 10053)
                        {
                            Console.ForegroundColor = ConsoleColor.Red;
                            Console.WriteLine("Client aborted connection.");
                        }
                        else
                        {
                            Console.ForegroundColor = ConsoleColor.White;
                            Console.WriteLine("\nError: " + ex + "\n");
                        }

                        DisconnectSockets(pollStateObj);
                        continue;
                    }
                    catch (Exception ex)
                    {
                        Console.ForegroundColor = ConsoleColor.White;
                        Console.WriteLine("\nError: " + ex + "\n");
                        continue;
                    }

                    try
                    {
                        //Test for disconnection
                        lock (pollStateObj.HandoffSocket)
                        {
                            pollStateObj.HandoffSocket.Send(new byte[1], 0, SocketFlags.None);
                        }
                    }
                    catch (ObjectDisposedException ex)
                    {
                        //do nothing
                        continue;
                    }
                    catch (SocketException ex)
                    {
                        //Was Socket remotely disconnected?
                        if (ex.ErrorCode == 10054)
                        {
                            Console.ForegroundColor = ConsoleColor.Red;
                            Console.WriteLine("Server disconnected.");
                        }
                        else if (ex.ErrorCode == 10053)
                        {
                            Console.ForegroundColor = ConsoleColor.Red;
                            Console.WriteLine("Server aborted connection.");
                        }
                        else
                        {
                            Console.ForegroundColor = ConsoleColor.White;
                            Console.WriteLine("\nError: " + ex + "\n");
                        }

                        DisconnectSockets(pollStateObj);
                        continue;
                    }
                    catch (Exception ex)
                    {
                        Console.ForegroundColor = ConsoleColor.White;
                        Console.WriteLine("\nError: " + ex + "\n");
                        continue;
                    }
                }
            }
        }

        private static void AcceptCallback(IAsyncResult ar)
        {
            //Accept incoming connection
            Socket listener = (Socket)ar.AsyncState;
            Socket handler = listener.EndAccept(ar);
            
            //Display Connection Status
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("Client is connected.");

            //Accept another incoming connection
            listener.BeginAccept(new AsyncCallback(AcceptCallback), listener);

            // Create the state object.
            StateObject state = new StateObject();
            state.WorkSocket = handler;
            state.HandoffSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            //Connect to other socket
            state.HandoffSocket.Connect(serviceHost, servicePort);

            //Create another state object to receive information from service
            StateObject recvState = new StateObject();
            recvState.WorkSocket = state.HandoffSocket;
            recvState.HandoffSocket = state.WorkSocket;

            //Set up the Service socket to receive information from service
            state.HandoffSocket.BeginReceive(recvState.Buffer, 0, StateObject.BufferSize, SocketFlags.None,
                new AsyncCallback(ReadCallback), recvState);

            //Set up the client socket to receive information from client
            handler.BeginReceive(state.Buffer, 0, StateObject.BufferSize, SocketFlags.None,
                new AsyncCallback(ReadCallback), state);

            //Reference this State object as the object to poll for disconnections
            pollStateObj = state;
        }

        private static void ReadCallback(IAsyncResult ar)
        {
            // Retrieve the state object and the handler socket
            // from the asynchronous state object.
            StateObject state = (StateObject)ar.AsyncState;

            if (state.WorkSocket.Connected == false)
            {
                return;
            }

            try
            {
                // Read data from the client socket. 
                int bytesRead;
                lock (state.WorkSocket)
                {
                    bytesRead = state.WorkSocket.EndReceive(ar);
                }

                if (bytesRead > 0)
                {
                    //Transform received data
                    byte[] transformedData = TransformData(state.Buffer, bytesRead, state);

                    //Transmit transformed data to other Socket
                    if (transformedData.Length > 0)
                    {
                        lock (state.HandoffSocket)
                        {
                            state.HandoffSocket.BeginSend(transformedData, 0, transformedData.Length, 
                                SocketFlags.None, new AsyncCallback(SendCallback), state);
                        }
                    }
                }

                lock (state.WorkSocket)
                {
                    state.WorkSocket.BeginReceive(state.Buffer, 0, StateObject.BufferSize, 0,
                        new AsyncCallback(ReadCallback), state);
                }
            }
            catch (ObjectDisposedException ex)
            {
                //do nothing
                return;
            }
            catch (SocketException ex)
            {
                if (ex.ErrorCode == 10054 || ex.ErrorCode == 10053)
                {
                    //do nothing
                    return;
                }
                Console.ForegroundColor = ConsoleColor.White;
                Console.WriteLine("\nError: " + ex + "\n");

                return;
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.White;
                Console.WriteLine("\nError: " + ex + "\n");
                return;
            }

        }

        private static void SendCallback(IAsyncResult ar)
        {
            // Retrieve the socket from the state object.
            StateObject state = (StateObject)ar.AsyncState;

            if (!state.HandoffSocket.Connected)
            {
                return;
            }

            // Complete send
            try
            {
                lock (state.HandoffSocket)
                {
                    state.HandoffSocket.EndSend(ar);
                }
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.White;
                Console.WriteLine("\nError: " + ex + "\n");
                return;
            }
        }

        private static byte[] TransformData(byte[] Data, int Length, StateObject State)
        {
            //Just display Transmitted Data
            if (State.WorkSocket == pollStateObj.WorkSocket)
                Console.ForegroundColor = ConsoleColor.Cyan;
            else
                Console.ForegroundColor = ConsoleColor.Green;
            
            Console.Write(Encoding.UTF8.GetString(Data, 0, Length));

            byte[] output = new byte[Length];
            Array.Copy(Data, output, Length);
            return output;
        }

        private static void DisconnectSockets(StateObject state)
        {
            lock (state.WorkSocket)
            {                                        
                if (state.WorkSocket.Connected)
                {
                    state.WorkSocket.Shutdown(SocketShutdown.Both);
                }
                state.WorkSocket.Close();                    
            }

            lock (state.HandoffSocket)
            {
                if (state.HandoffSocket.Connected)
                {
                    state.HandoffSocket.Shutdown(SocketShutdown.Both);
                }
                state.HandoffSocket.Close();
            }
        }
    }
}

It's important to note that the tcp relay server must also relay connections and disconnections from either end, in addition to transmitted data.

To test the relay server:

1. Create a new ASP.NET web application in Visual Studio.

2. Add the following line in the system.web section of the Web.config file:

<sessionState mode="StateServer" stateConnectionString="tcpip=localhost:24242" cookieless="false" timeout="20"/>
<!-- -->

3. Add the following lines of code to the Page_Load event handler.

 Session.Add("Test2", "Test");
 Session.Abandon();

4. Run the relay server.

5. Start the local ASP.NET State Service (Located at Control Panel -> Administrative Tools -> Services) .

6. Run the ASP.NET Application

You'll see the gem below in the relay server console window.



This tool will be extremely useful as I spec out my implementation of the state service. For instance, if I want to see how the state service reacts if there is no Exclusive header, I'll simply modify the TransformData method to detect and remove the header.

I have also found Microsoft’s specification of the ASP.NET state service protocol. It doesn't look coherent but it will help me make my implementation compatible with their specifications.