use Digest::HMAC;
use Digest::SHA;
use Data::Dumper;
+use File::Copy;
+use Cwd qw/realpath/;
+
#---------- public utilities ----------
'</a>');
}
+sub gen_srcdump_link_html ($$$$) {
+ my ($c,$r,$anchor,$specval) = @_;
+ my %params = ($r->{S}{srcdump_param_name} => [ $specval ]);
+ return '<a href="'.escapeHTML($r->url_with_query_params(\%params)).'">'.
+ $anchor."</a>";
+}
+sub gen_plain_licence_link_html ($$) {
+ my ($c,$r) = @_;
+ gen_srcdump_link_html($c,$r, 'GNU Affero GPL', 'licence');
+}
+sub gen_plain_source_link_html ($$) {
+ my ($c,$r) = @_;
+ gen_srcdump_link_html($c,$r, 'Source available', 'source');
+}
+
+sub gen_plain_footer_html ($$) {
+ my ($c,$r) = @_;
+ return ('<hr><address>',
+ ("Powered by Free / Libre / Open Source Software".
+ " according to the ".$r->_ch('gen_licence_link_html')."."),
+ $r->_ch('gen_source_link_html').".",
+ '</address>');
+}
+
+#---------- licence and source code ----------
+
+sub srcdump_dump ($$$) {
+ my ($c,$r, $thing) = @_;
+ die if $thing =~ m/\W/ || $thing !~ m/\w/;
+ my $path = $r->_get_path('srcdump');
+ my $ctf = new IO::File "$path/$thing.ctype", 'r'
+ or die "$path/$thing.ctype $!";
+ my $ct = <$ctf>;
+ chomp $ct or die "$path/$thing ?";
+ $ctf->close or die "$path/$thing $!";
+ my $df = new IO::File "$path/$thing.data", 'r'
+ or die "$path/$thing.data $!";
+ $r->_ch('dump', $ct, $df);
+}
+
+sub dump_plain ($$$$) {
+ my ($c, $r, $ct, $df) = @_;
+ $r->_print($c->header('-type' => $ct));
+ my $buffer;
+ for (;;) {
+ my $got = read $df, $buffer, 65536;
+ die $! unless defined $got;
+ return if !$got;
+ $r->_print($buffer);
+ }
+}
+
+sub srcdump_process_item ($$$$$$) {
+ my ($c, $v, $dumpdir, $item, $outfn, $needlicence, $dirsdone) = @_;
+ if ($v->_ch('srcdump_system_dir', $item)) {
+ $outfn->("srcdump_process_item: srcdump_system_dir, skipping $item");
+ return;
+ }
+ my $upwards = $item;
+ for (;;) {
+ $upwards =~ s#/+$##;
+ $upwards =~ s#/+\.$##;
+ last unless $upwards =~ m#[^/]#;
+ foreach my $try (@{ $v->{S}{srcdump_vcs_dirs} }) {
+#print STDERR "TRY $item $upwards $try\n";
+ if (!stat "$upwards/$try") {
+ $!==&ENOENT or $!==&ENOTDIR or die "check $upwards/$try $!";
+ next;
+ }
+#print STDERR "VCS $item $upwards $try\n";
+ if ($dirsdone->{$upwards}++) {
+ $outfn->("srcdump_process_item: did $upwards,".
+ " skipping $item");
+ return;
+ }
+#print STDERR "VCS $item $upwards $try GO\n";
+ $try =~ m/\w+/ or die;
+ return $v->_ch('srcdump_byvcs', $dumpdir, $upwards, $outfn, lc $&);
+ }
+ $upwards =~ s#/*[^/]+$##;
+ }
+ return $v->_ch('srcdump_novcs', $dumpdir, $item, $outfn);
+}
+
+sub srcdump_novcs ($$$$$) {
+ my ($c, $v, $dumpdir, $item, $outfn) = @_;
+ stat $item or die "$item $!";
+ if (-d _) {
+ my $script = 'find -type f -perm +004';
+ foreach my $excl (@{ $v->{S}{srcdump_excludes} }) {
+ $script .= " \\! -name '$excl'";
+ }
+ $script .= " -print0";
+ return srcdump_dir_cpio($c,$v,$dumpdir,$item,$outfn,'novcs',$script);
+ } elsif (-f _) {
+ return srcdump_file($c,$v,$dumpdir,$item,$outfn);
+ } else {
+ die "$item not file or directory";
+ }
+}
+
+sub srcdump_byvcs ($$$$$$) {
+ my ($c, $v, $dumpdir, $dir, $outfn, $vcs) = @_;
+#print STDERR "BYVCS GIT $dir\n";
+ return srcdump_dir_cpio($c,$v,$dumpdir,$dir,$outfn,$vcs,
+ $v->{S}{"srcdump_vcsscript_$vcs"});
+}
+
+sub srcdump_file ($$$$) {
+ my ($c,$v,$dumpdir,$file,$outfn) = @_;
+ my $outfile = $outfn->("srcdump_file saved $file", "src");
+ copy($file,$outfile) or die "$file $outfile $!";
+}
+
+sub srcdump_dir_cpio ($$$$$$$) {
+ my ($c,$v,$dumpdir,$dir,$outfn,$how,$script) = @_;
+ my $outfile = $outfn->("srcdump_dir_cpio $how saved $dir", "tar");
+#print STDERR "CPIO $dir >$script<\n";
+ my $pid = fork();
+ defined $pid or die $!;
+ if (!$pid) {
+ $SIG{__DIE__} = sub {
+ print STDERR "CGI::Auth::Flexible srcdump error: $@\n";
+ exit 127;
+ };
+ open STDOUT, ">", $outfile or die "$outfile $!";
+ chdir $dir or die "chdir $dir: $!";
+ exec '/bin/bash','-ec',"
+ set -o pipefail
+ (
+ $script
+ ) | (
+ cpio -Hustar -o --quiet -0 -R 1000:1000 || \
+ cpio -Hustar -o --quiet -0
+ )
+ ";
+ die $!;
+ }
+ $!=0; (waitpid $pid, 0) == $pid or die "$!";
+ die "$dir ($how $script) $outfile $?" if $?;
+}
+
+sub srcdump_dirscan_prepare ($$) {
+ my ($c, $v) = @_;
+ my $dumpdir = $v->_get_path('srcdump');
+ mkdir $dumpdir or $!==&EEXIST or die "mkdir $dumpdir $!";
+ my $lockf = new IO::File "$dumpdir/generate.lock", 'w+'
+ or die "$dumpdir/generate.lock $!";
+ flock $lockf, LOCK_EX or die "$dumpdir/generate.lock $!";
+ my $needlicence = "$dumpdir/licence.tmp";
+ unlink $needlicence or $!==&ENOENT or die "rm $needlicence $!";
+ if (defined $v->{S}{srcdump_licence_path}) {
+ copy($v->{S}{srcdump_licence_path}, $needlicence)
+ or die "$v->{S}{srcdump_licence_path} $!";
+ $needlicence = undef;
+ }
+ unlink <"$dumpdir/s.[a-z][a-z][a-z].*">;
+ my @srcfiles = qw(licence.data manifest.txt);
+ my $srcoutcounter = 'aaa';
+
+ my $reportfh = new IO::File "$dumpdir/manifest.txt", 'w' or die $!;
+ my $outfn = sub {
+ my ($message, $extension) = @_;
+ if (defined $extension) {
+ my $leaf = "s.$srcoutcounter.$extension";
+ $srcoutcounter++;
+ push @srcfiles, $leaf;
+ print $reportfh "$leaf: $message\n" or die $!;
+ return "$dumpdir/$leaf";
+ } else {
+ print $reportfh "none: $message\n" or die $!;
+ return undef;
+ }
+ };
+ my %dirsdone;
+ foreach my $item ($v->_ch('srcdump_listitems')) {
+ if ($item eq '.' && $v->{S}{srcdump_filter_cwd}) {
+ my @bad = grep { !m#^/# } values %INC;
+ die "filtering . from srcdump items and \@INC but already".
+ " included @bad " if @bad;
+ @INC = grep { $_ ne '.' } @INC;
+ next;
+ }
+ if (!lstat "$item") {
+ die "stat $item $!" unless $!==&ENOENT;
+ $outfn->("srcdump_dirscan_prepare stat ENOENT, skipping $item");
+ next;
+ };
+ if (-l _) {
+ $item = realpath($item);
+ if (!defined $item) {
+ die "realpath $item $!" unless $!==&ENOENT;
+ $outfn->("srcdump_dirscan_prepare realpath ENOENT,".
+ " skipping $item");
+ }
+ }
+ if (defined $needlicence) {
+ foreach my $try (@{ $v->{S}{srcdump_licence_files} }) {
+ last if copy("$item/$try", $needlicence);
+ $!==&ENOENT or $!==&ENOTDIR or die "copy $item/$try $!";
+ }
+ }
+ $v->_ch('srcdump_process_item', $dumpdir, $item,
+ $outfn, \$needlicence, \%dirsdone);
+ $dirsdone{$item}++;
+ }
+ close $reportfh or die $!;
+ srcdump_install($c,$v, $dumpdir, 'licence', 'text/plain');
+ $!=0;
+ my @cmd = (qw(tar -zvvcf), "$dumpdir/source.tmp",
+ "-C", $dumpdir, qw( --), @srcfiles);
+ my $r = system(@cmd);
+ if ($r) {
+ print STDERR "CGI::Auth::Flexible tar failed ($r $!) @cmd\n";
+ die "tar failed";
+ }
+ die "licence file not found" unless defined $needlicence;
+ srcdump_install($c,$v, $dumpdir, 'source', 'application/octet-stream');
+ close $lockf or die $!;
+}
+
+sub srcdump_install ($$$$$) {
+ my ($c,$v, $dumpdir, $which, $ctype) = @_;
+ rename "$dumpdir/$which.tmp", "$dumpdir/$which.data"
+ or die "$dumpdir/$which.data $!";
+ my $ctf = new IO::File "$dumpdir/$which.tmp", 'w'
+ or die "$dumpdir/$which.tmp $!";
+ print $ctf $ctype, "\n" or die $!;
+ close $ctf or die $!;
+ rename "$dumpdir/$which.tmp", "$dumpdir/$which.ctype"
+ or die "$dumpdir/$which.ctype $!";
+}
+
#---------- verifier object methods ----------
sub new_verifier {
assocdb_dbh => undef, # must have AutoCommit=0, RaiseError=1
assocdb_path => 'caf-assocs.db',
keys_path => 'caf-keys',
+ srcdump_path => 'caf-srcdump',
assocdb_dsn => undef,
assocdb_user => '',
assocdb_password => '',
dummy_param_name_prefix => 'caf__',
cookie_name => "caf_assocsecret",
password_param_name => 'password',
+ srcdump_param_name => 'caf_srcdump',
username_param_names => [qw(username)],
form_entry_size => 60,
logout_param_names => [qw(caf_logout)],
get_path_info => sub { $_[0]->path_info() },
get_cookie => sub { $_[0]->cookie($_[1]->{S}{cookie_name}) },
get_method => sub { $_[0]->request_method() },
+ check_https => sub { !!$_[0]->https() },
get_url => sub { $_[0]->url(); },
is_login => sub { defined $_[1]->_rp('password_param_name') },
login_ok => \&login_ok_password,
get_cookie_domain => \&get_cookie_domain,
encrypted_only => 1,
gen_start_html => sub { $_[0]->start_html($_[2]); },
+ gen_footer_html => \&gen_plain_footer_html,
+ gen_licence_link_html => \&gen_plain_licence_link_html,
+ gen_source_link_html => \&gen_plain_source_link_html,
gen_end_html => sub { $_[0]->end_html(); },
gen_login_form => \&gen_plain_login_form,
gen_login_link => \&gen_plain_login_link,
gen_postmainpage_form => \&gen_postmainpage_form,
+ srcdump_dump => \&srcdump_dump,
+ srcdump_prepare => \&srcdump_dirscan_prepare,
+ srcdump_licence_path => undef,
+ srcdump_licence_files => [qw(AGPLv3 CGI/Auth/Flexible/AGPLv3)],
+ srcdump_listitems => sub { (@INC, $ENV{'SCRIPT_FILENAME'}, $0); },
+ srcdump_filter_cwd => 1,
+ srcdump_system_dir => sub {
+ $_[2] =~ m#^/etc/|^/usr/(?!local/)(?!lib/cgi)#;
+ },
+ srcdump_process_item => \&srcdump_process_item,
+ srcdump_vcs_dirs => [qw(.git .hg .bzr .svn CVS)],
+ srcdump_vcsscript_git => "
+ git ls-files -z
+ git ls-files -z --others --exclude-from=.gitignore
+ find .git -print0
+ ",
+ srcdump_vcsscript_hg => "false hg",
+ srcdump_vcsscript_bzr => "false bzr",
+ srcdump_vcsscript_svn => "false svn",
+ srcdump_vcsscript_cvs => "false cvs",
+ srcdump_byvcs => \&srcdump_byvcs,
+ srcdump_novcs => \&srcdump_novcs,
+ srcdump_excludes => [qw(*~ *.bak *.tmp), '#*#'],
+ dump => \&dump_plain,
gettext => sub { gettext($_[2]); },
print => sub { print $_[2] or die $!; },
debug => sub { }, # like print; msgs contain trailing \n
}
bless $verifier, $class;
$verifier->_dbopen();
+ $verifier->_ch('srcdump_prepare');
return $verifier;
}
}
sub _get_path ($$) {
- my ($v,$keybase) = @_;
- my $leaf = $v->{S}{"${keybase}_path"};
- my $dir = $v->{S}{dir};
+ my ($r,$keybase) = @_;
+ my $leaf = $r->{S}{"${keybase}_path"};
+ return $r->_absify_path($leaf);
+}
+
+sub _absify_path ($$) {
+ my ($v,$leaf) = @_;
return $leaf if $leaf =~ m,^/,;
+ my $dir = $v->{S}{dir};
die "relying on cwd by default ?! set dir" unless defined $dir;
return "$dir/$leaf";
}
sub _check_divert_core ($) {
my ($r) = @_;
- my $meth = $r->_ch('get_method');
+ my $srcdump = $r->_rp('srcdump_param_name');
+ if ($srcdump) {
+ die if $srcdump =~ m/\W/;
+ return ({ Kind => 'SRCDUMP-'.uc $srcdump,
+ Message => undef,
+ CookieSecret => undef,
+ Params => { } });
+ }
+
my $cooks = $r->_ch('get_cookie');
+
+ if ($r->{S}{encrypted_only} && !$r->_ch('check_https')) {
+ return ({ Kind => 'REDIRECT-HTTPS',
+ Message => $r->_gt("Redirecting to secure server..."),
+ CookieSecret => undef,
+ Params => { } });
+ }
+
+ my $meth = $r->_ch('get_method');
my $parmh = $r->_rp('assoc_param_name');
my $cookh = defined $cooks ? $r->hash($cooks) : undef;
}
if ($r->_ch('is_loggedout')) {
die unless $meth eq 'GET';
- die unless $cookt;
- die unless $parmt;
+ die if $cookt eq 'y';
+ die if $parmt;
return ({ Kind => 'SMALLPAGE-LOGGEDOUT',
Message => $r->_gt("You have been logged out."),
CookieSecret => '',
my $params = $divert->{Params};
my $cookie = $r->construct_cookie($cookiesecret);
+ if ($kind =~ m/^SRCDUMP-(\w+)$/) {
+ $r->_ch('srcdump_dump', (lc $1));
+ return 0;
+ }
+
if ($kind =~ m/^REDIRECT-/) {
# for redirects, we honour stored NextParams and SetCookie,
# as we would for non-divert
$params->{$r->{S}{loggedout_param_names}[0]} = [ 1 ];
} elsif ($kind eq 'REDIRECT-LOGOUT') {
$params->{$r->{S}{logout_param_names}[0]} = [ 1 ];
- } elsif ($kind eq 'REDIRECT-LOGGEDIN') {
+ } elsif ($kind =~ m/REDIRECT-(?:LOGGEDIN|HTTPS)/) {
} else {
die;
}
my $new_url = $r->url_with_query_params($params);
+ if ($kind eq 'REDIRECT-HTTPS') {
+ my $uri = URI->new($new_url);
+ die unless $uri->scheme eq 'http';
+ $uri->scheme('https');
+ $new_url = $uri->as_string();
+ }
$r->_ch('do_redirect',$new_url, $cookie);
return 0;
}
$r->_print($r->{Cgi}->header($r->_cgi_header_args($cookie)),
$r->_ch('gen_start_html',$title),
- (join "\n", @body),
- $r->_ch('gen_end_html'));
+ (join "\n", (@body,
+ $r->_ch('gen_footer_html'),
+ $r->_ch('gen_end_html'))));
return 0;
}
=head1 SYNOPSYS
my $verifier = CGI::Auth::Flexible->new_verifier(setting => value,...);
- my $authreq = $verifier->new_request($cgi_request_object);
-
- my $authreq = CGI::Auth::Flexible->new_request($cgi_request_object,
- setting => value,...);
-
-=head1 USAGE PATTERN FOR SIMPLE APPLICATIONS
+ my $authreq = $verifier->new_request($cgi_query_object);
+ # simple applications
$authreq->check_ok() or return;
- blah blah blah
+ # sophisticated applications
+ my $divert_kind = $authreq->check_divert();
+ if ($divert_kind) { ... print diversion page and quit ... }
+
+ # while handling the request
+ $user = $authreq->get_username();
$authreq->check_mutate();
- blah blah blah
-=head1 USAGE PATTERN FOR FANCY APPLICATIONS
+=head1 DESCRIPTION
- my $divert_kind = $authreq->check_divert();
- if ($divert_kind) {
- if ($divert_kind eq 'LOGGEDOUT') {
- print "goodbye you are now logged out" and quit
- } elsif ($divert_kind eq 'NOCOOKIES') {
- print "you need cookies" and quit
- ... etc.
- }
- }
-
- blah blah blah
- $authreq->check_mutate();
- blah blah blah
+CGI::Auth::Flexible is a library which you can use to add a
+forms/cookie-based login facility to a Perl web application.
+
+CGI::Auth::Flexible doesn't interfere with your application's URL path
+namespace and just needs a few (configurable) form parameter and
+cookie name(s) for its own use. It tries to avoid making assumptions
+about the implementation structure of your application.
+
+Because CGI::Auth::Flexible is licenced under the AGPLv3, you will
+probably need to provide a facility to allow users (even ones not
+logged in) to download the source code for your web app. Conveniently
+by default CGI::Auth::Flexible provides (for pure Perl webapps) a
+mechanism for users to get the source.
+
+CGI::Auth::Flexible is designed to try to stop you accidentally
+granting access by misunderstanding the API. (Also it, of course,
+guards against cross-site scripting.) You do need to make sure to
+call CGI::Auth::Flexible before answering AJAX requests as well as
+before generating HTML pages, of course, and to call it in every
+entrypoint to your system.
+
+=head1 INITIALISATION
+
+Your application should, on startup (eg, when it is loaded by
+mod_perl) do
+C<< $verifier = CGI::Auth::Flexible->new_verifier(settings...) >>.
+This call can be expensive and is best amortised.
+
+The resulting verifier object can be used to process individual
+requests, in each case with
+C<< $authreq = CGI::Auth::Flexible->new_request($cgi_query) >>.
+
+=head1 SIMPLE APPLICATIONS
+
+The simplist usage is to call C<< $request->check_ok() >> which will
+check the user's authentication. If the user is not logged in it will
+generate a login form (or redirection or other appropriate page) and
+return false; your application should not then processing that request
+any further. If the user is logged in it will return true.
+
+After calling C<check_ok> you can use C<< $request->get_username >>
+to find out which user the request came from.
+
+=head1 SOPHISTICATED APPLICATIONS
+
+If you want to handle the flow control and to generate login forms,
+redirections, etc., yourself, you can say
+C<< $divert = $request->check_divert >>. This returns undef if
+the user is logged in, or I<divert spec> if some kind of login
+page or diversion should be generated.
+
+=head1 MUTATING OPERATIONS AND EXTERNAL LINKS INTO YOUR SITE
+
+By default CGI::Auth::Flexible does not permit external links into
+your site. All GET requests give a "click to continue" page which
+submits a form.
+
+This is because the alternative (for complicated reasons relating to
+the web security architecture) is to require your application to make
+a special and different check when the incoming request is going to do
+some kind of action (such as modifying the user's setup, purchasing
+goods, or whatever) rather than just display HTML pages.
+
+To support external links, pass C<< promise_check_mutate => 1 >> in
+I<settings>, and then call C<< $authreq->check_mutate() >> before
+taking any actions. If the incoming request is not suitable then
+C<< $authreq->check_mutate() >> will call C<die>. If you do this you
+must make sure that you have no mutating C<GET> requests in your
+application - but you shouldn't have any of those anyway.
+
+=head1 SOURCE CODE DOWNLOAD
+
+By default, CGI::Auth::Flexible provides a facility for users to
+download the source code for the running version of your web
+application.
+
+This facility makes a number of important assumptions which you need
+to check. Note that if the provided facility is not sufficient
+because your application is more sophisticated than it copes with (or
+if you disable the builtin facility), you may need to implement a
+functioning alternative to avoid violating the AGPLv3 licence.
+
+Here are the most important (default) assumptions:
+
+=over
+
+=item *
+
+Your app's source code is available by looking at @INC, $0 and
+S<$ENV{'SCRIPT_FILENAME'}> (the B<source items>). See
+C<srcdump_listitems>. Where these point to files or directories under
+revision control, the source item is the whole containing vcs tree.
+
+=item *
+
+Specifically, there are no compiled or autogenerated Perl
+files, Javascript resources, etc., which are not contained in one of
+the source item directories. (Files which came with your operating
+system install don't need to be shipped as they fall under the system
+library exceptio.)
+
+=item *
+
+You have not installed any modified versions of system
+libraries (including system-supplied) Perl modules in C</usr> outside
+C</usr/local>. See C<srcdump_system_dir>.
+
+=item *
+
+For each source item in a dvcs, the entire dvcs history does
+not contain anything confidential (or libellous). Also, all files which
+contain secrets are in the dvcs's C<.ignore> file. See
+C<srcdump_vcsscript_git> et al.
+
+=item *
+
+For each source item NOT in a dvcs, there are no confidential
+files with the world-readable bit set (being in a world-inaccessible
+directory is not sufficient). See C<srcdump_excludes>.
+
+=item *
+
+You have none of your app's source code in C</etc>.
+
+=item *
+
+You don't regard pathnames on your server as secret.
+
+=item *
+
+You don't intentionally load Perl code by virtule of C<.>
+being in C<@INC> by default. (See C<srcdump_filter_cwd>.)
+
+=back
+
+=head1 FUNCTIONS AND METHODS
+
+=over
+
+=item C<< CGI::Auth::Flexible::new_verifier(setting => value, ...) >>
+
+Initialises an instance and returns a verifier object.
+The arguments are setting pairs like a hash initialiser.
+See L</SETTINGS> below.
+
+=item C<< $verifier->new_request($cgi_query) >>
+
+Prepares to process a request. C<$cgi_query> should normally
+be the query object from L<CGI(3perl)>. Most of the default
+hook methods assume that it is; however if you replace enough of
+the hook methods then you can pass any value you like and it
+will be passed to your hooks.
+
+The return value is the authentication request object (C<$authreq>)
+which is used to check the incoming request and will contain
+information about its credentials.
+
+=item C<< $authreq->check_divert() >>
+
+Checks whether the user is logged in. Returns undef if the user is
+logged in and we should service the request. Otherwise returns a
+divert spec (see L</DIVERT SPEC>) saying what should happen instead.
+
+=back