Perl初学者入门 - 第四部分

编辑注:这个值得尊敬的系列正在进行更新。你可能对更新的版本感兴趣,可在以下位置找到:

现在是CGI时间了

Perl初学者入门

本系列的第1部分
本系列的第2部分
本系列的第3部分
本系列的第5部分
本系列的第6部分

什么是CGI?
一个真实的CGI程序
哎呀!
我们的第二个脚本
排序
不相信任何人
随意玩玩!

到目前为止,我们讨论了Perl作为处理数字、字符串和文件的语言——这是语言的原始用途。现在是时候讨论Perl在Web上能做什么了。在本部分中,我们将讨论CGI编程。

什么是CGI?

Web基于客户端-服务器模型:你的浏览器(客户端)向Web服务器发出请求。其中大部分是简单的对文档或图像的请求,服务器将它们传递给浏览器以进行显示。

当然,有时你希望服务器做得更多,而不仅仅是输出文件内容。你希望使用服务器端程序——不管这个“什么”是使用基于Web的电子邮件、在数据库中查找电话号码还是为你的技术狂热者订购一本《Evil Geniuses in a Nutshell》。这意味着浏览器必须能够向服务器发送信息(一个电子邮件地址、要查找的姓名、书籍的运送信息),并且服务器必须能够使用这些信息并将结果返回给用户。

用户Web浏览器与运行在Web服务器上的服务器端程序之间的通信标准被称为CGI,即通用网关接口。它被所有流行的Web服务器软件支持。为了充分利用这篇文章,你需要一个支持CGI的服务器。这可能是一个运行在你的桌面机器上的服务器,或者是一个通过你的ISP的账户(尽管可能不是免费网页服务)。如果你不知道你是否具有CGI功能,请向你的ISP或当地系统管理员询问如何设置。

请注意,我没有描述CGI是如何工作的;这是因为你不需要知道。有一个标准的Perl模块叫做CGI.pm,将为你处理CGI协议。CGI.pm是Perl核心分发的一部分,任何正确安装的Perl都应该有它可用。

告诉你的CGI程序你想使用CGI模块就像这样

use CGI ':standard';

use CGI ':standard';语句告诉Perl你想要在你的程序中使用CGI.pm模块。这将加载模块并将一组CGI函数提供给你的代码。

一个真实的CGI程序

让我们编写我们的第一个真正的CGI程序。我们不去做复杂的事情,我们将编写一个简单地回显我们给出的任何内容。我们将把这个脚本称为backatcha.cgi

#!/usr/local/bin/perl

use CGI ':standard';

print header();
print start_html();

for $i (param()) {
    print "<b>", $i, "</b>: ", param($i), "<br>\n";
}

print end_html();

如果您从未使用过HTML,那么这对 <b> 和 </b> 标签分别代表“开始粗体”和“结束粗体”,而 <br> 标签表示“换行”。关于HTML的好书是O'Reilly的《HTML & XHTML: The Definitive Guide》,在线上,我喜欢Web Design Group

将此程序安装到您的服务器上,并进行测试运行。(如果您没有自己的Web服务器,我们已将其副本放在线上供您使用此处。)以下是一些安装CGI程序的简要步骤

  1. 确保程序放置的位置可以让您的Web服务器将其识别为CGI脚本。这可能是特殊的 cgi-bin 目录,或者确保程序的文件名以 .pl.cgi 结尾。如果您不知道程序应该放在哪里,您的ISP或系统管理员应该知道。
  2. 确保程序可以被服务器运行。如果您使用的是Unix系统,您可能需要为Web服务器用户授予对程序的读取和执行权限。最简单的方法是使用 chmod filename 755 为所有人赋予这些权限。
  3. 记下程序的URL,它可能类似于 http://服务器名称/cgi-bin/backatcha.cgi,然后在浏览器中访问该URL。(如果您不知道程序的URL是什么,猜猜您应该做什么。提示:涉及“询问”、“你的”和“ISP”这些词。)

如果它运行正常,您将在浏览器中看到一个空白页面!别担心,这是应该发生的。backatcha.cgi 脚本会将您投入的东西返还回来,而我们还没有投入任何东西。我们很快就会给它一些东西来展示给我们。

如果它 没有 运行,您可能看到了错误消息或脚本的源代码。我们将在下一节中尝试诊断这些问题。

哎呀!

如果您看到了错误消息,您的Web服务器在运行CGI程序时遇到了问题。这可能是因为程序或文件权限有问题。

首先,您 确定 程序有正确的文件权限吗?您是否将程序的文件权限设置为755?如果不是,现在就做。 (Windows Web服务器将有不同的方法来做这件事。)再次尝试;如果您现在看到一个空白页面,那么您就成功了。

其次,您 确定 程序实际上工作吗? (别担心,即使是最好的我们也会遇到这种情况。)将程序中的 use CGI 行更改为

use CGI ':standard', '-debug';

现在从命令行运行程序。您应该看到以下内容

(offline mode: enter name=value pairs on standard input)

这条消息表明您正在 测试 脚本。现在您可以按Ctrl-D来告诉脚本继续运行,而不需要告诉它任何表单项。

如果Perl在脚本中报告任何错误,您现在可以修复它们。

-debug 选项非常有用。在您遇到CGI程序问题时使用它,否则可能会有危险。)

另一个常见的问题是您看到的是程序源代码,而不是程序运行的结果。有两个简单的问题可能导致这种情况。

首先,您 确定 您是通过Web服务器进行的吗?如果您使用浏览器中的“加载本地文件”选项(查看类似 /etc/httpd/cgi-bin/backatcha.cgi 而不是类似 http://localhost/cgi-bin/backatcha.cgi 的内容),您甚至都没有接触Web服务器!您的浏览器正在做您“想要”做的事情:加载本地文件的正文并显示它们。

其次,您是否确定Web服务器知道这是一个CGI程序?大多数Web服务器软件都会有一种特别的方式将文件指定为CGI程序,无论是特殊的cgi-bin目录,文件上的.cgi.pl扩展名,还是其他什么。除非您符合这些期望,否则Web服务器会认为该程序是一个文本文件,并以纯文本形式提供您的程序源代码。向您的ISP寻求帮助。

CGI程序在最好情况下都是难以驾驭的怪物;如果使它们正常运行需要一些工作,请不要担心。

使表单能够回复

此时,您应该有一个工作的backatcha.cgi副本,从Web服务器输出空白页面。让我们让它真正告诉我们一些信息。将以下HTML代码放入一个文件中

<FORM ACTION="putyourURLhere" METHOD=GET>
    <P>What is your favorite color? <INPUT NAME="favcolor"></P>
<INPUT TYPE=submit VALUE="Send form">
lt;/FORM>

务必将putyourURLhere替换为您的backatcha.cgi副本的实际URL!如果您愿意,可以使用Perl.com上安装的副本

这是一个简单的表单。它将显示一个文本框,您可以在此处输入您最喜欢的颜色,以及一个“提交”按钮,该按钮将您的信息发送到服务器。在浏览器中加载此表单并提交您最喜欢的颜色。您应该看到以下内容从服务器返回

favcolor: green

CGI函数

CGI.pm模块为您加载了几个特殊的CGI函数。这些函数是什么?

第一个是header(),用于在脚本可以显示HTML输出之前输出任何必要的HTTP头。尝试删除此行;您将在尝试运行时从Web服务器收到错误。这是另一个常见的错误来源!

start_html()函数是为了方便而存在的。它为您返回一个简单的HTML头。您可以通过使用散列传递参数给它,如下所示

print $cgi->start_html( -title => "My document" );

end_html()方法类似,但输出页面的页脚。)

最后,最重要的CGI函数是param()。调用它时,传入表单项的名称,将返回该表单项的所有值。(如果您请求标量,则无论列表中有多少个值,您只会得到第一个值。)

$yourname = param("firstname");
print "<P>Hi, $yourname!</P>\n";

如果您在未给出表单项名称的情况下调用param(),它将返回所有可用的表单项的列表。这种形式的param()是我们backatcha脚本的内核

for $i (param()) {
    print "<b>$i</b>: ", param($i), "<br>\n";
}

记住,单个表单项可以有多个值。您可能在提供在线订餐服务的披萨店网站上遇到这样的代码

    <P>Pick your toppings!<BR>
       <INPUT TYPE=checkbox NAME=top VALUE=pepperoni> Pepperoni <BR>
       <INPUT TYPE=checkbox NAME=top VALUE=mushrooms> Mushrooms <BR>
       <INPUT TYPE=checkbox NAME=top VALUE=ham> Ham <BR>
    </P>

想要所有三种配料的人会提交一个表单,其中表单项top有三个值:“pepperoni”、“mushrooms”和“ham”。服务器端代码可能包括以下内容

    print "<P>You asked for the following pizza toppings: ";
    @top = param("top");
    for $i (@top) {
        print $i, ". ";
    }
    print "</P>";

现在,要注意的是。再看一下披萨配料HTML代码。尝试将这个小的片段粘贴到backatcha表单中,就在<INPUT TYPE=submit...>标签上方。输入一个最喜欢的颜色,并选择所有三个配料。您会看到以下内容

    favcolor: burnt sienna
    top: pepperonimushroomsham

为什么会发生这种情况?当您调用param('name')时,您会返回一个列表,其中包含该表单项的所有值。这可以被认为是backatcha.cgi脚本中的一个错误,但很容易修复 - 使用join()来分隔项目值

    print "<b>$i</b>: ", join(', ', param($i)), "<br>\n";

或者首先以标量上下文调用Cparam()以获取第一个值

    $j = param($i);
    print "<b>$i</b>: $j
\n";

始终牢记表单项可以有多个值!

我们的第二个脚本

现在我们已经知道如何构建一个CGI程序,并且我们已经看到了一个简单的示例。让我们写一些有用的东西。在上篇文章中,我们编写了一个相当不错的HTTP日志分析器。为什么不让它实现网络功能呢?这将允许您在任何可以访问浏览器的地方查看您的使用情况。

下载HTTP日志分析器的源代码

首先,让我们决定我们想要如何使用我们的分析器。我们不一次性显示我们生成的所有报告,而是只显示用户选择的报告。其次,我们将让用户选择每个报告是显示整个项目列表,还是按访问次数排序的前10、20或50个。

我们将使用这样的表单作为我们的用户界面

    <FORM ACTION="/cgi-bin/http-report.pl" METHOD=POST>
        <P>Select the reports you want to see:</P>

 <P><INPUT TYPE=checkbox NAME=report VALUE=url>URLs requested<BR>
    <INPUT TYPE=checkbox NAME=report VALUE=status>Status codes<BR>
    <INPUT TYPE=checkbox NAME=report VALUE=hour>Requests by hour<BR>
    <INPUT TYPE=checkbox NAME=report VALUE=type>File types
 </P>

 <P><SELECT NAME="number">
     <OPTION VALUE="ALL">Show all
     <OPTION VALUE="10">Show top 10
     <OPTION VALUE="20">Show top 20
     <OPTION VALUE="50">Show top 50
 </SELECT></P>

 <INPUT TYPE=submit VALUE="Show report">
    </FORM>

(记住,您可能需要更改URL!)

在这个HTML页面中,我们发送两种不同类型的表单项。一种是系列复选框小部件,用于设置report表单项的值。另一种是一个下拉列表,将单个值分配给number:ALL、10、20或50。

看看原始的HTTP日志分析器。我们将从两个简单的更改开始。首先,原始程序从命令行参数获取使用日志文件的名称

      # We will use a command line argument to determine the log filename.
      $logfile = $ARGV[0];

显然,我们现在不能这样做,因为Web服务器不允许我们为CGI程序输入命令行!相反,我们将硬编码$logfile的值。我将使用“/var/log/httpd/access_log”作为示例值。

      $logfile = "/var/log/httpd/access_log";

其次,我们必须确保在我们打印任何其他内容之前向Web服务器输出所有必要的标题

      print header();
      print start_html( -title => "HTTP Log report" );

现在看看原始程序中的report()子程序。它有一个问题,与我们的新目标相关:它输出所有报告而不是我们选择的报告。我们将重写report(),使其遍历report表单项的所有值,并显示每个适当的报告。

 sub report {
    for $i (param('report')) {
 if ($i eq 'url') {
     report_section("URL requests", %url_requests);
 } elsif ($i eq 'status') {
     report_section("Status code requests", %status_requests);
 } elsif ($i eq 'hour') {
     report_section("Requests by hour", %hour_requests);
 } elsif ($i eq 'type') {
     report_section("Requests by file type", %type_requests);
 }
    }
 }

最后,我们将重写report_section()子程序,以输出HTML而不是纯文本。(我们将在稍后讨论我们正在使用的新sort方法。)

    sub report_section {
 my ($header, %type) = @_;
 my (@type_keys);

 # Are we sorting by the KEY, or by the NUMBER of accesses?
 if (param('number') ne 'ALL') {
     @type_keys = sort { $type{$b} <=> $type{$a}; } keys %type;

     # Chop the list if we have too many results
     if ($#type_keys > param('number') - 1) {
         $#type_keys = param('number') - 1;
     }
 } else {
     @type_keys = sort keys %type;
 }

 # Begin a HTML table
 print "<TABLE>\n";

 # Print a table row containing a header for the table
 print "<TR><TH COLSPAN=2>", $header, "</TH></TR>\n";

 # Print a table row containing each item and its value
 for $i (@type_keys) {
     print "<TR><TD>", $i, "</TD><TD>", $type{$i}, "</TD></TR>\n";
 }

 # Finish the table
 print "</TABLE>\n";
    }

排序

Perl允许您使用sort关键字对列表进行排序。默认情况下,排序将按字母数字顺序进行:数字在字母之前,大写字母在小写字母之前。这在99%的情况下是足够的。在其他1%的情况下,您可以编写一个定制的排序例程供Perl使用。

这个排序例程就像一个小子程序。在其中,您比较两个特殊变量$a$b,并根据您希望它们在列表中如何显示返回三个值之一。返回-1表示“$a应在排序列表中位于$b之前”,1表示“$b应在排序列表中位于$a之前”,0表示“它们相等,因此我不在乎哪个先出现。”Perl将运行此例程来比较列表中的每一对项目,并生成排序结果。

例如,如果您有一个名为%type的散列,您可以按以下方式按其散列中的降序排序其键。

    sort {
        if ($type{$b} > $type{$a}) { return 1; }
 if ($type{$b} < $type{$a}) { return -1; }
 return 0;
    } keys %type;

事实上,数字排序非常常见,Perl为此提供了一个方便的简写:<=>运算符。此运算符将为您执行上述比较,并返回适当的值。这意味着我们可以将该测试重写为

    sort { $type{$b} <=> $type{$a}; } keys %type

(事实上,这正是我们在日志分析器中使用的。)

您还可以使用sort比较字符串。ltgt运算符是<>的字符串等效,而cmp将执行与<=>相同的测试。(记住,字符串比较将数字排在字母之前,大写字母排在小写字母之前。)

例如,你有一个包含姓名和电话号码的列表,格式为“John Doe 555-1212。”你希望按姓氏对列表进行排序,当姓氏相同时按名字排序。这正是cmp的任务!

     @sorted = sort {
         ($c) = ($a =~ / (\w+)/);
  ($d) = ($b =~ / (\w+)/);
  if ($c eq $d) {   # Last names are the same, sort on first name
      ($c) = ($a =~ /^(\w+)/);
      ($d) = ($b =~ /^(\w+)/);
      return $c cmp $d;
  } else {
      return $c cmp $d;
  }
     } @phone_numbers;
     for $i (@sorted) { print $i, "\n"; }

不要相信任何人

现在我们知道了CGI程序可以完成你想做的事情,但让我们确保它们不会做你想做的事情。这比看起来要难,因为你不能相信任何人会按照你的期望行事。

这里有一个简单的例子:你想要确保HTTP日志分析器每次报告最多显示50个项目,因为将较大的报告发送给用户需要太长时间。简单的方法就是从我们的HTML表单中删除“全部”行,这样剩下的选项就只有10、20和50。这会非常简单——但却是错误的。

下载具有安全增强功能的HTTP分析器的源代码.

我们在将披萨配料示例代码粘贴到我们的backatcha页面时看到了你可以修改HTML表单。你还可以使用URL将表单项传递给脚本——尝试在浏览器中访问https://perldotcom.perl5.cn/2000/12/backatcha.cgi?itemsource=URL&typedby=you编辑注:或者不要这样做,因为这个链接已经不再有效)。显然,如果有人可以用backatcha脚本这样做,他们也可以用你的日志分析器,并将任何他们想要的值放入number中:“全部”或“25000”,或者“四十年七年前。”

你说你的表单不允许这样做。这有关系吗?人们会编写定制的HTML表单来利用你程序中的弱点,或者直接将错误的表单项传递给你的脚本。你不能相信用户或他们的浏览器告诉你的任何事情。

通过知道你期望用户输入什么,并禁止其他一切,你可以消除这些问题。你明确不允许的任何内容都是完全禁止的。安全的CGI程序在确认任何内容是无辜之前,都将其视为有罪。

例如,我们想限制我们HTTP日志分析器生成的报告的大小。我们决定这意味着number表单项必须有一个介于10和50之间的值。我们将这样验证它

    # Make sure that the "number" form item has a reasonable value
    ($number) = (param('number') =~ /(\d+)/);
    if ($number < 10) {
        $number = 10;
    } elsif ($number > 50) {
        $number = 50;
    }

当然,我们还需要更改report_section()子程序,使其使用$number变量。现在,无论用户尝试告诉你的日志分析器number的值是“10”、“200”、“432023”、“全部”还是“redrum”,你的程序都会将其限制在合理的值。

我们不需要对report做任何事情,因为我们只在我们期望的值之一时采取行动。如果用户尝试输入除了我们明确允许的值(“url”、“status”、“hour”或“type”)之外的内容,我们只需忽略它。

在你知道用户应该输入什么的地方使用这种逻辑。你可能会使用s/\D//g来从应该是数字的项目中删除非数字字符(然后测试以确保剩下的内容在你的允许的数字范围内!),或者使用/^\w+$/来确保用户输入了一个单词。

这一切有两个显著的好处。首先,你简化了错误处理代码,因为你在程序的早期就确保你在处理有效数据。其次,通过减少可能帮助攻击者破坏你的系统或干扰你Web服务器上其他用户的“不可能”值,你增加了安全性。

虽然如此,不要只听我的话。有关Perl中安全的CGI编程的更多信息,请参阅CGI安全FAQ,其中包含比你能想到的还要多的信息,包括一个列出一些真实CGI程序中的安全漏洞的部分。

玩一玩!

你现在应该足够了解CGI编程,可以编写有用的Web应用程序了。(哦,你还学到了一些有关排序和比较的知识。)

  1. 编写经典的CGI程序:一个留言簿。用户可以输入他们的姓名、电子邮件地址和一条简短的消息,这些消息将被追加到HTML文件中供所有人查看。

    小心!永远不要相信用户!一个良好的初始预防措施是禁止所有HTML,方法是从用户的所有信息中删除<和>字符,或者用<>字符实体来替换它们。

    也可以使用substr()来截取用户输入的内容,使其保持在一个合理的长度。要求“简短”的消息并不能阻止用户将一个500k的文件倒入消息字段中!

  2. 编写一个与用户玩井字棋的程序。确保计算机AI在子程序中,以便容易升级。(你可能需要稍微研究一下HTML,以了解如何输出井字棋盘。)

标签

反馈

这篇文章有问题吗?请在GitHub上打开一个问题或拉取请求来帮助我们。