Thursday, 30 September 2010

Bite Sized Web Server

A few months back I needed a way of pulling information off of a machine that wasn't capable of supporting IIS. I was going to go down the route of some bespoke client server protocol, but then I thought that since it's only basic information and files that I need to send I'd create my own small web server. After doing some research on the subject and having messed with TCP components in a previous project on windows mobile I had a pretty good foundation to start moving.

First off we need a TCP Listener, this helpful object sits there and tells us when someone is connecting on a specified port, the actual transmitting of data is handled via another object known as the socket but we'll get to that shortly

Public Class WebServer
    Dim mthdServer As Thread
    Dim mwebSession As WebSession
    Dim mtcpListener As TcpListener

    Public Sub New()
        Try
            ' Get the IP address of the machine
            Dim strHostName As String = Dns.GetHostName()
            Dim ipServer As IPAddress = Dns.GetHostEntry(strHostName).AddressList(0)
            ' Specify which Port we'll be using.
            Dim strPort As String = "81"
            ' Open a tcp listener on the port we've specified
            mtcpListener = New TcpListener(ipServer, Int32.Parse(strPort))
            ' Start it up
            mtcpListener.Start()
            ' Some internal info, you could also log this to a file or the event log.
            Console.WriteLine("Web server started at: " & ipServer.ToString() & ":" & strPort)
            ' Start up the worker thread and pass it our listening item
            mwebSession = New WebSession(mtcpListener)
            mthdServer = New Thread(New ThreadStart(AddressOf mwebSession.ProcessThread))
            mthdServer.Start()
        Catch ex As Exception
            ' Bomb
            Console.WriteLine(ex.StackTrace.ToString())
        End Try
    End Sub
    ' Clean up
    Protected Overrides Sub Finalize()
        MyBase.Finalize()
        ' clean up the thread
        mthdServer.Abort() : mthdServer = Nothing
        mwebSession = Nothing
        ' Stop listening on the port
        mtcpListener.Stop() : mtcpListener = Nothing
    End Sub
End Class

You'll notice above that we have a second class known as the webServer this is the core of the service and handles the transmission of the file data and is basically the work horse.

Public Class WebSession
    ' Internal store for the listener and also the socket for data movement
    Private mtcpListener As System.Net.Sockets.TcpListener
    Private msktClient As System.Net.Sockets.Socket

    ' We're passed the listener so store that away.
    Public Sub New(ByVal tcpListener As System.Net.Sockets.TcpListener)
        Me.mtcpListener = tcpListener
    End Sub

    ' Since we're a thread this is a starting point, we're just gonna sit in a loop
    ' get a request for a file and then return it
    Public Sub ProcessThread()
        While (True)
            Try
                ' Wait for a connection from the outside
                msktClient = mtcpListener.AcceptSocket()
                ' Get information on the client using the IPEndPoint
                Dim ipepClientInfo As IPEndPoint = CType(msktClient.RemoteEndPoint, IPEndPoint)
                ' Start a new thread for the request then the loop can go to process additional
                ' requests
                Dim mthdClient As New Thread(New ThreadStart(AddressOf ProcessRequest))
                mthdClient.Start()

                ' Fault? oh dear, let's bomb the current thread, the client will just
                ' get a connection fault and they'll have to try again.
            Catch ex As Exception
                Console.WriteLine(ex.StackTrace.ToString())
                If msktClient.Connected Then
                    msktClient.Close()
                End If
            End Try
        End While
    End Sub

    ' This is where we figure what file the client wants and then throw it back via a stream
    Protected Sub ProcessRequest()
        Dim recvBytes(1024) As Byte
        Dim strHTMLReq As String = Nothing
        Dim intBytes As Int32

        Try
            ' Receive the request from the client, we'll need some buffer space to shove
            ' it into
            intBytes = msktClient.Receive(recvBytes, 0, msktClient.Available, SocketFlags.None)
            ' Transfer the buffer to a readable format
            strHTMLReq = Encoding.ASCII.GetString(recvBytes, 0, intBytes)

            ' Could log this but I'm just using it to display what the request is
            Console.WriteLine("HTTP Request: ")
            Console.WriteLine(strHTMLReq)

            ' Get the path of to where our files are stored and the default page name
            Dim strDefaultPage As String = "index.html"
            Dim strPath As String = Directory.GetCurrentDirectory() & "\WWWRoot\"
            ' Some buffers we'll be using
            Dim strArray() As String
            Dim strRequest As String

            ' Chop up the request so it's eaier to Interpret
            strArray = strHTMLReq.Trim.Split(" ")
            ' Determine the HTTP method, we only use GET, in here we use our
            ' HTMLResponse function which is where the data is streamed
            If strArray(0).Trim().ToUpper.Equals("GET") Then
                strRequest = strArray(1).Trim
                ' Do we have a file name?
                If (strRequest.StartsWith("/")) Then
                    strRequest = strRequest.Substring(1)
                End If
                ' No file Name then default page.
                If (strRequest.EndsWith("/") Or strRequest.Equals("")) Then
                    strRequest = strRequest & strDefaultPage
                End If
                ' Send the data
                strRequest = strPath & strRequest
                HTMLResponse(strRequest)
            Else ' Not GET method, we'll go away passing back the errorpage.
                strRequest = strPath & "Error\" & "400.html"
                HTMLResponse(strRequest)
            End If

            ' Fault? Just kill the connection
        Catch ex As Exception
            Console.WriteLine(ex.StackTrace.ToString())
            If msktClient.Connected Then
                msktClient.Close()
            End If
        End Try
    End Sub

    ' This procedure will send back the file specified in a stream back to the client.
    Private Sub HTMLResponse(ByVal httpRequest As String)
        Try
            ' Open the file we've been given, we'll use the streamreader to pull everything
            ' into a string for easy transport.
            Dim strData As StreamReader = New StreamReader(httpRequest)
            Dim strBuff As String = strData.ReadToEnd()
            strData.Close() : strData = Nothing

            ' Push the string into a byte array
            Dim respByte() As Byte = Encoding.ASCII.GetBytes(strBuff)

            ' Set HTML Header, this is taken from the W3S consortium site, only thing
            ' you need to take note of is the GetContentType function used to pass
            ' back what type of document we're returning.
            Dim htmlHeader As String = _
                "HTTP/1.0 200 OK" & ControlChars.CrLf & _
                "Server: Stews Webserver 1.0" & ControlChars.CrLf & _
                "Content-Length: " & respByte.Length & ControlChars.CrLf & _
                "Content-Type: " & getContentType(httpRequest) & _
                ControlChars.CrLf & ControlChars.CrLf

            ' Again encode the header to bytes.
            Dim headerByte() As Byte = Encoding.ASCII.GetBytes(htmlHeader)

            ' Send HTML Header back to Web Browser via our socket
            msktClient.Send(headerByte, 0, headerByte.Length, SocketFlags.None)
            ' Send Contents back to Web Browser via our socet
            msktClient.Send(respByte, 0, respByte.Length, SocketFlags.None)
            ' Close HTTP Socket connection
            msktClient.Shutdown(SocketShutdown.Both)
            msktClient.Close()

            ' Error? Bomb
        Catch ex As Exception
            Debug.Print(ex.Message)

            If msktClient.Connected Then
                msktClient.Close()
            End If
        End Try
    End Sub

    ' Determines the type of returned content.
    Private Function GetContentType(ByVal httpRequest As String) As String
        If (httpRequest.EndsWith("html")) Then
            Return "text/html"
        ElseIf (httpRequest.EndsWith("htm")) Then
            Return "text/html"
        ElseIf (httpRequest.EndsWith("txt")) Then
            Return "text/plain"
        ElseIf (httpRequest.EndsWith("xml")) Then
            Return "text/xml"
        ElseIf (httpRequest.EndsWith("gif")) Then
            Return "image/gif"
        ElseIf (httpRequest.EndsWith("jpg")) Then
            Return "image/jpeg"
        ElseIf (httpRequest.EndsWith("jpeg")) Then
            Return "image/jpeg"
        ElseIf (httpRequest.EndsWith("pdf")) Then
            Return "application/pdf"
        ElseIf (httpRequest.EndsWith("pdf")) Then
            Return "application/pdf"
        ElseIf (httpRequest.EndsWith("doc")) Then
            Return "application/msword"
        ElseIf (httpRequest.EndsWith("xls")) Then
            Return "application/vnd.ms-excel"
        ElseIf (httpRequest.EndsWith("ppt")) Then
            Return "application/vnd.ms-powerpoint"
        Else
            Return "text/plain"
        End If
    End Function
End Class

And that's about it, as you can see it's all a lot more simpler than it used to be in C++ but that's the power of .NET for you. You can always make changes to the HTMLResponse proc if you want to implement your own type of data response.

And just before I go some additional information, we use the following imports
Imports System.Net
Imports System.Net.Sockets
Imports System.Threading
Imports System.Text
Imports System.IO

and to run the webserver tis easy.
Dim myWebServer As WebServer = New WebServer

1 comment: