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.
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
="1.0" ="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.
Sub New()
Try
xdoc = XDocument.Load(AppDomain.CurrentDomain.BaseDirectory & _
"\serverConfig.xml")
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
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.
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.
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
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.
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()
If ext = ".php" Then
proc.StartInfo.FileName = xdoc.Element_
("configuration").Element("php").Element("Path").Value & "\\php-cgi.exe"
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)
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.
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.
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.
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
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.
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.
If ext = ".aspx" Then
Dim aspxHost As New ASPClass()
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.
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.
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.
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