package Apache2::AuthHatena;
use strict;
use warnings;
use Apache2::RequestRec ();
use Apache2::ServerUtil ();
use Apache2::RequestIO ();
use Apache2::RequestUtil ();
use Apache2::Log ();
use Apache2::Access ();
use Apache2::Module ();
use Apache2::CmdParms ();
use Apache2::Const -compile => qw(
FORBIDDEN OK DECLINED REDIRECT OR_AUTHCFG TAKE1
);
use APR::Table ();
use CGI;
use CGI::Cookie;
use Digest::MD5;
use Time::Piece;
use Hatena::API::Auth;
our $VERSION = '0.05';
my @directives = (
{
name => 'HatenaAuthKey',
func => __PACKAGE__ . '::HatenaAuthKey',
req_override => Apache2::Const::OR_AUTHCFG,
args_how => Apache2::Const::TAKE1,
errmsg => 'HatenaAuthKey yourkey',
},
{
name => 'HatenaAuthSecret',
func => __PACKAGE__ . '::HatenaAuthSecret',
req_override => Apache2::Const::OR_AUTHCFG,
args_how => Apache2::Const::TAKE1,
errmsg => 'HatenaAuthSecret yoursecretkey',
},
{
name => 'HatenaAuthCallback',
func => __PACKAGE__ . '::HatenaAuthCallback',
req_override => Apache2::Const::OR_AUTHCFG,
args_how => Apache2::Const::TAKE1,
errmsg => 'HatenaAuthCallback http://sample.com/yourcallback',
},
);
eval {
Apache2::Module::add(__PACKAGE__, \@directives);
Apache2::ServerUtil->server->push_handlers(
PerlAuthenHandler => \&authen_handler
);
};
sub HatenaAuthKey {
my ($i, $params, $arg) = @_;
$i = Apache2::Module::get_config( __PACKAGE__, $params->server);
$i->{'api_key'} = $arg;
}
sub HatenaAuthSecret {
my ($i, $params, $arg) = @_;
$i = Apache2::Module::get_config( __PACKAGE__, $params->server);
$i->{'secret'} = $arg;
}
sub HatenaAuthCallback {
my ($i, $params, $arg) = @_;
$i = Apache2::Module::get_config( __PACKAGE__, $params->server);
$i->{'callback'} = $arg;
}
sub authen_handler {
my $r = shift;
$r->auth_type ne 'Hatena' and return Apache2::Const::DECLINED;
my $realm = $r->auth_name;
# $r->no-cache(1);
$r->err_headers_out->set('Pragma' => 'no-cache');
$r->err_headers_out->set('Cache-control' => 'private, no-cache, no-store, must-revalidate, max-age=0');
my $cf = Apache2::Module::get_config(__PACKAGE__, $r->server);
my $secret = $cf->{'secret'};
my $callback = $cf->{'callback'};
my $request_url = "http://".($r->hostname || '').($r->uri || '');
my $request_url_forwarded = "http://".($r->headers_in->{'X-Forwarded-Host'} || '').($r->uri || '');
if ($request_url eq $callback || $request_url_forwarded eq $callback) {
$r->set_handlers(PerlAuthzHandler => \&authz_handler_bypass);
$r->handler('modperl');
if ($r->args eq 'logout') {
$r->set_handlers(PerlResponseHandler => \&logout);
return Apache2::Const::OK;
} elsif ($r->args eq 'about') {
$r->set_handlers(PerlResponseHandler => \&about);
return Apache2::Const::OK;
}
return &callback($r);
}
my %cookie = CGI::Cookie->parse($r->headers_in->{Cookie});
unless (%cookie && $cookie{"Apache2-AuthHatena_$realm"}) {
return &process_forbidden( $r, 'no cookies');
}
my ($name, $token, $time) = $cookie{"Apache2-AuthHatena_$realm"}->value;
if (!$time || $time < time()) {
return &process_forbidden( $r, "id:$name, cookie is too old");
}
if (Digest::MD5::md5_hex($name,$secret.$time) ne $token) {
return &process_forbidden( $r, "d:$name, token is broken");
}
$r->set_handlers(PerlAuthzHandler => \&authz_handler);
$r->user($name);
return Apache2::Const::OK;
}
sub authz_handler {
my $r = shift;
$r->auth_type ne 'Hatena' and return Apache2::Const::DECLINED;
my $cf = Apache2::Module::get_config(__PACKAGE__, $r->server);
my $callback = $cf->{callback};
my $name = $r->user;
unless ($name) {
return &process_forbidden( $r, 'no Hatena ID found');
}
my %required_users = ();
my $validuser = 0;
my $requires = $r->requires;
for (@{$requires}) {
if ($_->{requirement} =~ /^user\s+(.+)$/) {
$required_users{$_} = 1 for (split /\s+/, $1);
} elsif ($_->{requirement} eq 'valid-user') {
$validuser = 1;
}
}
if ($validuser) {
return Apache2::Const::OK;
}
unless (exists $required_users{$name}) {
return &process_forbidden($r, "id:${name}, not permitted");
}
return Apache2::Const::OK;
}
sub process_forbidden {
my ($r, $reason) = @_;
$r->set_handlers(PerlAuthzHandler => \&authz_handler_bypass);
$r->handler('modperl');
$r->set_handlers(PerlResponseHandler => \&forbidden_handler);
$r->pnotes(reason => $reason);
return Apache2::Const::OK;
}
sub authz_handler_bypass {
return Apache2::Const::OK;
}
sub callback {
my $r = shift;
my $realm = $r->auth_name;
my $q = CGI->new($r);
my $referer = $q->param('r') || $r->headers_in->{Referer} || '';
my $cf = Apache2::Module::get_config(__PACKAGE__, $r->server);
my $api_key = $cf->{api_key};
my $secret = $cf->{secret};
my $api= Hatena::API::Auth->new({
api_key => $api_key,
secret => $secret,
});
if (my $cert = $q->param('cert')) {
my $user;
unless ($user = $api->login($cert)) {
return &process_forbidden($r, 'cert is broken');
}
my $name = $user->name;
$r->user($name);
my $time = time() + 3600;
my $expires = gmtime($time)->strftime;
my $token = Digest::MD5::md5_hex($name.$secret.$time);
my $cookie = CGI::Cookie->new(
-name => "Apache2-AuthHatena_$realm",
-value => [ $name, $token, $time ],
-expires => $expires
);
$r->err_headers_out->set('Set-Cookie' => $cookie);
if ($referer && $referer !~ m{^http://auth.hatena.ne.jp/auth}) {
$r->err_headers_out->set(Location => $referer);
return Apache2::Const::REDIRECT;
}
$r->set_handlers(PerlResponseHandler => \&about);
return Apache2::Const::OK;
} else {
my $uri = $api->uri_to_login(r => $referer);
$r->err_headers_out->add(Location => $uri);
return Apache2::Const::REDIRECT;
}
}
sub logout {
my $r = shift;
my $realm = $r->auth_name;
my $cookie = CGI::Cookie->new(
-name => "Apache2-AuthHatena_$realm",
-value => 'logout',
-expires => '-1d',
);
$r->err_headers_out->set('Set-Cookie' => $cookie);
$r->content_type('text/html; charset=UTF-8');
my $lang = $r->headers_in->{'Accept-Language'} =~ /ja/ ? 'ja' : 'en';
my $message = '';
if ($lang eq 'ja') {
$message = <
$message
"; return <<"EOF";