Perl初学者入门 - 第四部分
编辑注:这个值得尊敬的系列正在进行更新。你可能对更新的版本感兴趣,可在以下位置找到:
现在是CGI时间了
Perl初学者入门 |
•本系列的第1部分 |
到目前为止,我们讨论了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程序的简要步骤
- 确保程序放置的位置可以让您的Web服务器将其识别为CGI脚本。这可能是特殊的
cgi-bin
目录,或者确保程序的文件名以.pl
或.cgi
结尾。如果您不知道程序应该放在哪里,您的ISP或系统管理员应该知道。 - 确保程序可以被服务器运行。如果您使用的是Unix系统,您可能需要为Web服务器用户授予对程序的读取和执行权限。最简单的方法是使用
chmod filename 755
为所有人赋予这些权限。 - 记下程序的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日志分析器。为什么不让它实现网络功能呢?这将允许您在任何可以访问浏览器的地方查看您的使用情况。
首先,让我们决定我们想要如何使用我们的分析器。我们不一次性显示我们生成的所有报告,而是只显示用户选择的报告。其次,我们将让用户选择每个报告是显示整个项目列表,还是按访问次数排序的前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
比较字符串。lt
和gt
运算符是<
和>
的字符串等效,而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。这会非常简单——但却是错误的。
我们在将披萨配料示例代码粘贴到我们的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应用程序了。(哦,你还学到了一些有关排序和比较的知识。)
编写经典的CGI程序:一个留言簿。用户可以输入他们的姓名、电子邮件地址和一条简短的消息,这些消息将被追加到HTML文件中供所有人查看。
小心!永远不要相信用户!一个良好的初始预防措施是禁止所有HTML,方法是从用户的所有信息中删除<和>字符,或者用
<
和>
字符实体来替换它们。也可以使用
substr()
来截取用户输入的内容,使其保持在一个合理的长度。要求“简短”的消息并不能阻止用户将一个500k的文件倒入消息字段中!编写一个与用户玩井字棋的程序。确保计算机AI在子程序中,以便容易升级。(你可能需要稍微研究一下HTML,以了解如何输出井字棋盘。)
标签
反馈
这篇文章有问题吗?请在GitHub上打开一个问题或拉取请求来帮助我们。