Bug 122560 - Add error pages, add CGI version of SVNPropCheck

git-svn-id: svn://svn.office.custis.ru/3rdparty/bugzilla.org/trunk@1765 6955db30-a419-402b-8a0d-67ecbb4d7f56
custis
vfilippov 2013-08-16 11:40:00 +00:00
parent ee2ce6e57e
commit f1f79d5446
2 changed files with 342 additions and 16 deletions

View File

@ -41,6 +41,7 @@ $server->run();
package Bugzilla::HTTPServerSimple;
use Bugzilla;
use Bugzilla::Util qw(html_quote);
use Time::HiRes qw(gettimeofday tv_interval);
use IO::SendFile qw(sendfile);
use POSIX qw(strftime);
@ -100,12 +101,25 @@ sub net_server
return $self->{_config_hash}->{class};
}
sub print_error
{
my $self = shift;
my ($status_code, $status_line, $error_text) = @_;
warn $error_text;
print $self->{_cgi}->header(-status => $status_code).
"<html><head><title>$status_line</title></head>".
"<body><h1>$status_line</h1><p>".html_quote($error_text).
"</p><hr /><p>".$ENV{SERVER_SOFTWARE}."</p></body></html>";
return $status_code;
}
sub handle_request
{
my $self = shift;
my ($cgi) = @_;
# Set non-parsed-headers CGI mode
$cgi->nph(1);
$self->{_cgi} = $cgi;
$CGI::USE_PARAM_SEMICOLONS = 0;
# Determine SCRIPT_FILENAME
my $script = $ENV{SCRIPT_FILENAME};
@ -115,12 +129,17 @@ sub handle_request
($script) = $ENV{REQUEST_URI} =~ m!/+([^\?\#]*)!so;
}
$script ||= 'index.cgi';
# Check access
if ($self->{_config_hash}->{deny_regexp} &&
$script =~ /$self->{_config_hash}->{deny_regexp}/s)
{
return $self->print_error('403', 'Access Denied', "You are not allowed to access URL $script on this server.");
}
# Serve static files (should be done by nginx, but we support it for completeness)
my $fd;
$script =~ s!^/*!!so;
if (($script !~ /\.cgi$/iso || $script =~ /\//so) && open $fd, '<', $script)
{
print "HTTP/1.0 200 OK\r\n";
print $cgi->header(-type => guess_media_type($script), -Content_length => -s $script);
sendfile(fileno(STDOUT), fileno($fd), 0, -s $script);
close $fd;
@ -156,6 +175,10 @@ sub handle_request
{
my $start = [gettimeofday];
eval "$preload package main; $content";
if ($@)
{
return $self->print_error(500, 'Internal Server Error', "Error while running $script:\n$@");
}
my $elapsed = tv_interval($start) * 1000;
print STDERR strftime("[%Y-%m-%d %H:%M:%S]", localtime)." Served $script via require() in $elapsed ms\n";
return 200;
@ -166,8 +189,7 @@ sub handle_request
$subs{$script} = eval "package main; sub { $preload$content }";
if ($@)
{
warn "Error while loading $script:\n$@";
return 500;
return $self->print_error(500, 'Internal Server Error', "Error while loading $script:\n$@");
}
}
# Run cached sub
@ -178,7 +200,7 @@ sub handle_request
eval { &{$subs{$script}}(); };
if ($@ && (!ref($@) || ref($@) ne 'Bugzilla::HTTPServerSimple::FakeExit'))
{
warn "Error while running $script:\n$@";
return $self->print_error(500, 'Internal Server Error', "Error while running $script:\n$@");
}
eval { Bugzilla::_cleanup(); };
if ($@ && (!ref($@) || ref($@) ne 'Bugzilla::HTTPServerSimple::FakeExit'))
@ -188,8 +210,9 @@ sub handle_request
$in_eval = 0;
my $elapsed = tv_interval($start) * 1000;
print STDERR strftime("[%Y-%m-%d %H:%M:%S]", localtime)." Served $script in $elapsed ms\n";
return 200;
}
return 404;
return $self->print_error(404, 'Not Found', "The requested URL $script was not found on this server.")
}
# Override bad HTTP::Server::Simple::parse_headers implementation with a good one
@ -248,17 +271,17 @@ log_level 2
pid_file /var/run/bugzilla.pid
background 1
'http_env' specifies which environment variables to set from
a corresponding 'X-<name>' HTTP header (value is comma-separated).
For example to support multiple Bugzilla 'projects' specify
# HTTP 403 Access Denied will be shown for URLs matching deny_regexp:
# You are URGED also to disable these URLs on your frontend.
deny_regexp ^(localconfig|data/|.*\.(pm|pl|sh)($|\?)|.*\.(ht|svn|hg|bzr|git).*)
# 'http_env' specifies which environment variables to set from
# a corresponding 'X-<name>' HTTP header (value is comma-separated).
# For example to support multiple Bugzilla 'projects' specify:
http_env PROJECT
And specify an appropriate project in 'X-Project' header on your frontend.
For example, for nginx:
proxy_set_header X-Project 'project';
Or for Apache:
RequestHeader set X-Project project
# For http_env to work you need to push an appropriate header from your
# frontend. For example, for nginx:
# proxy_set_header X-Project 'project';
# Or for Apache:
# RequestHeader set X-Project project

303
SVNPropCheck.pm Normal file
View File

@ -0,0 +1,303 @@
#!/usr/bin/perl
# HTTP-апплет для проксирования запросов к Subversion-репозиториям
# с проверкой свойств по регулярному выражению или просто на существование
# (файлы, для которых проверка не удаётся, представляются как несуществующие)
# Для использования создайте файл svn.cgi со следующим содержимым:
#
# use URI::Escape;
# use SVNPropCheck;
# my $obj = SVNPropCheck->instance("instance name", { Хеш конфигурации });
# $obj->handler(uri_unescape($ENV{QUERY_STRING}));
#
# И обращайтесь к нему в духе /svn.cgi?<svn path>
package SVNPropCheck;
use strict;
use POSIX qw(strftime);
use Encode qw(from_to);
use File::Path 2.06 qw(make_path);
use IO::SendFile qw(sendfile);
use LWP::MediaTypes;
use SVN::Core;
use SVN::Client;
use SVN::Ra;
my $instances = {};
# кэш объектов SVN::Ra
my $RAS = {};
# Получение именованного экземпляра SVNPropCheck. Именованного - чтобы в одном
# Perl-интерпретаторе могло жить несколько SVNPropCheck'ов.
#
# use SVNPropCheck;
# my $obj = SVNPropCheck->instance("instance name", { Хеш параметров });
#
# Параметры конфигурации:
# 1. repos_url - URL к репозиторию Subversion, из которого будут браться файлы.
# В случае, если параметр не указан или имеет ложное значение, но задан
# параметр repos_parent, первый компонент всех дочерних URI берётся в качестве
# имени репозитория и приписывается к repos_parent.
# Пример: "https://svn.office.custis.ru/3rdparty/"
# 2. repos_parent - родительский URL, приписывая имя конкретного репозитория к
# которому, можно получать URL отдельных репозиториев.
# Пример: "https://svn.office.custis.ru/"
# 3. repos_username - имя пользователя Subversion (нужен доступ только на чтение)
# 4. repos_password - пароль пользователя Subversion (нужен доступ только на чтение)
# 5. check_prop_name - название свойства, значение которого делает файлы доступными
# Пример: "wiki:visible"
# 6. check_prop_re - регулярное выражение для проверки значения свойства.
# В случае, если параметр не указан или имеет значение undef, указанное
# свойство просто должно быть задано.
# 7. cache_path - директория локального кэша файлов.
# 8. enc_from_to - массив из двух названий кодировок. Первая из них - входная
# кодировка обрабатываемых адресов, вторая - кодировка, в которой имена файлов
# должны передаваться библиотекам Subversion для доступа. Параметр необязательный,
# и если он не указан, перекодировка не осуществляется.
# Пример: [ "cp1251", "utf8" ]
# 9. access_log - если true, то логгировать все запросы на STDERR
# 10. mime_types - путь к файлу /etc/mime.types или подобному
sub instance
{
my $class = shift;
$class = ref($class) || $class;
my ($instance_name, $params) = @_;
if ($instances->{$instance_name})
{
return $instances->{$instance_name};
}
my $ra;
unless ($params->{cache_path} &&
$params->{repos_username} && exists $params->{repos_password})
{
# ругаемся
warn __PACKAGE__.": parameters cache_path, repos_username, repos_password are mandatory";
return undef;
}
$params->{cache_path} =~ s!/+$!!so;
my $auth_providers = [
SVN::Client::get_ssl_server_trust_prompt_provider(sub {
$_[0]->accepted_failures(
$SVN::Auth::SSL::NOTYETVALID |
$SVN::Auth::SSL::EXPIRED |
$SVN::Auth::SSL::CNMISMATCH |
$SVN::Auth::SSL::UNKNOWNCA |
$SVN::Auth::SSL::OTHER
);
}),
SVN::Client::get_simple_provider(),
SVN::Client::get_simple_prompt_provider(sub {
$_[0]->username($params->{repos_username});
$_[0]->password($params->{repos_password});
}, 3),
];
if ($params->{repos_url})
{
# открываем репозиторий
$ra = SVN::Ra->new(
url => $params->{repos_url},
auth => $auth_providers,
);
}
if (!$ra && !$params->{repos_parent})
{
# ругаемся
warn __PACKAGE__.": need one of correct repos_url or repos_parent";
return undef;
}
# создаём объект себя
my $self = bless {
params => $params,
ra => $ra,
auth_prov => $auth_providers,
}, $class;
if (!%$instances)
{
# на первый раз инициализируем LWP::MediaTypes
# он всё равно глобальный, так что смысла
# всасывать типы из разных файлов нет
LWP::MediaTypes::read_media_types($params->{mime_types} || '/etc/mime.types');
}
$instances->{$instance_name} = $self;
return $self;
}
# Отправить сообщение об ошибке
sub print_error
{
my ($errmsg, $diemsg) = @_;
$diemsg ||= '';
$diemsg =~ s/ at \S+ line \d+.*$//so;
$errmsg =~ s/\.*$/./so;
$errmsg .= ":\n$diemsg" if $diemsg;
print STDERR (strftime("[%Y-%m-%d %H:%M:%S] ", localtime) . __PACKAGE__ . $errmsg . "\n");
$errmsg =~ s/\n/<br \/>/gso;
my $p = __PACKAGE__;
$errmsg = "<html><head><title>$p: Error</title></head><body><h1>Error</h1><p>$errmsg</p><hr /><p>$p/0.5</p></body></html>";
print $ENV{SERVER_PROTOCOL}." 200 OK\x0d\x0a".
"Server: ".$ENV{SERVER_SOFTWARE}."\x0d\x0a".
"Content-Type: text/html; charset=utf-8\x0d\x0a".
"\x0d\x0a".
$errmsg;
}
# Обработчик запроса. Выводит на STDOUT HTTP-ответ (то же, что режим CGI non-parsed headers).
#
# Вызывать после получения объекта с параметром, равным пути к требуемому SVN файлу
sub handler
{
my $self = shift;
my ($uri) = @_;
my $LP = strftime("[%Y-%m-%d %H:%M:%S] ", localtime) . __PACKAGE__;
# превращаем URL в относительный и получаем свойства файла
$uri =~ s!^/+!!so;
my $ra = $self->{ra};
my $rname = '';
unless ($ra)
{
# необходимо открыть репозиторий Subversion
$uri =~ s!^([^/]+)/*!!so;
unless ($rname = $1)
{
# пустой урл
return print_error("Requested URL does not contain repository name");
}
my $K = $self->{params}->{repos_username} . '@' . $self->{params}->{repos_parent} . $rname;
$ra = $RAS->{$K};
unless ($ra)
{
# открываем репозиторий
eval { $ra = SVN::Ra->new(
url => $self->{params}->{repos_parent} . $rname,
auth => $self->{auth_prov},
) };
unless ($ra)
{
# репозиторий не открывается
return print_error("Failed to open Subversion repository '$rname'", $@);
}
$RAS->{$K} = $ra;
}
}
if ($self->{params}->{enc_from_to})
{
# перекодируем имя файла
from_to($uri, $self->{params}->{enc_from_to}->[0], $self->{params}->{enc_from_to}->[1]);
}
my ($revnum, $props);
if ($uri !~ /\/$/so)
{
eval
{
($revnum, $props) = $ra->get_file($uri, $SVN::Core::INVALID_REVNUM, undef);
};
}
# проверяем, есть ли файл
if (!$props)
{
if ($@ && $@ =~ /405\s+Method\s+Not\s+Allowed/so)
{
return print_error("Unknown repository '$rname'", $@);
}
else
{
return print_error("File '$uri' not found in Subversion repository '$rname'", $@);
}
}
# кэшируем файл, если нужно
my $path = $self->{params}->{cache_path} . '/' . $rname . $uri;
my $dir = $path;
$dir =~ s!/+[^/]*$!!so;
unless (-d $dir || make_path($dir))
{
return print_error("Failed to create cache path '$dir'");
}
my ($uptodate, $mime_type, $fd, $cached_rev);
if (-f $path && open $fd, "<$path.rev")
{
$cached_rev = <$fd>;
$mime_type = <$fd>;
chomp $mime_type;
close $fd;
$cached_rev =~ s/^\s*//so;
$cached_rev =~ s/\s*$//so;
if ($props->{'svn:entry:committed-rev'} <= $cached_rev && $mime_type)
{
# закэшировано актуальное
if ($self->{params}->{access_log})
{
# логгируем запрос
print STDERR "$LP: file $rname$uri is up to date, latest ".$props->{'svn:entry:committed-rev'}.", cached $cached_rev\n";
}
$uptodate = 1;
}
}
if (!$uptodate)
{
# проверка значения свойства - только при обновлении
if ($self->{params}->{check_prop_name})
{
my ($n, $re) = ($self->{params}->{check_prop_name}, $self->{params}->{check_prop_re});
my $ok = defined $re && $props->{$n} =~ /$re/ || !defined $re && exists $props->{$n};
if ($self->{params}->{check_prop_inherit})
{
# тупое наследование - интересно, будут ли тормоза?
my $diruri = $uri;
my $props;
while (!$ok && $diruri =~ s!/+[^/]*$!!iso)
{
$props = {};
eval { (undef, undef, $props) = $ra->get_dir($diruri, $SVN::Core::INVALID_REVNUM) };
$ok = defined $re && $props->{$n} =~ /$re/ || !defined $re && $props->{$n};
}
}
if (!$ok)
{
return print_error("Access to '$uri' from Subversion repository '$rname' is forbidden");
}
}
# угадать MIME-тип
$mime_type = $props->{'svn:mime-type'};
if (!$mime_type || $mime_type eq 'application/octet-stream')
{
$mime_type = LWP::MediaTypes::guess_media_type($path);
}
# записываем содержимое файла
eval
{
die "Could not open $path: $!" unless open $fd, ">$path";
($revnum, $props) = $ra->get_file($uri, $revnum, $fd);
close $fd;
die "Could not open $path.rev: $!" unless open $fd, ">$path.rev";
print $fd $props->{'svn:entry:committed-rev'}, "\n", $mime_type, "\n";
close $fd;
};
if ($@)
{
return print_error("Failed to checkout '$uri' @ rev.$revnum from Subversion repository '$rname' into local file '$path': $@");
}
# логгируем запрос
if ($self->{params}->{access_log})
{
print STDERR $cached_rev
? "$LP: file $rname$uri, updated to latest $revnum = ".$props->{'svn:entry:committed-rev'}." from cached $cached_rev\n"
: "$LP: file $rname$uri, checked out $revnum\n";
}
}
if (!open $fd, '<', $path)
{
return print_error("Cannot read $path");
}
print $ENV{SERVER_PROTOCOL}." 200 OK\x0d\x0a".
"Server: ".$ENV{SERVER_SOFTWARE}."\x0d\x0a".
"Content-Type: $mime_type\x0d\x0a".
"Content-Length: ".(-s $path)."\x0d\x0a".
"\x0d\x0a";
sendfile(fileno(STDOUT), fileno($fd), 0, -s $path);
}
1;
__END__