use CGI qw/escapeHTML/;
use Locale::gettext;
use URI;
+use IO::File;
use Data::Dumper;
#---------- public utilities ----------
sub do_redirect_cgi ($$$$) {
my ($c, $r, $new_url, $cookie) = @_;
- my @ha = ('text/html',
- -status => '303 See other',
- -location => $new_url);
- push @ha, (-cookie => $cookie) if defined $cookie;
- $r->_print($c->header(@ha),
+ $r->_print($c->header($r->_cgi_header_args($cookie,
+ -status => '303 See other',
+ -location => $new_url)),
$r->_ch('gen_start_html',$r->_gt('Redirection')),
'<a href="'.escapeHTML($new_url).'">',
$r->_gt("If you aren't redirected, click to continue."),
my ($c,$r, $params) = @_;
my @form;
push @form, ('<form method="POST" action="'.
- escapeHTML($r->_ch('get_url')).'>'.
+ escapeHTML($r->_ch('get_url')).'">'.
'<table>');
my $sz = 'size="'.$r->{S}{form_entry_size}.'"';
foreach my $up (@{ $r->{S}{username_param_names}}) {
push @form, ('<tr><td>',$r->_gt(ucfirst $up),'</td>',
- '<td><input type="text" ',$sz.
- ' name=',$up,'></td></tr>');
+ '<td><input type="text" '.$sz.
+ ' name='.$up.'></td></tr>');
}
push @form, ('<tr><td>'.$r->_gt('Password'),'</td>',
'<td><input type="password" '.$sz.
' name="'.$r->{S}{password_param_name}.'"></td></tr>');
push @form, ('<tr><td colspan="2">',
'<input type="submit"'.
- ' name="'.$r->{S}{login_submit_name}.'"'.
+ ' name="'.$r->{S}{login_submit_name}[0].'"'.
' value="'.$r->_gt('Login').'"></td></tr>',
'</table>');
foreach my $n (keys %$params) {
assocdb_password => '',
assocdb_table => 'assocs',
random_source => '/dev/urandom',
- associdlen => 128, # bits
+ assocsecretlen => 128, # bits
login_timeout => 86400, # seconds
- assoc_param_name => 'cah_associd',
+ assoc_param_name => 'cah_assochash',
+ cookie_name => "cah_assocsecret",
password_param_name => 'password',
username_param_names => [qw(username)],
form_entry_size => 60,
do_redirect => \&do_redirect_cgi, # this hook is allowed to throw
cookie_path => "/",
get_cookie_domain => \&get_cookie_domain,
- encrypted_only => 0,
+ encrypted_only => 1,
gen_start_html => sub { $_[0]->start_html($_[2]); },
gen_end_html => sub { $_[0]->end_html(); },
gen_login_form => \&gen_plain_login_form,
gen_login_link => \&gen_plain_login_link,
gettext => sub { gettext($_[2]); },
+ print => sub { print $_[2] or die $!; },
},
Dbh => undef,
};
$v->_db_transaction(sub {
local ($dbh->{PrintError}) = 0;
$dbh->do("CREATE TABLE $v->{S}{assocdb_table} (".
- " associd VARCHAR PRIMARY KEY,".
+ " assochash VARCHAR PRIMARY KEY,".
" username VARCHAR,".
" last INTEGER NOT NULL".
")");
sub _print ($$) { my ($r, @t) = @_; return $r->_ch('print', join '', @t); }
sub construct_cookie ($$$) {
- my ($r, $cookv) = @_;
- return undef unless $cookv;
+ my ($r, $cooks) = @_;
+ return undef unless $cooks;
my $c = $r->{Cgi};
- my $cookt = $c->cookie(-name => $r->{S}{cookie_name},
- -value => $cookv,
+my @ca = (-name => $r->{S}{cookie_name},
+ -value => $cooks,
-path => $r->{S}{cookie_path},
-domain => $r->_ch('get_cookie_domain'),
-expires => '+'.$r->{S}{login_timeout}.'s',
-secure => $r->{S}{encrypted_only});
-print STDERR "CC $r $c $cookv $cookt\n";
- return $cookt;
+ my $cookie = $c->cookie(@ca);
+print STDERR "CC $r $c $cooks $cookie (@ca).\n";
+ return $cookie;
}
# pages/param-sets are
# O "you have just logged out" page load
# in cook and par,
-# a, aN anything including -
+# - no value supplied (represented in code as $cookt='')
+# n, nN value not in our db
# t, tN temporary value (in our db, no logged in user yet)
# y, yN value corresponds to logged-in user
-# n, nN value not in our db
+# and, aggregated conditions:
+# a, aN anything including -
# x, xN t or y
-# - no value supplied
# if N differs the case applies only when the two values differ
# (eg, a1 y2 does not apply when the logged-in value is supplied twice)
# - t POST i complain about cookies being disabled
# (with link to login form)
#
- # any n POST i complain about stale login form
- # show new login form
- #
- # x1 t2 POST i login (or switch user)
+ # t1 t1 POST i login (or switch user)
# if bad
# show new login form
# if good
- # revoke x1 if it was valid and !=t2
- # upgrade t2 to y2 in our db (setting username)
- # set cookie to t2
+ # upgrade t1 to y1 in our db (setting username)
# redirect to GET of remaining params
#
+ # y1 a2 POST i complain about stale login form
+ # revoke y1
+ # show new login form
+ #
+ # (other) POST i complain about stale login form
+ # show new login form
+ #
# t1 a2 ANY nrmu treat as - a2 ANY
#
# y - GET n cross-site link
# revoke y2
# treat as -/n n POST
#
- # -/n n GET n cross-site link but user not logged in
+ # -/n -/n GET n cross-site link but user not logged in
# show login form with redirect to orig params
+ # generate fresh cookie
#
# -/n n GET rmu user not logged in
# fail
my ($r) = @_;
my $meth = $r->_ch('get_method');
- my $cookv = $r->_ch('get_cookie');
- my $parmv = $r->_rp('assoc_param_name');
+ my $cooks = $r->_ch('get_cookie');
+ my $parmh = $r->_rp('assoc_param_name');
+ my $cookh = defined $cooks ? $r->hash($cooks) : undef;
- my ($cookt,$cooku) = $r->_db_lookup($cookv);
- my $parmt = $r->_db_lookup($parmv);
+ my ($cookt,$cooku) = $r->_db_lookup($cookh);
+ my $parmt = $r->_db_lookup($parmh);
+
+ print STDERR "_c_d_c cookt=$cookt parmt=$parmt\n";
if ($r->_ch('is_logout')) {
$r->_must_be_post();
die unless $parmt;
- $r->_db_revoke($cookv);
- $r->_db_revoke($parmv);
+ $r->_db_revoke($cookh);
+ $r->_db_revoke($parmh);
return ({ Kind => 'REDIRECT-LOGGEDOUT',
Message => "Logging out...",
- CookieVal => '',
+ CookieSecret => '',
Params => { } });
}
if ($r->_ch('is_loggedout')) {
die unless $parmt;
return ({ Kind => 'SMALLPAGE-LOGGEDOUT',
Message => "You have been logged out.",
- CookieVal => '',
+ CookieSecret => '',
Params => { } });
}
if ($r->_ch('is_login')) {
$r->_must_be_post();
- return ({ Kind => 'LOGIN-STALE',
- Message => "Stale session; you need to log in again.",
- CookieVal => $r->_fresh_cookie(),
- Params => { } })
- if $parmt eq 'n';
+ die unless $parmt;
+ if (!$cookt && $parmt eq 't') {
+ return ({ Kind => 'SMALLPAGE-NOCOOKIE',
+ Message => "You do not seem to have cookies enabled. ".
+ "You must enable cookies as we use them for login.",
+ CookieSecret => $r->_fresh_secret(),
+ Params => $r->_chain_params() })
+ }
+ if (!$cookt || $cookt eq 'n' || $cookh ne $parmh) {
+ $r->_db_revoke($cookh);
+ return ({ Kind => 'LOGIN-STALE',
+ Message => "Stale session; you need to log in again.",
+ CookieSecret => $r->_fresh_secret(),
+ Params => { } })
+ }
die unless $parmt eq 't' || $parmt eq 'y';
- return ({ Kind => 'SMALLPAGE-NOCOOKIE',
- Message => "You do not seem to have cookies enabled. ".
- "You must enable cookies as we use them for login.",
- CookieVal => $r->_fresh_cookie(),
- Params => $r->_chain_params() })
- if !$cookt && $parmt eq 't';
my $username = $r->_ch('login_ok');
- return ({ Kind => 'LOGIN-BAD',
- Message => "Incorrect username/password.",
- CookieVal => $cookv,
- Params => $r->_chain_params() })
- unless defined $username && length $username;
- $r->_db_revoke($cookv)
- if defined $cookv && !(defined $parmv && $cookv eq $parmv);
- $r->_db_record_login_ok($parmv,$username);
+ unless (defined $username && length $username) {
+ return ({ Kind => 'LOGIN-BAD',
+ Message => "Incorrect username/password.",
+ CookieSecret => $cooks,
+ Params => $r->_chain_params() })
+ }
+ $r->_db_record_login_ok($parmh,$username);
return ({ Kind => 'REDIRECT-LOGGEDIN',
Message => "Logging in...",
- CookieVal => $parmv,
+ CookieSecret => $cooks,
Params => $r->_chain_params() });
}
if ($cookt eq 't') {
}
die if $parmt eq 't';
- if ($cookt eq 'y' && $parmt eq 'y' && $cookv ne $parmv) {
- $r->_db_revoke($parmv) if $meth eq 'POST';
+ if ($cookt eq 'y' && $parmt eq 'y' && $cookh ne $parmh) {
+ $r->_db_revoke($parmh) if $meth eq 'POST';
$parmt = 'n';
}
if ($cookt ne 'y') {
die unless !$cookt || $cookt eq 'n';
die unless !$parmt || $parmt eq 'n' || $parmt eq 'y';
+ my $news = $r->_fresh_secret();
if ($meth eq 'GET') {
return ({ Kind => 'LOGIN-INCOMINGLINK',
Message => "You need to log in again.",
- CookieVal => $parmv,
+ CookieSecret => $news,
Params => $r->_chain_params() });
} else {
+ $r->_db_revoke($parmh);
return ({ Kind => 'LOGIN-FRESH',
Message => "You need to log in again.",
- CookieVal => $parmv,
+ CookieSecret => $news,
Params => { } });
}
}
if ($meth ne 'POST') {
return ({ Kind => 'MAINPAGEONLY',
Message => 'Entering via cross-site link.',
- CookieVal => $cookv,
+ CookieSecret => $cooks,
Params => { } });
# NB caller must then ignore params & path!
# if this is too hard they can spit out a small form
die unless $cookt eq 'y';
die unless $parmt eq 'y';
- die unless $cookv eq $parmv;
- $r->{Assoc} = $cookv;
+ die unless $cookh eq $parmh;
+ $r->{AssocSecret} = $cooks;
$r->{UserOK} = $cooku;
print STDERR "C-D-C OK\n";
return undef;
}
sub _db_lookup ($$) {
- my ($r,$v) = @_;
+ my ($r,$h) = @_;
# returns ($t,$username)
# where $t is one of "t" "y" "n", or "" (for -)
my $row = $dbh->selectrow_arrayref("SELECT username, last".
" FROM $r->{S}{assocdb_table}".
- " WHERE associd = ?", {}, $v);
+ " WHERE assochash = ?", {}, $h);
return ('') unless defined $row;
my ($nusername, $nlast) = @$row;
}
sub _db_revoke ($$) {
- # revokes $v if it's valid; no-op if it's not
- my ($r,$v) = @_;
+ # revokes $h if it's valid; no-op if it's not
+ my ($r,$h) = @_;
my $dbh = $r->{Dbh};
$dbh->do("DELETE FROM $r->{S}{assocdb_table}".
- " WHERE associd = ?", {}, $v);
+ " WHERE assochash = ?", {}, $h);
}
sub _db_record_login_ok ($$$) {
- my ($r,$v,$user) = @_;
- $r->_db_revoke($v);
+ my ($r,$h,$user) = @_;
+ $r->_db_revoke($h);
my $dbh = $r->{Dbh};
$dbh->do("INSERT INTO $r->{S}{assocdb_table}".
" (associd, username, last) VALUES (?,?,?)", {},
- $v, $user, time);
+ $h, $user, time);
}
sub check_divert ($) {
return $uri->as_string();
}
+sub _cgi_header_args ($$@) {
+ my ($r, $cookie, @ha) = @_;
+ unshift @ha, qw(-type text/html);
+ push @ha, (-cookie => $cookie) if defined $cookie;
+ print STDERR "_cgi_header_args ",join('|',@ha),".\n";
+ return @ha;
+}
+
sub check_ok ($) {
my ($r) = @_;
return 0 if $handled;
my $kind = $divert->{Kind};
- my $cookieval = $divert->{CookieVal};
+ my $cookiesecret = $divert->{CookieSecret};
my $params = $divert->{Params};
+ my $cookie = $r->construct_cookie($cookiesecret);
if ($kind =~ m/^REDIRECT-/) {
# for redirects, we honour stored NextParams and SetCookie,
die;
}
my $new_url = $r->url_with_query_params($params);
- my $cookie = $r->construct_cookie($r, $cookieval);
$r->_ch('do_redirect',$new_url, $cookie);
return 0;
}
die $kind;
}
- $r->_print($r->_ch('gen_start_html',$title),
- @body,
+ $r->_print($r->{Cgi}->header($r->_cgi_header_args($cookie)),
+ $r->_ch('gen_start_html',$title),
+ (join "\n", @body),
$r->_ch('gen_end_html'));
return 0;
}
$!=0;
read($rsf,$bin,$bytes) == $bytes or die "$rsp $!";
close $rsf;
- return unpack "H*", $bin;
+ my $out = unpack "H*", $bin;
+ print STDERR "_random out $out\n";
+ return $out;
}
-sub _fresh_cookie ($) {
+sub _fresh_secret ($) {
my ($r) = @_;
+ print STDERR "_fresh_secret\n";
my $bytes = ($r->{S}{associdlen} + 7) >> 3;
return $r->_random($bytes);
}
#---------- output ----------
-sub secret_val ($) {
+sub secret_cookie_val ($) {
+ my ($r) = @_;
+ $r->_assert_checked();
+ return defined $r->{AssocSecret} ? $r->{AssocSecret} : '';
+}
+
+sub secret_hidden_val ($) {
my ($r) = @_;
$r->_assert_checked();
- return defined $r->{Assoc} ? $r->{Assoc} : '';
+ return defined $r->{AssocSecret} ? $r->hash($r->{AssocSecret}) : '';
}
sub secret_hidden_html ($) {
my ($r) = @_;
return $r->{Cgi}->hidden(-name => $r->{S}{assoc_param_name},
- -default => $r->secret_val());
+ -default => $r->secret_hidden_val());
}
sub secret_cookie ($) {
my ($r) = @_;
+ my $secret = $r->secret_cookie_val();
+ return undef if !defined $secret;
#print STDERR "SC\n";
- my $cookv = $r->construct_cookie($r->secret_val());
+ my $cookv = $r->construct_cookie($secret);
#print STDERR "SC=$cookv\n";
return $cookv;
}