#!/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? 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/
/gso; my $p = __PACKAGE__; $errmsg = "$p: Error

Error

$errmsg


$p/0.5

"; 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__