为何DirectoryIndex指向Perl CGI脚本时无需手动输出Content-type?
问题分析与解答
操作背景
修改Apache的mods-available/dir.conf文件,设置默认索引页为Perl CGI脚本:
DirectoryIndex /cgi-bin/csv_auth
对应的csv_auth脚本代码如下:
#! /usr/bin/perl package Logger; use strict; use warnings; use POSIX qw( strftime ); sub debug { my $msg = shift; mkdir '/tmp/authorize'; my $fn = '/tmp/authorize/auth.txt'; open (my $fh, '>>', $fn) or die "Could not open file $fn: $!"; my $timestamp = strftime("%Y-%m-%d %H:%M:%S", localtime); say $fh "$timestamp : $msg"; } package CGI::Session::Auth::CSV; use strict; use warnings; use parent 'CGI::Session::Auth'; use Text::CSV; # Need to override new() sub new { my $class = shift; my ($params) = shift; Logger::debug('CGI::Session::Auth::CSV::new() called'); $class = ref($class) if ref($class); # initialize parent class my $self = $class->SUPER::new($params); bless($self, $class); return $self; } # Override the _login method to use a CSV file sub _login { my $self = shift; my ($user, $pass) = @_; $self->{username} = $user; Logger::debug('CGI::Session::Auth::CSV::_login() called'); my $csv = Text::CSV->new; open my $fh, '<', 'login.csv' or die "Can't open login.csv: $!"; # Loop through the rows and check for a match while (my $row = $csv->getline($fh)) { Logger::debug("CGI::Session::Auth::CSV::_login() : user and psswrd : $user $pass"); Logger::debug("CGI::Session::Auth::CSV::_login() : CSV row 0 and 1 : $row->[0] $row->[1]"); if ($row->[0] eq $user and $row->[1] eq $pass) { # Found a match, return true close $fh; Logger::debug("CGI::Session::Auth::CSV::_login() : found match for username: $user"); #return 1; } } # No match, return false close $fh; #return 0; } package main; use feature qw(say); use strict; use warnings; use CGI(-utf8); use CGI::Carp qw(fatalsToBrowser); use CGI::Cookie; use CGI::Session; use Data::Dumper qw(Dumper); use utf8; use File::Copy; # CGI object for headers, cookies, etc. my $cgi = CGI->new(); my $user = $cgi->param('log_username'); my $password = $cgi->param('log_password'); my $cmd = $cgi->param('cmd'); my %cgi_vars = $cgi->Vars; Logger::debug('cgiscript: rp.cgi : query parameters: ' . Dumper(\%cgi_vars)); Logger::debug('cgiscript: rp.cgi : cookies: ' . (join ',', $cgi->cookie())); Logger::debug('cgiscript: rp.cgi : cookie: CGISESSID : ' . Dumper($cgi->cookie('CGISESSID'))); # CGI::Session object for session handling my $session = CGI::Session->new(undef, $cgi, {Directory=>'/tmp/authorize'}); Logger::debug('cgiscript: rp.cgi : session = ' . $session->dump()); my $session_params = $session->dataref(); Logger::debug('cgiscript: rp.cgi : session data : ' . Dumper($session_params)); my $session_params_text = Dumper($session_params); if (defined $user) { $session->param('username', $user); $session->flush(); } else { $user = $session->param('username'); } my $cookie = $cgi->cookie( -name => $session->name, -value => $session->id ); print $cgi->header( -cookie=>$cookie ); # CGI::Session::Auth object for authentication my $auth = CGI::Session::Auth::CSV->new({CGI => $cgi, Session => $session}); Logger::debug('cgiscript: rp.cgi : auth object 1 : ' . Dumper($auth)); Logger::debug('cgiscript: rp.cgi : calling authenticate'); $auth->authenticate(); Logger::debug('cgiscript: rp.cgi : auth object 2 : ' . Dumper($auth)); if ($cmd eq 'logout') { logout($auth, $user); exit; } # check if visitor has already logged in if ($auth->loggedIn) { Logger::debug('cgiscript: rp.cgi : user is logged in'); print <<HTML !DOCTYPE html> <html> <body> <h3>Hello you are lógged in $user</h3> <p><a href='rp.cgi?cmd=logout'>Log out</a></p> </body> </html> HTML } else { Logger::debug('cgiscript: rp.cgi : user is not logged in'); show_login_page(); } $session->flush(); sub show_login_page { my $self = shift; print <<HTML; <!DOCTYPE html> <html> <head><title>Not logged in</title></head> <body> <h1>You are not logged in</h1> <p>Please log in to see the secret page:</p> <form action='/cgi-bin/rp.cgi' method='POST'> <p><input type='text' size='30'' name='log_username'</p> <p><input type='password' size='30' name='log_password'</p> <p><input type='submit'></p> </form> </body> </html> HTML } sub logout { my ($auth, $user) = @_; auth->logout(); print <<HTML; <!DOCTYPE html> <html> <head><title>Logged out</title></head> <body> <h1>You have logged out user $user.</h1> <p>Please log in to see the secret page:</p> <form action='maing.cgi' method='POST'> <p><input type='text' size='30' name='log_username'></p> <p><input type='email' size='30' name='log_password'></p> <p><input type='submit'></p> </form> </body> </html> HTML }
现象描述
执行脚本后,浏览器显示内容包含Content-type行:
Content-type: text/html; charset=utf-8 Hello you are logged in marcos
对应的响应头信息:
HTTP/1.1 200 OK Date: Thu, 03 Aug 2023 05:02:39 GMT Server: Apache/2.4.56 (Debian) Set-Cookie: CGISESSID=fc23b50f1b15cbf978e5f797fddc9986; path=/ Vary: Accept-Encoding Content-Encoding: gzip Content-Length: 240 Keep-Alive: timeout=5, max=100 Connection: Keep-Alive Content-Type: text/html; charset=ISO-8859-1
请求头信息:
GET / HTTP/1.1 Host: recordspreservation.org User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive Cookie: CGISESSID=fc23b50f1b15cbf978e5f797fddc9986 Upgrade-Insecure-Requests: 1 Sec-GPC: 1
问题解答
1. 为什么浏览器会显示Content-type行?
这是因为你手动打印了Content-type:字符串,且没有遵循CGI响应格式规范:
- CGI要求响应头和正文之间必须有一个空行。如果没有这个空行,Apache会把所有输出内容当作正文处理,所以浏览器会把
Content-type:字符串显示出来。 - 你的脚本中已经通过
print $cgi->header( -cookie=>$cookie );正确输出了响应头(包括Content-Type)和空行,这部分是正常的。但如果脚本中存在其他地方手动打印Content-type:字符串,就会被当作正文内容显示。
2. Perl CGI脚本是否需要手动输出Content-Type行?
必须输出Content-Type头,但不能手动打印字符串,要通过CGI模块的方法生成:
- CGI规范明确要求,所有CGI脚本必须输出响应头,Content-Type是必填项(除非是HTTP 204等无正文的状态码)。
- 使用Perl的CGI模块时,
$cgi->header()方法会自动生成符合规范的Content-Type头(你指定了-utf8参数,应该输出text/html; charset=utf-8),同时自动添加头和正文之间的空行,这是正确的做法。 - 绝对不要手动打印
Content-type:字符串,否则会导致它出现在浏览器的正文内容里。
脚本额外问题修正
你的脚本还有几个小错误,建议修正:
logout函数中auth->logout();缺少变量符号,应改为$auth->logout();,否则会引发语法错误。- 登录成功的HTML中
lógged拼写错误,应改为logged。 show_login_page函数里的输入框size='30''多了一个单引号,应改为size='30'。logout函数里的表单action是maing.cgi,但你的脚本入口是csv_auth或rp.cgi,路径不匹配,建议修正为正确的脚本路径。
内容的提问来源于stack exchange,提问作者Marcos Camargo
相关产品推荐
相关产品推荐

