Click here to Skip to main content
15,885,537 members
Articles / Programming Languages / XML

Simple Web Server with PHP Support

Rate me:
Please Sign up or sign in to vote.
4.74/5 (13 votes)
21 Jul 2011CPOL4 min read 69.4K   6.5K   24   17
Simple small web server with aspx and PHP/CGI support

Introduction

While studying net programming, I tried to create a simple web server. I have a lot of HTTP servers on the internet. But all of them were rather complicated and none of them work with PHP and EXE files. So I decided to code my own simple small web server that can work with PHP files.

Using the Code

What is a web server? Web server is a server application that accepts HTTP requests from the clients, usually from web browsers, and responds to them and sends to the clients HTML page or other content. And web client or browser creates the request like that:

GET /about.html HTTP/1.1 
Host: example.org 
User-Agent: SomeBrowser/5.0 
.................. 

A server processes the request and if successful, sends to the client a page called about.html and some headers about this page.

Main class of this server is the HttpServer class with some global variables.

VB.NET
Public Class HttpServer
    Private myListener As TcpListener
    Dim xdoc As XDocument
    Dim serverRoot As String
    Dim errorMessage As String
    Dim badRequest As String
    Dim randObj As New Object()
    Dim active As Boolean = True
    Dim SERVER_NAME As String

First, we should determine the constructor for this class that will initialize all the global variables, like message errors, port for listening. As it is better to configure such settings in a special file, I create the configuration file for the server.

serverConfig.xml

XML
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <serverName>localhost</serverName>
  <Host>
    <Dir>C:\EugeneServer</Dir>
    <Port>5555</Port>
  </Host>
  <php>
    <Path>c:\php</Path>
  </php>
  <Forbidden>
    <Path>C:\EugeneServer\bin\</Path>
  </Forbidden>
  <Default>
    <File>Index.html</File>
	So on....
  </Default>
  <Mime>
    <Values>
      <Ext>.htm</Ext>
      <Type>text/html</Type>
    </Values>
    So on...
  </Mime>
</configuration>

I think all configurations are clear: Dir is a full path to the folder with web pages, Port is port for listening, Forbidden is path to a folder or a file that is forbidden.

In PHP section, we should indicate the path to the PHP interpreter, for example - c:\\php. We certainly can leave this section empty, if we do not intend to work with PHP.

In the constructor, we load this document to memory and then we are going to read all the settings from it.

VB.NET
Sub New()
        Try
            'load xml-file with all configuration
            xdoc = XDocument.Load(AppDomain.CurrentDomain.BaseDirectory & _
		"\serverConfig.xml")
            'two messages about errors
            errorMessage = "<html><body><h2>Requested file not found</h2></body></html>"
            badRequest = "<html><body><h2>Bad Request</h2></body></html>"
            Dim port As Integer = _
		xdoc.Element("configuration").Element("Host").Element("Port").Value
            SERVER_NAME = xdoc.Element("configuration").Element("serverName").Value
            'determine the directory of the web pages
            serverRoot = _
		xdoc.Element("configuration").Element("Host").Element("Dir").Value
            myListener = New TcpListener(IPAddress.Any, port)
            myListener.Start()
        Catch ex As Exception
        End Try
    End Sub

For processing the requests, we need some useful methods, as we should get mime type of the content. As well, we need to obtain default pages.

VB.NET
Private Function GetMimeType(ByVal extention As String) As String
        For Each xel As XElement In xdoc.Element_
		("configuration").Element("Mime").Elements("Values")
            If xel.Element("Ext").Value = extention Then Return xel.Element("Type").Value
        Next
        Return "text/html"
    End Function
    Private Function Get_DefaultPage(ByVal serverFolder As String) As String
        For Each xel As XElement _
	In xdoc.Element("configuration").Element("Default").Elements("File")
            If File.Exists(serverFolder & "\" & xel.Value) Then
                Return xel.Value
            End If
        Next
        Return ""
    End Function

Then it needs to define methods for sending the headers and content. Both methods have as an argument socket with ip for receiving the answer.

VB.NET
Private Sub SendData(ByVal data As Byte(), ByRef sockets As Socket)
        Try
            sockets.Send(data, data.Length, SocketFlags.None)
        Catch ex As Exception
        End Try
    End Sub
    Private Sub SendHeader(ByVal HttpVersion As String, _
	ByVal MimeType As String, ByVal totalBytes As Integer, _
	ByVal statusCode As String, ByRef sockets As Socket)
        Dim ss As New StringBuilder()
        If MimeType = "" Then MimeType = "text/html"
        ss.Append(HttpVersion)
        ss.Append(statusCode).AppendLine()
        ss.AppendLine("Sever: EugeneServer")
        ss.Append("Content-Type: ")
        ss.Append(MimeType).AppendLine()
        ss.Append("Accept-Ranges: bytes").AppendLine()
        ss.Append("Content-Length: ")
        ss.Append(totalBytes).AppendLine().AppendLine()
        Dim data_ToSend As Byte() = Encoding.ASCII.GetBytes(ss.ToString())
        ss.Clear()
        SendData(data_ToSend, sockets)
    End Sub

More interesting method is GetCgiData. Thanks to it, we interact with PHP and EXE-applications. Among its parameters, there are SERVER_PROTOCOL, REFER<code>ER, REQUESTED_METHOD, USER_AGENT, which are used as global variables in PHP like getenv("REQUESTED_METHOD") or $_SERVER['REMOTE_ADDR']. The main thing is we create the process that will run the php-cgi.exe or exe-application, accepts all global variables, and then gives the output as string to the main thread.

VB.NET
Private Function GetCgiData(ByVal cgiFile As String, _
    ByVal QUERY_STRING As String, ByVal ext As String, ByVal remote_address As String, _
    ByVal SERVER_PROTOCOL As String, ByVal REFERER As String, _
    ByVal REQUESTED_METHOD As String, ByVal USER_AGENT As String, _
    ByVal request As String) As String
        Dim proc As New System.Diagnostics.Process()
        'indicate the executable to get stdout
        If ext = ".php" Then
            proc.StartInfo.FileName = xdoc.Element_
	     ("configuration").Element("php").Element("Path").Value & "\\php-cgi.exe"
            'if path to the php is not defined
            If Not File.Exists(proc.StartInfo.FileName) Then
                Return errorMessage
            End If
            proc.StartInfo.Arguments = " -q " & cgiFile & " " & QUERY_STRING
        Else
            proc.StartInfo.FileName = cgiFile
            proc.StartInfo.Arguments = QUERY_STRING
        End If
        Dim script_name As String = cgiFile.Substring(cgiFile.LastIndexOf("\"c) + 1)
        'Set some global variables and output parameters
        proc.StartInfo.EnvironmentVariables.Add("REMOTE_ADDR", remote_address.ToString())
        proc.StartInfo.EnvironmentVariables.Add("SCRIPT_NAME", script_name)
        proc.StartInfo.EnvironmentVariables.Add("USER_AGENT", USER_AGENT)
        proc.StartInfo.EnvironmentVariables.Add("REQUESTED_METHOD", REQUESTED_METHOD)
        proc.StartInfo.EnvironmentVariables.Add("REFERER", REFERER)
        proc.StartInfo.EnvironmentVariables.Add("SERVER_PROTOCOL", SERVER_PROTOCOL)
        proc.StartInfo.EnvironmentVariables.Add("QUERY_STRING", request)
        proc.StartInfo.UseShellExecute = False
        proc.StartInfo.RedirectStandardOutput = True
        proc.StartInfo.RedirectStandardInput = True
        proc.StartInfo.CreateNoWindow = True
        Dim str As String = ""
        proc.Start()
        str = proc.StandardOutput.ReadToEnd()
        proc.Close()
        proc.Dispose()
        Return str
    End Function

More complicated part - is processing aspx pages. For that, we should create the class Host. Its method ProcessFile through the object of the class SimpleWorkerRequest will pass the aspx page to the ASPNET Environment. Method HttpRuntime.ProcessRequest will process the page. To get the output, we should create the instance of Host class. Method CreateApplicationHost of ApplicationHost class takes three parameters: type of class, virtual directory of the file and physical directory. As the virtual directory, we set path "/" because we can as well get access to the file through the full name instead of the virtual path. CreateApplicationHost returns the Host object through which we get the HTML output.

VB.NET
Imports System.Web
Imports System.Web.Hosting
Imports System.IO
Public Class Host
    Inherits MarshalByRefObject
    Private Function ProcessFile(ByVal filename As String, _
	ByVal query_string As String) As String
        Dim sw As New StringWriter()
        Dim simpleWorker As New SimpleWorkerRequest(filename, query_string, sw)
        HttpRuntime.ProcessRequest(simpleWorker)
        Return sw.ToString()
    End Function
    Public Function CreateHost(ByVal filename As String, _
	ByVal serverRoot As String, ByVal query_string As String) As String
        Dim myHost As Host = CType(ApplicationHost.CreateApplicationHost_
	(GetType(Host), "/", serverRoot), Host)
        Return myHost.ProcessFile(filename, query_string)
    End Function
End Class

It is better to build the separate library with this class and then add to the project. Then it is better to add to GAC. Mu web server as windows service uses this library from GAC. Another way of using (without location in GAC) - console server that uses this library located in the same folder. But in this way, we must create the folder "bin" in the folder with web pages (that is Dir in configuration file) and then place the library in the bin folder. But there is an disadvantage. Although much of the web servers on net (both vbnet and c#) use this way to access ASP NET, I could not manage to work code behind the aspx pages as well.

The main part of the HttpServer class is a method HttpThread which combines all the methods above.

Firstly, we get the request from the client and decode it.

VB.NET
Private Sub HttpThread(ByVal sockets As Socket)
        Dim request As String
        Dim requestedFile As String = ""
        Dim mimeType As String = ""
        Dim filePath As String = ""
        Dim QUERY_STRING As String = ""
        Dim REQUESTED_METHOD As String = ""
        Dim REFERER As String = ""
        Dim USER_AGENT As String = ""
        Dim SERVER_PROTOCOL As String = "HTTP/1.1"
        Dim erMesLen As Integer = errorMessage.Length
        Dim badMesLen As Integer = badRequest.Length
        Dim logStream As StreamWriter
        Dim remoteAddress As String = ""
        If sockets.Connected = True Then
            remoteAddress = sockets.RemoteEndPoint.ToString()
            Dim received() As Byte = New Byte(1024) {}
            Dim i As Integer = sockets.Receive(received, received.Length, 0)
            Dim sBuffer As String = Encoding.ASCII.GetString(received)
            If sBuffer = "" Then
                sockets.Close()
                Exit Sub
            End If

Sure that is HTTP -request and get its version, get the request method and some other parameters.

VB.NET
Dim startPos As Integer = sBuffer.IndexOf("HTTP", 1)
            If startPos = -1 Then
                SendHeader(SERVER_PROTOCOL, "", badMesLen, "400 Bad Request", sockets)
                SendData(badRequest, sockets)
                sockets.Close()
                Exit Sub
            Else
                SERVER_PROTOCOL = sBuffer.Substring(startPos, 8)
            End If
            Dim params() As String = sBuffer.Split(New Char() {vbNewLine})
            For Each param As String In params
                If param.Trim.StartsWith("User-Agent") Then
                    USER_AGENT = param.Substring(12)
                ElseIf param.Trim.StartsWith("Referer") Then
                    REFERER = param.Trim.Substring(9)
                End If
            Next
            'Get request method. If POST then there is a query with 
            'parameters at the request body
            REQUESTED_METHOD = sBuffer.Substring(0, sBuffer.IndexOf(" "))
            Dim lastPos As Integer = sBuffer.IndexOf("/"c) + 1
            request = sBuffer.Substring(lastPos, startPos - lastPos - 1)
            Select Case REQUESTED_METHOD
                Case "POST"
                    requestedFile = request.Replace("/", "\").Trim()
                    QUERY_STRING = params(params.Length - 1).Trim()
                    Exit Select
                Case "GET"
                    lastPos = request.IndexOf("?"c)
                    If lastPos > 0 Then
                        requestedFile = request.Substring(0, lastPos).Replace("/", "\")
                        QUERY_STRING = request.Substring(lastPos + 1)
                    Else
                        requestedFile = request.Substring(0).Replace("/", "\")
                    End If
                    Exit Select
                Case "HEAD" : Exit Select
                Case Else
                    SendHeader(SERVER_PROTOCOL, "", badMesLen, "400 Bad Request", sockets)
                    SendData(badRequest, sockets)
                    sockets.Close()
                    Exit Sub
            End Select

Get the full name of the requested file. If the acess to the file is forbidden or there is no such file, we send the error message.

VB.NET
If requestedFile.Length = 0 Then
                requestedFile = Get_DefaultPage(serverRoot)
                If requestedFile = "" Then
                    SendHeader(SERVER_PROTOCOL, "", erMesLen, "404 Not Found", sockets)
                    SendData(errorMessage, sockets)
                End If
            End If
            filePath = serverRoot & "\" & requestedFile
            For Each forbidden As XElement In xdoc.Element_
		("configuration").Element("Forbidden").Elements("Path")
                If filePath.StartsWith(forbidden.Value) Then
                    SendHeader(SERVER_PROTOCOL, "", erMesLen, "404 Not Found", sockets)
                    SendData(errorMessage, sockets)
                    sockets.Close()
                    Exit Sub
                End If
            Next
            If File.Exists(filePath) = False Then
                SendHeader(SERVER_PROTOCOL, "", erMesLen, "404 Not Found", sockets)
                SendData(errorMessage, sockets)
            Else
                Dim ext As String = New FileInfo(filePath).Extension.ToLower()
                mimeType = GetMimeType(ext)

Process the web pages.

VB.NET
If ext = ".aspx" Then
                    'Create object of the ASPClass
                    Dim aspxHost As New ASPClass()
                    'Pass the filename to it and return the html output
                    Dim htmlOut As String = aspxHost.CreateHost(requestedFile, serverRoot)
                    erMesLen = htmlOut.Length
                    SendHeader(SERVER_PROTOCOL, mimeType, erMesLen, " 200 OK", sockets)
                    SendData(htmlOut, sockets)
                ElseIf ext = ".php" OrElse ext = ".exe" Then
                    Dim cgi2html As String = GetCgiData(filePath, QUERY_STRING, ext, _
                    sockets.RemoteEndPoint, SERVER_PROTOCOL, REFERER, REQUESTED_METHOD, _
			USER_AGENT)
                    If cgi2html = errorMessage Then
                        SendHeader(SERVER_PROTOCOL, "", _
				erMesLen, "404 Not Found", sockets)
                        SendData(errorMessage, sockets)
                    Else
                        erMesLen = cgi2html.Length
                        SendHeader(SERVER_PROTOCOL, mimeType, _
			erMesLen, " 200 OK", sockets)
                        SendData(cgi2html, sockets)
                    End If
                Else
                    Dim fs As New FileStream(filePath, FileMode.Open, _
				FileAccess.Read, FileShare.Read)
                    Dim bytes() As Byte = New Byte(fs.Length) {}
                    erMesLen = bytes.Length
                    fs.Read(bytes, 0, erMesLen)
                    fs.Close()
                    SendHeader(SERVER_PROTOCOL, mimeType, erMesLen, "200 OK", sockets)
                    SendData(bytes, sockets)
                End If
            End If
            sockets.Close()

Finally the server outputs to the log file. To avoid competition for the file, we should use monitor to black access.

VB.NET
Monitor.Enter(randObj)
            logStream = New StreamWriter("Server.log", True)
            logStream.WriteLine(Date.Now.ToString())
            logStream.WriteLine("Connected to {0}", sockets.RemoteEndPoint)
            logStream.WriteLine("Requested path {0}", request)
            logStream.WriteLine("Total bytes {0}", erMesLen)
            logStream.Flush()
            logStream.Close()
            Monitor.Exit(randObj)
        End If
    End Sub

And at last, we need some code to start and stop the server.

VB.NET
Protected Friend Sub StartListen()
        While active = True
            Dim sockets As Socket = myListener.AcceptSocket()
            Dim listening As New Thread(AddressOf HttpThread)
            listening.Start(sockets)
        End While
    End Sub
    Protected Friend Sub StopListen()
        active = False
    End Sub
End Class

Last thing to launch this simple server in the Windows service with Run methods that can initiate OnStart method.

VB.NET
Public Class EugeneServer
    Inherits System.ServiceProcess.ServiceBase
    Dim myServer As HttpServer
    Public Sub New()
        Me.ServiceName = "EugeneServer"
        Me.CanStop = True
        Me.CanPauseAndContinue = True
        Me.AutoLog = True
    End Sub
    Shared Sub Main()
        System.ServiceProcess.ServiceBase.Run(New EugeneServer)
    End Sub
    Protected Overrides Sub OnStart(ByVal args() As String)
        myServer = New HttpServer()
        Dim thread As New Thread(New ThreadStart(AddressOf myServer.StartListen))
        thread.Start()
    End Sub
    Protected Overrides Sub OnStop()
        myServer.StopListen()
        Threading.Thread.Sleep(1000)
        myServer = Nothing
    End Sub
End Class

In the files, there are two types of project for the server - Windows service and console application with the same functionality. To install the server as service, you should use the IstallUtil.exe and add the ASPX.dll to GAC. Console Server can be launched without any installations.

Conclusion

This is all about this small web server. Of course, it can hardly function as in real work because it should be improved a lot. But I find it useful for debugging some scripts. And I suppose it is enough for understanding and demonstrating how web server works. Thanks for reading this article.

History

  • 20th July, 2011: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Russian Federation Russian Federation
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHtml works PhP doesn't work Pin
josephchrzempiec201212-Apr-17 11:48
josephchrzempiec201212-Apr-17 11:48 
Questionsession problem Pin
dusty6619-Mar-15 10:10
dusty6619-Mar-15 10:10 
QuestionSession problems Pin
demmy868-Dec-12 22:35
demmy868-Dec-12 22:35 
QuestionProblems with PHP 5.4.8 Post Variables on Commandline Pin
demmy861-Nov-12 1:54
demmy861-Nov-12 1:54 
GeneralMy vote of 5 Pin
Member 18105428-Oct-12 20:14
Member 18105428-Oct-12 20:14 
Questionadapting code give one error - any idea how to solve it ? Pin
Member 181054226-Sep-12 1:25
Member 181054226-Sep-12 1:25 
AnswerRe: adapting code give one error - any idea how to solve it ? Pin
Eugene Popov8-Oct-12 0:50
Eugene Popov8-Oct-12 0:50 
GeneralRe: adapting code give one error - any idea how to solve it ? Pin
Member 18105428-Oct-12 20:20
Member 18105428-Oct-12 20:20 
Generalsome problems Pin
hiyeah8-Jul-12 22:53
hiyeah8-Jul-12 22:53 
I got some problems here:
1.If you use session,you'll get warnings like this:
VB
session_start() [function.session-start]: Cannot send session cookie - headers already sent by
 session_start() [function.session-start]: Cannot send session cache limiter - headers already sent

2.If the wwwroot path contains blank like "C:\program files\test",the php-cgi.exe may not find the php file and display error:"No input file specified".
To solve this problem ,you can change
HTML
"<Host><Dir>C:\program files\test</Dir>.." to "<Host><Dir>C:\progra~1\test</Dir>.."
in serverConfig.xml.

3.most of my php files(8K~15k) was truncated when I visit them in Browser.The html source code ends like this:
HTML
"<td>clas" or "<td" 

I don't know why.
GeneralRe: some problems Pin
Eugene Popov8-Jul-12 23:24
Eugene Popov8-Jul-12 23:24 
GeneralRe: some problems Pin
hiyeah9-Jul-12 15:33
hiyeah9-Jul-12 15:33 
GeneralRe: some problems Pin
Eugene Popov15-Jul-12 22:38
Eugene Popov15-Jul-12 22:38 
GeneralRe: some problems Pin
hiyeah18-Jul-12 21:52
hiyeah18-Jul-12 21:52 
GeneralRe: some problems Pin
RobVb65-Apr-13 9:05
RobVb65-Apr-13 9:05 
GeneralMy vote of 5 Pin
Member 432084426-Jul-11 6:45
Member 432084426-Jul-11 6:45 
QuestionHmm... Pin
Alex Jordan23-Jul-11 12:42
Alex Jordan23-Jul-11 12:42 
GeneralMy vote of 5 Pin
Wooters21-Jul-11 8:21
Wooters21-Jul-11 8:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.