mikeobrien.net Curriculum Vitae Blog Labs
Wednesday, October 21, 2009

I just changed the domain of my site and I wanted to create a page notifying that the site has moved and then auto redirect them after a few seconds. Here’s how I did it:

1) I’m using IIS 7.5 so I opted to use the IIS7 RewriteModule. I simply ran the installer and the applet appears under the site options.

2) I created a catch all site in IIS and bound it to all the old public domains:

image

3) Then created a UrlRewrite Rule to rewrite all requests to /Default.aspx (Except ones specifically to /Default.aspx):

image

4) Next created the redirection page (Stripped down for clarity):

<%@ Page Language="C#" %>
<%
string redirectUrl = string.Format("{0}://{1}{2}{3}", 
    Request.Url.Scheme,
    Request.Url.Host.Replace("mikeobrien.net", "mikeobrien.net"),  
    (Request.Url.Port != 80 ? ":" + Request.Url.Port : string.Empty), 
    Request.Headers["X-Original-URL"]);
%>
<html>
<head>
    <title>We've Moved</title>
    <script language="javascript">
        function RedirectPage(url, seconds)
        {
            self.setTimeout('self.location.href = \'' + url + '\';', seconds * 1000);
        }

        function CountDown(seconds, elementId)
        {
            if (seconds == 0) return;
            document.getElementById(elementId).innerHTML = seconds;
            self.setTimeout('CountDown(' + (seconds - 1) + ', \'' + elementId + '\');', 1000);
        }
    </script>
</head>
<body onLoad="RedirectPage('<% = redirectUrl %>', 5);CountDown(5, 'timeLeft');">
    <h3>We've moved!</h3>

    <p>
    The page you requested can now be found <a href="<% = redirectUrl %>">here</a>. 
    You will be redirected in <span id="timeLeft">5</span> seconds.
    </p>
</body>
</html>

5) And voila!

image

C# | IIS 7 | IIS7.5 | JavaScript
Wednesday, October 21, 2009 1:23:22 AM (GMT Daylight Time, UTC+01:00)  #   |  Comments [1]  |  Trackback
Friday, April 03, 2009

I'm doing due diligence on our new SOAP API and seeing what platforms can access it and also get a feel for how it is to use our API from different platforms. Unfortunately there are still a lot of Classic ASP developers/companies out there so I'm trying to see if they can use our SOAP interface or it they will have to just use our REST interface (Not really a bad thing actually, REST rocks, but sometimes clients want to use a specific technology). The only COM SOAP library I could find that supports SOAP 1.2 (Which is what our services are speaking) is PocketSOAP. Although I ran into a weird problem using it in Classic ASP on IIS7. When I would hit a service running over SSL I would get the following error:

Pocket.HTTP.1 error '80070005'

Error opening CertificateStore

/soap.asp, line 26

Struggled with this for a while and dug up a post from the PocketSOAP developer where he posts the actual source code that causes the error:

// Open the "MY" certificate store, which is where Internet Explorer
// stores its client
certificates.m_hMyCertStore = CertOpenSystemStore(0, _T("MY"));
if(!m_hMyCertStore)
    hr = AtlReportError(
        CLSID_CoPocketHTTP, OLESTR("Error opening CertificateStore"),
        IID_NULL, HRESULT_FROM_WIN32(GetLastError()));
else
    m_SslInitDone = true;

Classic ASP automatically impersonates, so if you enabled anonymous access in IIS7 the anonymous user defaults to the new built in account IUSR. If you used this default, authorization will be done under IUSR but since Classic ASP always impersonates, the code will also execute as IUSR (Whereas .NET does not default to impersonation and in this scenario authorization will be done as the IUSR and the code will execute as the app pool identity which is by default NetworkService). It doesn't appear that a user profile is ever loaded for the IUSR account so the call above is actually accessing the Default user "My" cert store (See below). The IUSR account doesn't have access to this so it fails. So there are a couple of ways to get around this; one is setting the anonymous account to be either the same as the app pool identity or a custom anon user account. In this case the user profile is loaded and the call above succeeds since the "My" cert store is in the user profile which gets loaded and the account has access to it (BTW, the loading of the app pool identity user profile can be toggled so it would need to be on, which is the default). The second way would be to give the IUSR account read/write/delete access the Default user profile cert store, but this doesn't seem like a very good idea to me since this is used as a template for new user profiles.

image

Hope this saves someone some time!

IIS 7 | SOAP | SSL | Web Services
Friday, April 03, 2009 11:28:10 PM (GMT Daylight Time, UTC+01:00)  #   |  Comments [1]  |  Trackback
Tuesday, January 20, 2009

A few notes about identities from the standpoint of ASP.NET:

WindowsIdentity.GetCurrent() - This WindowsIdentity represents the OS thread identity or more specifically an account token (Not to be confused with Thread.CurrentPrincipal.Identity which is just a simple container for your convenience). This token represents a LSA (Local Security Authority) or Active Directory account. This will always be the process identity set in the App Pool configuration (AKA the App Pool identity) unless you are doing impersonation. This is the actual identity (Or Windows account token) that code runs as. As far as Windows Security is concerned this is the only identity that matters. The only way to "change" this is to do impersonation which is done on a thread by thread basis and should be reverted ASAP to the original identity to avoid a security hole (And resource leak because of unclosed handles). New threads always inherit the process token regardless of if the creating thread is impersonating another user (Something to remember when doing async calls in ASP.NET while impersonating).

Thread.CurrentPrinciple.Identity & HttpContext.Current.User.Identity - These are set by ASP.NET during the authentication phase and will either be...

  1. ...an Anonymous WindowsIdentity when doing just anonymous auth
  2. ...a GenericIdentity when doing forms auth (Which implies anon auth).
  3. ...a custom identity when doing custom auth (Which implies anon auth).
  4. ...a WindowsIdentity representing the authenticating user when doing any other types of auth such as Basic, Windows or Challenge-Response. These two properties actually point to the same instance of the identity. This will be the same as the OS thread only when you are doing impersonation.

Request.LogonUserIdentity - This is a WindowsIdentity representing the authenticating user, regardless of the authentication type. This will be the same as the OS thread only when you are doing impersonation. It will be the same as Thread.CurrentPrinciple.Identity & HttpContext.Current.User.Identity only when you are not doing anonymous authentication.

Here is a listing of the identities set by IIS7 auth in a number of configurations. They remained the same in both integrated and classic pipeline modes.

Anonymous (Specific User, which happens to be IUSR)

Source Type Return Type Authentication Type Identity Name
WindowsIdentity.GetCurrent() WindowsIdentity System.Security.Principal.WindowsIdentity Negotiate NT AUTHORITY\NETWORK SERVICE
Thread.CurrentPrincipal.Identity IIdentity System.Security.Principal.WindowsIdentity    
HttpContext.Current.User.Identity IIdentity System.Security.Principal.WindowsIdentity    
Request.LogonUserIdentity WindowsIdentity System.Security.Principal.WindowsIdentity   NT AUTHORITY\IUSR

Anonymous (Specific User, which happens to be IUSR), Impersonation

Source Type Return Type Authentication Type Identity Name
WindowsIdentity.GetCurrent() WindowsIdentity System.Security.Principal.WindowsIdentity Negotiate NT AUTHORITY\IUSR
Thread.CurrentPrincipal.Identity IIdentity System.Security.Principal.WindowsIdentity    
HttpContext.Current.User.Identity IIdentity System.Security.Principal.WindowsIdentity    
Request.LogonUserIdentity WindowsIdentity System.Security.Principal.WindowsIdentity   NT AUTHORITY\IUSR

Anonymous (App Pool Identity, which happens to be NETWORK SERVICE)

Source Type Return Type Authentication Type Identity Name
WindowsIdentity.GetCurrent() WindowsIdentity System.Security.Principal.WindowsIdentity Negotiate NT AUTHORITY\NETWORK SERVICE
Thread.CurrentPrincipal.Identity IIdentity System.Security.Principal.WindowsIdentity    
HttpContext.Current.User.Identity IIdentity System.Security.Principal.WindowsIdentity    
Request.LogonUserIdentity WindowsIdentity System.Security.Principal.WindowsIdentity   NT AUTHORITY\NETWORK SERVICE

Anonymous (App Pool Identity, which happens to be NETWORK SERVICE), Impersonation

Source Type Return Type Authentication Type Identity Name
WindowsIdentity.GetCurrent() WindowsIdentity System.Security.Principal.WindowsIdentity Negotiate NT AUTHORITY\NETWORK SERVICE
Thread.CurrentPrincipal.Identity IIdentity System.Security.Principal.WindowsIdentity    
HttpContext.Current.User.Identity IIdentity System.Security.Principal.WindowsIdentity    
Request.LogonUserIdentity WindowsIdentity System.Security.Principal.WindowsIdentity   NT AUTHORITY\NETWORK SERVICE

Anonymous, Physical Path Credentials, LSA User

Source Type Return Type Authentication Type Identity Name
WindowsIdentity.GetCurrent() WindowsIdentity System.Security.Principal.WindowsIdentity Negotiate NT AUTHORITY\NETWORK SERVICE
Thread.CurrentPrincipal.Identity IIdentity System.Security.Principal.WindowsIdentity    
HttpContext.Current.User.Identity IIdentity System.Security.Principal.WindowsIdentity    
Request.LogonUserIdentity WindowsIdentity System.Security.Principal.WindowsIdentity   HOST\username

Anonymous, Impersonation, Physical Path Credentials, LSA User

Source Type Return Type Authentication Type Identity Name
WindowsIdentity.GetCurrent() WindowsIdentity System.Security.Principal.WindowsIdentity NTLM HOST\username
Thread.CurrentPrincipal.Identity IIdentity System.Security.Principal.WindowsIdentity    
HttpContext.Current.User.Identity IIdentity System.Security.Principal.WindowsIdentity    
Request.LogonUserIdentity WindowsIdentity System.Security.Principal.WindowsIdentity   HOST\username

Basic, LSA User (Same for AD user)

Source Type Return Type Authentication Type Identity Name
WindowsIdentity.GetCurrent() WindowsIdentity System.Security.Principal.WindowsIdentity Negotiate NT AUTHORITY\NETWORK SERVICE
Thread.CurrentPrincipal.Identity IIdentity System.Security.Principal.WindowsIdentity Basic HOST\username
HttpContext.Current.User.Identity IIdentity System.Security.Principal.WindowsIdentity Basic HOST\username
Request.LogonUserIdentity WindowsIdentity System.Security.Principal.WindowsIdentity Basic HOST\username

Impersonation, Basic Auth, and LSA User (Classic Pipeline Mode or Integrated Pipeline and validateIntegratedModeConfiguration=false)

Source Type Return Type Authentication Type Identity Name
WindowsIdentity.GetCurrent() WindowsIdentity System.Security.Principal.WindowsIdentity NTLM HOST\username
Thread.CurrentPrincipal.Identity IIdentity System.Security.Principal.WindowsIdentity Basic HOST\username
HttpContext.Current.User.Identity IIdentity System.Security.Principal.WindowsIdentity Basic HOST\username
Request.LogonUserIdentity WindowsIdentity System.Security.Principal.WindowsIdentity Basic HOST\username

Impersonation, Basic Auth, and AD User (Classic Pipeline Mode or Integrated Pipeline and validateIntegratedModeConfiguration=false)

Source Type Return Type Authentication Type Identity Name
WindowsIdentity.GetCurrent() WindowsIdentity System.Security.Principal.WindowsIdentity Kerberos DOMAIN\username
Thread.CurrentPrincipal.Identity IIdentity System.Security.Principal.WindowsIdentity Basic DOMAIN\username
HttpContext.Current.User.Identity IIdentity System.Security.Principal.WindowsIdentity Basic DOMAIN\username
Request.LogonUserIdentity WindowsIdentity System.Security.Principal.WindowsIdentity Basic DOMAIN\username

Forms, Anonymous Auth

Source Type Return Type Authentication Type Identity Name
WindowsIdentity.GetCurrent() WindowsIdentity System.Security.Principal.WindowsIdentity Negotiate NT AUTHORITY\NETWORK SERVICE
Thread.CurrentPrincipal.Identity IIdentity System.Security.Principal.GenericIdentity    
HttpContext.Current.User.Identity IIdentity System.Security.Principal.GenericIdentity    
Request.LogonUserIdentity WindowsIdentity System.Security.Principal.WindowsIdentity   NT AUTHORITY\IUSR
 
Windows, LSA User (Same for AD user)
 
Source Type Return Type Authentication Type Identity Name
WindowsIdentity.GetCurrent() WindowsIdentity System.Security.Principal.WindowsIdentity Negotiate NT AUTHORITY\NETWORK SERVICE
Thread.CurrentPrincipal.Identity IIdentity System.Security.Principal.WindowsIdentity Negotiate HOST\username
HttpContext.Current.User.Identity IIdentity System.Security.Principal.WindowsIdentity Negotiate HOST\username
Request.LogonUserIdentity WindowsIdentity System.Security.Principal.WindowsIdentity Negotiate HOST\username

Impersonation, Windows Auth, and LSA User (Classic Pipeline Mode or Integrated Pipeline and validateIntegratedModeConfiguration=false)

Source Type Return Type Authentication Type Identity Name
WindowsIdentity.GetCurrent() WindowsIdentity System.Security.Principal.WindowsIdentity NTLM HOST\username
Thread.CurrentPrincipal.Identity IIdentity System.Security.Principal.WindowsIdentity Negotiate HOST\username
HttpContext.Current.User.Identity IIdentity System.Security.Principal.WindowsIdentity Negotiate HOST\username
Request.LogonUserIdentity WindowsIdentity System.Security.Principal.WindowsIdentity Negotiate HOST\username

Impersonation, Windows Auth, and AD User (Classic Pipeline Mode or Integrated Pipeline and validateIntegratedModeConfiguration=false)

Source Type Return Type Authentication Type Identity Name
WindowsIdentity.GetCurrent() WindowsIdentity System.Security.Principal.WindowsIdentity Kerberos DOMAIN\username
Thread.CurrentPrincipal.Identity IIdentity System.Security.Principal.WindowsIdentity Negotiate DOMAIN\username
HttpContext.Current.User.Identity IIdentity System.Security.Principal.WindowsIdentity Negotiate DOMAIN\username
Request.LogonUserIdentity WindowsIdentity System.Security.Principal.WindowsIdentity Negotiate DOMAIN\username

.NET | IIS 7
Tuesday, January 20, 2009 8:46:56 PM (GMT Standard Time, UTC+00:00)  #   |  Comments [2]  |  Trackback
Wednesday, October 22, 2008

I have been having XmlTraceListener woes... None of which are showstoppers just major time wasters! The following bug is fixed as of version 4.1! The first of which has to do the message. The message is not escaped and/or CDATA qualified. So if you have an ampersand or greater/less than sign in the message it will throw this cryptic error:

An error occurred while parsing EntityName. Line x, position y.

I ended up manually running the log entry object through the Format() method on the XmlLogFormatter class to get the raw xml and see what exactly it was complaining about. Sure enough there was an ampersand. Adding an extension method to the string class and manually escaping the LogEntry message alleviated this to some degree. Bad thing is though, the message is escaped for all listeners and you probably don't want to see HTML entities in your event log entry or email message. I think the only way around this would be to write your own xml trace listener.

namespace MyApp.Runtime.Extensions
{
    public static class String
    {
        public static string EscapeUnsafeXmlCharacters(this string value)
        {
            return value.Replace("<", "&lt;").Replace(">", "&gt;").Replace("&", "&amp;");
        }
    }
}

Issue #2 has to do with where the log file is saved. The FlatFileTraceListner actually saves the log file relative to the application root (Where the .config file is). Take a look at the RootFileNameAndEnsureTargetFolderExists() method in the FormattedTextWriterTraceListener class (The FlatFileTraceListener's base class). Before it passes the filename to it's base class constructor, it runs it through this method. The XmlTraceListener on the other hand does not follow this pattern which had me going nuts trying to figure out what I was doing wrong (Thinking that it just wasn't saving at all). I wasn't getting any errors from the XmlTraceListener (More on this below) so I started to get the feeling that perhaps it was writing to the log but just not where I expected. So I fired up Process Monitor and didn't see any log writes. Then I switched the app (It was a web application project BTW) to use the built in web server instead of IIS. Ran everything again and voilà:

image

So it was saving it somewhere else, but only when running under the built in web server. Looking at the code in reflector you can see that, unlike the FormattedTextWriterTraceListener, the XmlWriterTraceListener does not modify the path to be the application root before it passes it to it's base class constructor. So the directory ends up being that of the entry assembly. Bottom line is you have to supply an explicit path when you're using the XmlTraceListener in a web application.

This leads me to the third oddity; where it would save running under the builtin web server but not IIS. The base class, TextWriterTraceListener, calls the EnsureWriter() method (Shown below) before writing to the file. If it returns true it writes the entry, otherwise it doesn't. Notice that if there is a UnauthorizedAccessException it just returns false and then subsequently, in the calling Write method, silently skips writing. When I was using IIS as my web server (And the code was running as the Network Service account) I didn't see any exceptions and no log writes were showing up in Process Monitor. Which was very confusing! But when I switched over to using the built in web server (Which was obviously running as my interactive account) I saw the log writes. So when it was running under the Network Service account it obviously did not have permissions to save the log file to wherever it was trying to save it and no exception was raised... Very confusing behavior this. Reminds me of the advice in Jeffrey Richter's CLR Via C# book not to ever swallow exceptions! So keep that in mind, if you don't see any errors and log writes with the XmlTraceListner, it may be permissions and/or path related.

Well, hopefully this wasn't too long winded and hopefully it clears up some oddities with the XmlTraceListener.

public class TextWriterTraceListener : TraceListener
{
    public override void Write(string message)
    {
        if (this.EnsureWriter())
        {
            if (base.NeedIndent)
            {
                this.WriteIndent();
            }
            this.writer.Write(message);
        }
    }
    internal bool EnsureWriter()
    {
        bool flag = true;
        if (this.writer == null)
        {
            flag = false;
            if (this.fileName == null)
            {
                return flag;
            }
            Encoding encodingWithFallback = GetEncodingWithFallback(new UTF8Encoding(false));
            string fullPath = Path.GetFullPath(this.fileName);
            string directoryName = Path.GetDirectoryName(fullPath);
            string fileName = Path.GetFileName(fullPath);
            for (int i = 0; i < 2; i++)
            {
                try
                {
                    this.writer = new StreamWriter(fullPath, true, encodingWithFallback, 0x1000);
                    flag = true;
                    break;
                }
                catch (IOException)
                {
                    fileName = Guid.NewGuid().ToString() + fileName;
                    fullPath = Path.Combine(directoryName, fileName);
                }
                catch (UnauthorizedAccessException)
                {
                    break;
                }
                catch (Exception)
                {
                    break;
                }
            }
            if (!flag)
            {
                this.fileName = null;
            }
        }
        return flag;
    }

    // Other members removed for brevity...
}
Wednesday, October 22, 2008 4:54:38 AM (GMT Daylight Time, UTC+01:00)  #   |  Comments [0]  |  Trackback
Thursday, September 04, 2008

Recently I had the need to setup multiple SSL enabled sites on my local machine for development. These sites all had the same root domain but differed by sub domain. Traditionally you need to have a certificate and an IP address per SSL binding because of a "chicken or the egg" problem resolving the host headers in an encrypted HTTP conversation. If you have multiple sites with a common root domain that require SSL you can get around this limitation by using a wildcard certificate for all those sites.

So first I setup mappings in my hosts file (<SystemRoot>\System32\drivers\etc\hosts) as follows:

# SomeSite Dev Mappings
127.0.0.1          www.dev.somesite.net
127.0.0.1     services.dev.somesite.net
127.0.0.1        admin.dev.somesite.net


Next I need to create a self signed wildcard certificate. If you tried the self signed cert "feature" in IIS7 you probably quickly discovered that it is pretty much worthless since you cannot define the common name (CN), it's automatically set to the host name (Why does MS have a habit of giving you a powerful, feature rich car that can only make right turns?). One way to get around this is to generate your self signed cert with a tool and add it to the local machine store. IIS6 ships with a util called selfssl but this requires you to install the IIS6 ResKit (See more about that here). While this works, it bothers me to install tools from a previous version of IIS to accomplish this. Shouldn't the newer version of IIS do more than it's predecessor? One other alternative I found on the internets is to use the certificate creation tool that ships with the .NET 2.0 SDK. For some reason this "feels" better than using a tool from IIS6, probably just a mental thing... Plus you probably already have the SDK installed and are using it.

First create the self signed issuer certificate which will be set as a root cert authority (Fill in the red items):

"C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin\makecert.exe" -n "CN=My Company Development Root CA,O=My Company,OU=Development,L=Wallkill,S=NY,C=US" -pe -ss Root -sr LocalMachine -sky exchange -m 120 -a sha1 -len 2048 -r

Next create a cert for your sites that is issued from this authority. You must specify the common name (CN=<IssuerName>) you entered above in the issuer name field below (-in <IssuerName>). Also I'm creating a wildcard certificate that will serve all sites with the dev.somesite.net root domain as this is a requirement to use host headers. If I add other sites in the future with a different subdomain I can choose this certificate and all is good. Specifying an asterisk as the subdomain will signify this (Fill in the red items):

"C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin\makecert.exe" -n "CN=*.dev.somesite.net" -pe -ss My -sr LocalMachine -sky exchange -m 120 -in "My Company Development Root CA" -is Root -ir LocalMachine -a sha1 -eku 1.3.6.1.5.5.7.3.1

You should now see this cert show up in the IIS manager on the "Server Certificates" page:

image

Now again, MS gets you part of the way there in the UI but not all the way. As in IIS6 (SP1+) you cannot specify a host header for SSL bindings in the IIS7 UI because of, as mentioned above, issues with resolving the host headers in an encrypted HTTP request. But since we are using a wildcard certificate these issues are moot and IIS can do it but we have to configure it through the command line with the new appcmd util. The following command must be executed on each site that requires SSL. This command will create the SSL binding and set the host header. Make sure you specify the correct site name and host header for each site (In red):

C:\Windows\System32\inetsrv\appcmd set site /site.name:MySite /+bindings.[protocol='https',bindingInformation='*:443:www.dev.somesite.net']

Next go to the site bindings and you'll now see an SSL binding with a host header defined (Before this field would be disabled for SSL). You will need to select the the wildcard certificate you created earlier in the cert drop down and save your changes.

image

.NET 2.0 | IIS 7 | SSL
Thursday, September 04, 2008 8:47:54 PM (GMT Daylight Time, UTC+01:00)  #   |  Comments [0]  |  Trackback
Creative Commons License