Wednesday, April 16, 2008

I had the most frustrating dasBlog issue over the past couple of days:

I suddenly found myself unable to post anything on my blog.  This coincidentally happened at the exact same time my hosting provider had me try a web.config change to alleviate some session/viewstate issues I was having.  I would try to make a post, get no errors, and be returned to the front page and see that my post was missing.  Referral logs were strangely missing, and the dasBlog event log was also strangely empty.  The only thing I could get a vague error message from was trying to post a comment.

Apparently, my "Anonymous Asp.Net user" had suddenly hit its disk-space quota.  I don't know if this quota was created recently or not.  It seems odd indeed that dasBlog would cruise along and never report an error when it's out of disk space.  I wonder if this is something dasBlog is doing or something that's happening because of the hosting environment?

Wednesday, April 16, 2008 11:39:55 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Wednesday, April 02, 2008

Upgraded to DasBlog 2, tried my hand at modifying a theme.  It's Orange.

Wednesday, April 02, 2008 7:46:32 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Sunday, March 30, 2008

A small part in my decision to make the leap to Vista 64 was Scott Hanselman's findings upon building his home office and a new workstation therein: moving to 64bit was a non issue.  I wish I could report the same.

My first issue was with installation.  With certain hardware configurations Vista x64 will crash with 4gb of RAM installed until a hotfix is applied.  My understanding of the state of the 64bit world is this: the current 64bit processors are not entirely 64bit.  They have 64 bit registers and some 64 bit instructions.  To the average PC hotrodder or developer the preceived benefit is the ability to use 4gb of RAM instead of 2gb.  I installed the hotfix, got my 4gb of RAM working, and set about playing the Sweeny Todd soundtrack whilst I decided what order to install things in...

The drivers for my X-Fi sound card installed, I started Windows Media Player.  Windows Media Player crashes the first time I start it 100% of the time.  Vista Sp1 and a horde of updates has not fixed this.  Since the X-Fi is probably the most popular aftermarket sound card in the known universe at this time this ought to to work.  It may have nothing to do with sound drivers at all.  The 2nd or 3rd time I launch WMP it will work happily forever.

My graphics card is a PNY GeForce 8800GT series.  It clearly lables itself as having both 32 and 64 bit drivers on the driver disc.  I experienced an new Vista phenomenon with this product: installing the drivers using the OEM program declared that no changes were made to the system.  I pointed Vista at the .inf file on the disc for x64 and Vista basically said its own driver ("Standard VGA Port", yeah right) was better and refused to take the OEM drivers.  Only the Nvidia unified driver install would work.  Odd...

My next stage is to get my various messenger programs up and running so I can chat whilst I wait on things to intall.  MSN messenger is objectively the best messenging experience by far so that went worst, no issues there.  I have used Pidigin for AIM (most CarSpot folks and some friends are on AIM) since having crashtastic experiences with Trillian and an absolutely astounding clusterfuck with the actual AIM product ruining a Vista install; it would seem AOL is the reigning champ for the national heavyweight Invasive Install Championships.  Pidgin has similar behavior as WMP, it will crash and then run perfectly for as long as the machine is up.

My Blu-Ray drive was working fine as a Serial ATA DVD drive.  Folks, installations of things like Office 2007 are actually quite painless with something better than an IDE CD drive.  This alone is worth the extra expense.  Still, it came with software that can play Blu-Ray and there are nights (like tonight) where I'd like to watch a movie on my 1080p monitor while some long-running tasks scroll by on my ancient 19" CRT.  My first attempts at getting Blu-Ray playback working met with failure.  The install process (damn you Pioneer) demanded ridiculously old and specific versions of DirectX that were clearly not going to happen on Vista 64.  Some combination of Windows Update and SP1 magically fixed this, so I watched Blood Diamond on Blu-Ray while logs scrolled by tonight. 

My last two 64bit issues dealt with actual software development.  Visual Studio and the like installed and ran fine except for one issue so infuriating I have a dent in my forehead: copying an ASP.net solution from one machine to another suddenly started claiming that "{MyCustomThinger} RoleProvider has already been added", and commenting this part of Web.config out certainly allows the site to run.  As expected, putting this web.config with the role provider commented out on any other machine (including my development laptop and the two servers where the site actually lies) crashes because of course the configuration is incorrect.  I still can't explain this one.  The next issue was the most time consuming.

MySQL comes in 32 and 64 bit flavors for Windows.  I needed MySQL for one of my development efforts.  MySQL x64 does not like Vista x64.  In fact, the slightly out of date platform notes on their site claims Vista x64 is not supported, but there are enough success stories out there suggesting this is just a CYA that I gave it a whirl anyway.  MySQL 64bit would not run, claimin a "side by side configuration" was incorrect.  OK, knowing very litting about anything this sounds like some sort of thunking layer issue, so I'll try the 32bit version.  Same error.  What does Google say?  Google says that the Application Manifest in the server configuration process is broken for 64bit windows.  The MySQL forums claim that only a program called Reshack can fix this.  The problem with Reshack is that it runs on 32bit platforms only.  For those who don't know: the various Icons, embedded resources, and execution manifests for a Windows .exe get compiled into a special section of a windows PE and programs like Reshack can read/write this area without doing anything to the code itself.  I eventually found a Delphi program posted by a company in the UK that would work for me.  Having had some bad experience with libraries without a strong name that I am missing the code to recompile, I was glad MySQL doesn't attempt to use authenticode signatures with their releases.  I changed the Vista application manifest XML to the appropriate "requireAdministrator" and I was finally off and running.

The last issue I experienced this weekend was with some code I "inherited".  It uses Microsoft Jet ODBC to treat a file as a "data source", access the file rows using DataReader constructs, and sort the contents in a DataGrid.  This code bombed when I ran it.  Google tells me that Jet does not exist for x64.  I had planned on rewriting this code anyway but wasn't up to the task tonight, so I kept looking.  .NET programs are usually targetted to "any CPU" by default, but Google told me changing the target specifically to x86 would allow some extra Thunking Magic to happen, and as soon as I did this I was back on my way with Jet magically found now.

After these experiments, I'm definately going to test all of my production code to make sure I haven't done anything that won't work with 64bit editions of Windows or the .NET framework.  I also need to take it upon myself to do some research: what does 64bit do for me and for Joe Consumer besides 4gb of RAM on Vista?

.NET | ASP.NET | Rant
Sunday, March 30, 2008 1:17:26 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Tuesday, March 18, 2008

It would appear, at least on my machine, that I can add "No script debugging" to my list of IE8 complaints.  Despite making the necessary Internet Options changes I can't hit breakpoints in javascript in VS2008 any longer.  It's a beta browser and I don't really need this feature, but be warned.

Tuesday, March 18, 2008 9:19:01 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Thursday, December 13, 2007

A note to Asp.Net developers:

I cannot think of any valid reason why <div> tags opened in one UserControl should not also be closed in the same UserControl.  In fact, closing <div> tags from UserControl1.ascx in Default.aspx or in UserControl2.ascx makes markup extremely annoying to maintain. 

That is all.

Thursday, December 13, 2007 9:03:55 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Tuesday, September 25, 2007

ClickOnce is so close to greatness, as I’ve said before.  Yes, I know the “official word” is that this is not really meant for software ISVs deploying software to end users but it seems with a few extra things it could indeed be very good for this.  With some features in the Orcas Beta 2, I think ClickOnce is getting much closer to providing the plumbing I’ve been wanting in some cases and home rolling in others.  A new product I’m working on has some Architectural Desires.  I call them “desires” because we’ve been selling desktop applications without these architectural features for a while but I really want to meet some Service Level Requirements that will make my life easier, the customer’s life easier, and our support staff’s life easier.

Environment

·         I have an existing web system that users sign into.  Credentials are stored in an app-specific schema in a MySQL database.  Users of the current desktop application must be in this MySQL database and authenticate over web services.  Users of the new system increasingly do not need access to the web based system.  Putting their credentials in MySQL is polluting this system.

·         We have an OpenLDAP server we are slowing moving credential stores to.

·         We have a Windows2003 server that’s not doing much right now

·         We have a client application that ships with specialized hardware and an install disk.

·         Upgrades go out to customers via installing a new MSI

Requirements/Desires

1.       My software requires some specialized hardware.  I’d like to ship this without install disks.

2.       I’d like the desktop application to be able to have the Role information and authenticate against our existing credential stores.  I don’t want users to be able to run this within a very short time period if we turn them off.

3.       I’d like to stop polluting the existing web system credential store with users that should not ever be able to log into this system, however SOME users of the web based system will also need to be able to authenticate using the client application.

4.       I’d like to be able to keep random people from stealing software.

5.       I’d like to be able to keep random people from installing my software.

 

Solution in Code and UML

Let’s take the big red pill and see where it leads us.

ClickOnce - Part 1

I start with .NET 3.5 Beta 2, VS 2008 Beta 2, and my current environment.  Setting up ClickOnce deployment is easy enough, I set the application to check for updates before it runs, and verify that this works.  Oddly enough with Beta 2, 100% of my testers who have tried to click “Install” rather than installing .NET 3.5 and choosing “launch” have had the install .exe lock up.  Hopefully this gets fixed.

Client Application Services – Server Side

There is a wonderful bit of plumbing available now Called Client Application Services.  In order to use this you need an ASP.Net web site.  You need to add a reference to System.Web.Extensions, and configure your web.config such that the Role and Membership services are enabled.  With .NET 3.5 this is done like so:

      <system.web.extensions>

            <scripting>

                  <webServices>

                        <authenticationService enabled="true" requireSSL="false"/>                   

                        <roleService enabled="true"/>

Now, the Role and Membership providers I have configured are my LDAP based home rolled code.  Discussing these is outside the scope of this article; suffice to say they are straight up Provider implementations.  I will need to set up my Forms authentication a certain way to meet my single sign on goals:

            <authentication mode="Forms">

                  <forms domain=".office.carspot"

                           enableCrossAppRedirects="true"

                           cookieless="UseCookies"

                           slidingExpiration="false"

                           defaultUrl="Cookies.aspx"

                           path="/"

                           protection="Encryption"

                           timeout="1440"

                           name="FooBar"/>

            </authentication>

For testing I am allowing the clients to sign on once for a day.  The domain attribute is important, as the cookie created by my web site will now be passed to anysite.office.carspot.  There is one more thing to configure here.  At this point I intended that I would manually provide the Symmetric Key to Trusted Applications written in PERL or Python etc. to decrypt the data in the cookie.  I therefore manually created some keys for a MachineKey element.  This will still be needed when I cluster this solution.

      <system.web>

            <machineKey validationKey="FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"

            decryptionKey="FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"

            validation="SHA1" decryption="AES" />

Client Application Services – Client Side

With the server side set up, I return to my ClickOnce application.  There will be some work to do in the Properties of my app and some work to do in code.  The first step is to enable Client Application Services.

Only the root directory of the ASP.Net application is required.  This also requires references to System.Web and System.Web.Extensions in my Client application.  This is the first hole I’ve seen thus far in my argument that a Client-only .NET install should be available.  The advanced tab bears some discussion now and also a little later:

In my case I want to make sure they are authenticating to the server at regular intervals.  The Use custom connection string setting is the key to a later problem.  Interested readers may want to use reflector on System.Web.ClientServices.Providers.ClientData,  System.Web.ClientServices.Providers.ClientDataManager, and System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, as well as exploring the new System.IO.IsolatedStorage namespace, unless using Reflector on system assemblies is against some rule, in which case you should not and I never have done so.

Now I need to write some code and see if I can log in.  Note that back in the Services tab I included a Class and Assembly name for a credential provider type.  This tells the plumbing what method to invoke in order to request credentials from the users.  The class must implement IClientFormsAuthenticationCredentialsProvider, in my case this is a login window written in WPF.  Insofar as my research has gone so far, authenticating members and checking roles must be done explicitly.  The code to do so is simple:

bool valid = System.Web.Security.Membership.ValidateUser("", "");

This will cause the plumbing to invoke my Login window and give me an answer as to whether authentication was successful, and since Role management is enabled I should be able to get at my roles as well.  Now I’m going to set up a proxy and try to log in.  Here’s what Fiddler shows me:

 

You can see the AJAX/JSON requests going back and forth, and I’m showing the headers of the response in order to show you the persistant cookie.  The actual body of the response is just {“d”, true}.  Now I’m going to try to get this to authenticate against a Web service written in another language on a different server.

This Cookie is not for you

My thought was to use cookies with a common Domain in order to authenticate against my ASP.NET site via Client Services but have the information available in a usable form to other non-.NET systems.  This turned out to be a pipe dream at first.

·         Authenticating via Login.aspx returns a cookie as expected.  Cookies are turned off for HttpWebRequest by default.  Nothing I could find would get Windows/.NET to nicely populate cookies on my HttpWebRequest once I gave it a CookieContainer.  This is probably desired behavior.

·         Authenticating via Client Services does not create a cookie in the same way IE does anyway (more on this in a moment).

·         Any IE specific hacks I do are likely to be broken in Firefox, and supposedly ClickOnce will work with Firefox later this year.

I started by experimenting with the Use Custom Connection String setting, but realized this may be problematic in ClickOnce since the actual Application directory is not a clean or dependable Path.  When facing a dilemma like this I pull out my handy File System Watcher program and go to work. 

While logging in, this “User_damonpayne.clientdata” file created and changed looks promising, and it turns out to be.  I can already tell by calling Thread.CurrentPrincipal.Identity.Name that my server-side user name “damonpayne”, is indeed available on the Client.  Opening up this .clientdata file reveals the following:

<?xml version="1.0" encoding="utf-8"?>

<ClientData>

      <LastLoggedInUserName></LastLoggedInUserName>

      <LastLoggedInDateUtc>1C6E0B92F1DBE7E</LastLoggedInDateUtc>

      <PasswordHash></PasswordHash>

      <PasswordSalt></PasswordSalt>

      <Roles>

            <item>Default</item>

            <item>Technology</item>

            <item>ScrumMaster</item>

      </Roles>

      <RolesCachedDateUtc>1C7FF8AA84CDD3E</RolesCachedDateUtc>

      <SettingsNames></SettingsNames>

      <SettingsStoredAs></SettingsStoredAs>

      <SettingsValues></SettingsValues>

      <SettingsNeedReset>0</SettingsNeedReset>

      <SettingsCacheIsMoreFresh>0</SettingsCacheIsMoreFresh>

      <CookieNames>

            <item>09a630885b744552b25cf95da4f7e20f</item>

      </CookieNames>

      <CookieValues>

      <item>SoloAuth=9816CAC289A2ADBE7E88A0498E71733E0C480CAD631CB57460BF8AB9822747A43FE1ABE6F0D20DCAA95D8339FE8EC866C878A10A1A2BB188AE42B50DCB4C9862</item>

      </CookieValues>

</ClientData>

Those are indeed my roles, and that does look like my server cookie value.  How convenient.  Of course from the Client I can simply call System.Web.Security.Roles.GetRolesForUser() to get my roles, but remember that I sought to pass a token to someone else that would vouch for who I am.  I will now spare you some of the details of my research and skip to the punch line.  By converting my Hex string from <machinekey/> back to a byte array I was hoping to privately share this Symmetric Algorithm key with trusted systems and allow them to decrypt the cookie value.  Well, this didn’t work, and switching algorithms from AES to 3DES didn’t work, and changing the key sizes didn’t work.  Digging through FormsAuthentication through reflector I found the 1st issue: the data is not plain text but rather a binary serialized object.  Ok, so one could use BitConverter to pull out the desired values.  The real sticking point is that FormsAuthentication, when it creates a Symmetric Algorithm of the type specified by your <machineKey/> setting, creates a random Initialization Vector that is not available to you.  How this voodoo works in a clustered environment I don’t know, but this behavior is somewhat disappointing.   Community Server, for example, allows you to put the IV in the web.config as well since this is a very necessary part of the cryptographic process.  My hopes of sharing a symmetric key with a trusted application were killed, and I spent quite a bit of time on Google and USENET looking for a solution.  This was one of those situations where the obvious answer eluded me for a while because I had been too close to this problem for too many hours. 

I can still write code to dig out the cookie value:

        /// <summary>

        /// Assuming local storage, get the client data cookie for this application

        /// </summary>

        /// <returns></returns>

        public static string GetClientDataCookie()

        {

            string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);

            string ver = Assembly.GetCallingAssembly().GetName().Version.ToString();

            appData = Path.Combine(appData, @"CarSpot\CarSpot.Solo\" + ver);

 

            string userName = Thread.CurrentPrincipal.Identity.Name;

 

            string userFile = "User_" + userName + ".clientdata";

            string path = Path.Combine(appData, userFile);

            XmlDocument doc = new XmlDocument();

            doc.Load(path);

            string token = doc["ClientData"]["CookieValues"]["item"].InnerText;

            token = token.Substring(token.IndexOf("=") + 1);

            return token;

        }

… and I still have a public facing Web server that magically (in memory?) has access to the Initialization Vector:

        [WebMethod(Description="Attempt to decrypt a Forms auth ticket issued from this server farm")]

        [SoapDocumentMethod(ParameterStyle=SoapParameterStyle.Bare)]

        public ValidateClientTicketResponse ValidateClientTicket(string ticketString)

        {

            FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(ticketString);

            ValidateClientTicketResponse response = new ValidateClientTicketResponse();

            response.Expiration = ticket.Expiration;

            response.Name = ticket.Name;

            response.UserData = ticket.UserData;

            return response;

        }

… and of course, for trusted systems, I can secure this Endpoint however I please.  I can pass my Ticket from the client to anyone, who can call this service to determine if I am who I say I am and if this is a valid, unexpired ticket.

 

Solution Diagram

So, this is my environment with the solution in place.  Users who only need my Client Application and its accompanying Web Application are created in LDAP.  Users of the special Perl web site that also need to have access to my client are pushed to LDAP via a replication scheme.    My client application can pass it’s authentication ticket value as a SOAP header or an in-band argument when communicating with other trusted endpoints. 

Since I worked through this step by step, there may be a slightly cleaner way to do some of this.  I have been meaning to see if WS-Federation could be used, if only as a more standard API for the web service.

Final Thoughts

Of my original requirements, I have met most of them.  I can keep random people from stealing my code by deploying Obfuscated assemblies via ClickOnce.  I can keep people from running my application using the authentication, and with this method many customers will only have a single password to remember for all their CarSpot products.  I did not write about it here but the ASP.Net Profile information is available via this method as well.  This opens up the door for many unexpected niceness for our users; by implementing a new custom Profile provider, someone might log into one of the Perl web sites and change their display preferences, to discover them magically reflected in their client application as well. 

As for keeping random people from ever even installing my software to begin with, that will have to come later or Obfuscation will have to do.  It would be ideal to protect the .application file for my program so that unwanted visitors cannot even download the manifest in the first place.  Are you listening, Microsoft?  ClickOnce should be able to respond to an authorization challenge or SSL certificate warning in some fashion.   In our tests, Firefox still does not work for launching a ClickOnce application. Are you listening Microsoft?

Tuesday, September 25, 2007 11:57:32 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Thursday, September 20, 2007

The installation of .NET 3.5 beta 2 does not function out of the box with Windows Server 2003.  An error regarding unknown attribute xmlns="" is shown starting on line 67 in the machine level web.config.  Removing the 4-5 xmlns="" from the <add assembly/> lines seems to allow my web app to work but I don't know what the other consequences might be.

Thursday, September 20, 2007 1:15:49 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Wednesday, September 05, 2007

Suppose you are developing an ASP.Net application in a primarily open source/linux/MySQL environment, you might run into some odd issues with interoperability.  A top-secret project of mine (that I intend to open source once I've got a handle on which license is appropriate) needs minimally to work with MySQL and OpenLDAP.  MySQL is no problem as the builds of the MySQL connector for ADO.NET 2 have been very solid, though it does give me odd "warnings" about unnecessary conversions when using reader.GetInt32(0) and such. 

OpenLDAP, well, it seems decent enough for our purposes but there are some stumbling blocks.  The ActiveDirectory Membership and Role providers that come with .NET 2.0 will not work for you.  Oh, I know the connection string misleadingly takes LDAP://ou=blah blah;cn=blahblah;dnblahblah syntax but the implementation uses various AD specific calls so you have to home roll your own.  There are two decent solutions here: you can use the classes in System.DirectoryServices to implement the various searches.  I was concerned at first that this would not work under Mono, which is one thing I'm shooting for, but I should have done my homework as the Mono project does have a System.DirectoryServices implementation.  I also found some old Novell C# code hanging around that gives you some LDAP specific abstractions.  Either way, you can roll your own Role and Membership providers that talk to OpenLDAP.  OpenLDAP does not support the memberOf syntax, apparently, which is annoying.  You can search under the ou=Groups for objects of type posixGroup and get the memberUid attribute to sort of go at it backwards to implement the GetUsersForRole functionality for your Role provider:

public List<string> GetUsersForRoleName(string roleName)

{

List<string> userNames = new List<string>();

string searchPattern = "(&(objectclass=posixGroup)(cn={0}))";

string searchStr = string.Format(searchPattern, roleName);

LdapSearchResults sr = _ldapCon.Search("ou=Groups," + _rootDn, LdapConnection.SCOPE_SUB, searchStr, null, false);

while (sr.hasMore())

{

LdapEntry entry = sr.next();

LdapAttribute att = entry.getAttribute("memberUid");

string[] names = att.StringValueArray;

for (int i = 0; i < names.Length; ++i)

{

userNames.Add(names[i]);

}

}

return userNames;

}

 

I should surely post this project once it's mostly done, as it's potentially quite useful.

Wednesday, September 05, 2007 2:36:18 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Friday, May 18, 2007

I've had some issues with Service Unavailable messages the past several days.  This appears to be related to me creating an Application below the main Application at my site root in order to test something.  This should be supported behavior so I am working on this with my hosting provider.

Friday, May 18, 2007 8:50:51 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Thursday, April 19, 2007

We have a cool feature in our backend data hub we call Image Overlay.  Basically given images of a vehicle we can do all sorts of transformations including overlaying text ("Buy this car!"), overlaying images ("GM Certified logo") scaling and matte-ing (shrinking the input image down to have a single color margin to draw text in) and support for various geometric primitives.  Our implementation is one of the few in our industry and is probably far more complete, flexible, and better-er than the few competing implementations.  Our backend uses some PERL stuff for this and I wondered if a .NET engine would be as fast or faster and how it would scale, given that the claim has been made that C# is 1:1 performance with C++ and despite my love of C# I found this claim questionable.  So, supposing I have an input image which is assuredly not a car:

And a tiny screenshot of my most anticipated upcoming PS3 game, LAIR:

And I want to do a few operations in realtime in .NET:

  • create a Matte
  • scale the source image
  • Add some text
  • import the Dragon screenshot from LAIR
  • Time the operation

Here is the source code for entering some parameters, which could obviously come from some other source:

<div>

<h3>Source Image:</h3>

<asp:HyperLink ID="_exampleLnk" runat="server" Target="_blank" NavigateUrl="~/sample.jpg">

View source image

</asp:HyperLink>

<br />

Matte and overlay some text:

<asp:TextBox ID="_overlayTxt" runat="server" Text="go MSFT!"></asp:TextBox>

<br />

Import Img X <asp:TextBox runat="server" ID="_xTxt" Width="2em" Text="100"></asp:TextBox>

Import Img Y <asp:TextBox runat="server" ID="_yTxt" Width="2em" Text="100"></asp:TextBox>

<asp:Button ID="_submitBtn" runat="server" Text="Lay it over" OnClick="_submitBtn_Click" />

<br />

<asp:Label ID="_resultTimeLbl" runat="server"></asp:Label>

<br />

<asp:Image ID="_resultImg" runat="server" />

</div