Visual Studio 2005 comes with a built-in webserver, called Cassini. While it works fine for most things, it has a few limitations (which are reasonable for a built-in webserver). One of the limitations is that it only serves local requests. Another limitation is that it doesn't use http.sys, which means it can't share a port with for example IIS.

I decided to put together a little webserver using HttpListener, which is a managed interface for http.sys. This webserver should be able to do everything that Cassini can do and more. You can still limit requests to for example localhost by specifying a different host ('prefix') when starting the server. Hopefully this turns out to be useful for someone playing around with Indigo/WCF, or for someone who wants to let others see a local webapp.

License:
This project is released under Ms-PL. Please contact me directly for questions or exceptions.

Downloads:

Most recent updates:

  • Fixed: Removed a hardcoded path in the GUI project file Program.cs
  • Fixed: Virtual path is now guaranteed to include a trailing slash (which other bits of code rely on)
  • Fixed: GetTranslatedPath may return an invalid path in certain scenarios.

Thanks to Nicko (see comments) for both pointing out these bugs and providing fixes.

Thanks a lot,I always thinking about something like that.

Best wishes.
I downloaded your implementation eagerly, as I'd love it if someone other than me would make this work. :)

I have a few issues, all of which, I think, are easily solved.

1) I'd like to see a command-line version of this, so it's more appropriate for use in batch/scripting/build situations. Perhaps something like a "webserverstart.exe" and "webserverstop.exe" that would communicate with another exe. Or something.
2) As far as I can tell, you don't deal with relative paths. This is handy for the same sort of batch/scripting/build scenarios.
3) You require manual copying of wilco.webserver.dll to the bin directory (or GACing it, I guess, but I hate extra setup steps), since otherwise the runtime can't load your server. I'd like to see this done automatically for the user.

I've done 2 & 3 in my own version, and you're welcome to the code, although I suspect it would take you about ten seconds to figure them out. Your version is much more polished, and - for most people, I think - more useful. I'd like to see you set up and maintain this project to include these (and other) features. I can help you get set up on SourceForge or wherever if you're interested. Email me at candera@wangdera.com if you like.
Craig,

Re #1: Previously there was only a console to run the webserver (which you would close to shutdown the webserver). Do you think it would be sufficient to have something like a /nogui switch that would simply spawn a console window?

Re #2: When you say relative paths, are you talking about the path specified using the /path switch? If so, I will make this change too.

Re #3: I could copy the webserver dll to the /bin directory in of the webserver, and delete it when the server shuts down.
Issue #4: your code doesn't deal with PathInfo either. E.g. the URL

http://localhost/fwtest/default.aspx/FlexWiki/HomePage.html

is not processed correctly. This appears to be due to faulty processing in GetFilePath. It would be better to proceed forward from the vpath, rather than relying on the placement of a dot in the URI.

Feature request #5: I'd like to see this set up so that if I don't want to listen on a port, I don't have to. As I'm sure you know, I can execute pages directly in memory without the overhead of an HttpListener. Your code has just about everything needed for that, except that the Request class would need to be made public and probably have a few overloads added.
As you can see an update is available which should include the first 4 points you mentioned. I will experiment with #5 later this week and post an update to reflect this feedback as well.
Shazam, that was fast. :) Nice work.

However, extended URLs still don't work. I fired up FlexWiki by running this:

webdev.webserver2 /port:8888 /vpath:/fwtest /path:FlexWiki.Web

And then surfed to this:

http://localhost:8888/fwtest/default.aspx/MyWiki/HomePage.html

And I get a 404, which is incorrect. Here's the code I'm using to calculate the path in my version:

public static RequestInfo ParseRequestInfo(HttpListenerContext context, string vpath)
{
RequestInfo requestInfo = new RequestInfo();

Uri uri = context.Request.Url;
string path = uri.AbsolutePath;

if (!path.StartsWith(vpath))
{
throw new Exception("path " + path + " does not start with " + vpath);
}

requestInfo.Page = path.Substring(vpath.Length);
requestInfo.Query = context.Request.Url.Query;

return requestInfo;

}

You'll note that I'm simply stripping the vpath off the beginning, rather than looking for a dot. As some URLs will not have a dot (although ASP.NET seems to incorrectly assume they will), I don't think looking for one is a great idea.
Also, I was thinking more about automation scenarios, and I realized that the console app approach has problems. I.e. how do you shut it down? Clearly, hitting "enter" isn't the right way (not a fault on you - my version does the same thing).

I think that the GUI one is actually the right way to go. I played around with an approach that I sort of like. What I do is to watch for a request for a page called "quit.quit". When this request is made, I shut down the webserver. Makes it real easy to start and stop the webserver during a series of tests from a batch file or build script.

Anyway, more wood for the fire.
Interesting... It does work here. I started the webserver like this:

XML:
1 

WebDev.WebServer2.exe /port:8888 /vpath:/fwtest /path:irrelevant



Then I navigated to http://localhost:8888/fwtest/default.aspx/MyWiki/Default.html. In the root of my website I've got a file Default.aspx, which simply returns some request info of the current request. The response I got for this URL is:

XML:
1 
2 

Path info: /MyWiki/Default.html
File path: /fwtest/default.aspx



Are you sure the latest version of Wilco.WebServer.dll was copied to your webserver? Make sure that this DLL isn't there already, since the webserver won't overwrite it.
As for the scripting support... I could add something like a /daemon:<name> flag, which would run the webserver with an invisible form with the title set to <name>. To shutdown the webserver, you could run TASKKILL /FI "WINDOWTITLE eq <name>". Would this be reasonable?
To add to my last comment: you could ofcourse already do something similar to this... You could run the console like 'start "MyWebServer" WebDev.WebServer2Console ...', which would open the webserver in its own console (and not block the current console - if there is any). Afterwards you can kill the webserver by using its window title.
Your scripting approach seems completely reasonable. I like it!

As for the pathinfo problem, I'll have to investigate more. Hopefully it's just the DLL issue. What's your rationale for not overwriting what's there?
As you can see there's an update available which incorporates some of your feedback.

I've also given your feature request some more thought... I'm not sure yet if it's a good idea to make the Request object reusable. There are a few things that go through my mind right now:
1. Is there a good reason to use this Request object instead of say the SimpleWorkerRequest in .NET?
2. The Request implementation in this webserver depends on the HttpListenerContext, while HttpContext relies on a HttpWorkerRequest object... The relations are the other way round.
3. HttpListenerContext and HttpContext are 2 completely different implementations (ie. they don't have a shared interface). I could work around this by adding a few interfaces and create adapters for both HttpListenerContext and HttpContext to work around this, though.
1) I'm not totally sure. I know that SimpleWorkerRequest is fairly limited, although it covers the basics. Perhaps for the type of testing I'm thinking of doing it'll be sufficient. I'll admit that I'm not yet completely sure whether either an in-memory or webdev.webserver2-type model is really going to cut it for testing.
2 & 3) You can always invert the relationship with another layer of abstraction. I understand that it would be a nontrivial refactoring, though.

At any rate, I wouldn't worry about it right now. The part about it running in memory is really wishlist stuff - first I'd like to figure out whether the EXE still has the problems I mentioned with PathInfo. Still haven't had a chance to check into what's going wrong there.
What license is WebDev.WebServer2 available under? I'd love to bundle it with some .NET apps (free stuff, just hobby hacking) so they can run turnkey without needing some existing server installed. Sort of a .NET answer to Jetty, which comes with a lot of java apps.
There's no license. Feel free to use, modify or extend it.
Thank you Wilco,

It all works very well except when it has been idle for a while.

I tried changing the InitializeLifetimeService (without knowing much) and without success.

I will try again.

John

Details below -->
--------------------------------------------------
private void OnBeginRequest(HttpListenerContext context)
{
this.server.HandleBeginRequest(new RequestEventArgs(String.Format("{0}", context.Request.Url.LocalPath)));
}

--------------------------------------------------
System.Runtime.Remoting.RemotingException was unhandled by user code
Message="Object '/2495f1cc_40fe_4d3a_99ad_9b00d8ea2a05/bj4snxfx6winibi4ppmkol8t_8.rem' has been disconnected or does not exist at the server."
Source="mscorlib"
StackTrace:
Server stack trace:
at System.Runtime.Remoting.Channels.ChannelServices.CheckDisconnectedOrCreateWellKnownObject(IMessage msg)
at System.Runtime.Remoting.Channels.ChannelServices.SyncDispatchMessage(IMessage msg)
Exception rethrown at [0]:
at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
at Wilco.WebServer.IServer.HandleBeginRequest(RequestEventArgs args)
at Wilco.WebServer.Host.OnBeginRequest(HttpListenerContext context)
at Wilco.WebServer.Host.ProcessRequest()
at System.Runtime.Remoting.Messaging.StackBuilderSink._PrivateProcessMessage(IntPtr md, Object[] args, Object server, Int32 methodPtr, Boolean fExecuteInContext, Object[]& outArgs)
at System.Runtime.Remoting.Messaging.StackBuilderSink.PrivateProcessMessage(RuntimeMethodHandle md, Object[] args, Object server, Int32 methodPtr, Boolean fExecuteInContext, Object[]& outArgs)
at System.Runtime.Remoting.Messaging.StackBuilderSink.SyncProcessMessage(IMessage msg, Int32 methodPtr, Boolean fExecuteInContext)
Thanks John. I'm kind of on a tight schedule at the moment, so it'll take a few days before I will actually look into this. I'll make sure that a fix is available ASAP.
How do you start debuging on this server? I am playing with it for a while and always getting "Unable to start debugging on the web server. An authentication error occured while communicating with the web server.". Does somebody know how to fix it?

Thanks
I made the following change to Response.cs

C#:
1 
2 
3 
4 
5 

public override void SendResponseFromFile(string filename, long offset, long length)
{
  byte[] data =System.IO.File.ReadAllBytes(filename);
  this.context.Response.OutputStream.Write(data, 0, data.Length);
}

That post is unrelated to my original post.
I just wanted to make a contribution.
Thanks John, you've been more than helpful. The bug you pointed out should be fixed now (I forgot to take the lifetime service of a certain object into account, which meant it would timeout after 5 minutes). The 2 methods for sending responses from files are also implemented now.

Dmytro: You should be able to attach a debugger to the web server process (WebDev.WebServer2*.exe). If that doesn't work, please give me some more information, such as whether you are running as admin, etc.
This is very interesting. I have considered taking this to another level more on the lines of a real web server with virtual hosts, etc...time will tell.

A few notes:

Your CommandLine class appears in multiple places (Wilco.WebServer\Wilco.WebServer\CommandLine.cs and Wilco.WebServer\WebDev.WebServer2\CommandLine.cs but not in Wilco.WebServer\WebDev.WebServer2Console despite it being used there) appears to have a minor bug. This code:

C#:
1 

if ((ch == '/') || (ch != '-'))



does the same thing as:

C#:
1 

if (ch != '-')



since '/' always does not equal '-'

This directory:

Wilco.WebServer\WebDev.WebServer2Console\Wilco.WebServer

and everything under it seems extraneous (am I missing something?)

Due to the ease of hosting with HttpListener/http.sys why not just write a web service and/or web form for controlling the server that can be specified at another virtual path? With a web service you can write your own GUI application and the service could even use .NET soap or binary remoting. With a web form/page you can get around having to write any client, requiring administrating the server with a browser.

An application host created via System.Web.Hosting.ApplicationHost.CreateApplicationHost() like your Wilco.WebServer.Host should not need to be configured with the applications virtual and physical paths. These were already provided to the new AppDomain in the CreateApplicationHost() call. I question whether such needs to be passed to the HttpWorkerRequest each time too as they are always instaniated in the proper AppDomain and never leave it. Taking a quick look at SimpleWorkerRequest one can see it does this to obtain such:

C#:
1 
2 

this._appPhysPath = Thread.GetDomain().GetData(".appPath").ToString();
      this._appVirtPath = Thread.GetDomain().GetData(".appVPath").ToString();


Thanks Uzume. I've updated the CommandLine.cs and removed the extraneous directory. I also no longer pass in the directories and instead get those from the HttpRuntime.

My goal with this project was to create a basic webserver similar to Cassini, except that it builds on top of HTTP.SYS. It was not my intention to create a fully-featured webserver for actually hosting websites. I may reconsider things if there are good reasons for building a more exotic webserver.
I realize your intention was just to create an ASP.NET development webserver for hosting a few (perhaps just one) application (the WebDev.WebServer name is a good clue since it is designed to mimic/replace the same thing in VS8).

I have been thinking about the whole web server larger picture with regard to more than just a simple development hosting platform. I think passing in the vpath/URI does sort of make sense if one allows a single application to live at multiple URIs. I could be wrong as I have not tested this extensively but I believe IIS does not support this (I only have 5.1 available for testing just now). Multiple applications can be defined to the same physical path and things will work great but this will result in two distinct applications (domains and all) and anything shared across an application (session, cache, etc.) will not be shared.

I have been looking at other CIL targeted web servers. Most are also tiny developmental/single application type servers. One more interesting one is the mono project's XSP which seems to have some larger infrastructure in it with a server concept that maps requests to one of a number of applications. It also seems to allow for multiple HTTP request sources (IWebSource). There are several Cassini-based servers that I have not looked at much (but I did download the code and hope to get time to study them).

Perhaps I should write an XSP IWebSource to use HttpListener (with several other supporting classes including application host and worker request, etc.) and see how that works. Perhaps I should submit it back to the mono project too. I know there has been some work to implement the HttpListener in mono land (though exactly how much I am not sure; I saw some code check-in logs that mentioned the classes but I have yet to look at source).

Another interesting thing might be to rip out mono HttpListener into another namespace and use such code on CLR (I am aware code license issues). I presume it is user-land socket-based but this might prove an interesting common interface for web server development. Too bad it was not designed for such though (no real interface type and I think it is sealed too as memory serves).

I also wonder about HttpListener implementations on other platforms like mono and dotGNU's Portable.NET, etc. I have not checked these out alot and not sure how Platform/Invoke, COM and native type support works but I know there is/was a Linux kernel mode web server about. I am not sure how that worked either.

Well this is getting rather long-winded. Perhaps I should start my own weblog someplace.
"I could be wrong as I have not tested this extensively but I believe IIS does not support this [multiple websites]"

IIS6 does support this. Previous version don't, most likely because of the fact that only IIS6 builts on top of HTTP.SYS.

It should be trivial to build a GUI on top of the webserver that lets you create multiple applications. Using prefixes you can define whether the application should be a website or a nested application. It should also be possible to use prefixes to implement hostheaders.
Parties interested in this topic might find the above MSDN sample of interest.
Hi,

Thanks for this great code. I have got one question. I am trying to pass a custom object which is inherits from MarshalbyRef class from the Host to the aspx pages via httpcontext, but unable to do so. Does any one has any idea. Is there any other way to communicate between the runtime pages and host server.

Thanks

Sazzad
A great web server for all of us stuck with the dog that is IIS 5.1!! Thanks.

I had to make a couple of changes to get it to work for me:

In Server.cs CreateWebServer method I full specified the location of the Wilco.WebServer.dll that is copied. The code assumes that the current working directory contains the dll, this was not the case for me.

C#:
1 

File.Copy(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Wilco.WebServer.dll"), destFileName, true);



In Request.cs I had to change the GetFilePathTranslated method because ASP.NET was throwing an exception in CheckSuspiciousPhysicalPath. The physical path had a double path seperator at the boundary of the physical and virtual paths. I changed the code to Trim slashes from the start of the path and then use Path.Combine to build the full physical path.

C#:
1 
2 
3 
4 
5 
6 

private string GetFilePathTranslated(string path)
{
    path = path.Substring(this.virtualDir.Length);
    path = path.Replace('/', '\\').TrimStart('\\');
    return Path.Combine(this.physicalDir, path);
}



I don't really know why I was getting this behaviour as I would have thought it would effect other users as well?

Thanks again.
It looks like you have left your test data hard coded into the source for the GUI Programe.cs Main method. The first line of this method overrides the programme arguments to specify a local path on your machine. Removing this line seems to resolve the issue.

Another thing is the way in which the HttpListener is setup. Currently the prefix is specified as the root of the host e.g. http://localhost:88/. However if I have specified a VirtualPath of /test then the HttpListener only needs to register for the prefix http://localhost:88/test/. This is important because it allows me to run 2 copies of the webserver on the same port as long as they have different VirtualPaths. This should also allow the WebDev server to listen on port 80 on a machine running IIS6 as long as it specifies a VirtualPath that IIS has not registered.

Anyway I made the folling change to Host.cs Start method

C#:
1 
2 
3 
4 
5 
6 

string prefix = this.prefix + ":" + this.port + virtualPath;
if (!prefix.EndsWith("/"))
{
	prefix += "/";
}
this.listener.Prefixes.Add(prefix);



Cheers.
Nicko: Thank you for your contributions. They're now part of the new download.
One thing that should be noted by your decision to use the HttpListener class is that it only runs on Windows XP SP2 and Windows Server 2003. Your implementation of these things is really great, but that's the main downside.

Also, with your prefixes, you need to make sure that the user knows to specify http://* if they are using a port other than 80 and want to have all requests go to your server. If you just enter *, it fails.

Thanks for the great code examples!
Hello.

I've also implemented a simple web server (using system.web.hosting).

I wondered if you had run across any problems in serving WebResource.axd files (these are automatically created by the .net 2.0 framework to send embedded resources-- javascript, etc.-- to the browser).

My server grabs the correct output from the .axd files but, for some reason, the javascript functions that it contains are not available in the browser.

I was just wondering if you'd had any problems with it.

Michael
Is remoting supported? I am getting the following stack when attempting to access a remoting service on the server. We are using a binary formatter for this remoting service as you can see below.

Server stack trace:
at System.Runtime.Serialization.Formatters.Binary.__BinaryParser.Run()
at System.Runtime.Serialization.Formatters.Binary.ObjectReader.Deserialize(HeaderHandler handler, __BinaryParser serParser, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage)
at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream serializationStream, HeaderHandler handler, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage)
at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.UnsafeDeserializeMethodResponse(Stream serializationStream, HeaderHandler handler, IMethodCallMessage methodCallMessage)
at System.Runtime.Remoting.Channels.CoreChannel.DeserializeBinaryResponseMessage(Stream inputStream, IMethodCallMessage reqMsg, Boolean bStrictBinding)
at System.Runtime.Remoting.Channels.BinaryClientFormatterSink.DeserializeMessage(IMethodCallMessage mcm, ITransportHeaders headers, Stream stream)
at System.Runtime.Remoting.Channels.BinaryClientFormatterSink.SyncProcessMessage(IMessage msg)

Exception rethrown at [0]:
at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
I was able to get it to work with binary-formatting and remoting (I assume what you mean is that you are using his webserver as the host). The error message you have is indicating that you are getting some sort of text-based error message, which means that your remoting just isn't set up right. The same thing happens in IIS, but if it works for you there but not here, then probably one of your config files (or some other needed resource) is not being loaded right. You can use an HTTP trace program to get the text of the error message (I hate having to do that with remoting, if anyone has a better suggestion, please let me know). This probably means that your appdomain is failing to load for whatever reason, so it would be a good idea to check the application event log on the host server to see if there is anything more specific in there. Depending on what point it is failing at, you might well find the best answer in there.
I'm not having any luck getting Microsoft's new ASP.NET AJAX to work with the wilco.webserver. I'm just testing out an update panel. I don't see any errors on the asp.net side or javascript side. It just kind of looks like it isn't posting back. The same code works in the web server built into visual studio though.

very good,but cookies is wrong!
中国用户支持你!
Anyone know how I could get this to accept file uploads? I have no idea how to split the multipart/form-data. Some hints in the right direction would be fantastic.
The Neokernel Web Server (www.neokernel.com) has a file upload demo to show you how to handle multipart mime uploads. It also works with the ASP.NET AJAX stuff and cookies. It is a 100% managed code assembly that you can run as a standalone webserver or bootstrap from within your own application.
Your message will be encoded/formatted when it is displayed. If you want to post code, please put the code inside [code=X][/code] tags, where X is the language of your code (C#, ASPX, SQL, etc).
Name:
Email:
(will be encoded using JavaScript to keep it functional and prevent it from being picked up by spammers)
Url:
 
Message:
3 + 3 =