Chris Sells
Microsoft Corporation
2003 年 5 月 23 日
摘要:Chris Sells 共享某些自定义代码,您可以将其用在客户端和服务器端中以创建自动部署 Windows 窗体应用程序。(本文还包含英文链接。请注意,在示例文件中,程序员的注释使用的是英文,本文中将其译为中文是为了便于读者理解。)
我最容易被问及的问题之一是在人们开始使用“自动部署 (NTD) 应用程序”时,即可以用 URL 启动的 Windows 窗体应用程序,如何将命令行参数传递给这些程序。很明显,用户想要在其 Web 页面上提供不同启动选项的链接,或者想要基于当前用户的会话,用命令行参数动态生成 URL。有关后者的示例,请仔细查看图 1。
图 1. 带有指向 NTD 应用程序的链接的 Web 页面
图 1 显示了一个 Web 页面,它明显地针对访问的用户进行了个性化。在 Web 页面上的链接指向一个 NTD 应用程序,该应用程序在第一次运行时类似于图 2。
图 2. NTD 应用程序登录窗体
注意,此应用程序启动时预填充了用户名和 ID。我执行此操作的方法是使用如下格式的 URL 传递参数:
http://localhost/vacaplan/vacaplan.exe?uid=csells&uname=Chris%20Sells
从本地硬盘驱动器中启动被管理的应用程序时,命令行参数可以从传递到 Main 的字符串数组中获得:
// 获得命令行参数
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;
}
}
}
遗憾的是,支持从启动 URL 拖出命令行参数对于 Microsoft .NET Framework 1.1 来说是全新的并且几乎没有说明。而且,因为启动 URL 被用来创建指向 .config 文件的路径,所以对命令行参数的完全支持还需要在服务器端上运行某些代码。
注意:在 NET 1.0 中可以使用一种替代方法将命令行参数拖出启动 URL,本文稍后将对其进行介绍。
NTD 参数的客户端支持
要将参数拖出启动 URL,首先需要从 NTD 应用程序内部访问启动 URL。为此,.NET 1.1 提供了来自应用程序域的 APP_LAUNCH_URL 数据变量:
AppDomain domain = AppDomain.CurrentDomain;
object obj = domain.GetData( " APP_LAUNCH_URL " );
string appLaunchUrl = (obj != null ? obj.ToString() : "" );
MessageBox.Show(appLaunchUrl);
从 APP_LAUNCH_URL 中可以提供完整的用来启动 NTD 应用程序(包括参数)的 URL。遗憾的是,APP_LAUNCH_URL 在 .NET 1.0 中不可用。但是,因为指向 NTD 应用程序的 .config 文件的路径就是将“.config”加在最后的 URL(包括参数),因此您可以在 .NET 1.0 中拖出与 APP_LAUNCH_URL 等效的参数。例如,从以下位置启动应用程序:
http://foo/foo.exe?foo=bar@quux
将产生以下 .config 文件路径:
http://foo/foo.exe?foo=bar@quux.config
因为应用程序域提供对 .config 文件路径的访问权限,所以对于 .NET 1.0 和 .NET 1.1 来说,您都可以使用此权限和少量字符串操纵获得对 NTD 启动 URL 的访问:
AppDomain domain = AppDomain.CurrentDomain;
object obj = domain.GetData( " APP_LAUNCH_URL " );
string appLaunchUrl = (obj != null ? obj.ToString() : "" );
// 回到 .NET 1.0
if ( appLaunchUrl == string .Empty ) {
const string ext = ".config";
string configFile = domain.SetupInformation.ConfigurationFile;
appLaunchUrl =
configFile.Substring(0, configFile.Length - ext.Length);
}
无论哪种方法,针对 .NET 1.x 的默认 Intranet 和 Internet访问权限都允许从被部分信任的客户端访问命令行参数信息。一旦您获得了完整的 URL,就可以用它来对参数进行解码和分析参数。
安全性考虑
在了解如何获得参数之前,我必须简要说明有关在 NTD 应用程序中处理命令行参数的安全性含义。当参数被传递到已经安装在计算机中的 EXE 时,此操作通过 shell 或命令行控制台完成,因此您可以信任此操作。但是,在 Web 上,用户随时会受骗打开电子邮件附件或在浏览器中单击对其不利的链接。例如,将 HotMail 设想为允许下列参数的 NTD:
http://hotmail.com/edit.exe?uid=csells&forward=doctor@evil.com&ui=no
在这种情况下,没有任何确认 UI,我们就将指定转发电子邮件意图的参数传递给了其他人。如果此链接被嵌入到 Web 页面中并标为“Free Chocolate!”,老实说您能不受诱惑自己不去单击此链接?在将参数发送到不受控制的、混乱的 Web 世界之前,请确保已仔细考虑命令行参数组合暴露的可能性并提防那些居心不良的人。
对 URL 进行编码和分析
可以控制安全性以后,您将需要分析 URL 本身的参数。有时您需要将特殊字符编码到传送这些字符的 URL 中,就象您将这些字符传递到服务器端的一段代码一样。例如,要对 uname
参数中的空格进行编码,需要用 %dd 语法对空格字符的 ASCII 值进行编码:
http://localhost/vacaplan/vacaplan.exe?uid=csells&uname=Chris%20Sells
在服务器端,System.Web 不是提供一个,而是提供两个用于对 URL 进行解码的类(HttpServerUtility 和 HttpUtility)。遗憾的是,Intranet 或 Internet 区域中的 NTD 客户端都不允许使用任何一类中的 UrlDecode 方法。幸运的是,对 .NET Framework 类库进行稍有创意的反向工程可以产生一个您可以使用的 UrlDecode(已随本文的源代码一起提供)。对 URL 进行解码后可将类似 %20 等字符还原为实际字符:
http://localhost/vacaplan/vacaplan.exe?uid=csells&uname=Chris Sells
一旦将 URL 解码,您就可以拖出实际参数,并使用问号作为 URL 参数部分的标记,& 作为参数之间的标记。遗憾的是,尽管 System.Web 提供了一个类以执行此操作 (HttpValueCollection),该类只能在内部使用以实现查询字符串分析,其本身并不可单独使用。幸运的是,对此字符串分析功能进行反向工程以使用此功能(即将参数拆分成关键字/值对)以供使用并不难,示例也此提供了该操作的代码。
抽取命令行参数
在示例中,URL 解码和查询字符串分析都被一起捆绑在 WebCommandLineHelper 类中:
// 获得命令行参数(从 Main 或从启动 URL)
string uid = "";
string uname = "";
string[]
args = WebCommandLineHelper.GetCommandLineArgs(argsFromMain);
foreach( string arg in args ) {}
}
WebCommandLineHelper 类的 GetCommandLineArgs 静态方法将从 Main 中检索到的参数作为输入参数。在 GetCommandLineArgs 方法内,首先检查应用程序是否从 URL 启动:
get {
try {
// 查看是否有站点
string url = (string)AppDomain.CurrentDomain.GetData("APPBASE");
System.Security.Policy.Site.CreateFromUrl(url);
return true;
}
catch( ArgumentException ) {
return false;
}
}
}
NET 程序集的 appbase 是其来自的位置;例如,硬盘驱动器中的某个位置或某个 URL。如果可以从 appbase 提取站点,则应用程序从 URL 启动。如果不是从 URL 启动应用程序,则 GetCommandLineArgs 只返回来自 Main 的传递到其本身的参数。
static public string[] GetCommandLineArgs(string[] argsFromMain) { if( !LaunchedFromUrl ) return argsFromMain; ... // 解码和分析参数的 URL }
通过这种方法,应用程序并不真正关心参数所来自何处。重要的是参数本身。为方便起见,WebCommandLineHelper 将合并参数的两个源。
对 NTD 参数的服务器端支持
遗憾的是,客户端不是简单的为 NTD 应用程序拖出和处理命令行参数。因为 URL(包括参数)用来生成指向 .config 文件的路径,所以如果您要为 .config 文件提供服务,您需要某些服务器端代码来为 .config 文件转译请求,格式如下:
http://foo/foo.exe?foo=bar@quux.config
在类此的请求中,参数已除去:
http://foo/foo.exe.config
IIS 查看 foo.exe.blah.config 时,它将此映射为对 .exe 文件的请求,而不是 .config 文件。这意味着查找 .config 文件的 .NET 的各部分(例如程序集解析进程、自定义配置读取器和 Web 服务),请求 .config 位时将都被传递 .exe 位。尽管前两个堆栈将无提示地失败,就好像缺少 .config 文件一样,.NET Web 服务堆栈在其获得 .exe 位时将会引发异常,即使最初并没有要服务的 .config 文件。很明显,这不是我们所要的。
让 ASP.NET 处理 .EXE 文件
在存在 URL 参数的情况下服务相应的 .config 文件是一个多步骤过程。步骤一是将对 .exe 文件的请求转交给 ASP.NET,以便您可以阻止某些自定义代码对 .exe 文件的请求。默认情况下,所有 Web 应用程序都配置为直接分发 .exe 文件。要将 .exe 扩展名映射到 ASP.NET,请使用 IIS 配置工具调整您的 Web 应用程序的配置,如图 3 所示。
图 3. 将 .exe 文件映射到 IIS 中的 ASP.NET
处理 .EXE 文件
一旦 ASP.NET 处理 .exe 文件,您就可以添加一个 HTTP 处理程序,它关于 ASP.NET 如何允许您写入自定义代码以处理请求。一个处理程序只是 IHttpHandler 的一个实现:
// 只是文件系统的 .exe 部分
string path = context.Request.PhysicalPath;
// 整个请求 URL,包括参数和配置
string url = context.Request.RawUrl;
// 如果有人请求配置,则提取参数
string ext = ".config";
if( url.ToLower().EndsWith(ext) ) {
context.Response.WriteFile(path + ext);
}
// 如果有人请求配置,则 .exe,则发送 .exe
else {
context.Response.ContentType = "application/octet-stream";
context.Response.WriteFile(path);
}
}
public bool IsReusable {
get { return true; }
}
}
IHttpHandler 接口只有一个有趣的方法 - ProcessRequest。此 ProcessRequest 方法使用某些处理程序获得的上下文(例如指向客户端请求文件的物理路径和同一请求的原始 URL)并检查 URL 是否以“.config”结尾。如果符合这些情况,我们构建被请求的 .exe 文件的物理路径(请记住,IIS 和 ASP.NET 将 .exe 文件,而不是 .config 文件,看作被请求的文件),将“.config.”加在最后并生成相应的 .config 文件。
因为我们已经更改了 .exe 文件到 ASP.NET 的映射,此处理程序将负责分发 .exe 文件和 .exe.blah.config 文件。如果某人正在请求 .exe 文件,则我们设置相应的 MIME 类型并生成此文件。
不论何种情况,如果缺少被请求的文件,则会生成一个 404 错误,.NET 可以处理此错误。
将 .EXE 文件映射到处理程序
将 .EXE 文件映射到 ASP.NET,并实现了一个 .NET 处理程序以生成 .exe 和 .exe.blah.config 文件以后,您仍旧需要让 ASP.NET 知道哪个处理程序要用于 .exe 文件。您在 web.config 文件中为您的 ASP.NET 应用程序执行此操作:
< 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 >
当 ASP.NET 查看到 <httpHandlers> 部分中的此 <add> 元素时,它将所有路径以 .exe 结尾的 HTTP 映射到 ConfigHandler 程序集以及 Genghis.Web.ConfigFileHandler 类(先前显示的 IHttpHandler 实现)。ASP.NET 将在 Web 应用程序的根中查找 web.config 文件并在 Web 应用程序根处的 bin 目录中查找程序集,因此确保将文件放在正确的位置。
允许 IIS 和 ASP.NET 分发 .config 文件
将对 .exe 文件请求从 IIS 路由到 ASP.NET 后,除了写入处理程序以传递回 .config 文件、将对 .exe 文件的 ASP.NET 请求路由到该处理程序,还有另外两个配置步骤。第一是因为 IIS 不允许将 .config 文件发出(除非启用匿名访问)。您将需要在您的 IIS Web 应用程序的 “目录安全性”的“验证方法”对话框中检查“匿名访问”,如图 4 所示。
图 4. 在 IIS 中启用对 Web 应用程序的匿名访问以允许使用 .config 文件
并且,在 ASP.NET 正确地允许分发 .exe 文件和 .exe.args.config 文件时,默认情况下,不允许分发 .config 文件。换句话说,我们用当前配置仅可以分发以下四种 URL 类型中的前三种:
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
前三种 URL 类型显示在我们的处理程序中。如果您无需任何 URL 参数,但是仍需要 .config 文件来启动您的 NTD 应用程序,则第四种 URL 类型很重要。默认情况下,ASP.NET 不分发 *.config 文件的原因是因为您可能将各种敏感信息保存在您的 web.config 文件中。要启用 *.config 文件以分发该文件,但是仍保持 web.config 文件受到保护,请添加下列项:
< 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 >
在这种情况下,ASP.NET 将 .config 文件映射到 HttpForbiddenHandler 中,这确保不会将它们分发回客户端。我们将删除该处理程序的映射,以便允许 .config 文件,然后再次将其添加回 web.config 文件,以使那些剩下的文件仍旧受保护。
我们所处的位置
现在可能非常清楚了,尽管 .NET Framework 1.1 提供对 NTD 应用程序的启动 URL 的访问,并无内建的真正支持以在启动 NTD 应用程序时处理 URL 参数。但是,使用某些创意的编码,您仍旧可以做到:
- 在客户端上,使用 APP_LAUNCH_URL 或 ConfigurationFile 以获得启动 URL,根据需要对其进行解码和分析。
- 如果服务器中使用的是 ASP.NET,请:
- 配置 IIS 以将对 .exe 文件的请求路由到 ASP.NET。
- 实现一个处理程序以生成 .exe 和 .exe?blah.config 文件。
- 将处理程序映射到用于您的 ASP.NET 应用程序的 .exe 文件。
- 配置 IIS 匿名访问以允许使用 .config 文件。
- 删除禁止所有 .config 文件的 ASP.NET 处理程序映射。
- 添加 ASP.NET 处理程序以禁止 web.config 文件。
因此,尽管并非如我们希望的那样完全支持参数,.NET 仍足够灵活,并在客户端和服务器端具备某些智能,您完全可以将参数传递到 NTD 应用程序中。
参考
- .NET Zero Deployment:Security and Versioning Models in the Windows Forms Engine Help You Create and Deploy Smart Clients,MSDN 杂志,2002 年 7 月
- Essential ASP.NET with Examples in C#, Fritz Onion, Addison-Wesley, 2003
- http://www.genghisgroup.com/
注意:本材料的某些内容改写自 Addison-Wesley 即将出版的标题为 Windows Forms Programming in C# 一书,作者 Chris Sells (0321116208)。
Chris Sells 是 MSDN Online 的内容战略家,当前专注于 Longhorn(Microsoft 的下一个操作系统)。他已撰写了若干著作,包括 Mastering Visual Studio .NET 和 Windows Forms for C# Programmers。在业余时间内,Chris 主持各种会议,指导 Genghis 可用源项目,和 Rotor 玩,通常会在 blogsphere 中捣捣乱。有关 Chris 及其各种项目的详细信息,可从站点 http://www.sellsbrothers.com/ 获得。