Skip to content

Instantly share code, notes, and snippets.

@riywo
Created March 27, 2011 15:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save riywo/889285 to your computer and use it in GitHub Desktop.
Save riywo/889285 to your computer and use it in GitHub Desktop.
use strict;
use warnings;
use Plack::Builder;
use Plack::Session;
use Plack::Session::State::Cookie;
use Plack::Session::Store::Cache;
use Authen::Simple::DBI;
use Data::Section::Simple;
use Text::Xslate;
use Cache::Memcached::Fast;
use DBI;
use Digest;
=pod
CREATE TABLE `user_data` (
`user_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`password` varchar(64) NOT NULL,
PRIMARY KEY (`user_id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `auto_login` (
`token` varchar(48) NOT NULL,
`user_id` int(10) unsigned NOT NULL,
`expires` int(10) unsigned NOT NULL,
PRIMARY KEY (`token`),
KEY `i1` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
=cut
my $fixed_salt = 'aaaaaaaaaaaaaaaaaa';
my $stretch_count = 1000;
my $app = sub {
my $env = shift;
my $req = Plack::Request->new($env);
my $res = $req->new_response(200);
my $session = Plack::Session->new($env);
my $tx = Text::Xslate->new(
path => [
Data::Section::Simple->new()->get_data_section(),
],
);
## login?
my $dbh = DBI->connect('dbi:mysql:database=test;', 'root', '', {RaiseError => 1, AutoCommit => 1});
my $user_id = $session->get('user_id');
my $username;
if ($user_id) {
$username = $dbh->selectrow_array(
'select username from user_data where user_id = ?', {}, $user_id);
}
elsif ($req->cookies->{'token'}){
my $expires;
($user_id, $expires) = $dbh->selectrow_array(
'select user_id, expires from auto_login where token = ?', {}, $req->cookies->{'token'});
if ($expires < time) {
$dbh->do('delete from auto_login where token = ?', {}, $req->cookies->{'token'});
$user_id = undef;
}
else {
## auto login using token
$session->set('user_id', $user_id);
$username = $dbh->selectrow_array(
'select username from user_data where user_id = ?', {}, $user_id);
$dbh->do('delete from auto_login where token = ?', {}, $req->cookies->{'token'});
my $token = _set_auth_token($dbh, $user_id, $expires);
$res->cookies->{'token'} = {value => $token, expires => $expires};
}
}
my $arg = {
session => {
sid => $session->id,
%{$session->session},
username => $username,
},
cookies => $req->cookies,
};
if ($req->path_info eq '/') {
$res->content_type('text/html');
$res->body($tx->render('index.tx', $arg));
$res->finalize;
}
elsif ($req->path_info eq '/mypage') {
if (!$user_id) {
$res->redirect('/');
$res->finalize;
}
else {
my ($password) = $dbh->selectrow_array(
'select password from user_data where user_id = ?', {}, $user_id);
$arg->{'password'} = $password;
$res->content_type('text/html');
$res->body($tx->render('mypage.tx', $arg));
$res->finalize;
}
}
elsif ($req->path_info eq '/login') {
if ($user_id) {
## already login
$res->redirect('/');
$res->finalize;
}
elsif ($req->method eq 'GET') {
$session->set('redir_to', $env->{HTTP_REFERER} || '/');
$res->content_type('text/html');
$res->body($tx->render('login.tx', $arg));
$res->finalize;
}
elsif ($req->method eq 'POST') {
## check username & password
my $auth_result;
my $req_user_id;
eval {
my $authen = Authen::Simple::DBI->new(
dsn => 'dbi:mysql:host=localhost;database=test;',
username => 'root',
statement => 'select password from user_data where user_id = ?',
);
($req_user_id) = $dbh->selectrow_array(
'select user_id from user_data where username = ?', {}, $req->param('username'));
die if(!$req_user_id);
$auth_result = $authen->authenticate(
$req_user_id, _get_password_hash($req_user_id, $req->param('password')));
};
$auth_result = undef if($@);
if ($auth_result) {
## login successed
$req->session_options->{change_id}++;
$session->set('user_id', $req_user_id);
if ($req->param('auto_login')) {
my $expires = time + (24 * 60 * 60);
my $token = _set_auth_token($dbh, $req_user_id, $expires);
$res->cookies->{'token'} = {value => $token, expires => $expires};
}
my $redir_to = $session->get('redir_to');
$session->remove('redir_to');
$res->redirect($redir_to);
$res->finalize;
}
else {
## login failed
$res->content_type('text/html');
$res->body($tx->render('login_fail.tx', $arg));
$res->finalize;
}
}
}
elsif ($req->path_info eq '/logout') {
if ($req->method eq 'POST' and $req->param('token') eq $session->id and $user_id) {
$res->cookies->{'token'} = {expires => 0};
$dbh->do('delete from auto_login where user_id = ?', {}, $user_id);
$session->expire;
$res->redirect('/');
$res->finalize;
}
}
else {
$res->status(404);
$res->body("Not Found");
$res->finalize;
}
};
sub _get_salt {
my $username = shift;
return $username . pack("H*", $fixed_salt);
}
sub _get_password_hash {
my ($username, $password) = @_;
my $salt = _get_salt($username);
my $hash = '';
my $sha = Digest->new('SHA-256');
for (1..$stretch_count) {
$sha->add($hash, $password, $salt);
$hash = $sha->hexdigest;
}
return $hash;
}
sub _set_auth_token {
my ($dbh, $user_id, $expires) = @_;
my $token;
my $sth = $dbh->prepare('insert into auto_login (token, user_id, expires) values (?, ?, ?)');
for (1..10) {
$token = _get_token();
eval {
$sth->execute($token, $user_id, $expires);
};
if (!$@) {
return $token;
}
elsif ($sth->err == 1062) {
next;
}
}
die;
}
sub _get_token {
open my $fh, '<', '/dev/urandom';
read $fh, my $buf, 24;
close $fh;
return unpack("H*", $buf);
}
builder {
enable 'Debug';
enable 'Session',
store => Plack::Session::Store::Cache->new(
cache => Cache::Memcached::Fast->new({
servers => [{address => 'localhost:11211'}],
}),
),
state => Plack::Session::State::Cookie->new(
session_key => 'sid',
);
$app;
};
__DATA__
@@ base.tx
<html>
<head><title>Session Test</title></head>
<body>
<h2>Body</h2>
<div>
<: block body {} :>
</div>
<h2>Session</h2>
<ul>
<li>HTTP Request Cookie
<ul>
: for $cookies.keys() -> $key {
<li><: $key :>=<: $cookies[$key] :></li>
: }
</ul>
</li>
<li>in Plack Session
<ul>
: for $session.keys() -> $key {
<li><: $key :>=<: $session[$key] :></li>
: }
</ul>
</li>
<li>Cookie Info (using JavaScript without http-only)
<ul><li><script>document.write(document.cookie.split(';').join('</li><li>'));</script></li></ul>
</li>
</ul>
<h2>Verified ?</h2>
: if $session.user_id {
verified username = <: $session.username :>
: }
: else {
not verified
: }
<h2>Link</h2>
<ul>
<li><a href="http://localhost:5000/">top</a></li>
<li><a href="http://localhost:5000/mypage">mypage</a></li>
<li><a href="http://localhost:5000/login">login</a></li>
<li><form action="/logout" method="POST">
<input type="hidden" name="token" value="<: $session.sid :>">
<input type="submit" value="logout">
</form></li>
</ul>
</body>
</html>
@@ index.tx
: cascade base;
: override body -> {
Top Page
: }
@@ mypage.tx
: cascade base;
: override body -> {
Hi <: $session.username :> ! This is Your My Page! <br />
Your Password Hash is <: $password :> <br />
:}
@@ login.tx
: cascade base;
: override body -> {
<form action="/login" method="POST">
username: <input type="text" name="username" /><br />
password: <input type="password" name="password" /><br />
auto login(24 hours): <input type="checkbox" name="auto_login" /><br />
<input type="submit" value="login" />
</form>
: }
@@ login_fail.tx
: cascade base;
: override body -> {
<p><em>username or password failed!</em></p>
<form action="/login" method="POST">
username: <input type="text" name="username" /><br />
password: <input type="password" name="password" /><br />
auto login(24 hours): <input type="checkbox" name="auto_login" /><br />
<input type="submit" value="login" />
</form>
:}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment