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-67ecbb4d7f56custis
parent
ee2ce6e57e
commit
f1f79d5446
|
@ -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
|
||||
|
|
|
@ -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__
|
Loading…
Reference in New Issue