如果您正在操作Web服务器,那么您很可能不仅仅是在提供静态文本和图像。您可能也在运行一些Web应用程序,在这些应用程序中,某些程序或脚本使用CGI(通用网关接口)动态生成页面。考虑到博客软件、bug跟踪器、新闻网站和内容管理系统--任何将浏览器从文档查看器转变为用户界面的东西。而且,您可能会自己编写或至少调整其中的一些内容。
本文展示了如何使用称为SCGI(简单通用网关接口)的CGI替代方案来构建速度更快的Web应用程序。SCGI是一个协议,而不仅仅是一个程序,但它的作者也提供了一个参考实现,这就是我们在这里使用的。它包括从Apache或lighttpd和Python类使用SCGI的模块,以帮助您创建SCGI应用程序。其他语言的实现也是可用的,但是我们在这里研究Apache2.x和Python的组合。
通常,Web应用程序在Web服务器的子进程中短暂但非常频繁地运行。当客户端请求页面时,Web服务器查询其配置,发现请求应该转到应用程序。它将请求委派给子进程,子进程随后加载并运行应用程序。程序可以是二进制的,也可以是Perl、Python或PHP的脚本、shell命令或其他任何东西。CGI标准定义了程序如何接收有关请求的详细信息,包括请求的URL、请求的正文、经过身份验证的用户身份和源IP地址。程序读取这些内容,生成一个页面来响应客户端的请求,然后退出。所有这些都会在下一次请求时再次发生。
加载、运行和退出程序的成本可能很高。对于草率的程序来说,这确实是有意义的:例如,它们可能会使用内存,而再也不会释放内存。在这种情况下,您希望程序短暂运行,然后让操作系统在运行后进行清理。但是,以今天流行的语言--Perl、Python、PHP、Java和Shell脚本--来说,这确实没有太多问题。一个编写良好的应用程序确实应该能够在一次运行中处理多个请求。
SCGI允许您的程序启动一次,并根据需要继续为请求提供服务。它的工作原理如下:一个独立的服务器进程(称为SCGI服务器)独立于Web服务器运行,并管理一个Web应用程序。Web服务器将该应用程序的所有请求转发到该应用程序的SCGI服务器。它以与常规CGI几乎相同的形式传递有关请求的详细信息。
SCGI服务器将请求委托给子进程,就像Web服务器对常规CGI应用程序所做的那样。子进程也会运行应用程序,但相似之处到此为止。应用程序可以静静等待新的请求,而不是在处理完那个请求后退出。SCGI服务器的每个子进程都运行应用程序的一个实例,每个进程都处于休眠状态,直到有工作要做。
当没有新的子进程可用于处理最新的请求时,SCGI服务器会生成一个新的子进程-当然,最多可以达到一个可配置的最大值。它还可以清理崩溃或退出的子进程,因此如果出现问题,您的Web应用程序仍然可以脱离困境。但是,大多数情况下,当请求到达时,应用程序已经准备好并等待它。这就是为什么Web应用程序框架Ruby on rails附带了在SCGI上运行的选项;否则它会太慢。
如果加速对您来说还不够,还有更多。SCGI服务器进程可以与Web服务器运行在同一系统上,但不必如此。您可以通过将一些Web应用程序委托给单独的系统来卸载服务器,最好是在防火墙后面,在那里只有Web服务器可以访问它们。
即使只有一台服务器,您也可以使用SCGI来保证无懈可击。正常的CGI应用程序以与Web服务器进程相同的用户身份启动。如果攻击者设法翻转正常的CGI应用程序,您的整个网站可能会处于危险之中。另一方面,AnSCGI服务器可以在它自己的用户身份下运行,因此即使它确实不正常地运行,也不会轻易影响Web服务器或其他应用程序。相反,您不再需要授予Web服务器访问应用程序的代码或数据的权限,只需要访问SCGI服务器运行的应用程序即可。其他所有人都必须通过Web服务器,该服务器依次与SCGI服务器通信。
您还可以在chroot环境或虚拟化服务器中运行应用程序。使用CGI,这很快就会变得昂贵且难以管理。使用SCGI时,您只需在隔离环境中启动一个服务器进程-无论它是chroot监狱、虚拟化服务器、不同的用户身份还是另一台计算机-整个应用程序都会留在那里。
您需要两个组件:用于构建SCGI应用程序的Python类和用于使Web服务器对应用程序“讲话SCGI”的模块。如果您使用Red Hat软件包管理(RPM),则可以使用yum install python-scgiapache2-mod_scgi;安装这些软件包。Debian APT用户可以使用apt-get install python-scgilibapache2-mod-scgi。
您也可以手动安装任一组件。Apache模块需要C编译器和Apache的APXS脚本。有些发行版将apxs保存在单独的开发包中,而不是将其作为常规Apache包的一部分进行安装。
假设您现在有了这些组件,接下来下载源tarball scgi-1.12.tar.gz,并运行清单1中所示的命令。
#从tarball tar xzf scgi-1.12.tar.gzcd scgi-1.12解压源目录scgi-1.12#构建Python partpython setup.py build#安装Python模块;我们需要root特权sudo python setup.py install#现在构建并安装Apache module ecd apache2sudo make install#在Apache中启用SCGI模块。这可能会失败,#取决于您的Apache版本,但没关系。sudo a2enmod scgi#使Apache的新配置生效sudo/etc/init.d/apache2强制重新加载。
现在,让我们确保一切正常。Python包是一个带有一些类的模块,通常情况下,您需要将您的应用程序编写为一个移植该模块的程序。但是,对于调试,您也可以将其作为独立的应用程序运行。当它收到来自Web服务器的请求时,它只是将请求的详细信息打印为文本页面。非常适合第一次测试-不需要编码!
在您的系统上找到scgi_server.py模块。它应该是installedin/usr/lib/python2.4/site-package/scgi(2.4在您的系统上可能是2.3或2.5)。然后,运行该模块:
这将在系统的TCP端口上侦听来自Web服务器的请求,默认情况下使用端口4000。您可以通过将所需的端口号作为命令行参数传递,使其侦听不同的端口,例如:
模块会一直运行,直到你杀死它,所以在一个单独的外壳中启动它。请记住,即使在Web服务器的身份下,您也不需要以Rootor身份运行SCGI服务器。
既然SCGI应用程序正在等待请求,那么在您的网站上选择一个位置来委托给该应用程序。假设您希望它响应此服务器上对“/scgitest”的所有请求。将清单2所示的Apache配置片段写入/etc/apache2/conf.d中的一个新文件。
#加载SCGI模块。只有在手动安装并且";a2enmod scgi#命令失败的情况下才真正需要此功能。LoadModule scgi_module/usr/lib/apache2/modules/mod_scgi.so<;Location";/scgitest";>;#在#其他属性(如Access#control#...<;/location>;)上启用SCGI SCGIHandler。#运行#/scgitest的SCGI服务器的主机名和端口号。#localhost(127.0.0.1)上的端口4000是默认值。SCGIMount/scgitest 127.0.0.1:4000。
正如您在这里看到的,SCGI服务器实际上不需要与Web服务器运行在同一台计算机上。只需确保SCGI服务器的端口已正确安装防火墙,这样只有您的Web服务器才能访问它!这样,您的应用程序可以确保所有的CGI参数都已首先由Web服务器验证。如果攻击者可以直接连接到您的SCGI应用程序,您将无法信任该信息。例如,CGI参数AUTHENTATED_USER告诉您的应用程序请求来自特定的登录用户。只有当您从正确配置的Web服务器听到它时,您才能相信这一点。
使用sudo/etc/init.d/apache2reload让Apache重新加载其配置。您的服务器现在应该服务于一个新位置/scgitest,当您访问它时,它只会打印您的请求的CGI参数。通过在浏览器中查找来验证这一点。如果您的服务器地址是example.org,请将浏览器指向http://example.org/scgitest.。您应该会看到一个如清单3所示的页面。
服务器软件:';Apache';script_name:';/scgitest&39;request_method:';get';server_protocol:';HTTP/1.1';query_string:';';content_length:';0';HTTP_Accept_Charset:';UTF-8,*';HTTP_USER_AGENT:';Mozilla/5.0';Server_name:';testserver.example.org';Remote_ADDR:';10.99.11.99';server_Port:';80';server_addr:';192.0.34.166';document_root:';/srv/www/';server_admin:';[email protected]';HTTP_host:';testserver.example.org';REQUEST_URI:';/scgitest';HTTP_ACCEPT:';text/html,文本/纯文本,*/*;q=0.5';远程端口:';47088';HTTP_ACCEPT_LANGUAGE:';EN';SCGI:';1';HTTP_ACCEPT_ENCODING:';GZIP,DEFLATE&39;
如果这不是您看到的,请看一下您放置模块的shell。它可能在那里打印了一些有用的错误消息。或者,如果SCGI服务器没有任何反应,则可能是请求一开始就没有到达它;请检查Apache错误日志。
运行此程序后,恭喜您-最糟糕的情况已经过去。请停止您的SCGI服务器进程,使其不会干扰我们下一步要重新执行的操作。
我们导入SCGI Python模块,然后编写应用程序作为通过Web服务器传入的SCGI请求的处理程序。处理程序采用我们从SCGIHandler派生的类的形式。CALLME没有想象力,但我已经将示例处理程序称为classTimeHandler。我们稍后将填写实际代码,但先从这个框架开始:
#!/usr/bin/pythonimport scgiimport scgi.scgi_serverclass TimeHandler(scgi.scgi_server.SCGIHandler):pass#(这里还没有代码)#main程序:创建一个SCGIServer对象以#侦听端口4000。我们告诉SCGIServer实现我们的应用程序的#HANDLER类。server=scgi.scgi_server.SCGIServer(HANDLER_CLASS=TimeHandler,port=4000)#告诉SCGIServer开始服务请求。#this loops forever.server.serve()。
您可能会觉得奇怪,我们必须向SCGIServer传递我们的Handler类,而不是处理程序对象。原因是服务器对象将根据需要创建给定类的处理程序对象。
TimeHandler的第一个实例在本质上仍然与最初的SCGIHandler相同,因此它所做的一切就是打印请求参数。要在实际操作中看到这一点,请尝试运行此程序并像以前一样在浏览器中打开scgitestpage。您应该再次看到类似清单3的内容。
现在,我们想以ABROWER能理解的形式打印时间。我们不能简单地开始发送文本或HTML;我们首先必须发出一个HTTP标头,告诉浏览器需要什么样的输出。在这种情况下,让我们继续使用简单文本。在程序顶部附近TimeHandler类定义的正上方添加以下内容:
Import timedef print_time(Outfile):#描述我们大约要生成的页面的HTTP头。必须以双MS-DOS样式的#";CR/LF&34;行尾序列结束。在Python中,#转换为";\r\n.outfile.write(";Content-Type:Text/Plain\r\n\r\n";)#现在以纯文本outfile.write(time.ctime()+";)#写入我们页面:时间。
到目前为止,您可能想知道如何让我们的处理程序类调用this函数。使用SCGI 1.12或更高版本,这很容易。我们可以编写一个method TimeHandler.Production()来覆盖SCGIHandler的默认操作:
类TimeHandler(scgi.scgi_server.SCGIHandler):#(删除";pass";语句--现在我们在这里有了真正的#代码)#这是我们接收请求的地方:def Production(self,env,bodysize,input,output):#做我们的工作:使用输出PRINT_TIME(OUTPUT)的时间写入页面。
我们在这里忽略它们,但是Production()有几个参数:env是禁止将CGI参数名映射到它们的值的。接下来,BodySize是请求正文或有效负载的大小(以字节为单位)。如果您对请求正文感兴趣,可以从以下参数INPUT中读取最大正文大小的字节。最后,输出是我们将输出页面写入到的文件。
如果您使用的是SCGI 1.11或更早版本,则需要一些包装器代码来实现此功能。在这些旧版本中,您可以覆盖不同的方法SCGIHandler.handleconnection(),并且您可以自己完成更多的工作。简单地将清单4中的样板代码复制到TimeHandler类中。它将进行正确的设置并调用Production(),因此其他内容不会改变,我们可以编写Production(),就像我们有了较新版本的SCGI一样。
#将此定义插入您的处理程序类:类TimeHandler(scgi.scgi_server.SCGIHandler):#...。Def handle_connection(self,conn):input=connec.makefile(";r";)output=connec.makefile(";w";)env=self.read_env(Input)bodysize=int(env.get(';content_length';,0))try:self.product(env,bodysize,input,output)last:output.close()input.close()Conn.close()。
接下来,为了让事情更有趣,让我们将一些参数传递给request,并让程序处理它们。Web应用程序的参数约定是在URL上加一个问号,后跟一系列参数,参数之间用“与”号分隔。每个参数的格式为name=value。如果我们想要向程序传递一个名为Pizza、值为Hawaii的参数,以及另一个名为Drink、值为Beer的参数,我们的URL看起来应该是likehttp://example.org/scgitest?pizza=hawaii&;drink=beer.。
访问者传递给程序的任何参数都以单个CGI参数query_string结束。在本例中,参数应该是“Pizas=Hawaii&;Drink=Beer”。下面是我们的TimeHandler可以用它做的一些事情:
Class TimeHandler(scgi.scgi_server.SCGIHandler):def Production(self,env,bodysize,input,output)#读取参数argstring=env[';query_string';]#将参数字符串分解为#对的列表,如";name=value";arglist=argstring.plit(';&;';)#为arglist中的arg设置将参数名称#映射到值args={}的字典:(key,value)=arg.plit(';=';)args[key]=value#打印时间,与以前一样,但带有一点额外建议print_time(Output)output.write(";吃披萨的时间。我会吃%s,然后大口喝%s!\n&34;%(args[';披萨],args[#39;饮料])),我会吃%s,喝%s!\n#34;%(args[';披萨],args[#39;饮料]))。
现在,我们编写的应用程序不仅打印时间,还建议在URL中传递披萨和饮料。试试看!您还可以尝试清单3中的其他CGI参数,以查找您的SCGI应用程序可以做的更多事情。
一旦您习惯了使用SCGI编写程序,您可能希望调整现有的应用程序以使用它。一些著名的Web应用程序(如MoinMoin(维基)和Trac(基于维基的协作开发环境))被实现为Python模块。这两个示例都带有可以从Apache调用的Python CGI脚本。CGIscripts非常短;除了导入应用程序的模块并调用它们上的函数外,它们实际上不做任何事情。
如果您找到这样一个应用程序,您真正需要做的就是将少量的Python代码移到Production()方法中,如您在这里看到的示例所示。如果您使用的是SCGI1.12或更高版本,您可能还想看看另一个SCGIHandler方法Production_cgilike()。
这差不多是我们能容纳的全部地方了。如果您想知道CGI参数是如何工作的,请尝试查看CGI标准,该标准将它们称为“请求元变量”(请参阅参考资料)。
最后,给你一个警告。您将注意到,如果您不能传递预期的参数,那么最后一个示例程序就会死得很惨。SCGIS服务器会替换失败的进程,因此在这种情况下,没有真正的问题。但是,这应该会提醒您在编写Web应用程序时需要多么小心。永远不要相信你从外面收到的信息!如果一个程序可以崩溃,那么很可能有人会颠覆它,或者让它停止运行。世界各地的人都是为了好玩或盈利而做这样的事,所以要认真对待风险。
Jeroen Vermeulen就职于泰国软件产业促进局的开源部门。他目前正在研究Suriyan,这是一个为那些没有时间使用服务器系统的人设计的服务器系统。