6 $Git::RepoCGI
::basedir
= '/home/repo/repomgr'; # path to the Girocco files (checkout of this project)
7 $Git::RepoCGI
::mqueuedir
= '/home/repo/repodata'; # path to the directory of the mirror queue
8 $Git::RepoCGI
::chroot = "/home/repo/j"; # ssh push chroot
9 $Git::RepoCGI
::reporoot
= "/srv/git"; # repository collection
10 $Git::RepoCGI
::name
= "repo.or.cz"; # title of the service
11 $Git::RepoCGI
::gitweburl
= "http://repo.or.cz/w"; # URL of gitweb (pathinfo mode)
12 $Git::RepoCGI
::webadmurl
= "http://repo.or.cz/m"; # URL of the 'repo' CGI web admin interface
13 $Git::RepoCGI
::httppullurl
= "http://repo.or.cz/r"; # HTTP URL of the repository collection
14 $Git::RepoCGI
::gitpullurl
= "git://repo.or.cz/"; # Git URL of the repository collection
15 $Git::RepoCGI
::pushurl
= "ssh://repo.or.cz/srv/git"; # Pushy URL of the repository collection
16 $Git::RepoCGI
::jurisdiction
= "Czech Republic"; # legal jurisdiction of the site
17 $Git::RepoCGI
::giroccourl
= "$Git::RepoCGI::gitweburl/repo.git"; # URL of gitweb of this Girocco instance (set as undef if you're not nice to the community)
18 $Git::RepoCGI
::mob
= "mob"; # set to undef to disable the special 'mob' user
19 $Git::RepoCGI
::moburl
= "/mob.html"; # URL of the explanation of the mob user
20 $Git::RepoCGI
::mirror
= 1; # enable mirroring mode
21 $Git::RepoCGI
::push = 1; # enable push mode
22 $Git::RepoCGI
::group
= 'repo'; # UNIX group owning the repositories
23 $Git::RepoCGI
::git_bin
= '/home/pasky/bin/git'; # path to Git binary to use
29 our @ISA = qw(Exporter);
30 our @EXPORT = qw(scrypt html_esc jailed_file
32 filedb_atomic_append filedb_atomic_edit
33 proj_get_forkee_name proj_get_forkee_path
34 valid_proj_name valid_user_name valid_email valid_repo_url valid_web_url);
36 use CGI
qw(:standard :escapeHTML -nosticky);
37 use CGI
::Util
qw(unescape);
38 use CGI
::Carp
qw(fatalsToBrowser);
39 use Digest
::SHA1
qw(sha1_hex);
50 $repo->{cgi
} = CGI
->new;
52 print $repo->{cgi
}->header(-type
=>'text/html', -charset
=> 'utf-8');
55 <?xml version="1.0" encoding="utf-8"?>
56 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
57 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
60 <title>$Git::RepoCGI::name :: $heading</title>
61 <link rel="stylesheet" type="text/css" href="/gitweb.css"/>
62 <link rel="shortcut icon" href="/git-favicon.png" type="image/png"/>
67 <div class="page_header">
68 <a href="http://git.or.cz/" title="Git homepage"><img src="/git-logo.png" width="72" height="27" alt="git" style="float:right; border-width:0px;"/></a>
69 <a href="/">$Git::RepoCGI::name</a> / administration / $heading
80 my $cgiurl = $cgi->url(-absolute
=> 1);
81 my ($cginame) = ($cgiurl =~ m
#^.*/\([a-zA-Z0-9_.\/-]+?\.cgi\)$#); #
82 if ($cginame and $Git::RepoCGI
::giroccourl
) {
85 <a href="$Git::RepoCGI::giroccourl?a=blob;f=cgi/$cginame">(view source)</a>
102 print "<p style=\"color: red\">@_</p>\n";
108 my $err = $self->{err
};
109 $err and print "<p style=\"font-weight: bold\">Operation aborted due to $err errors.</p>\n";
116 my $val = $self->{cgi
}->param($param);
117 $val =~ s/^\s*(.*?)\s*$/$1/;
122 ### Random utility functions
126 crypt($pwd, join ('', ('.', '/', 2..9, 'A'..'Z', 'a'..'z')[rand 64, rand 64]));
132 $str =~ s/</</g; $str =~ s/>/>/g;
133 $str =~ s/"/"/g;
139 $Git::RepoCGI
::chroot."/$filename";
147 use Errno
qw(EEXIST);
148 use Fcntl
qw(O_WRONLY O_CREAT O_EXCL);
150 my $handle = new IO
::Handle
;
152 unless (sysopen($handle, $path, O_WRONLY
|O_CREAT
|O_EXCL
)) {
154 while (not sysopen($handle, $path, O_WRONLY
|O_CREAT
|O_EXCL
)) {
155 ($! == EEXIST
) or die "$path open failed: $!";
156 ($cnt++ < 16) or die "$path open failed: cannot open lockfile";
160 # XXX: filedb-specific
161 chmod 0664, $path or die "$path g+w failed: $!";
169 rename "$path.lock", $path or die "$path unlock failed: $!";
172 sub filedb_atomic_append
{
173 my ($file, $line) = @_;
176 open my $src, $file or die "$file open for reading failed: $!";
177 my $dst = lock_file
($file);
180 my $aid = (split /:/)[2];
181 $id = $aid + 1 if ($aid >= $id);
183 print $dst $_ or die "$file(l) write failed: $!";
186 $line =~ s/\\i/$id/g;
187 print $dst "$line\n" or die "$file(l) write failed: $!";
189 close $dst or die "$file(l) close failed: $!";
197 sub filedb_atomic_edit
{
198 my ($file, $fn) = @_;
200 open my $src, $file or die "$file open for reading failed: $!";
201 my $dst = lock_file
($file);
204 print $dst $fn->($_) or die "$file(l) write failed: $!";
207 close $dst or die "$file(l) close failed: $!";
213 sub proj_get_forkee_name
{
217 sub proj_get_forkee_path
{
218 my $forkee = $Git::RepoCGI
::reporoot
.'/'.proj_get_forkee_name
($_[0]).'.git';
219 -d
$forkee ?
$forkee : '';
221 sub valid_proj_name
{
223 (not m
#/# or -d proj_get_forkee_path($_)) # will also catch ^/
226 and m
#^[a-zA-Z0-9+./_-]+$#;
228 sub valid_user_name
{
230 /^[a-zA-Z0-9+._-]+$/;
234 /^[a-zA-Z0-9+._-]+@[a-zA-Z0-9-.]+$/;
238 /^http:\/\
/[a-zA-Z0-9-.]+(\/[_\
%a-zA
-Z0
-9.\
/~-]*)?(#[a-zA-Z0-9._-]+)?$/;
242 /^http:\/\
/[a-zA-Z0-9-.]+(\/[_\
%a-zA
-Z0
-9.\
/~-]*)?$/ or
243 /^git:\/\
/[a-zA-Z0-9-.]+(\/[_\
%a-zA
-Z0
-9.\
/~-]*)?$/;
249 package Git
::RepoCGI
::Project
;
251 BEGIN { use Git
::RepoCGI
; }
255 my @pelems = split('/', $self->{name
});
256 pop @pelems; # do not create dir for the project itself
257 my $path = $self->{base_path
};
258 foreach my $pelem (@pelems) {
260 (-d
"$path") or mkdir $path or die "mkdir $path: $!";
261 chmod 0775, $path; # ok if fails (dir may already exist and be owned by someone else)
268 desc
=> 'description',
269 README
=> 'README.html',
276 $self->{path
}.'/'.$name;
282 $propmap{$name} or die "unknown property: $name";
283 open P
, $self->_property_path($propmap{$name}) or return undef;
286 my $value = join('', @value); chomp $value;
292 my ($name, $value) = @_;
293 $propmap{$name} or die "unknown property: $name";
295 my $P = lock_file
($self->_property_path($propmap{$name}));
296 $value ne '' and print $P "$value\n";
298 unlock_file
($self->_property_path($propmap{$name}));
301 sub _properties_load
{
303 foreach my $prop (keys %propmap) {
304 $self->{$prop} = $self->_property_fget($prop);
308 sub _properties_save
{
310 foreach my $prop (keys %propmap) {
311 $self->_property_fput($prop, $self->{$prop});
317 $self->_property_path('.nofetch');
323 my $np = $self->_nofetch_path;
325 open X
, '>'.$np or die "nofetch failed: $!";
328 unlink $np or die "yesfetch failed: $!";
332 sub _alternates_setup
{
334 return unless $self->{name
} =~ m
#/#;
335 my $forkee_name = proj_get_forkee_name
($self->{name
});
336 my $forkee_path = proj_get_forkee_path
($self->{name
});
337 return unless -d
$forkee_path;
338 mkdir $self->{path
}.'/refs'; chmod 0775, $self->{path
}.'/refs';
339 mkdir $self->{path
}.'/objects'; chmod 0775, $self->{path
}.'/objects';
340 mkdir $self->{path
}.'/objects/info'; chmod 0775, $self->{path
}.'/objects/info';
342 # We set up both alternates and http_alternates since we cannot use
343 # relative path in alternates - that doesn't work recursively.
345 my $filename = $self->{path
}.'/objects/info/alternates';
346 open X
, '>'.$filename or die "alternates failed: $!";
347 print X
"$forkee_path/objects\n";
349 chmod 0664, $filename or warn "cannot chmod $filename: $!";
351 if ($Git::RepoCGI
::httppullurl
) {
352 $filename = $self->{path
}.'/objects/info/http-alternates';
353 open X
, '>'.$filename or die "http-alternates failed: $!";
354 my $upfork = $forkee_name;
355 do { print X
"$Git::RepoCGI::httppullurl/$upfork.git/objects\n"; } while ($upfork =~ s
#/?.+?$## and $upfork);
357 chmod 0664, $filename or warn "cannot chmod $filename: $!";
360 symlink "$forkee_path/refs", $self->{path
}.'/refs/forkee';
365 mkdir $self->{path
}.'/ctags'; chmod 0775, $self->{path
}.'/ctags';
371 $xtra .= join(',', @
{$self->{users
}});
372 filedb_atomic_append
(jailed_file
('/etc/group'),
373 join(':', $self->{name
}, $self->{crypt}, '\i', $xtra));
378 my $xtra = join(',', @
{$self->{users
}});
379 filedb_atomic_edit
(jailed_file
('/etc/group'),
383 if ($self->{name
} eq (split /:/)[0]) {
384 # preserve readonly flag
385 s/::([^:]*)$/:$1/ and $xtra = ":$xtra";
386 return join(':', $self->{name
}, $self->{crypt}, $self->{gid
}, $xtra)."\n";
396 filedb_atomic_edit
(jailed_file
('/etc/group'),
398 $self->{name
} ne (split /:/)[0] and return $_;
406 $self->{path
}.'/hooks/'.$name;
412 open SRC
, "$Git::RepoCGI::basedir/$name-hook" or die "cannot open hook $name: $!";
413 open DST
, '>'.$self->_hook_path($name) or die "cannot open hook $name for writing: $!";
414 while (<SRC
>) { print DST
$_; }
417 chmod 0775, $self->_hook_path($name) or die "cannot chmod hook $name: $!";
422 foreach my $hook ('update') {
423 $self->_hook_install($hook);
427 # private constructor, do not use
430 my ($name, $base_path, $path) = @_;
431 valid_proj_name
($name) or die "refusing to create project with invalid name ($name)!";
432 $path ||= "$base_path/$name.git";
433 my $proj = { name
=> $name, base_path
=> $base_path, path
=> $path };
438 # public constructor #0
439 # creates a virtual project not connected to disk image
440 # you can conjure() it later to disk
443 my ($name, $mirror) = @_;
444 my $self = $class->_new($name, $mirror ?
"$Git::RepoCGI::mqueuedir/to-clone" : $Git::RepoCGI
::reporoot
,
445 $mirror ?
"$Git::RepoCGI::mqueuedir/to-clone/$name" : $Git::RepoCGI
::reporoot
."/$name.git");
447 $self->{mirror
} = $mirror;
451 # public constructor #1
456 open F
, jailed_file
("/etc/group") or die "project load failed: $!";
460 next unless (shift eq $name);
462 my $self = $class->_new($name, $Git::RepoCGI
::reporoot
);
463 (-d
$self->{path
}) or die "invalid path (".$self->{path
}.") for project ".$self->{name
};
466 ($self->{crypt}, $self->{gid
}, $ulist) = @_;
468 $self->{users
} = [split /,/, $ulist];
469 $self->{mirror
} = ! -e
$self->_nofetch_path;
470 $self->{ccrypt
} = $self->{crypt};
472 $self->_properties_load;
479 # $proj may not be in sane state if this returns false!
483 my $cgi = $repo->cgi;
485 my $pwd = $cgi->param('pwd');
486 if ($pwd ne '' or not $self->{crypt}) {
487 $self->{crypt} = scrypt
($pwd);
490 if ($cgi->param('pwd2') and $pwd ne $cgi->param('pwd2')) {
491 $repo->err("Our high-paid security consultants have determined that the admin passwords you have entered do not match each other.");
494 $self->{cpwd
} = $cgi->param('cpwd');
496 $self->{email
} = $repo->wparam('email');
497 valid_email
($self->{email
})
498 or $repo->err("Your email sure looks weird...?");
500 $self->{url
} = $repo->wparam('url');
502 valid_repo_url
($self->{url
})
503 or $repo->err("Invalid URL. Note that only HTTP and Git protocol is supported. If the URL contains funny characters, contact me.");
506 $self->{desc
} = $repo->wparam('desc');
507 length($self->{desc
}) <= 1024
508 or $repo->err("<b>Short</b> description length > 1kb!");
510 $self->{README
} = $repo->wparam('README');
511 length($self->{README
}) <= 8192
512 or $repo->err("README length > 8kb!");
514 $self->{hp
} = $repo->wparam('hp');
516 valid_web_url
($self->{hp
})
517 or $repo->err("Invalid homepage URL. Note that only HTTP protocol is supported. If the URL contains funny characters, contact me.");
520 # FIXME: Permit only existing users
521 $self->{users
} = [grep { valid_user_name
($_) } $cgi->param('user')];
523 not $repo->err_check;
529 name
=> $self->{name
},
530 email
=> $self->{email
},
532 desc
=> html_esc
($self->{desc
}),
533 README
=> html_esc
($self->{README
}),
535 users
=> $self->{users
},
543 $self->{ccrypt
} or die "Can't authenticate against a project with no password";
544 $self->{cpwd
} or $repo->err("No password entered.");
545 unless ($self->{ccrypt
} eq crypt($self->{cpwd
}, $self->{ccrypt
})) {
546 $repo->err("Your admin password does not match!");
555 $self->_mkdir_forkees;
556 mkdir $self->{path
} or die "mkdir failed: $!";
557 chmod 0775, $self->{path
} or die "chmod failed: $!";
558 $self->_properties_save;
559 $self->_alternates_setup;
561 $self->_group_add(':');
567 $self->_mkdir_forkees;
569 mkdir($self->{path
}) or die "mkdir $self->{path} failed: $!";
570 my $gid = scalar(getgrnam($Git::RepoCGI
::group
));
571 chown(-1, $gid, $self->{path
}) or die "chgrp $gid $self->{path} failed: $!";
572 chmod(2775, $self->{path
}) or die "chmod 2775 $self->{path} failed: $!";
573 system($Git::RepoCGI
::git_bin
, '--git-dir='.$self->{path
}, 'init', '--bare', '--shared=group')
574 or die "git init $self->{path} failed: $!";
575 system($Git::RepoCGI
::git_bin
, '--git-dir='.$self->{path
}, 'config', 'receive.denyNonFastforwards', 'false')
576 or die "disabling receive.denyNonFastforwards failed: $!";
579 $self->_properties_save;
580 $self->_alternates_setup;
583 $self->_hooks_install;
589 $self->_properties_save;
590 $self->_group_update;
593 sub update_password
{
597 $self->{crypt} = scrypt
($pwd);
598 $self->_group_update;
601 # You can explicitly do this just on a ghost() repository too.
605 if (-d
$self->{path
}) {
606 system('rm', '-r', $self->{path
}) == 0
607 or die "rm -r failed: $?";
609 $self->_group_remove;
615 return glob($Git::RepoCGI
::reporoot
.'/'.$self->{name
}.'/*');
621 valid_proj_name
($name) or die "tried to query for project with invalid name $name!";
623 or -d
$Git::RepoCGI
::mqueuedir
."/cloning/$name"
624 or -d
$Git::RepoCGI
::mqueuedir
."/to-clone/$name");
628 valid_proj_name
($name) or die "tried to query for project with invalid name $name!";
629 (-d
$Git::RepoCGI
::reporoot
."/$name.git");
635 package Git
::RepoCGI
::User
;
637 BEGIN { use Git
::RepoCGI
; }
641 filedb_atomic_append
(jailed_file
('/etc/passwd'),
642 join(':', $self->{name
}, 'x', '\i', 65534, $self->{email
}, '/', '/bin/git-shell'));
647 '/etc/sshkeys/'.$self->{name
};
652 open F
, "<".jailed_file
($self->_sshkey_path) or die "sshkey load failed: $!";
657 if (/^ssh-(?:dss|rsa) /) {
659 } elsif (/^# REPOAUTH ([0-9a-f]+) (\d+)/) {
661 $auth = $1 unless (time >= $expire);
665 my $keys = join('', @keys); chomp $keys;
671 open F
, ">".jailed_file
($self->_sshkey_path) or die "sshkey failed: $!";
672 if (defined($self->{auth
}) && $self->{auth
}) {
673 my $expire = time + 24 * 3600;
674 print F
"# REPOAUTH $self->{auth} $expire\n";
676 print F
$self->{keys}."\n";
678 chmod 0664, jailed_file
($self->_sshkey_path);
681 # private constructor, do not use
685 valid_user_name
($name) or die "refusing to create user with invalid name ($name)!";
686 my $proj = { name
=> $name };
691 # public constructor #0
692 # creates a virtual user not connected to disk record
693 # you can conjure() it later to disk
697 my $self = $class->_new($name);
701 # public constructor #1
706 open F
, jailed_file
("/etc/passwd") or die "user load failed: $!";
710 next unless (shift eq $name);
712 my $self = $class->_new($name);
714 (undef, $self->{uid
}, undef, $self->{email
}) = @_;
715 ($self->{keys}, $self->{auth
}) = $self->_sshkey_load;
723 # $user may not be in sane state if this returns false!
727 my $cgi = $repo->cgi;
729 $self->{name
} = $repo->wparam('name');
730 valid_user_name
($self->{name
})
731 or $repo->err("Name contains invalid characters.");
733 $self->{email
} = $repo->wparam('email');
734 valid_email
($self->{email
})
735 or $repo->err("Your email sure looks weird...?");
737 $self->keys_fill($repo);
743 my $cgi = $repo->cgi;
745 $self->{keys} = $cgi->param('keys');
746 length($self->{keys}) <= 4096
747 or $repo->err("The list of keys is more than 4kb. Do you really need that much?");
748 foreach (split /\r?\n/, $self->{keys}) {
749 /^ssh-(?:dss|rsa) .* \S+@\S+$/ or $repo->err("Your ssh key (\"$_\") appears to have invalid format (do not start by ssh-dss|rsa or do not end with @-identifier) - maybe your browser has split a single key to multiple lines?");
752 not $repo->err_check;
764 $self->{auth
} = Digest
::SHA1
::sha1_hex
(time . $$ . rand() . $self->{keys});
772 delete $self->{auth
};
785 valid_user_name
($name) or die "tried to query for user with invalid name $name!";
786 (-e jailed_file
("/etc/sshkeys/$name"));