RepoCGI: move site params to Git::RepoCGI::Config
[girocco/ztw.git] / cgi / Git / RepoCGI.pm
blob6b3fe34e34e111fb8acd9770463d441e15096314
1 package Git::RepoCGI;
3 use strict;
4 use warnings;
6 ### Administrativa
8 my ( $bin_path, $jail_path, $repomgr_path, $repodata_path, $repo_path );
9 my ( $site_domain, $group_file, $user_file, $sshkeys_path );
10 BEGIN {
11 our $VERSION = '0.1';
12 our @ISA = qw(Exporter);
13 our @EXPORT = qw(genpwd scrypt html_esc jailed_file
14 lock_file unlock_file
15 filedb_atomic_append filedb_atomic_edit
16 proj_get_forkee_name proj_get_forkee_path
17 valid_proj_name valid_user_name valid_email valid_repo_url valid_web_url);
19 use CGI qw(:standard :escapeHTML -nosticky);
20 use CGI::Util qw(unescape);
21 use CGI::Carp qw(fatalsToBrowser);
22 use Digest::SHA1 qw(sha1_hex);
23 use Git::RepoCGI::Config;
25 $bin_path = $Git::RepoCGI::Config::defaults{bin_path};
26 $jail_path = $Git::RepoCGI::Config::defaults{jail_path};
27 $repomgr_path = $Git::RepoCGI::Config::defaults{repomgr_path};
28 $repodata_path = $Git::RepoCGI::Config::defaults{repodata_path};
29 $repo_path = $Git::RepoCGI::Config::defaults{repo_path};
31 $group_file = $Git::RepoCGI::Config::defaults{group_path};
32 $user_file = $Git::RepoCGI::Config::defaults{user_file};
33 $sshkeys_path = $Git::RepoCGI::Config::defaults{sshkeys_path};
35 $site_domain = $Git::RepoCGI::Config::defaults{site_domain};
37 $ENV{PATH} = $bin_path . ':' . $ENV{PATH};
41 ### RepoCGI object
43 sub new {
44 my $class = shift;
45 my ($heading) = @_;
46 my $repo = {};
48 $repo->{cgi} = CGI->new;
50 print $repo->{cgi}->header(-type=>'text/html', -charset => 'utf-8');
52 print <<EOT;
53 <?xml version="1.0" encoding="utf-8"?>
54 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
55 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
57 <head>
58 <title>$site_domain :: $heading</title>
59 <link rel="stylesheet" type="text/css" href="/gitweb.css"/>
60 <link rel="shortcut icon" href="/git-favicon.png" type="image/png"/>
61 </head>
63 <body>
65 <div class="page_header">
66 <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>
67 <a href="/">$site_domain</a> / administration / $heading
68 </div>
70 EOT
72 bless $repo, $class;
75 sub DESTROY {
76 my $self = shift;
77 my $cgi = $self->cgi;
78 my $cginame = $cgi->url(-absolute => 1);
79 $cginame =~ s#^/m/##;
80 if ($cginame =~ /^[a-zA-Z0-9_.\/-]+\.cgi$/) {
81 print <<EOT;
82 <div align="right">
83 <a href="http://$site_domain/w/repo.git?a=blob;f=cgi/$cginame">(view source)</a>
84 </div>
85 EOT
87 print <<EOT;
88 </body>
89 </html>
90 EOT
93 sub cgi {
94 my $self = shift;
95 $self->{cgi};
98 sub bye {
99 my $self = shift;
100 print "<p>", @_, "</p>\n" if @_;
101 exit
104 sub err {
105 my $self = shift;
106 print "<p style=\"color: red\">@_</p>\n";
107 $self->{err}++;
110 sub err_check {
111 my $self = shift;
112 my $err = $self->{err};
113 $err and print "<p style=\"font-weight: bold\">Operation aborted due to $err errors.</p>\n";
114 $err;
117 sub sparam {
118 my ( $self, $param ) = @_;
119 $self->{cgi}->param($param) || '';
121 sub wparam {
122 my $self = shift;
123 my $val = $self->sparam(@_);
124 $val =~ s/^\s*(.*?)\s*$/$1/;
125 $val;
129 ### Random utility functions
131 sub genpwd {
132 # FLUFFY!
133 substr(crypt(rand, rand), 2);
136 sub scrypt {
137 my ($pwd) = @_;
138 crypt($pwd, join ('', ('.', '/', 2..9, 'A'..'Z', 'a'..'z')[rand 64, rand 64]));
141 sub html_esc {
142 my ($str) = @_;
143 $str =~ s/&/&amp;/g;
144 $str =~ s/</&lt;/g; $str =~ s/>/&gt;/g;
145 $str =~ s/"/&quot;/g;
146 $str;
149 sub jailed_file {
150 my ($filename) = @_;
151 "$jail_path/$filename";
154 sub lock_file {
155 my ($path) = @_;
157 $path .= '.lock';
159 use Errno qw(EEXIST);
160 use Fcntl qw(O_WRONLY O_CREAT O_EXCL);
161 use IO::Handle;
162 my $handle = new IO::Handle;
164 unless (sysopen($handle, $path, O_WRONLY|O_CREAT|O_EXCL)) {
165 my $cnt = 0;
166 while (not sysopen($handle, $path, O_WRONLY|O_CREAT|O_EXCL)) {
167 ($! == EEXIST) or die "$path open failed: $!";
168 ($cnt++ < 16) or die "$path open failed: cannot open lockfile";
169 sleep(1);
172 # XXX: filedb-specific
173 chmod 0664, $path or die "$path g+w failed: $!";
175 $handle;
178 sub unlock_file {
179 my ($path) = @_;
181 rename "$path.lock", $path or die "$path unlock failed: $!";
184 sub filedb_atomic_append {
185 my ($file, $line) = @_;
186 my $id = 65536;
188 open my $src, $file or die "$file open for reading failed: $!";
189 my $dst = lock_file($file);
191 while (<$src>) {
192 my $aid = (split /:/)[2];
193 $id = $aid + 1 if ($aid >= $id);
195 print $dst $_ or die "$file(l) write failed: $!";
198 $line =~ s/\\i/$id/g;
199 print $dst "$line\n" or die "$file(l) write failed: $!";
201 close $dst or die "$file(l) close failed: $!";
202 close $src;
204 unlock_file($file);
206 $id;
209 sub filedb_atomic_edit {
210 my ($file, $fn) = @_;
212 open my $src, $file or die "$file open for reading failed: $!";
213 my $dst = lock_file($file);
215 while (<$src>) {
216 print $dst $fn->($_) or die "$file(l) write failed: $!";
219 close $dst or die "$file(l) close failed: $!";
220 close $src;
222 unlock_file($file);
225 sub proj_get_forkee_name {
226 $_ = $_[0];
227 (m#^(.*)/.*?$#)[0];
229 sub proj_get_forkee_path {
230 my $forkee = $repo_path . proj_get_forkee_name($_[0]).'.git';
231 -d $forkee ? $forkee : '';
233 sub valid_proj_name {
234 $_ = $_[0];
235 (not m#/# or -d proj_get_forkee_path($_)) # will also catch ^/
236 and (not m#\./#)
237 and (not m#/$#)
238 and m#^[a-zA-Z0-9+./_-]+$#;
240 sub valid_user_name {
241 $_ = $_[0];
242 /^[a-zA-Z0-9+._-]+$/;
244 sub valid_email {
245 $_ = $_[0];
246 /^[a-zA-Z0-9+._-]+@[a-zA-Z0-9-.]+$/;
248 sub valid_web_url {
249 $_ = $_[0];
250 /^http:\/\/[a-zA-Z0-9-.]+(\/[_\%a-zA-Z0-9.\/~-]*)?(#[a-zA-Z0-9._-]+)?$/;
252 sub valid_repo_url {
253 $_ = $_[0];
254 /^http:\/\/[a-zA-Z0-9-.]+(\/[_\%a-zA-Z0-9.\/~-]*)?$/ or
255 /^git:\/\/[a-zA-Z0-9-.]+(\/[_\%a-zA-Z0-9.\/~-]*)?$/;
258 sub load_project {
259 my ( $self, $name ) = @_;
261 $self->bye("I need the project name as an argument now.")
262 unless $name;
264 $self->bye("Invalid project name. Go away, sorcerer.")
265 unless valid_proj_name($name);
267 $self->bye("Sorry but this project does not exist. "
268 . "Now, how did you <em>get</em> here?!")
269 unless Git::RepoCGI::Project::does_exist($name);
271 $self->bye("Sorry but your project has not finished mirroring yet. "
272 . "If this process takes excessive time, "
273 . "please inform the site administrator.")
274 unless Git::RepoCGI::Project::available($name);
276 my $proj = Git::RepoCGI::Project->load($name);
277 die "not found project $name, that's really weird!" unless $proj;
278 $proj
281 sub load_user {
282 my ( $self, $name ) = @_;
283 $self->bye("User '$name' is not registered.")
284 unless valid_user_name($name)
285 && Git::RepoCGI::User::does_exist($name);
287 my $user = Git::RepoCGI::User->load($name);
288 $self->bye("User '$name' failed to load") unless $user;
289 return $user;
292 ### Project object
294 package Git::RepoCGI::Project;
296 BEGIN { use Git::RepoCGI; }
298 sub _mkdir_forkees {
299 my $self = shift;
300 my @pelems = split('/', $self->{name});
301 pop @pelems; # do not create dir for the project itself
302 my $path = $self->{base_path};
303 foreach my $pelem (@pelems) {
304 $path .= "/$pelem";
305 (-d "$path") or mkdir $path or die "mkdir $path: $!";
306 chmod 0775, $path; # ok if fails (dir may already exist and be owned by someone else)
310 our %propmap = (
311 url => 'base_url',
312 email => 'owner',
313 desc => 'description',
314 README => 'README.html',
315 hp => 'homepage',
318 sub _property_path {
319 my $self = shift;
320 my ($name) = @_;
321 $self->{path}.'/'.$name;
324 sub _property_fget {
325 my $self = shift;
326 my ($name) = @_;
327 $propmap{$name} or die "unknown property: $name";
328 open P, $self->_property_path($propmap{$name}) or return undef;
329 my @value = <P>;
330 close P;
331 my $value = join('', @value); chomp $value;
332 $value;
335 sub _property_fput {
336 my $self = shift;
337 my ($name, $value) = @_;
338 $propmap{$name} or die "unknown property: $name";
340 my $P = lock_file($self->_property_path($propmap{$name}));
341 $value ne '' and print $P "$value\n";
342 close $P;
343 unlock_file($self->_property_path($propmap{$name}));
346 sub _properties_load {
347 my $self = shift;
348 foreach my $prop (keys %propmap) {
349 $self->{$prop} = $self->_property_fget($prop);
353 sub _properties_save {
354 my $self = shift;
355 foreach my $prop (keys %propmap) {
356 $self->_property_fput($prop, $self->{$prop});
360 sub _nofetch_path {
361 my $self = shift;
362 $self->_property_path('.nofetch');
365 sub _nofetch {
366 my $self = shift;
367 my ($nofetch) = @_;
368 my $np = $self->_nofetch_path;
369 if ($nofetch) {
370 open X, '>'.$np or die "nofetch failed: $!";
371 close X;
372 } else {
373 unlink $np or die "yesfetch failed: $!";
377 sub _alternates_setup {
378 my $self = shift;
379 return unless $self->{name} =~ m#/#;
380 my $forkee_name = proj_get_forkee_name($self->{name});
381 my $forkee_path = proj_get_forkee_path($self->{name});
382 return unless -d $forkee_path;
383 mkdir $self->{path}.'/refs'; chmod 0775, $self->{path}.'/refs';
384 mkdir $self->{path}.'/objects'; chmod 0775, $self->{path}.'/objects';
385 mkdir $self->{path}.'/objects/info'; chmod 0775, $self->{path}.'/objects/info';
387 # We set up both alternates and http_alternates since we cannot use
388 # relative path in alternates - that doesn't work recursively.
390 my $filename = $self->{path}.'/objects/info/alternates';
391 open X, '>'.$filename or die "alternates failed: $!";
392 print X "$forkee_path/objects\n";
393 close X;
394 chmod 0664, $filename or warn "cannot chmod $filename: $!";
396 $filename = $self->{path}.'/objects/info/http-alternates';
397 open X, '>'.$filename or die "http-alternates failed: $!";
398 my $upfork = $forkee_name;
399 do { print X "/r/$upfork.git/objects\n"; } while ($upfork =~ s#/?.+?$## and $upfork);
400 close X;
401 chmod 0664, $filename or warn "cannot chmod $filename: $!";
403 symlink "$forkee_path/refs", $self->{path}.'/refs/forkee';
406 sub _ctags_setup {
407 my $self = shift;
408 mkdir $self->{path}.'/ctags'; chmod 0775, $self->{path}.'/ctags';
411 sub _group_add {
412 my $self = shift;
413 my ($xtra) = @_;
414 $xtra .= join(',', @{$self->{users}});
415 filedb_atomic_append(jailed_file($group_file),
416 join(':', $self->{name}, $self->{crypt}, '\i', $xtra));
419 sub _group_update {
420 my $self = shift;
421 my $xtra = join(',', @{$self->{users}});
422 filedb_atomic_edit(jailed_file($group_file),
423 sub {
424 $_ = $_[0];
425 chomp;
426 if ($self->{name} eq (split /:/)[0]) {
427 # preserve readonly flag
428 s/::([^:]*)$/:$1/ and $xtra = ":$xtra";
429 return join(':', $self->{name}, $self->{crypt}, $self->{gid}, $xtra)."\n";
430 } else {
431 return "$_\n";
437 sub _group_remove {
438 my $self = shift;
439 filedb_atomic_edit(jailed_file($group_file),
440 sub {
441 $self->{name} ne (split /:/)[0] and return $_;
446 sub _hook_path {
447 my $self = shift;
448 my ($name) = @_;
449 $self->{path}.'/hooks/'.$name;
452 sub _hook_install {
453 my $self = shift;
454 my ($name) = @_;
455 open SRC, "$repomgr_path/$name-hook" or die "cannot open hook $name: $!";
456 open DST, '>'.$self->_hook_path($name) or die "cannot open hook $name for writing: $!";
457 while (<SRC>) { print DST $_; }
458 close DST;
459 close SRC;
460 chmod 0775, $self->_hook_path($name) or die "cannot chmod hook $name: $!";
463 sub _hooks_install {
464 my $self = shift;
465 foreach my $hook ('update') {
466 $self->_hook_install($hook);
470 # private constructor, do not use
471 sub _new {
472 my $class = shift;
473 my ($name, $base_path, $path) = @_;
474 valid_proj_name($name) or die "refusing to create project with invalid name ($name)!";
475 $path ||= "$base_path/$name.git";
476 my $proj = { name => $name, base_path => $base_path, path => $path };
478 bless $proj, $class;
481 # public constructor #0
482 # creates a virtual project not connected to disk image
483 # you can conjure() it later to disk
484 sub ghost {
485 my $class = shift;
486 my ($name, $mirror) = @_;
488 my $path = $mirror ? "$repodata_path/to-clone" : $repo_path;
489 my $repo = "$path/$name";
490 $repo .= '.git' unless $mirror;
492 my $self = $class->_new($name, $path, $repo);
493 $self->{users} = [];
494 $self->{mirror} = $mirror;
495 $self;
498 # public constructor #1
499 sub load {
500 my $class = shift;
501 my ($name) = @_;
503 open F, jailed_file($group_file) or die "project load failed: $!";
504 while (<F>) {
505 chomp;
506 @_ = split /:+/;
507 next unless (shift eq $name);
509 my $self = $class->_new($name, $repo_path);
510 (-d $self->{path}) or die "invalid path (".$self->{path}.") for project ".$self->{name};
512 my $ulist;
513 ($self->{crypt}, $self->{gid}, $ulist) = @_;
514 $ulist ||= '';
515 $self->{users} = [split /,/, $ulist];
516 $self->{mirror} = ! -e $self->_nofetch_path;
517 $self->{ccrypt} = $self->{crypt};
519 $self->_properties_load;
520 return $self;
522 close F;
523 undef;
526 # $proj may not be in sane state if this returns false!
527 sub cgi_fill {
528 my $self = shift;
529 my ($repo) = @_;
530 my $cgi = $repo->cgi;
532 my $pwd = $cgi->param('pwd');
533 if ($pwd ne '' or not $self->{crypt}) {
534 $self->{crypt} = scrypt($pwd);
537 if ($cgi->param('pwd2') and $pwd ne $cgi->param('pwd2')) {
538 $repo->err("Our high-paid security consultants have determined that the admin passwords you have entered do not match each other.");
541 $self->{cpwd} = $cgi->param('cpwd');
543 $self->{email} = $repo->wparam('email');
544 valid_email($self->{email})
545 or $repo->err("Your email sure looks weird...?");
547 $self->{url} = $repo->wparam('url');
548 if ($self->{url}) {
549 valid_repo_url($self->{url})
550 or $repo->err("Invalid URL. Note that only HTTP and Git protocol is supported. If the URL contains funny characters, contact me.");
553 $self->{desc} = $repo->wparam('desc');
554 length($self->{desc}) <= 1024
555 or $repo->err("<b>Short</b> description length > 1kb!");
557 $self->{README} = $repo->wparam('README');
558 length($self->{README}) <= 8192
559 or $repo->err("README length > 8kb!");
561 $self->{hp} = $repo->wparam('hp');
562 if ($self->{hp}) {
563 valid_web_url($self->{hp})
564 or $repo->err("Invalid homepage URL. Note that only HTTP protocol is supported. If the URL contains funny characters, contact me.");
567 # FIXME: Permit only existing users
568 $self->{users} = [grep { valid_user_name($_) } $cgi->param('user')];
570 not $repo->err_check;
573 sub form_defaults {
574 my $self = shift;
576 name => $self->{name},
577 email => $self->{email},
578 url => $self->{url},
579 desc => html_esc($self->{desc}),
580 README => html_esc($self->{README}),
581 hp => $self->{hp},
582 users => $self->{users},
586 sub authenticate {
587 my $self = shift;
588 my ($repo) = @_;
590 $self->{ccrypt} or die "Can't authenticate against a project with no password";
591 $self->{cpwd} or $repo->err("No password entered.");
592 unless ($self->{ccrypt} eq crypt($self->{cpwd}, $self->{ccrypt})) {
593 $repo->err("Your admin password does not match!");
594 return 0;
596 return 1;
599 sub premirror {
600 my $self = shift;
602 $self->_mkdir_forkees;
603 mkdir $self->{path} or die "mkdir failed: $!";
604 chmod 0775, $self->{path} or die "chmod failed: $!";
605 $self->_properties_save;
606 $self->_alternates_setup;
607 $self->_ctags_setup;
608 $self->_group_add(':');
611 sub conjure {
612 my $self = shift;
614 $self->_mkdir_forkees;
615 system('cg-admin-setuprepo', '-g', 'repo', $self->{path}) == 0
616 or die "cg-admin-setuprepo failed: $?";
617 system('git', '--git-dir='.$self->{path}, 'config', 'receive.denyNonFastforwards', 'false');
618 $self->_nofetch(1);
619 $self->_properties_save;
620 $self->_alternates_setup;
621 $self->_ctags_setup;
622 $self->_group_add;
623 $self->_hooks_install;
626 sub update {
627 my $self = shift;
629 $self->_properties_save;
630 $self->_group_update;
633 sub update_password {
634 my $self = shift;
635 my ($pwd) = @_;
637 $self->{crypt} = scrypt($pwd);
638 $self->_group_update;
641 # You can explicitly do this just on a ghost() repository too.
642 sub delete {
643 my $self = shift;
645 if (-d $self->{path}) {
646 system('rm', '-r', $self->{path}) == 0
647 or die "rm -r failed: $?";
649 $self->_group_remove;
652 # static method
653 sub does_exist {
654 my ($name) = @_;
655 valid_proj_name($name) or die "tried to query for project with invalid name $name!";
656 (available($name)
657 or -d "$repodata_path/cloning/$name"
658 or -d "$repodata_path/to-clone/$name");
660 sub available {
661 my ($name) = @_;
662 valid_proj_name($name) or die "tried to query for project with invalid name $name!";
663 (-d "$repo_path/$name.git");
667 ### User object
669 package Git::RepoCGI::User;
671 BEGIN { use Git::RepoCGI; }
673 sub _passwd_text {
674 my $self = shift;
675 join(':', $self->{name}, $self->{crypt}, $self->{uid}, 65534, $self->{email}, '/', '/bin/git-shell')
678 sub _user_update {
679 my $self = shift;
680 my $xtra = join(',', @{$self->{users}});
681 filedb_atomic_edit(jailed_file($user_file),
682 sub {
683 $_ = $_[0];
684 chomp;
685 if ($self->{name} eq (split /:/)[0]) {
686 return $self->_passwd_text . "\n";
687 } else {
688 return "$_\n";
694 sub _passwd_add {
695 my $self = shift;
696 $self->{uid} = '\i';
697 filedb_atomic_append(jailed_file($user_file), $self->_passwd_text);
700 sub update_password {
701 my $self = shift;
702 my ($pwd) = @_;
704 $self->{crypt} = scrypt($pwd);
705 $self->_user_update;
707 sub authenticate {
708 my $self = shift;
709 my ($repo) = @_;
711 $self->{ccrypt} or die "Can't authenticate against a user with no password";
712 $self->{cpwd} or $repo->err("No password entered.");
713 unless ($self->{ccrypt} eq crypt($self->{cpwd}, $self->{ccrypt})) {
714 $repo->err("Your admin password does not match!");
715 return 0;
717 return 1;
720 sub _sshkey_path {
721 my $self = shift;
722 $sshkeys_path . '/' . $self->{name};
725 sub _sshkey_load {
726 my $self = shift;
727 open F, "<".jailed_file($self->_sshkey_path) or die "sshkey load failed: $!";
728 my @keys;
729 my $auth;
730 while (<F>) {
731 chomp;
732 if (/^ssh-(?:dss|rsa) /) {
733 push @keys, $_;
734 } elsif (/^# REPOAUTH ([0-9a-f]+) (\d+)/) {
735 my $expire = $2;
736 $auth = $1 unless (time >= $expire);
739 close F;
740 my $keys = join('', @keys); chomp $keys;
741 ($keys, $auth);
744 sub _sshkey_save {
745 my $self = shift;
746 open F, ">".jailed_file($self->_sshkey_path) or die "sshkey failed: $!";
747 if (defined($self->{auth}) && $self->{auth}) {
748 my $expire = time + 24 * 3600;
749 print F "# REPOAUTH $self->{auth} $expire\n";
751 print F $self->{keys}."\n";
752 close F;
753 chmod 0664, jailed_file($self->_sshkey_path);
756 # private constructor, do not use
757 sub _new {
758 my $class = shift;
759 my ($name) = @_;
760 valid_user_name($name) or die "refusing to create user with invalid name ($name)!";
761 my $proj = { name => $name };
763 bless $proj, $class;
766 # public constructor #0
767 # creates a virtual user not connected to disk record
768 # you can conjure() it later to disk
769 sub ghost {
770 my $class = shift;
771 my ($name) = @_;
772 my $self = $class->_new($name);
773 $self;
776 # public constructor #1
777 sub load {
778 my $class = shift;
779 my ($name) = @_;
781 open F, jailed_file($user_file) or die "user load failed: $!";
782 while (<F>) {
783 chomp;
784 @_ = split /:+/;
785 next unless (shift eq $name);
787 my $self = $class->_new($name);
789 ($self->{crypt}, $self->{uid}, undef, $self->{email}) = @_;
790 ($self->{keys}, $self->{auth}) = $self->_sshkey_load;
791 $self->{ccrypt} = $self->{crypt};
793 return $self;
795 close F;
796 undef;
799 # $user may not be in sane state if this returns false!
800 sub cgi_fill {
801 my $self = shift;
802 my ($repo) = @_;
804 $self->{name} = $repo->wparam('name');
805 valid_user_name($self->{name})
806 or $repo->err("Name contains invalid characters.");
808 $self->{email} = $repo->wparam('email');
809 valid_email($self->{email})
810 or $repo->err("Your email sure looks weird...?");
812 my $pwd = $repo->sparam('pwd');
813 if ($pwd ne '' or not $self->{crypt}) {
814 $self->{crypt} = scrypt($pwd);
817 if ($repo->sparam('pwd2') and $pwd ne $repo->sparam('pwd2')) {
818 $repo->err("Our high-paid security consultants have determined that the passwords you have entered do not match each other.");
821 $self->{cpwd} = $repo->sparam('cpwd');
823 $self->keys_fill($repo);
826 sub keys_fill {
827 my $self = shift;
828 my ($repo) = @_;
829 my $cgi = $repo->cgi;
831 $self->{keys} = $cgi->param('keys');
832 length($self->{keys}) <= 4096
833 or $repo->err("The list of keys is more than 4kb. Do you really need that much?");
834 foreach (split /\r?\n/, $self->{keys}) {
835 /^ssh-(?:dss|rsa) .* \S+@\S+$/ or $repo->err("Your ssh key (\"$_\") appears to have invalid format (does not start by ssh-dss|rsa or does not end with @-identifier) - maybe your browser has split a single key to multiple lines?");
838 not $repo->err_check;
841 sub keys_save {
842 my $self = shift;
844 $self->_sshkey_save;
847 sub gen_auth {
848 my $self = shift;
850 $self->{auth} = Digest::SHA1::sha1_hex(time . $$ . rand() . $self->{keys});
851 $self->_sshkey_save;
852 $self->{auth};
855 sub del_auth {
856 my $self = shift;
858 delete $self->{auth};
861 sub conjure {
862 my $self = shift;
864 $self->_passwd_add;
865 $self->_sshkey_save;
868 # static method
869 sub does_exist {
870 my ($name) = @_;
871 valid_user_name($name) or die "tried to query for user with invalid name $name!";
872 (-e jailed_file("$sshkeys_path/$name"));
874 sub available {
875 does_exist(@_);