Export (0) Print
Expand All
15 out of 19 rated this helpful - Rate this topic

Launching No-Touch Deployment Applications with Command Line Arguments

 

Chris Sells
Microsoft Corporation

May 23, 2003

Summary: Chris Sells shares some custom code that you can use on the client- and server-side to create no-touch deployment Windows Forms applications. (11 printed pages)


Download the winforms05152003.msi sample file.

Easily one of the questions I get asked the most once folks start using no-touch deployment (NTD) applications, that is Windows Forms applications that can launched with an URL, is how they can pass command line arguments to them. Apparently folks want to provide links on their Web pages with different launch options or they want to generate URLs with command line arguments on the fly based on the current user's session. For an example of the latter, consider Figure 1.

Figure 1. A Web page with a link to an NTD application

Figure 1 shows a Web page that's clearly been personalized for the person surfing to it. The link on the Web page is to a NTD application that looks like Figure 2 when it's first run.

Figure 2. Login form for the NTD application

Notice that this application starts up pre-populated with the user's name and ID. The way I did this is to pass arguments using an URL formatted like so:

http://localhost/vacaplan/vacaplan.exe?uid=csells&uname=Chris%20Sells

When launching a managed application from the local hard drive, command line parameters are available from the string array passed to Main:

static void Main(string[] args) {
  // Get command line args
  string uid = "";
  string uname = "";
  foreach( string arg in args ) {
    string[] pair = arg.Split('=');
    switch( pair[0].ToLower() ) {
      case "uid":
        uid = pair[1];
        break;

      case "uname":
        uname = pair[1];
        break;
    }
  }
  ...
}

Unfortunately, the support for pulling command line arguments out of the launching URL is new to the Microsoft .NET Framework 1.1 and nearly undocumented. Also, since the launching URL is used to create the path to .config file, full support for command line arguments requires some code running on the server-side as well.

Note   There is a workaround to enable pulling command line arguments out of the launching URL that works in .NET 1.0, too, and it's covered later in this article.

Client-Size Support for NTD Arguments

To pull the arguments out of the launching URL first requires access to the launching URL from within the NTD application. For this, .NET 1.1 provides the APP_LAUNCH_URL data variable from the application domain:

// Only works for .NET 1.1+
AppDomain domain = AppDomain.CurrentDomain;
object obj = domain.GetData("APP_LAUNCH_URL");
string appLaunchUrl = (obj != null ? obj.ToString() : "");
MessageBox.Show(appLaunchUrl);

The URL used to launch the NTD application, including arguments, is provided in full from APP_LAUNCH_URL. Unfortunately, APP_LAUNCH_URL isn't available in .NET 1.0. However, since the path to a NTD application's .config file is just the URL (including arguments) with ".config" tacked onto the end, you can pull out the equivalent of the APP_LAUNCH_URL in .NET 1.0. For example, launching an application from:

http://foo/foo.exe?foo=bar@quux

Yields the following .config file path:

http://foo/foo.exe?foo=bar@quux.config

Since the application domain provides access to the .config file path, you can use that and a little string manipulation to get access to the NTD launching URL for both .NET 1.0 and .NET 1.1:

// Only works for .NET 1.1+
AppDomain domain = AppDomain.CurrentDomain;
object obj = domain.GetData("APP_LAUNCH_URL");
string appLaunchUrl = (obj != null ? obj.ToString() : "");

// Fall back for .NET 1.0
if( appLaunchUrl == string.Empty ) {
  const string ext = ".config";
  string configFile = domain.SetupInformation.ConfigurationFile;
  appLaunchUrl =
    configFile.Substring(0, configFile.Length - ext.Length);
}

Either way, both default intranet and Internet permissions for .NET 1.x allow access to command line argument information from partially trusted clients. Once you've got the full URL, it can be decoded and parsed for the arguments.

Security Considerations

Before diving into how to get at the arguments, I have to make a quick note about the security implications of handling command line arguments in an NTD application. When arguments are passed to an EXE already installed on the machine, that's done through the shell or the command line console by the user, so you can trust that things are likely to be okay. However, on the Web, users are tricked all the time into opening attachments in e-mail or clicking on links in the browser that are not in their best interest. For example, imagine HotMail as an NTD allowing the following arguments:

http://hotmail.com/edit.exe?uid=csells&forward=doctor@evil.com&ui=no

In this case, we're passing arguments specifying our intension to forward our e-mail to somebody else without any confirmation UI. If this link was embedded on a Web page and marked "Free Chocolate!" can you honestly say that you wouldn't be tempted to click it yourself? Make sure that you carefully evaluate the possibilities exposed by your command line argument combinations with an eye towards evil-doers before you unleash it onto the wild and wooly world of the Web.

Decoding and Parsing the URL

Once you're happy that you've got the security considerations well in hand, you'll need to parse the arguments from the URL itself. Sometimes you'll need to encode special characters into an URL to pass them, just as if you were passing them to a server-side piece of code. For example, to encode the space in the uname argument requires encoding the ASCII value of the space character with the %dd syntax:

http://localhost/vacaplan/vacaplan.exe?uid=csells&uname=Chris%20Sells

On the server-side, System.Web provides not one, but two classes that know how to decode an URL (HttpServerUtility and HttpUtility). Unfortunately, NTD clients from both the intranet or Internet zones are forbidden from using the UrlDecode method from either class. Luckily, a little creative reverse-engineering of the .NET Framework Class Library yields an UrlDecode that you can use (and is provided with the source of this article). Decoding the URL will expand things like %20 with their actual characters:

http://localhost/vacaplan/vacaplan.exe?uid=csells&uname=Chris Sells

Once an URL has been decoded, you're ready to pull the actual arguments out, using the question mark as the marker for the argument part of the URL and the ampersand as the marker between arguments. Unfortunately, while System.Web provides a class to do this (HttpValueCollection), it's used internally to implement query string parsing and isn't available by itself. Fortunately, it's not hard to reverse-engineer this string parsing functionality for our use, splitting out the arguments into key/value pairs, and the sample provides the code for that, too.

Abstract Command Line Arguments

In the sample, both the URL decoding and the query string parsing are bundled together into the WebCommandLineHelper class:

static void Main(string[] argsFromMain) {
  // Get command line args (either from Main or from launching URL)
  string uid = "";
  string uname = "";
  string[]
    args = WebCommandLineHelper.GetCommandLineArgs(argsFromMain);
  foreach( string arg in args ) {...}
  ...
}

The GetCommandLineArgs static method of the WebCommandLineHelper class takes as an input parameter the arguments that were retrieved from Main. Inside the GetCommandLineArgs method, the first thing that happens is a check to see if the application is launched from an URL or not:

static public bool LaunchedFromUrl {
  get {
    try {
      // Check if we have a site
      string  url = (string)AppDomain.CurrentDomain.GetData("APPBASE");
      System.Security.Policy.Site.CreateFromUrl(url);
      return true;
    }
    catch( ArgumentException ) {
      return false;
    }
  }
}

The appbase of a .NET assembly is where it's from; for example, a spot on the hard drive or an URL. If a site can be extracted from the appbase, the application was launched from an URL. If the application wasn't launched from an URL, GetCommandLineArgs just returns the arguments passed in that came from Main:

static public string[] GetCommandLineArgs(string[] argsFromMain) {
  if( !LaunchedFromUrl ) return argsFromMain;
  ... // Decode and parse URL for arguments
}

In this way, the application doesn't really care about where the arguments come from. All that matters is the arguments themselves. The WebCommandLineHelper unifies the two sources of arguments for your convenience.

Server-Side Support for NTD Arguments

Unfortunately, the client-side is not all there is to pulling handling command line arguments for NTD applications. Since the URL, including arguments, is used to produce the path to the .config file, if you want to serve up a .config file, you need some server-side code to translate requests for .config files formed like this:

http://foo/foo.exe?foo=bar@quux.config

Into requests like this, with the arguments stripped away:

 
http://foo/foo.exe.config

When IIS sees foo.exe?blah.config, it maps this to a request for a .exe file, not a .config file. That means that various parts of .NET that look for .config files, like the assembly resolution process, the custom configuration reader, and Web services will all be handed the .exe bits when they request the .config bits. While the former two stacks will fail silently as if the .config file is missing, the .NET Web services stack will throw an exception when it gets the .exe bits, even if there's no .config file to serve up in the first place. Clearly, this isn't what we're after.

Letting ASP.NET Handle .EXE Files

Serving up the appropriate .config file in the presence of URL arguments is a multi-step process. Step one is to hand off requests for .exe files to ASP.NET so that you can hook up some custom code to requests for .exe files. By default, all Web applications are configured to hand out .exe files directly. To map the .exe extension to ASP.NET, use the IIS configuration tool to adjust the Configuration of your Web application, as shown in Figure 3.

Figure 3. Mapping .exe files to ASP.NET in IIS

Handling .EXE Files

Once ASP.NET is handling .exe files, you can add an HTTP handler, which is how ASP.NET lets you write custom code to handle a request. A handler is simply an implementation of IHttpHandler:

public class ConfigFileHandler : IHttpHandler {
    // Just the .exe part in the file system
    string path = context.Request.PhysicalPath;

    // The entire request URL, include args and .config
    string url = context.Request.RawUrl;

    // If someone's asking for a .config, strip the arguments
    string ext = ".config";
    if( url.ToLower().EndsWith(ext) ) {
      context.Response.WriteFile(path + ext);
    }
    // If someone's asking for the .exe, send it
    else {
      context.Response.ContentType = "application/octet-stream";
      context.Response.WriteFile(path);
    }
  }

  public bool IsReusable {
    get { return true; }
  }
}

The IHttpHandler interface only has a single interesting method—ProcessRequest. This ProcessRequest method uses some context that a handler gets, like the physical path to the file being request by the client and the raw URL of that same request, and checks if the URL ends with ".config". If it does, we compose a physical path of the .exe file being requested (remember, IIS and ASP.NET considers the .exe file as the file being requested, not the .config file), tack ".config." on the end and serve up the appropriate .config file.

Because we've changed the mapping for .exe files to ASP.NET, this handler will be responsible for handing out both .exe files and .exe?blah.config files. If someone is requesting the .exe file, we set the MIME type appropriate and serve that file up.

In either case, if the file being requested is missing, we'll serve up a 404 error, which .NET can deal with.

Mapping .EXE Files to the Handler

Once you've mapped .EXE files to ASP.NET and implemented a .NET handler to serve up .exe and .exe?blah.config files, you still need to let ASP.NET know which handler to use for .exe files. You do this in the web.config files for your ASP.NET application:

<configuration>
  <system.web>
    <httpHandlers>
      <!-- map .exe and .exe?blah.config files to our handler -->
      <add verb="*" path="*.exe"
           type="Genghis.Web.ConfigFileHandler, ConfigHandler" />
    </httpHandlers>
 </system.web>
</configuration>

When ASP.NET sees this <add> element in the <httpHandlers> section, it'll map all HTTP for paths ending in .exe to the ConfigHandler assembly and the Genghis.Web.ConfigFileHandler class (the IHttpHandler implementation shown previously). ASP.NET will look for the web.config file in the root of the Web application and for the assembly in the bin directory at the root of Web application, so be sure to put the files in the correct place.

Letting IIS and ASP.NET Hand Out .config Files

After routing requests for .exe files from IIS to ASP.NET, writing the handler to pass back .config files, routing ASP.NET requests for .exe files to that handler, there remains two additional configuration steps. The first is because IIS doesn't allow .config files to go out the door unless anonymous access is enabled. You'll need to check the Anonymous access option in the Authentication Methods dialog of your IIS Web application's Directory Security, as shown in Figure 4.

Figure 4. Enabling anonymous access to a Web application in IIS to allow .config files to be served

And, while ASP.NET is properly letting us hand out .exe files and .exe?args.config files, by default, we're not allowed to hand out .config files. In other words, of the following four URL types, we're only handing out the first three with the current configuration:

1. http://foo/foo.exe?foo=bar@quux
2. http://foo/foo.exe?foo=bar@quux.config
3. http://foo/foo.exe
4. http://foo/foo.exe.config

The first three URL types show up in our handler. The fourth URL type is important if you launch your NTD application without any URL arguments, but still need the .config file. The reason that ASP.NET doesn't hand out *.config files by default is because you could be keeping all kinds of sensitive information in your web.config file. To enable *.config files to go out but still keep your web.config files protected, add the following entries:

<configuration>
  <system.web>
    <httpHandlers>
      <!-- map .exe and .exe?blah.config files to our handler -->
      <add verb="*" path="*.exe"
           type="Genghis.Web.ConfigFileHandler, ConfigHandler" />
      
      <!-- allow .config files but disable web.config files -->
      <remove verb="*" path="*.config" />
      <add verb="*" path="web.config"
           type="System.Web.HttpForbiddenHandler"/>
    </httpHandlers>
 </system.web>
</configuration>

In this case, ASP.NET has .config files mapped to the HttpForbiddenHandler, which ensures that they're not handed back to the client. We remove that handler mapping so that .config files are allowed, but then add it back again for just web.config files so that those remain protected.

Where Are We

It's probably pretty clear by now that while the .NET Framework 1.1 provides access to the launching URL of a NTD application, there's no real support built in to handle URL arguments when launching NTD applications. However, with some creative coding, you can still do it:

  • On the client, use APP_LAUNCH_URL or ConfigurationFile to get the launching URL, decoding and parsing it as necessary.
  • If you're using ASP.NET on the server:
    1. Configure IIS to route requests for .exe files to ASP.NET.
    2. Implement a handler to serve up .exe and .exe?blah.config files.
    3. Map the handler to .exe files for your ASP.NET application.
    4. Configure IIS for anonymous access to allow .config files to be served.
    5. Remove the ASP.NET handler mapping that forbids all .config files.
    6. Add the ASP.NET handler to forbid web.config files.

So, while arguments aren't really supported as thoroughly as we'd like, .NET is flexible enough that with some cleverness on the client side and on the server side, you can absolutely pass arguments to NTD applications anyway.

References

Note   Some of this material is adapted from the forthcoming Addison-Wesley title Windows Forms Programming in C# by Chris Sells (0321116208).

Chris Sells is a Content Strategist for MSDN Online, currently focused on Longhorn, Microsoft's next operating system. He's written several books, including Mastering Visual Studio .NET and Windows Forms for C# Programmers. In his free time, Chris hosts various conferences, directs the Genghis source-available project, plays with Rotor and, in general, makes a pest of himself in the blogsphere. More information about Chris, and his various projects, is available at http://www.sellsbrothers.com.

Show:
© 2014 Microsoft. All rights reserved.