opf/openproject

View on GitHub
extra/Apache/OpenProjectRepoman.pm

Summary

Maintainability
Test Coverage
package Apache::OpenProjectRepoman;

use strict;
use warnings FATAL => 'all', NONFATAL => 'redefine';

use File::Path qw(rmtree);
use File::Spec ();
use File::Copy qw(move);

use Apache2::Module;
use Apache2::Access;
use Apache2::ServerRec qw();
use Apache2::Response ();
use Apache2::RequestRec qw();
use Apache2::RequestUtil qw();
use Apache2::RequestIO qw();
use Apache2::Const -compile => qw(FORBIDDEN OK OR_AUTHCFG TAKE1 HTTP_UNPROCESSABLE_ENTITY HTTP_BAD_REQUEST OK);
use APR::Table ();

use JSON;
use Carp;


##
# Add AccessSecret directive to Apache, which is checked during configtest
my @directives = (
  {
    name => 'AccessSecret',
    req_override => Apache2::Const::OR_AUTHCFG,
    args_how => Apache2::Const::TAKE1,
    errmsg => 'Secret access token used to access the repository wrapper.',
  }
);
Apache2::Module::add(__PACKAGE__, \@directives);

##
# Accepts and tests the access secret value given in the Apache configuration
sub AccessSecret {
  my ($self, $parms, @args) = @_;
  $self->{token} = $args[0];
  unless (length($self->{token}) >= 8) {
    die "Use at least 8 characters for the repoman access token!";
  }
}

##
# Creates an actual repository on disk for Subversion and Git.
sub create_repository {
  my ($r, $vendor, $repository, $repository_root, $request) = @_;

  my $command = {
    git => "git init $repository --shared --bare",
    subversion => "svnadmin create $repository"
  }->{$vendor};

  die "No create command known for vendor '$vendor'\n" unless defined($command);
  die "Could not create repository.\n" unless system($command) == 0;
}

##
# Removes the repository with a given identifier on disk.
sub delete_repository {
  my ($r, $vendor, $repository, $repository_root, $request) = @_;
  rmtree($repository) if -d $repository;
}

sub relocate_repository {
  my ($r, $vendor, $repository, $repository_root, $request) = @_;

  # Determine validity of the old repository identifier as a dir name
  my $old_identifier = $request->{old_identifier};
  die ("Old repository identifier is empty") unless (length($old_identifier) > 0);
  die ("Old repository identifier is an invalid filename") if ($old_identifier =~ m{[\\/:*?"<>|]});

  my $old_repository = File::Spec->catdir($repository_root, $old_identifier);
  move($old_repository, $repository) if -d $old_repository;
}

##
# Extract and return JSON request from the Apache request handler.
sub parse_request {
  my $r = shift;
  my $len  = $r->headers_in->{'Content-Length'};

  die "Request invalid.\n"  unless (defined($len) && $len > 0);
  die "Request too large.\n"  if ($len > (2**13));

  my ($buf, $content);
  while($r->read($buf, $len)) {
    $content .= $buf;
  }

  my $json = JSON->new->allow_blessed->allow_nonref->convert_blessed;
  return $json->decode($content);
}

##
# Returns a JSON error and sets the HTTP response code to $type.
sub make_error {
    my ($r, $type, $msg) = @_;
    my $response = {
      success => JSON::false,
      message => $msg
    };

    $r->status($type) ;
    return $response;
  }

##
# Actual incoming request handler, that receives the JSON request
# and determines the necessary local action from the request.
sub _handle_request {
  my $r = shift;

  # Parse JSON request
  my $request = parse_request($r);

  # Get repository root for the current vendor
  my %paths = $r->dir_config->get('ScmVendorPaths');

  my $vendor = $request->{vendor};
  my $repository_root = $paths{$vendor};

  # Compare access token
  my $passed_token = $request->{token};
  my $cfg = Apache2::Module::get_config( __PACKAGE__, $r->server, $r->per_dir_config );

  unless (length($passed_token) >= 8 && ($passed_token eq $cfg->{token})) {
    return make_error($r, Apache2::Const::FORBIDDEN, 'Invalid access token');
  }

  # Abort unless repository root is configured in the Apache configuration
  unless (defined($repository_root)) {
    return make_error($r,
      Apache2::Const::HTTP_UNPROCESSABLE_ENTITY,
      "Vendor '$vendor' not configured.");
  }

  # Abort unless the repository root actually exists
  unless (-d $repository_root) {
    return make_error($r,
      Apache2::Const::HTTP_UNPROCESSABLE_ENTITY,
      "Repository path for vendor '$vendor' does not exist.");
  }

  # Determine validity of the identifier as a dir name
  my $repository_identifier = $request->{identifier};
  if ($repository_identifier =~ m{[\\/:*?"<>|]}) {
    return make_error($r,
      Apache2::Const::HTTP_UNPROCESSABLE_ENTITY,
      "Repository identifier is an invalid filename");
  }

  # Call the necessary action on disk
  my $target = File::Spec->catdir($repository_root, $repository_identifier);
  my %actions = (
    'create' => \&create_repository,
    'relocate' => \&relocate_repository,
    'delete' => \&delete_repository
    );

  my $action = $actions{$request->{action}};
  die "Unknown action.\n"  unless defined($action);
  $action->($r, $vendor, $target, $repository_root, $request);

  return {
    success => JSON::true,
    message => "The action has completed successfully.",
    repository => $target,
    path => $target,
    # This is only useful in the packager context
    url => 'file://' . $target
  };
}

##
# Handler subroutine that is called for each request by Apache
sub handler {
  my $r = shift;

  my $response;
  $r->content_type('application/json');

  eval {
    $response = _handle_request($r);
    1;
  } or do {
    my $err = $@;
    chomp $err;
    $response = make_error($r, Apache2::Const::HTTP_BAD_REQUEST, $err);
  };


  my $json = JSON->new->allow_blessed->allow_nonref->convert_blessed;
  print $json->encode($response);
  return Apache2::Const::OK;
}

1;