CGI Support

Oct 1, 2008 at 3:52 PM
Edited Oct 1, 2008 at 3:53 PM

This project is very nice and a great alternative to HttpListener (http.sys).

I would like to implement basic CGI Support (support for external scripting engine such as PHP) in your project. It seems that your architecture is modular enough to add this functionality quite easily. Can you please tell me how I can make this fit in your application:

1) If extension is ".php", go to PHP custom handler.
2) In the PHP Custom Handler I would use the current context (GET or POST) data to feed the CGI environment and call the CGI binary file.
3) I will have the CGI output back into a string? How do I flush it to the web response?

Oct 1, 2008 at 4:11 PM
I would do something like this:

/// <summary>
    /// Module to invoke php scripts.
    /// </summary>
    public class PhpModule : HttpModule
        private readonly string _basePath;

        /// <summary>
        /// Initializes a new instance of the <see cref="PhpModule"/> class.
        /// </summary>
        /// <param name="basePath">Absolute path to the "public"/"inetpub" directory.</param>
        public PhpModule(string basePath)
            _basePath = basePath;
            if (_basePath.EndsWith("\\"))
                _basePath = _basePath.Remove(0, _basePath.Length - 1);

        /// <summary>
        /// Method that process the url
        /// </summary>
        /// <param name="request">Information sent by the browser about the request</param>
        /// <param name="response">Information that is being sent back to the client.</param>
        /// <param name="session">Session used to </param>
        /// <returns>true if this module handled the request.</returns>
        public override bool Process(IHttpRequest request, IHttpResponse response, IHttpSession session)
            // not a php file.
            if (!request.Uri.IsFile || !request.Uri.AbsolutePath.EndsWith(".php"))
                return false;

            string path = _basePath + request.Uri.AbsolutePath.Replace('/', '\\').Replace("..", "");
            return File.Exists(path) && InvokePhpScript(request, response, session);

        // you should call php.exe in this method and then fill the response.
        private bool InvokePhpScript(IHttpRequest request, IHttpResponse response, IHttpSession session)
            return true;
Oct 1, 2008 at 4:12 PM
then you add it to the webserver like this:

webserver.Add(new PhpModule("C:\\inetpub\\wwwroot"));
Oct 1, 2008 at 6:37 PM
Thanks for that quick and great answer.

Using a TCP sniffer between the server and the browser, I discovered the server is returning headers name in all lowercase. I don't know what's in the HTTP protocol regarding case. Don't you think you should stick to the usual capitalization for headers to ensure greater browser compatibility.

HTTP/1.1 200 OK
date: Wed, 01 Oct 2008 13:29:59 GMT
content-length: 11
content-type: text/html
server: Tiny WebServer
keep-alive: timeout=20, max=400
connection: keep-alive

Oct 1, 2008 at 7:07 PM
First I made everything lowercase for speed, but then I read that the NameValueCollection is case insensitive, so it doesnt really matter. I can remove the lowecase modification.
the RFC says that header names should be case insensitive.
Oct 1, 2008 at 7:51 PM
OK thanks for the answer.

The following request is generating an exception during parsing. It is probably related to the phpinfo?=PHP part which we don't see often in query strings.

Can you confirm that?

GET /phpinfo.php?=PHPE9568F35-D428-11d2-A769-00AA001ACF42 HTTP/1.1
Host: localhost:8887
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv: Gecko/2008092417 Firefox/3.0.3
Accept: image/png,image/*;q=0.8,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://localhost:8887/phpinfo.php
Cache-Control: max-age=0

Oct 1, 2008 at 7:52 PM
Here is the Stack Trace:

   at HttpServer.HttpHelper.Add(IHttpInput input, String name, String value) in \HttpServer\HttpHelper.cs:line 108
   at HttpServer.HttpHelper.ParseQueryString(String queryString) in \HttpServer\HttpHelper.cs:line 66
   at HttpServer.HttpRequest.set_UriPath(String value) in \HttpServer\HttpRequest.cs:line 183
   at HttpServer.HttpRequestParser.OnFirstLine(String value) in \HttpServer\HttpRequestParser.cs:line 127
   at HttpServer.HttpRequestParser.ParseMessage(Byte[] buffer, Int32 offset, Int32 size) in \HttpServer\HttpRequestParser.cs:line 226
   at HttpServer.HttpClientContextImp.OnReceive(IAsyncResult ar) in \HttpServer\HttpClientContextImp.cs:line 145
   at System.Net.LazyAsyncResult.Complete(IntPtr userToken)
   at System.Net.ContextAwareResult.CompleteCallback(Object state)
   at System.Threading.ExecutionContext.runTryCode(Object userData)
   at System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode, Object userData)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
Oct 2, 2008 at 7:01 AM
The query string is invalid. It should say something like /phpinfo.php?PHPSESSID=PHPE9568F35-D428-11d2-A769-00AA001ACF42

We have as a rule to throw exceptions in debug mode, to let the developer handle them. In release mode, we catch and log them .
Oct 2, 2008 at 9:06 PM

Thanks for your input! I really appreciate. Here's what RFC 3986 says about query string. It seems the query string described here does not break the RFC spec:

GET /phpinfo.php?=PHPE9568F35-D428-11d2-A769-00AA001ACF42 HTTP/1.1

Are there any reasons you are parsing the query string? At best, isn't it possible to ignore invalid key=value pair when parsing instead of throwing or log an error?



3.4. Query

   The query component contains non-hierarchical data that, along with
   data in the path component (Section 3.3), serves to identify a
   resource within the scope of the URI's scheme and naming authority
   (if any).  The query component is indicated by the first question
   mark ("?") character and terminated by a number sign ("#") character
   or by the end of the URI.

      query       = *( pchar / "/" / "?" )

   The characters slash ("/") and question mark ("?") may represent data
   within the query component.  Beware that some older, erroneous
   implementations may not handle such data correctly when it is used as
   the base URI for relative references (Section 5.1), apparently
   because they fail to distinguish query data from path data when
   looking for hierarchical separators.  However, as query components
   are often used to carry identifying information in the form of
   "key=value" pairs and one frequently used value is a reference to
   another URI, it is sometimes better for usability to avoid percent-
   encoding those characters.

Oct 3, 2008 at 8:59 AM
QueryString values can now be added without a set key '?=value' and will be mapped with string.Empty, hope this helps.