65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C# 理解 CGI

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (70投票s)

2005年1月27日

Apache

31分钟阅读

viewsIcon

429226

downloadIcon

4312

利用 C# 中的通用网关接口。

Image from web page example.

目录

引言

本文将尝试涵盖您使用 C# 处理通用网关接口所需了解的一切,包括读取 POST 和 GET 的输入。虽然这听起来是个宏伟的目标,但 CGI 实际上很容易上手,并且其大部分功能都源于其简单性。尽管简单,我仍将本文标记为“中级”,因为为了充分利用 CGI,我不得不加入一些中级内容,例如多线程。尽管如此,多线程的呈现方式尽可能直观,并且只被简要使用,所以请不要担心。即使您以前从未在 C# 中使用过多线程,我相信您也不会在这一部分或任何获得“中级”标记的其他部分遇到麻烦。

通用网关接口(CGI)是一个长期存在的 W3C 标准[^],用于在网页和 Web 服务器上可用的应用程序之间进行通信。使用 CGI 的服务器应用程序拥有任何其他本地应用程序的所有功能,例如数据库访问或读取输入文件。然而,在当今世界,ASP.NET、PHP、Perl 以及许多其他脚本语言提供了基本相同的功能,您可能会想,为什么还要费力用 C# 创建自己的 CGI 应用程序呢?我可以想到几个可能适用的原因。

为什么您可能想使用 C# CGI

首先,ASP、PHP、Perl 等解释器本质上是利用 CGI 的应用程序。在许多情况下,它们直接内置于 Web 服务中(例如 IIS 的 ISAPI),但它们也都有 CGI 形式(ASP 可通过 Novell 的 Mono 项目以 CGI 形式提供)。为了更好地理解您喜欢的服务器端脚本语言的能力和限制,您可能对其底层工作方式感兴趣。

另一个原因是,您可能想编写自己的迷你脚本解释器。ASP 等语言是通用目的的,尽管它们可以做任何事情,但有些事情用更具特定目的的脚本来完成会更好。

例如,这描述了我最初涉足 CGI 和 C# 领域的原因。我的公司对报告有非常高的需求。尽管员工可用的工具种类繁多,但由于经常出差,网站仍然是定制化报告的首选资源。所有报告在图形要求方面都有各种非常挑剔的要求,从格式、图表到提供动态生成的 Excel 工作簿。ASP 代码开始显得复杂,于是我们创建了一个自定义的 C# 应用程序来解释一个报告语言,该语言对我们的业务需求非常特定,以至于非编程的普通用户也可以帮助创建和修改报告需求。(这是另一篇文章的主题吗?)

CGI 的另一个好处是它与 Web 服务器无关。同一个可执行文件在 IIS、Apache 或任何其他符合 W3C 标准的 Web 服务器上都可以工作。与某些脚本语言不同,该可执行文件是编译过的,因此执行速度快,源代码也不可见。

C# 是一个非常适合与 CGI 结合使用的语言,因为它具有 .NET 框架的强大功能。您甚至可以在没有 Web 服务器的情况下测试利用 CGI 的 C# 应用程序,因为它只是作为控制台应用程序运行。稍后将详细介绍。

最后,有些 Web 服务提供商允许您使用自定义 CGI 应用程序,但可能不允许您使用 Apache 模块或 ISAPI IIS DLL。原因是 Web 服务器中的集成模块可能对 Web 服务器造成的损害比独立的、可控的可执行文件要大得多。现在我们已经看到了一些使用 C# CGI 的原因,让我们来看看一些不使用它的原因。

为什么您可能想使用 C# CGI

您可能不使用 CGI 的一个原因是,每次 Web 页面请求时,CGI 应用程序都会作为一个单独的进程执行。这与某些形式的 Apache 模块或 ISAPI IIS DLL 不同,后者在加载一次后会一直保留在内存中。然而,这并不总是需要担心。有许多高端 Web 服务器提供 Perl 作为 CGI 模块,并且运行良好。您的 CGI 应用程序消耗的资源不太可能接近 perl.exe。尽管如此,如果发现您的 Web 服务器资源开始变得紧张,您可能会考虑切换到内置模块。幸运的是,您的大部分代码将保持不变,只需替换不同的输入和输出,并添加一些多线程。

您可能要避免使用 CGI 的另一个原因是,与服务器端脚本语言相比,CGI 的安全负担更高。脚本语言存在一些在您自己的应用程序中不存在的限制。因此,需要格外小心,尤其是在处理来自(您希望是)用户的输入时。请查看安全部分,了解用于使您的 CGI 应用程序更安全的各种方法。

最后,即使您决定使用 CGI,也要记住 C# 并不总是最佳选择。尽管 C# 可以编译成一个小巧的可执行文件,但它通常与 .NET 框架一起使用,这意味着 .NET 运行时需要不时地重新加载,具体取决于应用程序被请求的频率。(是的,我知道也可以将 C# 编译为独立的可执行文件。)在速度至上的环境中,您可能需要考虑使用 C 或 C++,或重新审视内置模块的优势。

对通用网关接口的全面理解

尽管您可能从 W3C 标准中推断,CGI 并不是一种语言或协议。借用标准描述中的一句话,它“是 HTTP 服务器实现者之间关于如何集成”您的应用程序的约定。换句话说,它定义了当网页请求时,Web 服务器如何与您的程序通信。由于 Web 服务器可以跨越许多平台和许多操作系统环境,因此标准委员会认为有三种通信形式可以依赖。它们是

  • 标准输入,
  • 标准输出,和
  • 环境变量

如果您以前使用过 C# 控制台应用程序,您可能知道标准输入通常是通过键盘输入的。标准输出是程序在控制台屏幕上显示的内容。环境变量通常是操作系统功能的领域。您可能认识的环境变量包括 PATH。从命令提示符(开始 -> 运行 -> cmd)输入 SET 命令,即可查看计算机上环境变量的完整列表。您会看到有很多。 (如果使用 Windows 2000 或更高版本,您还可以右键单击“我的电脑”,选择“属性”,然后选择“高级”选项卡,最后选择“环境变量”,以图形方式查看当前环境。)下图中,“用户变量”是指专门为当前用户设置的环境,“系统变量”是指为所有人默认的基础环境。

Environment Variables.

您还可以使用 SET 命令在命令提示符下临时设置环境变量。只需键入 SET,后跟您要创建的变量名、一个等号以及您要设置的值。以下面的示例为例,我们创建一个名为 ZNewVariable 的变量,然后再次使用 'set' 显示它

c:\>set ZNewVariable="I am setting an Environment Variable."
c:\>set
...
windir=C:\Windows
ZNewVariable=I am setting an Environment Variable.

在 C# 中,环境变量通过 System.Environment 命名空间访问。稍后我们将深入介绍。

关于环境变量还需要注意的一点是,变量的可用长度因平台而异。经验法则是,环境变量应小于 512 个字符。正如我们将看到的,这是 Web 表单 GET 方法的固有限制之一。

因此,CGI 标准规定 Web 服务器通过标准输入、标准输出和环境变量与您的应用程序进行通信。将其与实际情况联系起来,可以认为标准输入是一种从网页向您的应用程序发送大量数据的方式。在 Web 表单中,它被称为 POST。POST 不会出现在请求的 URL 中,通常用于容纳非常大、动态或敏感的信息。

环境变量是一种向您的应用程序发送少量信息的便捷方式。在 Web 表单中,这就是 GET(尽管它实际上仍然发送信息,并且不像 POST 那样“获取”信息)。GET 会出现在请求的 URL 中,因此可以将其加入书签以备将来使用。这对于论坛或搜索之类的功能很有用,这些功能具有一定的动态性,但通常是为了“从”CGI 应用程序“获取”信息,而不是发送要由 CGI 应用程序存储的有用信息,因此得名。示例如下 URL:

https:///cgi_csharp?TestVariable=123

标准输出是您的应用程序作为对 POST 或 GET 的响应发送到 Web 浏览器的内容。这通常是 HTML 格式文本。

使用 CGI 时,标准输入(下称 stdin)和标准输出(下称 stdout)会被“管道”到 Web 服务器,这样当页面发出请求时,所有这些操作都会在后台进行,而不会在服务器上实际启动控制台屏幕。您无需编写任何代码,因为所有这些都由服务器处理。

通过示例理解 CGI

我认为理解事物的最佳方式是动手实践。虽然我暗示过您可以只通过一个打开的控制台并自己使用 set 命令来测试您的 CGI 应用程序,但我认为玩真实的东西更有趣——即 Web 浏览器。在本节中,我将集中精力配置 IIS,因为我知道阅读本文的大多数人可能已经安装了 IIS。不过,这些概念适用于任何 Web 服务器,如果您实在没有办法,可以通过在执行 CGI 应用程序之前在命令行上设置环境变量来假装自己是一个 Web 服务器。

在设置我们的服务器之前,我们需要有一个 CGI 应用程序。打开您喜欢的 C# 编辑器,创建一个控制台项目,然后输入以下小程序

using System;
namespace CgiInCsharp
{
    class Cgi
        {
        [STAThread]
        static void Main(string[] args)
        {
            Console.Write("Content-Type: text/html\n\n");
            Console.Write("<html><head><title>CGI" + 
                " in C#</title></head><body>" +
                "CGI Environment:<br />");
            Console.Write("<table border = \"1\"><tbody><tr><td>The" + 
                " Common Gateway " +
                "Interface revision on the server:</td><td>" +
                System.Environment.GetEnvironmentVariable("GATEWAY_INTERFACE") +
                "</td></tr>");
            Console.Write("<tr><td>The serevr's hostname or IP address:</td><td>" +
                System.Environment.GetEnvironmentVariable("SERVER_NAME") + 
                "</td></tr>");
            Console.Write("<tr><td>The name and" + 
                " version of the server software that" +
                " is answering the client request:</td><td>" +
                System.Environment.GetEnvironmentVariable("SERVER_SOFTWARE") +
                "</td></tr>");
            Console.Write("<tr><td>The name and revision of the information " +
                "protocol the request came in with:</td><td>" +
                System.Environment.GetEnvironmentVariable("SERVER_PROTOCOL") +
                "</td></tr>");
            Console.Write("<tr><td>The method with which the information request" +
                "was issued:</td><td>" +
                System.Environment.GetEnvironmentVariable("REQUEST_METHOD") +
                "</td></tr>");
            Console.Write("<tr><td>Extra path information passed to a CGI" +
                " program:</td><td>" +
                System.Environment.GetEnvironmentVariable("PATH_INFO") + 
                "</td></tr>");
            Console.Write("<tr><td>The translated version of the path given " +
                "by the variable PATH_INFO:</td><td>" +
                System.Environment.GetEnvironmentVariable("PATH_TRANSLATED") +
                "</td></tr>");
            Console.Write("<tr><td>The GET information passed to the program. " +
                "It is appended to the URL with a \"?\":</td><td>" +
                System.Environment.GetEnvironmentVariable("QUERY_STRING") +
                "</td></tr>");
            Console.Write("<tr><td>The remote IP address of the user making +"
                "the request:</td><td>" +
                System.Environment.GetEnvironmentVariable("REMOTE_ADDR") +
                "</td></tr>");
            Console.Write("</tbody></table></body></html>");
        }  // End of Main().
    }  // End of Cgi class.
} // End of CgiInCsharp namespace.

完成第一个示例

这就足够我们入门了。代码分析:我们看到唯一需要通过 using 指令添加的命名空间是 System。由于我们知道将使用 stdout 与 Web 浏览器通信,因此我们可以使用一系列 Console.Write() 命令来编写 Web 浏览器要查看的输出。第一个

Console.Write("Content-Type: text/html\n\n");

不是 HTML,而是 HTTP 标头。它告诉 Web 浏览器它应该期望收到什么类型的文档。如果您想向 Web 浏览器发送 HTML 以外的内容,例如图像或动画,这将非常有用。它还可用于将其他元数据发送回 Web 客户端,例如请求 cookie。HTTP 协议非常直接,有很多好的教程,但官方 W3C 文档非常难以理解。我认为了解 HTTP 标头信息的一个好方法是通过示例。访问几个网站并查看返回的标头信息。(您可以使用类似 Firefox[^] 和 Web Developer Tools 扩展[^] 的 Web 浏览器来完成此操作。)为了帮助您入门,以下代码是我查看此网站时收到的标头信息。请注意,每个 HTTP 命令都必须单独占一行。

Server: Microsoft-IIS/5.0
Date: Fri, 28 Jan 2005 19:04:10 GMT
X-Powered-By: ASP.NET
Content-Length: 13444
Content-Type: text/html
Set-Cookie: cat=1; expires=Sat, 28-Jan-2006 05:00:00 GMT; path=/
SessionGUID=%7XXXXXXXXXXXXXXXX%2XXXXXXXXXXXXXX%2XXXXXXXXXXXXX%7X; path=/
Cache-Control: private
Content-Encoding: gzip
Vary: Accept-Encoding

唯一必需的部分是 Content-Type: 标签。HTTP 标头必须是您的 CGI 应用程序返回的第一个内容,后跟两个新行(因此是 \n\n)。额外的空行告诉浏览器我们已准备好接收文档内容。稍后,您可能希望从我们的 CGI 应用程序中注释掉标头行,然后尝试用不同的 Web 浏览器运行它,看看每个浏览器如何处理错误。

其余的 Console.Write() 行开始显示 Web 服务器设置的一些有趣的环境变量。它们使用 .NET 框架内置的 System.Environment.GetEnvironmentVariable() 方法。有关符合标准的 Web 服务器提供的变量的完整列表,请参阅 官方 CGI 1.1 规范数据表[^]。一旦我们设置了一个 Web 页面并使用了这个 CGI 应用程序,我们将更详细地探讨更重要的变量。

到目前为止,源代码就这些了。我们有一个非常基本的框架,它查看有趣的环境变量以显示我们正在接收输入,并使用 stdout Console.Write() 发送输出。我们还没有处理 stdin,也没有处理安全问题,但我很想看到一些结果,所以让我们继续检查我们的 IIS Web 服务器设置。

使用 IIS 在网页中启动我们的应用程序

在此示例中,我们将使用 IIS 中的默认 Web 站点。如果您已经有一些正在运行的网站,请继续创建一个新的 Web 站点或虚拟目录。我将向您展示两种使用 CGI 应用程序的方法:第一种方法要求可执行文件位于 IIS 可浏览的目录中,第二种方法更安全,并且不需要将可执行文件移到编译它的文件夹(但设置起来会更复杂一些)。

要开始第一种方法,请打开 Internet Information Services 管理器。找到您要使用的 Web 站点,然后右键单击它(在我们的示例中是“默认 Web 站点”)。然后转到“属性”。

IIS Manager.

单击“主目录”选项卡。确保“执行权限”设置为“脚本”“可执行文件”。如果之前未设置过,它会警告您这是一个安全风险。稍后我们会将其改回,所以现在就确认此更改。单击“应用”,然后单击“确定”。

IIS Manager.

您已准备好将编译好的可执行文件复制到您的 Web 站点目录中。复制后,打开 Internet Explorer 等 Web 浏览器,通过 IIS 直接浏览到该可执行文件,以 https:// 开头。如果您已按照这里的说明操作,则可以在地址栏中键入:https:///cgi_csharp.exe[^]。

为了好玩,您还可以将可执行文件重命名为以 .com 结尾,使其看起来更像 Web 文件,因为 Windows 会将 .com.exe 都视为可执行文件。这样链接看起来会像这样:https:///cgi_csharp.com[^]。

您的 Web 浏览器输出应该如下所示:

The Common Gateway Interface revision on the server: CGI/1.1
The serevr's hostname or IP address: localhost
The name and version of the server software 
    that is answering the client request: Microsoft-IIS/5.1
The name and revision of the information protocol 
    the request came in with: HTTP/1.1
The method with which the information request was issued: GET
Extra path information passed to a CGI program:
The translated version of the path given by the variable PATH_INFO:
The GET information passed to the program. It is appended to the URL with a "?":
The remote IP address of the user making the request: 127.0.0.1

如果某些变量为空,请不要担心。其中一些尚不适用,另一些可能不是您的 Web 服务器提供的。此实验的一部分是了解我们收到的信息类型。请参阅本文开头处的源代码和演示附件,以获取更全面的项目列表。

现在让我们添加一个 Web 表单,以便我们可以动态地将信息传递给 CGI 程序。在您已将 CGI 程序复制到的同一个 Web 文件夹中创建一个新的文本文件,方法是右键单击并选择“新建”->“文本文档”。将其命名为 index.txt。双击该文档以在记事本中打开它,然后输入以下 HTML 表单:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta content="text/html; charset=ISO-8859-1" http-equiv="content-type">
<title>CGI in C#</title>
</head>
 
<body>
<h2>Form for Testing CGI Application</h2>
 
<form action="cgi_csharp.com" method="GET">
Enter your own input here: <INPUT TYPE="text" NAME="play" SIZE="30" /><br />
<input type="submit" value="Submit Report with GET" />
</form>
 
</body>
</html>

输入完成后,保存文档并退出记事本(或其他编辑器)。然后将文件重命名为 index.html。如果 Windows 未提示您询问是否确定要重命名扩展名,那么它可能在隐藏文件扩展名,并且只假装正确命名。要查看如何修复 XP 中的视图,请转到“工具”->“文件夹选项”->“查看”选项卡,然后取消选中“隐藏已知文件类型的扩展名”。

我假设您已经理解了大部分 HTML,或者可以自己查找。重要的部分是 <form action="cgi_csharp.com" method="GET">。此代码将输入框中的内容发送到 CGI 应用程序 cgi_csharp.com。如果您将 CGI 应用程序命名为其他名称(例如 cgi_csharp.exe),只需在 action 属性中替换您的名称即可。“method”属性设置 CGI 应用程序接收信息的方式。请注意,我们指定这是一个 GET 请求。

当您单击“提交”按钮时,请查看 URL 顶部,您将看到您在文本框中输入的任何内容。这就是 GET 方法允许您稍后将请求加入书签的方式。您不必使用 HTML Web 表单,而是可以修改 URL 并直接提交信息。这对于提供更丰富、更强大的用户体验非常有用。然而,这也是人们滥用您的 CGI 应用程序的常见方式,通过忽略 HTML 客户端中的任何过滤,直接将危险的输入发送到服务器。

单击“提交”后,您可以看到我们的应用程序确实已将 GET 数据作为环境变量拾取

The GET information passed to the program. 
      It is appended to the URL with a "?": Testing+1+2+3

任何空格都已转换为 + 号,因为您不能在 GET 中传递空格。请随意尝试一下,看看您能从输入中产生什么样的输出。

将 POST 方法添加到您的工具箱

到这里,大多数教程就结束了,这不一定不好,因为我们已经可以通过 CGI 做很多事情了。然而,GET 并不满足所有需求。如果您想发送大量数据,或者发送不出现在 Web 浏览器 URL 中的数据,那么您需要的是 POST 方法。许多教程在 GET 处结束,因为获取 POST 信息比看起来更棘手。问题实际上出在 .NET 框架的 1.x 版本上。Microsoft 表示将在 2.0 版本中修复该问题,但在撰写本文时,尚未实现。限制在于 .NET 中控制台应用程序的实现不佳。我们需要一个更强大的 Console.Read() 方法。

问题出在 Console.Read() 每次只返回一个字符,这正是我们想要的,但控制台默认处于一种模式,即输入会被缓冲,直到用户按下 Enter 键。这意味着除非您的 HTML 表单的最后一个字符是 ASCII 13(回车),否则您将没有任何 POST 信息。不幸的是,我不确定修复 Enter 键缓冲对跨平台的影响。如果您打算使用 Mono 或 DotGnu 运行时,您将不得不尝试和错误,或者只需在所有 Web 表单 POST 中添加一个 Enter。

尽管这可能是您愿意接受的限制,但我们的 POST 面临一个更严重的问题。Console.Read() 是阻塞的。这意味着一旦您的应用程序请求 Console.Read(),在读取一个字符之前,它就无法处理任何其他事情。如果它正在等待 POST 信息但没有收到,那么您的 CGI 应用程序将挂起,并且浏览器最终会超时。(这是一个安全问题和令人烦恼的问题。)

幸运的是,这两个问题都可以在几行代码中轻松解决。

解决 POST 的控制台输入缓冲难题

为了更改控制台模式,使其不再需要 Enter 键,我们需要调用一个 Win32 方法。为此,我们必须告诉 C# 程序该方法在操作系统中的位置。首先,在 CS 文件顶部添加一个新的命名空间

using System.Runtime.InteropServices;

我们要找的方法是 SetConsoleMode()。它位于 Windows 内核中。为了告诉 C# 在哪里找到它,在定义类之后,添加一个 DllImport 指令,如下所示

...
        class Cgi
        {
        [DllImport("kernel32", SetLastError=true)]
        static extern int SetConsoleMode (int hConsoleHandle, int dwMode);
...

然后,您只需要调用该方法。为此,请在 Main() 方法的顶部附近添加这一行

SetConsoleMode(3, 0);

为什么参数是 3 和 0?嗯,基本答案是我查阅了 SetConsoleMode 的 C 头文件,发现第一个参数期望 stdin 等于 3。因为这是头文件中的一个硬编码定义,所以我假设 3 是所有 Windows 平台的正确答案。如果有人有更优雅的解决方案,请告诉我,我将进行更正。第二个参数 0 告诉 SetConsoleMode 如何处理第一个参数 stdin。我们想清除所有内容并使用原始控制台,因此将其设置为 0。同样,这基于检查 C 头文件。

解决 POST 的输入阻塞难题

既然我们可以全天候读取字符,我们就需要注意 stdin 的输入阻塞,否则,如果 POST 数据为空或有问题,我们的 CGI 应用程序可能会容易死锁。与最后一个 POST 难题一样,这需要一些中级编程,但它也是一个非常快速的修复。

由于 .NET 不支持非阻塞控制台输入,我们只能自己实现它。为此,我们将创建一个专门用于获取 stdin 输入的方法,然后将该方法与 Main() 分开处理。Main() 将跟踪我们自定义方法的进度,如果它在预定的超时时间内未返回我们期望的信息,我们将优雅地报告错误并退出。当然,这听起来像是同时做两件事,对吧?我警告过您我们会涉及多线程。

首先,我们需要添加最后一个命名空间

using System.Threading;

然后,我们需要设置用于读取 stdin 的方法。

public static void GatherPostThread()
{
    if(PostLength > 2048) PostLength = 2048;
    // Max length for POST data for security.

    for(;PostLength>0;PostLength--)
      PostData += Convert.ToChar(Console.Read()).ToString();
}

请注意,在此示例中,我们将 POST 长度限制为 2048 个字符。这是一个任意数字,可能相当低。但是,如果有人试图将他们的 Web 浏览器用作武器,您可能不希望让他们向您的程序发送无限弹药,因此请选择适合您目的的值。

嘿,PostDataPostLength 变量是从哪里来的?嗯,我们将使用这些变量在两个线程之间进行通信。我通常尽量尽量减少静态成员(可能是因为我的 C++ 背景),但这是我们为快速简单的解决方案定义的:

class Cgi
{
  ...
  [DllImport("kernel32", SetLastError=true)]   // Old code.
  static extern int SetConsoleMode ( int hConsoleHandle, int dwMode); // Old code.
 
  private static string PostData;  // New code.
  private static int PostLength;  // New code.
  ...

PostData 字符串将用于存储传入的 POST 流。PostLength 实际上是一个应该作为环境变量传入的值,它为我们提供了关于可能已 POST 给我们的数据量的一些线索。我们从 Main() 的以下行加载该变量:

PostLength = 
  Convert.ToInt32(System.Environment.GetEnvironmentVariable("CONTENT_LENGTH"));

我们现在可以设置一个新线程来运行我们的 GatherPostThread() 方法,同样在 Main() 中:

...
ThreadStart ThreadDelegate = new ThreadStart(GatherPostThread);
Thread PostThread = new Thread(ThreadDelegate);
...
int LengthCompare = PostLength;
 
if(PostLength>0) PostThread.Start();
...

条件语句检查 Web 服务器是否告诉我们有数据。如果没有,那么就没有必要浪费我们的资源来启动线程。我还向 Main() 添加了一个名为 LengthCompare 的本地变量。我们将在 Main() 方法的末尾使用它来检查并确保我们的 GatherPostThread() 仍在工作,而不是在偷懒。

...
while(PostLength > 0)
    {
    Thread.Sleep(100);
    if(PostLength < LengthCompare)
      LengthCompare = PostLength;
    else
      {
      PostData += "Error with POST data or connection problem.";
      break;
      }
    }
...

现在回想一下,在我们的多线程方法中,每次读取一个字符时,我们都在倒计时 PostLength。这里,我们检查以确保它的值确实在减小。每 100 毫秒,我们将其与之前的值进行比较。如果相同,那么我们就知道有问题,因为尽管 100 毫秒对我们来说很快,但对于 POST 线程读取另一个字节的数据来说,这已经足够了。(对于一个小的 POST,我敢打赌在我们到达那个循环之前数据就已经读完了。)现在让我们再添加一行表格,展示我们的新 POST 功能:

Console.Write("<tr><td>The POST data passed" + 
  " to the program through standard input:" +
  "</td><td>" + PostData + 
  "</td></tr>");

POST 多线程难题只剩下最后一块了,那就是如果 Web 服务器对我们撒谎怎么办。如果 PostLength 的值不是等待我们的数据的实际长度,那么即使我们的主线程可能关闭,GatherPostThread 方法仍然会徒劳地运行。为了确保我们避免任何不必要的麻烦,我们只需添加

Environment.Exit(0);

Main() 的末尾。

您可能会认为这看起来(确实,整个示例)非常程序化。而且,毕竟,如果不利用 C# 强大的面向对象设计能力,有什么意义呢?最终,这是您必须自己回答的问题,但这是我的观点。

首先,就设计而言,我们无法让这个应用程序成为事件驱动的。这实际上没有意义,因为没有人机交互,并且用户的 Web 体验将在很大程度上取决于应用程序执行其任务并退出的速度。尽管我们没有利用 C# 的这些方面,但对于足够复杂的项目,我们仍然可以利用面向对象的 [设计] (https://codeproject.org.cn/Articles/3466/Object-Oriented-Design-Patterns-and-the-DotNet-fra)。这个小小的 CGI 应用不符合要求,但如果您打算制作下一个 Perl,您可能会大量使用 OOP。另外,对我来说,C# 是最漂亮的语言,它利用了多功能 .NET 框架。仅仅因为您不使用 C# 或 .NET 的所有功能,并不意味着您与它一起使用的东西就没有增加很多价值。好了,够了,偏题了。我们继续。

重新编译程序并将其复制到您正在使用的 Web 目录中。使用文本编辑器编辑 index.html 页面,并将 FORM method="GET" 更改为 method="POST"。重新加载页面,您现在应该会看到数据出现在 POST 部分下。太棒了!我们已经完成了棘手的部分!

关于您自己的脚本,或者如何摆脱 IIS 脚本和可执行文件安全问题

这一节实际上也可以称为“如何使 CGI 的扩展看起来像您想要的任何内容”,或者“如何在编译位置保留可执行文件,这样您就不必每次想测试更改时都复制它”。所有这些事情将同时通过一个重要的更改来实现。我们所要做的就是将我们的可执行文件关联为任意脚本扩展的首选 CGI 应用程序。在 IIS 中,这称为“应用程序映射”,在其他 Web 服务器中也类似。

我之所以把它推迟到现在,是因为我想清楚地表明 CGI 应用程序独立于任何脚本语言。事实上,我们即将做的事情也完全独立于任何脚本,但因为它可用于将您的应用程序变成脚本解释器,所以我想等到区分更清楚时再使用此方法。

重新设置 IIS

应用程序映射的真正目的是将脚本文件与其解释器关联起来。通过创建自己的扩展来映射到我们的 CGI 应用程序,我们不再受可执行文件位置的限制,也不再受启动它时 Web 服务器显示的名称的限制。自定义脚本文件本身,正如我们很快就会看到的,可以完全为空。但如果您关心,本节将展示如何选择性地读取其内容。

再次打开 IIS 管理器,然后右键单击您的 Web 站点。在我们的示例中,它仍然称为“默认 Web 站点”。

IIS Manager.

返回“主目录”选项卡。将“执行权限”改回“脚本”,然后单击旁边的“配置...”按钮。

IIS Manager.

现在,单击“可执行文件”字段旁边的“浏览”按钮。找到生成可执行文件的目录并选择它。在“扩展名”下,键入任何您喜欢的名称,只要它不与常见的扩展名冲突即可。我使用了 .csx 来模仿 aspx,但用于 C#。您也可以使用 .test.your_initials。从这里开始,我将假设使用了 .csx,但请理解这是任意的。

“动词”部分允许您限制信息传递给 CGI 应用程序的类型。例如,您可以将自己限制为仅 GET 或仅 POST。您可能会看到其他扩展名列出了 HEAD、PUT 或 DELETE 等。这些是 GET 和 POST 的 HTTP 变体,有助于阐明请求页面的意图。因为它们很少使用(出于好目的),许多 Web 服务器会阻止或将这些请求转换为普通的 GET 和 POST,所以除非您是公共 Web 站点的系统管理员,否则不必过多担心它们。对您的 CGI 应用程序而言,HEAD 通常看起来就像 GET,而其他所有内容通常看起来就像 POST。您只需保留 IIS 中的“动词”部分。

如果您想从编译它的位置执行应用程序,请将“脚本引擎”复选框保留为选中状态。如果您希望它仅从专门为 CGI 应用程序指定的文件夹执行,可以取消选中此选项。但现在,请保留它,以免在出现问题时进行额外的故障排除。

“检查文件是否存在”意味着 Web 服务器将确保请求的 .csx 文件存在。缺点是这会耗费额外的时间和资源,而这些事情本应在您的应用程序中完成,如果您正在创建一个脚本解释器的话。我通常会取消选中此项,但这对于本次讨论来说没有区别。

现在单击“应用”和“确定”,我们就可以开始工作了。我们只需要在 Web 站点中有一个扩展名为 .csx 的空文件。您可以右键单击 Web 文件夹,选择“新建”->“文本文件”,然后将文件重命名为 csharp.csx 等来创建它。同样,如果一开始不起作用,请不要忘记检查 Windows 是否隐藏了文件扩展名。

现在将您的 Web 浏览器指向您的文件。在我们的示例中,您会在地址栏中输入:https:///csharp.csx[^]。运气好的话,您应该会看到 CGI 输出。现在编辑您的 index.html,并将 FORM action="cgi_csharp.com" 更改为 action="csharp.csx"。现在,当您重新加载 index.html(您可能需要刷新以确保)并在文本字段中输入信息时,它将启动看起来像 csharp.csx 的内容,但会显示您的 C# 应用程序的输出,包括 POST 或 GET 数据。

如果您想阅读 CSharp.csx,该如何做

既然我们已经告诉 IIS 扩展名为 .csx 的文件是我们的 CGI 应用程序的脚本文件,您实际上可能对启动您的应用程序的 .csx 文件的内容感兴趣。同样,您根本不必对其做任何事情,但如果您想这样做,您可能一开始会很难弄清楚哪个 .csx 文件调用了您的程序。

我一开始遇到了这个问题,因为我假设 IIS 会将文件名作为命令行参数传递给应用程序,类似于用“perl.exe script.pl”启动 Perl 脚本。但事实并非如此。相反,我们必须再次查看环境变量。具体来说,是名为 PATH_TRANSLATED 的那个。请注意,我们已经在 CGI 应用程序中检查了 PATH_TRANSLATED 的值。然而,直到我们设置了与 .csx 文件的关联,这个字段一直为空。现在,它显示了请求我们程序的 csx 文件的完整路径和名称。您只需设置一个文本读取器或任何您喜欢的东西,然后以您想要的方式处理文件。(别忘了检查文件是否真的存在!)

安全

在 CGI 1.1 规范的官方定义中,简要地看了 一些安全问题[^]。我想在这里从不同的角度来解决这个问题。安全的最大问题是确保您使来自“外面”的程序的所有输入都安全。永远不要信任客户端。发送不良信息一点也不难。最常见的问题是当您的 CGI 应用程序提供了一个从外部世界进入本地系统的接口时,例如数据库。如果您只是盲目地将来自网页的输入发送到数据库,您就为数据库创建了一个后门,从而使它的安全模型无效,并且会被利用。如果您允许 Web 用户发送电子邮件,情况也一样。如果您代表动态请求执行 shell 命令,情况也一样。

为了提供安全性,我建议您真正了解您的应用程序在用户输入方面的目的,然后只考虑最坏的情况来过滤用户输入。即使您无法想到输入如何被滥用,也要确保只允许您能想到的合法内容,并拒绝所有其他内容

另外,这是我的观点,尽量不要破坏另一个系统的安全性。例如,如果数据库通常需要身份验证才能使用,那么就让用户进行身份验证,而不是将用户名和密码硬编码到您的应用程序中。

最后,如果发送到您 CGI 应用程序的内容可以在浏览器中以某种方式重新显示,也要考虑您用户的安全性和稳定性。通过剔除论坛帖子中的 HTML,或者只允许某些类型的格式,您可以保护您的其他用户免受潜在的恶意意图。由于我们的示例应用程序会在 HTML 中重新显示它接收到的 POST 和 GET 数据,因此这是一个提供快速示例的绝佳机会。

让我们创建一个名为 Sanitize 的新方法

public static string Sanitize(string Raw)
{
    string Clean = "";
    int Walk;
    char[] ByCharacter;
    if(Raw == null)return Clean;
    ...
    Raw=Raw.Replace("%22", "\"");  // Example GET encoding cleanup. (%hexnumber)
    Raw=Raw.Replace("&#60;", "<"); // Example HTML encoding cleanup.
    ....
    ByCharacter = raw.ToCharArray();
    for(Walk = 0; Walk < Raw.Length;Walk++)
      {
      if(ByCharacter[Walk] == '\'') Clean += "'";
      else if(ByCharacter[Walk] == '"') Clean += "\"";
      else if(ByCharacter[Walk] == ' ') Clean += "&nbsp;";
      else if(ByCharacter[Walk] == '&') Clean += "<br />";
      else if(ByCharacter[Walk] >= 'A' && ByCharacter[Walk] <= 'z' ||
              ByCharacter[Walk] >= '0' && ByCharacter[Walk] <= '9' ||
              ByCharacter[Walk] == '=' || ByCharacter[Walk] == ',' ||
              ByCharacter[Walk] == '.' || ByCharacter[Walk] == '@' ||
              ByCharacter[Walk] == '#')
                  Clean += ByCharacter[Walk].ToString();
      else Clean += "^";
      }
 
    return Clean;
    }  // End of Sanitize() method.

Sanitize() 方法以一个可疑字符串作为参数,尝试翻译任何 GET 或 HTML 格式,然后逐个字符地扫描字符串,根据(在此示例中)一组有限的可接受集来允许或拒绝。它会将它不接受的字符替换为 '^' 字符。有更优雅的方法可以做到这一点,事实上,我建议研究正则表达式,但这似乎是一种直观的方法来帮助说明观点。由于代码量大,我省略了大量用于翻译 HTML 和 GET 请求的 Raw=xxx 代码,但只需下载 示例源代码 即可获得完整的过滤器。请记住,我创建它时是以英语为考虑的,所以您可能想扩展它的功能,并尝试一些创造性的过滤方法。

要使用我们的 Sanitize() 方法,只需将 PostDataQUERY_STRING 字符串包装在其中即可。

...
    Console.Write("<tr><td>The GET information passed to the program. " +
      "It is appended to the URL with a \"?\":</td><td>" +
      Sanitize(System.Environment.GetEnvironmentVariable("QUERY_STRING")) +
      "</td></tr>");
...
    Console.Write("<tr><td>The POST data passed" + 
      " to the program through standard input:" +
      "</td><td>" + Sanitize(PostData) + "</td></tr>")
...

总结

alas,时间到了。我希望这对您理解 C# CGI 的所有细节有所启发。我觉得可能缺少的是一个很好的处理二进制输入和输出的示例。不过,这更多是一个 C# 的讨论,而不是 CGI 的讨论,所以也许这又是另一篇文章或代码片段的素材。在此期间,学习的最佳方法是更改一些内容并重新编译,看看会发生什么。尽情享受黑客您的 CGI 吧,祝您编码愉快。

修订历史

  • 版本 1.00:2005 年 1 月 27 日

    首次上传到 Code Project。

  • 版本 1.01:2005 年 1 月 27 日,稍后

    这是我发布的第一个文章,我错过了一些标准的格式,比如在链接后面添加 [^]。

  • 版本 1.02:2005 年 1 月 28 日

    添加了一些似乎相关的 HTTP 注释。

© . All rights reserved.