Simple C# Web Server with PHP Support using CGI

The world is full of capable web servers, but there might come a point, where you need a small, portable .NET web server. And for compatibility reasons, you might want to use PHP with it, but you can’t find any guides on how to do that. And for that day, I have written this post.

A Use Case

When I started looking into using PHP from C#, I found a few code snippets that were enough to run a simple script, maybe even handle some GET parameters, but that was about it. Examples for POST were difficult to come by, and I don’t believe I found anything on file uploads.

Why was I even looking into this though? When I write an application that needs a web server, like a server emulator for a game that provides some services over HTTP, I want to include a simple web server with the application. Just so it’s there, and the whole package can be used as is, without setting up more additional software. And ideally I want an upgrade path to a more capable web server, like Apache or NGIX, in case a user wants to integrate everything into their existing eco-system, and for that, I want to be able to use PHP.

Web Server

The web server part of the equation is actually fairly simple, as there are several viable options out there, such as NHttp or EmbedIO. I won’t go into too much detail about that here, as these packages are fairly self-explanatory and not difficult to use at all. Personally I’m currently using EmbedIO, which can be easily integrated into any .NET application and works perfectly fine for my purposes. You can be up and running in minutes if all you want is serve static content, but the interesting part is the dynamic part, like with PHP scripts.

CGI

One way web servers often times handle dynamic content is via CGI, which in this context stands for “Common Gateway Interface”. It’s a standard for passing information from a web server to an application or script, which can then do something with that information and return data, such as HTML code, to send back to the browser. In the real world this might look something like this.

You make a request to a file called “foobar.php”, the web server recognizes that this is a PHP file, it passes that request to a “php-cgi.exe”, PHP runs the script with the information from the web server, such as the GET variables, and then writes the result to the standard output, which the web server reads and sends back to the browser.

This sounds pretty easy, and it is, you just have to pass the right information.

Executing PHP

Let’s get straight to it, this is an example of what a request handler that calls PHP might look like for EmbedIO, though it should be simple to adjust it for other libraries.

protected override async Task OnRequestAsync(IHttpContext context)
{
	var fileInfo = fileSystemProvider.MapUrlPath(context.RequestedPath, context);

	// Skip if file isn't a PHP file
	if (!fileInfo.IsFile || Path.GetExtension(fileInfo.Path) != ".php")
		return;

	// Get query string from URL
	var index = context.Request.RawUrl.IndexOf("?");
	var queryString = index == -1 ? "" : context.Request.RawUrl.Substring(index + 1);

	// Read body for POST requests
	byte[] requestBody;
	using (var ms = new MemoryStream())
	{
		context.Request.InputStream.CopyTo(ms);
		requestBody = ms.ToArray();
	}

	// Get paths for PHP
	var documentRootPath = "C:/MyWebServer/";
	var scriptFilePath = Path.GetFullPath(fileInfo.Path);
	var scriptFileName = Path.GetFileName(fileInfo.Path);
	var scriptFolderPath = Path.GetDirectoryName(fileInfo.Path);
	var tempPath = Path.GetTempPath();

	// Execute PHP
	using (var process = new Process())
	{
		process.StartInfo.FileName = "php-cgi.exe";
		process.StartInfo.UseShellExecute = false;
		process.StartInfo.RedirectStandardOutput = true;
		process.StartInfo.RedirectStandardInput = true;
		process.StartInfo.CreateNoWindow = true;

		process.StartInfo.EnvironmentVariables.Clear();

		process.StartInfo.EnvironmentVariables.Add("GATEWAY_INTERFACE", "CGI/1.1");
		process.StartInfo.EnvironmentVariables.Add("SERVER_PROTOCOL", "HTTP/1.1");
		process.StartInfo.EnvironmentVariables.Add("REDIRECT_STATUS", "200");
		process.StartInfo.EnvironmentVariables.Add("DOCUMENT_ROOT", documentRootPath);
		process.StartInfo.EnvironmentVariables.Add("SCRIPT_NAME", scriptFileName);
		process.StartInfo.EnvironmentVariables.Add("SCRIPT_FILENAME", scriptFilePath);
		process.StartInfo.EnvironmentVariables.Add("QUERY_STRING", queryString);
		process.StartInfo.EnvironmentVariables.Add("CONTENT_LENGTH", requestBody.Length.ToString());
		process.StartInfo.EnvironmentVariables.Add("CONTENT_TYPE", context.Request.ContentType);
		process.StartInfo.EnvironmentVariables.Add("REQUEST_METHOD", context.Request.HttpMethod);
		process.StartInfo.EnvironmentVariables.Add("USER_AGENT", context.Request.UserAgent);
		process.StartInfo.EnvironmentVariables.Add("SERVER_ADDR", context.LocalEndPoint.Address.ToString());
		process.StartInfo.EnvironmentVariables.Add("REMOTE_ADDR", context.Request.RemoteEndPoint.Address.ToString());
		process.StartInfo.EnvironmentVariables.Add("REMOTE_PORT", context.Request.RemoteEndPoint.Port.ToString());
		process.StartInfo.EnvironmentVariables.Add("REFERER", context.Request.UrlReferrer?.ToString() ?? "");
		process.StartInfo.EnvironmentVariables.Add("REQUEST_URI", context.RequestedPath);
		process.StartInfo.EnvironmentVariables.Add("HTTP_COOKIE", context.Request.Headers["Cookie"]);
		process.StartInfo.EnvironmentVariables.Add("HTTP_ACCEPT", context.Request.Headers["Accept"]);
		process.StartInfo.EnvironmentVariables.Add("HTTP_ACCEPT_CHARSET", context.Request.Headers["Accept-Charset"]);
		process.StartInfo.EnvironmentVariables.Add("HTTP_ACCEPT_ENCODING", context.Request.Headers["Accept-Encoding"]);
		process.StartInfo.EnvironmentVariables.Add("HTTP_ACCEPT_LANGUAGE", context.Request.Headers["Accept-Language"]);
		process.StartInfo.EnvironmentVariables.Add("TMPDIR", tempPath);
		process.StartInfo.EnvironmentVariables.Add("TEMP", tempPath);

		process.Start();

		// Write request body to standard input, for POST data
		using (var sw = process.StandardInput)
			sw.BaseStream.Write(requestBody, 0, requestBody.Length);

		// Write headers and content to response stream
		var headersEnd = false;
		using (var sr = process.StandardOutput)
		using (var output = context.OpenResponseText())
		{
			string line;
			while ((line = sr.ReadLine()) != null)
			{
				if (!headersEnd)
				{
					if (line == "")
					{
						headersEnd = true;
						continue;
					}

					// The first few lines are the headers, with a
					// key and a value. Catch those, to write them
					// into our response headers.
					index = line.IndexOf(':');
					var name = line.Substring(0, index);
					var value = line.Substring(index + 2);

					context.Response.Headers[name] = value;
				}
				else
				{
					// Write non-header lines into the output as is.
					output.WriteLine(line);
				}
			}
		}
	}

	// Set context to handled, so no more modules get the request
	context.SetHandled();

	await Task.CompletedTask;
}

The important part is to get all the information PHP needs to properly handle the request and to read back the response it gives. In this example we create a new process, set it to our “php-cgi.exe”, which comes with PHP, and set the respective variables to not actually show the PHP interpretor and redirect the in- and output, so we can use it. Some of the arguments you might recognize if you’ve used PHP before, such as SCRIPT_FILENAME or REQUEST_URI.

Just by passing QUERY_STRING, you are able to handle GET requests, because PHP will parse that string for variables. For correct handling of POST requests, you need to read the data sent to the web server, pass the correct CONTENT_LENGTH and CONTENT_TYPE, and write the entire body of the request you read from the web server to the standard input of the process, so PHP can read it.

Finally, all you do is read the result from the process, to be found in the standard output, and use it in your web server’s response. Parse the response for headers, to integrate them into your web server’s response handling, and write the remaining lines into the response as is. The seperator between the headers and the content is a simple, empty line. That’s it.

Gotchas

GET requests are relatively simple with this snippet, POST requests are as well, as long as you properly read and write the data to and from the standard input and output. To support uploading files however, you need to make sure that PHP has access to a temp folder, and that it knows where it is. It does not default to the system’s default temp directory. To fix this, you can either set the option upload_tmp_dir in the php.ini, or pass the two arguments TMPDIR and TEMP to php-cgi, which are the two environment variables in use for different operating systems.

Conclusion

While this code works great for simple applications, I want to mention that you shouldn’t use this in a high-traffic production evironment. CGI is actually rarely used nowadays, because starting an instance of php-cgi for every request isn’t very efficient. Instead, most web servers either use plugins or FastCGI, the latter of which is a system to keep instances running in the background, and just pass the information to them, to generate a response. But if you have such a requirement, you should probably use a more well established web server anyway.

Well, I hope this will be helpful to someone out there. I wish I had found a complete example of this anywhere, that would’ve saved me some time^^