Running with Code Like with scissors, only more dangerous

27May/080

WCF for Fun and Profit

Posted by Rob

I've alluded in the past that I got my start in computers because of games; I also ended up getting started in programming because of games.  I happen to be a big fan of the WarCraft and StarCraft series produced by Blizzard Entertainment, and it was my association with (and desire to make a good website for!) a group of people on their gaming service called Battle.net that got me involved with programming, and that's where the "Fun" part of this comes from.  I'm not sure where "Profit" comes into play.

With the knowledge that StarCraft 2 is on the horizon, and the new announcement of Diablo 3, I've been working on a series of open-source projects collectively named after my cat Jinx (we had the name WAY before the movie), which includes a third-party utility library that manages client connections to Battle.net's chat service, a full-blown client, and some plugins for the client.  When I first got into the bot-making community I found a project called WebChannel and WebBot -- these projects allowed clients to broadcast their channel information via another service called BotNet, which then rendered the content over an ISAPI Extension.  At the time I only really knew the basics of C#, and I could not find a way to mimic the functionality (WebChannel streamed content over the wire by removing Content-Length from HTTP headers, and then never closing the client connections).  To my knowledge, there is not a way to get ASP.NET to not send the Content-Length header; I guess I've never pursued it after about three years ago.

When starting this project, I knew from the start that I wanted to recreate the functionality (particularly because WebChannel has been unavailable for a long time).  But I also knew that I wanted to do it without recreating a lot of functionality; in the past, submitting complex types to Web Services caused the complex types to be generated as proxies as part of the client assembly, and there was no support for inheritance/polymorphism.  I knew that all of my event arguments would derive from a single class, and so polymorphism seemed to be the way to go.  Plus, all complex types serialized to and from XML needed to have a default public constructor and public gettable/settable properties, which in my mind would severely break encapsulation.  The question was, how to go about pushing my client content to a web site.

It turns out that WCF Data Contracts were the answer to my problem.  By decorating all of my classes with [DataContract] and [DataMember] I was (mostly) able to easily serialize the classes over the wire, and the best part was that I only needed one method, and inheritance was observed!  I didn't even have to break encapsulation; consider the ChatMessageEventArgs class.  By applying [DataMember] to fields instead of properties, I didn't need to make my properties settable, which means that the objects should be (more or less) immutable to code consumers.  By sharing the common code between the web server and the client plugin, I was (mostly) able to avoid any kind of object recreation, and even more fun, the types were rendered to JavaScript automatically as part of ASP.NET AJAX.

The astute reader might have noticed a lot of qualifying "mostly" asides in that last paragraph.  I'll get to that.

The final result - well, not really final - was a very Meebo-esque client that shows a live display of the channel Diablo II USA-1 on the US-East Battle.net server.  (If it's not up I apologize).  (Also, I definitely take no credit for what is said in the channel; I'm just showing that it can be done).

Headaches

There have been some trials on this project.

  • Proxy generation ignored shared types -- this occurred once I started sending some certain complex types over the wire.  When I added the service reference and looked at the Object Browser in Visual Studio, I would see all of my BNSharp.dll-driven classes being recreated.  This was what I wanted to avoid!  It turned out to be that I was not decorating some enumerations with [EnumMember] - once I did this, proxy generation returned to normal.
  • I received an error that "more than one http binding cannot be configured" when attempting to view a WCF service.  Because I use a shared web host, and multiple hosts were configured on the same IP, I was getting an error when setting up WCF services on my host.  It turned out that this was corrected with a web.config setting in .NET 3.5 called <baseAddressPrefixFilters>.
  • I then received an error that my plugin was no longer authorized once I moved it into the shared hosting environment.  I eventually tracked this down to using wsHttpBinding (the more secure Web Services HTTP binding), and had to change to basicHttpBinding.
  • I then received an error that my application was sending content-type "application/soap+xml, charset='utf-8'", which was not the expected "text/xml, charset='utf-8'".  Ultimately this ended up being traced to not using basicHttpBinding on the client as well.  That one had to be the most frustrating; honestly, it's the same thing.

What needs to yet be done

I've noticed that the client tends to cache the content returned from the server.  I need to access the HttpResponse and set its cacheability.

Other than that, it's just the user interface that needs to be done.  We've got the basics, but it needs a channel list and the ability for users to type a password into the client to communicate with the host application and then interact with the channel.

Tagged as: , No Comments
22May/080

Breakout in Model-View-Presenter

Posted by Rob

I sat down tonight and in about three hours or so cranked out the first vestiges of something that kind of maybe resembles Breakout, only using graphics that are possibly worse than anything ever shown on Atari 2600 because well, I made them.

Thanks to Joel Neubeck for the video player, and thanks to digital blasphemy for the background (yes I know it's not a free image, I'll be sure to remove it before it goes anywhere).

I should also point out: I used some sounds from the Spacewar demo in XNA Game Studio 2.0.  Those sound AWESOME.

This app is built with a model in a separate assembly; the model portion of the project contains the mathematics, collision detection, and all the game objects on the screen.  We've got tiles, a ball, and a paddle.  I should also point out that there's a game board (that's the big black empty space on the screen).

The XNA game application, then, instantiates a GameBoard (that's the containing class that owns all of the rest of the objects) and then instantiates a GameBoardPresenter.  GameBoardPresenter is part of the game application itself, and it owns a copy of the GameBoard because it's intimately tied to its inner workings.

To keep things relatively efficient, the GameBoard class exposes all of its object lists as properties that return the lists.  This way, object references aren't duplicated (except for the references to the Lists), and we're not wasting unnecessary memory and copy operations.  (If you're not clear what I mean, here are the properties:)

   1: public Paddle Player
   2: {
   3:     get { return m_player; }
   4: }
   5:  
   6: public List<Ball> Balls
   7: {
   8:     get { return m_balls; }
   9: }
  10:  
  11: public List<Tile> Tiles
  12: {
  13:     get { return m_tiles; }
  14: }
  15:  
  16: public List<Projectile> Projectiles
  17: {
  18:     get { return m_projectiles; }
  19: }

Since the balls are the only pieces of the game that move, each call to Update() on the GameBoard instance iterates through each Ball object, moving it accordingly and handling any collisions.  When a collision is detected, it fires an event corresponding to the type of collision, which is consumed by the GameBoardPresenter.

One other advantage of separating out the presentation and the actual game logic is that I can scale the graphics size to anything I want; for instance, the video above was recorded and 960x720, but I was also able to run it at 1920x1200 without any apparent differences in visualization.  The internal game units actually default to a game board size of 4800x4800, and scaling is simply done within the GameBoardPresenter's Draw() method:

   1: public void Draw(SpriteBatch target)
   2: {
   3:     Texture2D currentTexture;
   4:     Vector2 scale = new Vector2(m_scale);
   5:     Vector2 offset = new Vector2(m_xOffset, 0);
   6:  
   7:     currentTexture = m_textures[BoardItemType.GameBoard];
   8:     target.Draw(currentTexture, offset,  null, Color.White, 0f, Vector2.Zero, scale, SpriteEffects.None, 1f);
   9:  
  10:     currentTexture = m_textures[BoardItemType.BasicTile];
  11:     foreach (Tile t in m_tiles)
  12:     {
  13:         target.Draw(currentTexture, t.Position * scale + offset, null, Color.White, 0f, Vector2.Zero, m_scale, SpriteEffects.None, 0f);
  14:     }
  15:  
  16:     currentTexture = m_textures[BoardItemType.Paddle];
  17:     target.Draw(currentTexture, m_paddle.Position * scale + offset, null, Color.White, 0f, Vector2.Zero, m_scale, SpriteEffects.None, 0f);
  18:  
  19:     currentTexture = m_textures[BoardItemType.Ball];
  20:     foreach (Ball b in m_balls)
  21:     {
  22:         target.Draw(currentTexture, b.Position * scale + offset, null, Color.White, 0f, Vector2.Zero, m_scale, SpriteEffects.None, 1f);
  23:     }
  24: }

The scale is calculated when the backbuffer is sized.

Source code for this isn't quite ready yet; there are some things that I want to refactor (for instance, I want the Model part to define input "actions" and that View/Presenter part to keymap inputs to actions). 

Tagged as: , , , No Comments
19May/080

Using C# Enumerations as LINQ-to-SQL Entity Properties

Posted by Rob

Have you ever created a database object and said "OK, this column is going to correspond to this enumeration"?  If you're obsessive like me, you might have even gone so far as to create column restrictions for the valid range of values, and created a special SQL type that doesn't really do anything except give you peace of mind.

Well, I've got about three such fields on a couple entities on a recent project.  Since I wanted those properties to go into C# enumerations, I tried the natural thing: I typed the enumeration's type name into the "Type" property.

Setting a column's Type property.

Unfortunately, doing this didn't work.  In fact, it seemed to break Visual Studio; updates stopped propagating to my LINQ-to-SQL classes, and in fact since I had done this before a single save, I didn't get any entity classes. 

It turns out that Matt Davis found the answer to the problem: qualify the enumeration's type name with the global:: namespace qualifier if it doesn't live in a namespace (for instance, code within an ASP.NET App_Code folder).

The global:: qualifier is important.

Once I added the qualifier to my type names, saving the DBML file correctly updated the LINQ-to-SQL classes, and I was off and running!

Tagged as: , , No Comments
15May/080

Adding Collision Detection

Posted by Rob

There's already rudimentary collision detection in the app, but only for the sides.  What if I wanted the balls to detect when they collide?

In the demo video you can already see the balls colliding (and, incidentally, the gravity changing from down, then left, up, right, and down again).  But how did I get there?  Wouldn't you know that XNA has a built-in structure called BoundingSphere that can handle that for you?  I defined the bounding sphere as centered around the ball's center point (which was defined by the texture's dimensions), and used the z-coordinate as 0.  Intrinsically, then, the bounding sphere acts like a bounding circle as long as everything is on the same Z-plane.

Next, I created a couple interfaces that are going to help me out.  Interfaces such as IPhysical and IPhysicalSphere -- they provide necessary information for collidable objects.  Here they are:

   1: public interface IPhysical
   2: {
   3:     Vector3 CenterOfMass
   4:     {
   5:         get;
   6:     }
   7:  
   8:     Vector3 Speed
   9:     {
  10:         get;
  11:         set;
  12:     }
  13:  
  14:     float Mass
  15:     {
  16:         get;
  17:     }
  18: }
  19:  
  20: public interface IPhysicalSphere : IPhysical
  21: {
  22:     BoundingSphere Bounds
  23:     {
  24:         get;
  25:     }
  26: }

By implementing these properties, a separate class can be constructed to perform the necessary math for bouncing balls.  When first implemented, colliding balls would simply switch speed vectors.  However, that only works if balls collide straight on (so that movement vectors would be inverse of each other).  When balls collide like this:

Amazing MS-Paint Art!

(Awesome MS-Paint artwork, huh?)

When balls collide like that, they don't simply exchange vectors.  What actually happens is that the speed vectors are changed according to the normal line between the two circles (or the normal plane between the two spheres).

I created a static class to do this work for me:

   1: public static class Collision
   2: {
   3:     public static void ApplyCollision(IPhysicalSphere sphereA, IPhysicalSphere sphereB)
   4:     {
   5:         Vector3 x = sphereB.CenterOfMass - sphereA.CenterOfMass;
   6:         x.Normalize();
   7:  
   8:         Vector3 v1 = sphereA.Speed;
   9:         float x1 = Vector3.Dot(x, v1);
  10:  
  11:         Vector3 v1x = x * x1;
  12:         Vector3 v1y = v1 - v1x;
  13:  
  14:         float m1 = sphereA.Mass;
  15:  
  16:         x = -x;
  17:         Vector3 v2 = sphereB.Speed;
  18:         float x2 = Vector3.Dot(x, v2);
  19:  
  20:         Vector3 v2x = x * x2;
  21:         Vector3 v2y = v2 - v2x;
  22:  
  23:         float m2 = sphereB.Mass;
  24:  
  25:         float combinedMass = m1 + m2;
  26:  
  27:         Vector3 newVelA = (v1x * ((m1 - m2) / combinedMass)) + (v2x * ((2f * m2) / combinedMass)) + v1y;
  28:         Vector3 newVelB = (v1x * ((2f * m1) / combinedMass)) + (v2x * ((m2 - m1) / combinedMass)) + v2y;
  29:  
  30:         sphereA.Speed = newVelA;
  31:         sphereB.Speed = newVelB;
  32:     }
  33: }

This was inspired by a blog post I found that covered it exceptionally.  I wish I could name the offer but I couldn't find the author's name!

Calling this method to adjust the speed vectors is done whenever collisions are detected.  That part is handled in the Game class during the Update method:

   1: for (int i = 0; i < m_balls.Count; i++)
   2: {
   3:     Ball b = m_balls[i];
   4:     for (int j = i + 1; j < m_balls.Count; j++)
   5:     {
   6:         Ball test = m_balls[j];
   7:         if (b.Bounds.Intersects(test.Bounds))
   8:         {
   9:             Collision.ApplyCollision(b, test);
  10:             b.Update(gameTime, GraphicsDevice.Viewport);
  11:             test.Update(gameTime, GraphicsDevice.Viewport);
  12:             b = null;
  13:             break;
  14:         }
  15:     }
  16:  
  17:     if (b != null)
  18:         b.Update(gameTime, GraphicsDevice.Viewport);
  19: }

And that's about all there is to it!  New source code is up!  Here's the next demo:

One other thing -- as a note, the red ball with the orange gradient has a mass of 8.0f; the others have a mass of 4.0f.

15May/080

My First XNA Application: The Bouncing Ball

Posted by Rob

My first application in XNA!  It's a... well, it's a ball that bounces.

It's bouncing!!

I created a new Windows XNA 2.0 application project.  I decided to abstract away a Ball object, as well as a GravitySource object.  There's a lot of cross-talk -- I'm not sure if this is good or not -- but it's happy enough for me. :-)

Gravity seems to be pretty straightforward to implement; the applied speed is going to be a vector direction with a constant speed as the magnitude.  Here's the code for the gravity source class:

   1: public class GravitySource
   2: {
   3:     private const float GRAVITY = 9.8f;
   4:  
   5:     private Vector2 m_gravitySpeed;
   6:  
   7:     public void ApplyGravity(ref Vector2 currentSpeed)
   8:     {
   9:         currentSpeed += m_gravitySpeed;
  10:     }
  11:  
  12:     public void ResetToDirection(Direction target)
  13:     {
  14:         switch (target)
  15:         {
  16:             case Direction.Up:
  17:                 m_gravitySpeed = new Vector2(0f, -GRAVITY);
  18:                 break;
  19:             case Direction.Left:
  20:                 m_gravitySpeed = new Vector2(-GRAVITY, 0f);
  21:                 break;
  22:             case Direction.Right:
  23:                 m_gravitySpeed = new Vector2(GRAVITY, 0f);
  24:                 break;
  25:             case Direction.Down:
  26:                 m_gravitySpeed = new Vector2(0f, GRAVITY);
  27:                 break;
  28:             case Direction.DownLeft:
  29:                 m_gravitySpeed = Vector2.Normalize(new Vector2(-1, 1)) * GRAVITY;
  30:                 break;
  31:             case Direction.DownRight:
  32:                 m_gravitySpeed = Vector2.Normalize(new Vector2(1, 1)) * GRAVITY;
  33:                 break;
  34:             case Direction.UpLeft:
  35:                 m_gravitySpeed = Vector2.Normalize(new Vector2(-1, -1)) * GRAVITY;
  36:                 break;
  37:             case Direction.UpRight:
  38:                 m_gravitySpeed = Vector2.Normalize(new Vector2(1, -1)) * GRAVITY;
  39:                 break;
  40:         }
  41:     }
  42: }

Note that I added a Direction enumeration to make it nice to read.

The game class has a reference to the ball object, and it handles the keyboard input to change the gravity direction all within the Update method:

   1: protected override void Update(GameTime gameTime)
   2: {
   3:     // Allows the game to exit
   4:     if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
   5:         this.Exit();
   6:  
   7:     KeyboardState keybstate = Keyboard.GetState();
   8:     if (keybstate.IsKeyDown(Keys.Down))
   9:     {
  10:         if (keybstate.IsKeyDown(Keys.Left))
  11:         {
  12:             m_grav.ResetToDirection(Direction.DownLeft);
  13:         }
  14:         else if (keybstate.IsKeyDown(Keys.Right))
  15:         {
  16:             m_grav.ResetToDirection(Direction.DownRight);
  17:         }
  18:         else
  19:         {
  20:             m_grav.ResetToDirection(Direction.Down);
  21:         }
  22:     }
  23:     else if (keybstate.IsKeyDown(Keys.Up))
  24:     {
  25:         if (keybstate.IsKeyDown(Keys.Left))
  26:         {
  27:             m_grav.ResetToDirection(Direction.UpLeft);
  28:         }
  29:         else if (keybstate.IsKeyDown(Keys.Right))
  30:         {
  31:             m_grav.ResetToDirection(Direction.UpRight);
  32:         }
  33:         else
  34:         {
  35:             m_grav.ResetToDirection(Direction.Up);
  36:         }
  37:     }
  38:     else if (keybstate.IsKeyDown(Keys.Left))
  39:     {
  40:         m_grav.ResetToDirection(Direction.Left);
  41:     }
  42:     else if (keybstate.IsKeyDown(Keys.Right))
  43:     {
  44:         m_grav.ResetToDirection(Direction.Right);
  45:     }
  46:  
  47:     m_ball.Update(gameTime, graphics.GraphicsDevice.Viewport);
  48:  
  49:     base.Update(gameTime);
  50: }

The game's drawing method is actually quite simple:

   1: protected override void Draw(GameTime gameTime)
   2: {
   3:     graphics.GraphicsDevice.Clear(Color.Black);
   4:  
   5:     spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
   6:     m_ball.Render(gameTime, spriteBatch);
   7:     spriteBatch.End();
   8:  
   9:     base.Draw(gameTime);
  10: }

And that gets implemented in the Ball class; here are the meat and potatoes:

   1: public void Render(GameTime time, SpriteBatch spriteBatch)
   2: {
   3:     spriteBatch.Draw(m_texture, m_position, Color.White);
   4: }
   5:  
   6: public void Update(GameTime time, Viewport bounds)
   7: {
   8:     m_position += m_speed * (float)time.ElapsedGameTime.TotalSeconds;
   9:  
  10:     int maxX = bounds.Width - m_texture.Width;
  11:     int maxY = bounds.Height - m_texture.Height;
  12:  
  13:     if (m_position.X > maxX)
  14:     {
  15:         m_speed.X *= -1.0f;
  16:         m_position.X = maxX;
  17:     }
  18:     else if (m_position.X < 0)
  19:     {
  20:         m_speed.X *= -1.0f;
  21:         m_position.X = 0;
  22:     }
  23:     else
  24:     {
  25:         // else we're in a freefall to the right!
  26:         m_gravity.ApplyGravity(ref m_speed);
  27:     }
  28:  
  29:     if (m_position.Y > maxY)
  30:     {
  31:         m_speed.Y *= -1.0f;
  32:         m_position.Y = maxY;
  33:     }
  34:     else if (m_position.Y < 0)
  35:     {
  36:         m_speed.Y *= -1.0f;
  37:         m_position.Y = 0;
  38:     }
  39:     else
  40:     {
  41:         // else we're in a freefall
  42:         // v(t) = at + k
  43:         // in this case a = -9.8
  44:         // Inverted because we're going down which is positive Y ;-)
  45:         m_gravity.ApplyGravity(ref m_speed);
  46:     }
  47: }

One kind of odd part is that it's designed to be simply a perfectly elastic ball (m_speed.Y *= -1.0f) but it actually bounces higher over time.

Source code is downloadable here.  Thanks to Betsy Aoki's samples online - they were incredibly helpful!

Tagged as: , , No Comments
13May/080

Awesome Code Snippet Plugin for Windows Live Writer

Posted by Rob

I've enjoyed Windows Live Writer as a blog editing tool for a while now (in fact, I think all of my blog entries have been written with it).  WLW includes a nifty SDK (one that I haven't had a chance to play with yet) that enables add-in creation.  Leo Vildosola created it, based on the more recent plugin I used in fact.

This is the third code snippet plugin I've used (just started this one today, in fact) and it's definitely the best.  It has some nice features; if you look at the previous post, you'll see that it enables code to be contained within a container element, so that long listings don't need to take up pages.  Very nice.  It also has a mini mode!  What more could you want?

I need to make a stamp graphic or something that says "Rob's Stamp of Approval". :-)

Tagged as: , , No Comments
13May/080

Creating a Sniffer for ASP.NET

Posted by Rob

This afternoon, some co-workers were a bit perplexed about why an 'ñ' character in a Spanish name wasn't displaying on a Facebook application we were working on, but rather was coming across as a '?'.  Because we were pretty sure that Facebook was UTF-8-encoded, we thought the problem was on our side.  Unfortunately, we were on a client's staging/test server, and we weren't able to debug on that machine (we have Visual Studio installed on our own test server).  Since we didn't have privileges on the server, we couldn't even jump on WireShark to validate the outgoing packets from our server.  We needed an alternative method.

One of the cool things about HTTP Modules in ASP.NET is that they sit between the client and the server; with them, we can get all the information we need.  In addition, we can pass the output through a filter - a Stream object through which the page output passes.  This is exposed through the HttpResponse.Filter property.

The solution to this problem is straightforward: we'll inject a filter into the response stream and write all of the output to a log file.  Then, we'll be able to see whether we're sending the offending ?, or if Facebook is misreading our character and translating it to a ?.

First, I created the HTTP Module:

   1: using System;
   2: using System.Web;
   3:  
   4: public class SnifferHttpModule : IHttpModule
   5: {
   6:     #region IHttpModule Members
   7:  
   8:     public void Dispose()
   9:     {
  10:         
  11:     }
  12:  
  13:     public void Init(HttpApplication context)
  14:     {
  15:         context.BeginRequest += new EventHandler(context_BeginRequest);
  16:     }
  17:  
  18:     void context_BeginRequest(object sender, EventArgs e)
  19:     {
  20:         HttpContext.Current.Response.Filter = GetFilterFor(HttpContext.Current);
  21:     }
  22:     #endregion
  23:  
  24:     private static System.IO.Stream GetFilterFor(HttpContext httpContext)
  25:     {
  26:         return new LogFilter(httpContext, httpContext.Response.Filter);
  27:     }
  28: }

Then we had to implement an actual filter stream object:

   1: using System;
   2: using System.Web;
   3: using System.IO;
   4: using MBNCSUtil;
   5: using System.Text;
   6:  
   7: /// <summary>
   8: /// Summary description for LogFilter
   9: /// </summary>
  10: public class LogFilter : Stream
  11: {
  12:     private Stream _logStream, _sink, _binaryStream;
  13:     private StreamWriter _binaryWriter;
  14:     public LogFilter(HttpContext context, Stream baseStream)
  15:     {
  16:         _logStream = File.OpenWrite(
  17:             Path.Combine(
  18:                 context.Server.MapPath("~/logs"),
  19:                 string.Concat(DateTime.Now.ToString("yyyy-MM-dd-hh-mm-ss"), ".txt")
  20:                 )
  21:             );
  22:  
  23:         WriteHeadersToLog(_logStream, "Request Headers", context.Request.Headers);
  24:         
  25:         _binaryStream = File.OpenWrite(
  26:             Path.Combine(
  27:                 context.Server.MapPath("~/logs"),
  28:                 string.Concat(DateTime.Now.ToString("yyyy-MM-dd-hh-mm-ss"), "-binary.txt")
  29:                 )
  30:             );
  31:         _binaryWriter = new StreamWriter(_binaryStream, Encoding.UTF8);
  32:         _sink = baseStream;
  33:     }
  34:  
  35:     private static void WriteHeadersToLog(Stream logStream, string title, System.Collections.Specialized.NameValueCollection nameValueCollection)
  36:     {
  37:         StreamWriter sw = new StreamWriter(logStream);
  38:         sw.WriteLine(title);
  39:         sw.WriteLine();
  40:         foreach (string name in nameValueCollection.AllKeys)
  41:         {
  42:             string value = nameValueCollection[name];
  43:             if (value == null)
  44:                 value = "(nil)";
  45:             else if (value.Length == 0)
  46:                 value = "(empty string)";
  47:  
  48:             sw.WriteLine("{0}: {1}", name, value);
  49:         }
  50:         sw.WriteLine();
  51:         sw.Flush();
  52:     }
  53:  
  54:     public override bool CanRead
  55:     {
  56:         get { return false; }
  57:     }
  58:  
  59:     public override bool CanSeek
  60:     {
  61:         get { return false; }
  62:     }
  63:  
  64:     public override bool CanWrite
  65:     {
  66:         get { return true; }
  67:     }
  68:  
  69:     public override void Flush()
  70:     {
  71:         _logStream.Flush();
  72:         _sink.Flush();
  73:         _binaryStream.Flush();
  74:     }
  75:  
  76:     public override long Length
  77:     {
  78:         get { return _sink.Length; }
  79:     }
  80:  
  81:     public override long Position
  82:     {
  83:         get
  84:         {
  85:             return _sink.Length;
  86:         }
  87:         set
  88:         {
  89:             throw new InvalidOperationException();
  90:         }
  91:     }
  92:  
  93:     public override int Read(byte[] buffer, int offset, int count)
  94:     {
  95:         throw new InvalidOperationException();
  96:     }
  97:  
  98:     public override long Seek(long offset, SeekOrigin origin)
  99:     {
 100:         throw new InvalidOperationException();
 101:     }
 102:  
 103:     public override void SetLength(long value)
 104:     {
 105:         _sink.SetLength(value);
 106:     }
 107:  
 108:     public override void Write(byte[] buffer, int offset, int count)
 109:     {
 110:         _sink.Write(buffer, offset, count);
 111:         _logStream.Write(buffer, offset, count);
 112:         string text = DataFormatter.Format(buffer);
 113:         _binaryWriter.WriteLine(text);
 114:     }
 115:  
 116:     public override void Close()
 117:     {
 118:         base.Close();
 119:         _logStream.Close();
 120:         _sink.Close();
 121:         _binaryWriter.Close();
 122:         _binaryStream.Close();
 123:     }
 124: }

Notable in this class --

  • It's not readable or seekable. 
  • It creates two log files per transaction: a dump of the request headers and the response page, and a dump in hex and binary.  The hex-and-binary dump uses the DataFormatter class (username: 'mbncsutil_anonymous', no password) of the MBNCSUtil library, which I won't include here.
  • The Write, Flush, and Close operations are overridden to perform those operations on both of the logs and the contained stream.
  • The existing Response.Filter object is passed as a constructor to this new filter, and is contained as the member field _sink.  If the existing Response.Filter is not used, an exception is raised, because it means that the output isn't getting written to the stream that will eventually be sent to the client.
  • I implemented a helper method called WriteHeadersToLog because I wanted to dump both request and response headers to the log file.  However, I was rudely informed by ASP.NET that it would require IIS7 Integrated Pipeline mode.  So I removed it.

There are some nice files produced.  Here's an example of one:

   1: Request Headers
   2:  
   3: Connection: keep-alive
   4: Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
   5: Accept-Encoding: gzip, deflate
   6: Accept-Language: en-US
   7: Host: localhost:62905
   8: User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/523.15 (KHTML, like Gecko) Version/3.0 Safari/523.15
   9:  
  10:  
  11:  
  12: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  13:  
  14: <html xmlns="http://www.w3.org/1999/xhtml">
  15: <head><title>
  16:     Untitled Page
  17: </title></head>
  18: <body>
  19:     <form name="form1" method="post" action="Default.aspx" id="form1">
  20: <div>
  21: <input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwUJNzgzNDMwNTMzZGRurqWwMv9+8ZxlGC8bdg4AJen5vA==" />
  22: </div>
  23:  
  24:     <div>
  25:         This is a test.
  26:     </div>
  27:     </form>
  28: </body>
  29: </html>

And here's an example of the binary:

0000   0d 0a 0d 0a 3c 21 44 4f  43 54 59 50 45 20 68 74    ....<!DOCTYPE ht

0010   6d 6c 20 50 55 42 4c 49  43 20 22 2d 2f 2f 57 33    ml PUBLIC "-//W3

0020   43 2f 2f 44 54 44 20 58  48 54 4d 4c 20 31 2e 30    C//DTD XHTML 1.0

0030   20 54 72 61 6e 73 69 74  69 6f 6e 61 6c 2f 2f 45     Transitional//E

0040   4e 22 20 22 68 74 74 70  3a 2f 2f 77 77 77 2e 77    N" "http://www.w

0050   33 2e 6f 72 67 2f 54 52  2f 78 68 74 6d 6c 31 2f    3.org/TR/xhtml1/

0060   44 54 44 2f 78 68 74 6d  6c 31 2d 74 72 61 6e 73    DTD/xhtml1-trans

0070   69 74 69 6f 6e 61 6c 2e  64 74 64 22 3e 0d 0a 0d    itional.dtd">...

0080   0a 3c 68 74 6d 6c 20 78  6d 6c 6e 73 3d 22 68 74    .<html xmlns="ht

0090   74 70 3a 2f 2f 77 77 77  2e 77 33 2e 6f 72 67 2f    tp://www.w3.org/

00a0   31 39 39 39 2f 78 68 74  6d 6c 22 3e 0d 0a 3c 68    1999/xhtml">..<h

00b0   65 61 64 3e 3c 74 69 74  6c 65 3e 0d 0a 09 55 6e    ead><title>...Un

00c0   74 69 74 6c 65 64 20 50  61 67 65 0d 0a 3c 2f 74    titled Page..</t

00d0   69 74 6c 65 3e 3c 2f 68  65 61 64 3e 0d 0a 3c 62    itle></head>..<b

00e0   6f 64 79 3e 0d 0a 20 20  20 20 3c 66 6f 72 6d 20    ody>..    <form

00f0   6e 61 6d 65 3d 22 66 6f  72 6d 31 22 20 6d 65 74    name="form1" met

0100   68 6f 64 3d 22 70 6f 73  74 22 20 61 63 74 69 6f    hod="post" actio

0110   6e 3d 22 44 65 66 61 75  6c 74 2e 61 73 70 78 22    n="Default.aspx"

0120   20 69 64 3d 22 66 6f 72  6d 31 22 3e 0d 0a 3c 64     id="form1">..<d

0130   69 76 3e 0d 0a 3c 69 6e  70 75 74 20 74 79 70 65    iv>..<input type

0140   3d 22 68 69 64 64 65 6e  22 20 6e 61 6d 65 3d 22    ="hidden" name="

0150   5f 5f 56 49 45 57 53 54  41 54 45 22 20 69 64 3d    __VIEWSTATE" id=

0160   22 5f 5f 56 49 45 57 53  54 41 54 45 22 20 76 61    "__VIEWSTATE" va

0170   6c 75 65 3d 22 2f 77 45  50 44 77 55 4a 4e 7a 67    lue="/wEPDwUJNzg

0180   7a 4e 44 4d 77 4e 54 4d  7a 5a 47 52 75 72 71 57    zNDMwNTMzZGRurqW

0190   77 4d 76 39 2b 38 5a 78  6c 47 43 38 62 64 67 34    wMv9+8ZxlGC8bdg4

01a0   41 4a 65 6e 35 76 41 3d  3d 22 20 2f 3e 0d 0a 3c    AJen5vA==" />..<

01b0   2f 64 69 76 3e 0d 0a 0d  0a 20 20 20 20 3c 64 69    /div>....    <di

01c0   76 3e 0d 0a 20 20 20 20  20 20 20 20 54 68 69 73    v>..        This

01d0   20 69 73 20 61 20 74 65  73 74 2e 0d 0a 20 20 20     is a test...  
01e0   20 3c 2f 64 69 76 3e 0d  0a 20 20 20 20 3c 2f 66     </div>..    </f

01f0   6f 72 6d 3e 0d 0a 3c 2f  62 6f 64 79 3e 0d 0a 3c    orm>..</body>..<

0200   2f 68 74 6d 6c 3e 0d 0a                              /html>..

Some notes about using this code:

  • It's not meant to be used in production code.  It doesn't even attempt to synchronize file I/O.
  • It *can* be used as a starter if, for instance, you wanted to compress HTTP traffic.  (This is the same method the Blowery compression module takes).
  • You need to add an HTTP Module declaration to your web.config (see below).
  • Facebook is difficult to debug.

Setting up your Web.config file

Remember that you need to add the <httpModules> section to your web.config file.  If you don't already have one, this will suffice:

   1: <httpModules>
   2:     <add name="Filter" type="SnifferHttpModule, __code"/>
   3: </httpModules>

This assumes that the module is being added from App_Code.

Complete source code can be downloaded here.  Totally free, except for DataFormatter - that's licensed under a BSD-ish license. :-)