959 lines
26 KiB
Perl
959 lines
26 KiB
Perl
|
|
# This code was forked from the LiveJournal project owned and operated
|
||
|
|
# by Live Journal, Inc. The code has been modified and expanded by
|
||
|
|
# Dreamwidth Studios, LLC. These files were originally licensed under
|
||
|
|
# the terms of the license supplied by Live Journal, Inc, which can
|
||
|
|
# currently be found at:
|
||
|
|
#
|
||
|
|
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
|
||
|
|
#
|
||
|
|
# In accordance with the original license, this code and all its
|
||
|
|
# modifications are provided under the GNU General Public License.
|
||
|
|
# A copy of that license can be found in the LICENSE file included as
|
||
|
|
# part of this distribution.
|
||
|
|
|
||
|
|
package LJ::Session;
|
||
|
|
use strict;
|
||
|
|
use Carp qw(croak);
|
||
|
|
use Digest::HMAC_SHA1 qw(hmac_sha1 hmac_sha1_hex);
|
||
|
|
use LJ::Utils qw(rand_chars);
|
||
|
|
|
||
|
|
use constant VERSION => 1;
|
||
|
|
|
||
|
|
# NOTES
|
||
|
|
#
|
||
|
|
# * fields in this object:
|
||
|
|
# userid, sessid, exptype, auth, timecreate, timeexpire, ipfixed
|
||
|
|
#
|
||
|
|
# * do not store any references in the LJ::Session instances because of serialization
|
||
|
|
# and storage in memcache
|
||
|
|
#
|
||
|
|
# * a user makes a session(s). cookies aren't sessions. cookies are handles into
|
||
|
|
# sessions, and there can be lots of cookies to get the same session.
|
||
|
|
#
|
||
|
|
# * this file is a mix of instance, class, and util functions/methods
|
||
|
|
#
|
||
|
|
# * the 'auth' field of the session object is the prized possession which
|
||
|
|
# we might hide from XSS attackers. they can steal domain cookies but
|
||
|
|
# they're not good very long and can't do much. it's the ljmastersession
|
||
|
|
# containing the auth that we care about.
|
||
|
|
#
|
||
|
|
|
||
|
|
############################################################################
|
||
|
|
# CREATE/LOAD SESSIONS OBJECTS
|
||
|
|
############################################################################
|
||
|
|
|
||
|
|
sub instance {
|
||
|
|
my ( $class, $u, $sessid ) = @_;
|
||
|
|
|
||
|
|
return undef unless $u && !$u->is_expunged;
|
||
|
|
|
||
|
|
# try memory
|
||
|
|
my $memkey = _memkey( $u, $sessid );
|
||
|
|
my $sess = LJ::MemCache::get($memkey);
|
||
|
|
return $sess if $sess;
|
||
|
|
|
||
|
|
# try master
|
||
|
|
$sess = $u->selectrow_hashref(
|
||
|
|
"SELECT userid, sessid, exptype, auth, timecreate, timeexpire, ipfixed "
|
||
|
|
. "FROM sessions WHERE userid=? AND sessid=?",
|
||
|
|
undef, $u->{'userid'}, $sessid
|
||
|
|
) or return undef;
|
||
|
|
|
||
|
|
bless $sess;
|
||
|
|
LJ::MemCache::set( $memkey, $sess );
|
||
|
|
return $sess;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub active_sessions {
|
||
|
|
my ( $class, $u ) = @_;
|
||
|
|
return unless $u && !$u->is_expunged;
|
||
|
|
|
||
|
|
my $sth = $u->prepare( "SELECT userid, sessid, exptype, auth, timecreate, timeexpire, ipfixed "
|
||
|
|
. "FROM sessions WHERE userid=? AND timeexpire > UNIX_TIMESTAMP()" );
|
||
|
|
$sth->execute( $u->{userid} );
|
||
|
|
my @ret;
|
||
|
|
while ( my $rec = $sth->fetchrow_hashref ) {
|
||
|
|
bless $rec;
|
||
|
|
push @ret, $rec;
|
||
|
|
}
|
||
|
|
return @ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub create {
|
||
|
|
my ( $class, $u, %opts ) = @_;
|
||
|
|
|
||
|
|
# validate options
|
||
|
|
my $exptype = delete $opts{'exptype'} || "short";
|
||
|
|
my $ipfixed = delete $opts{'ipfixed'}; # undef or scalar ipaddress FIXME: validate
|
||
|
|
my $nolog = delete $opts{'nolog'} || 0; # 1 to not log to loginlogs
|
||
|
|
croak("Invalid exptype") unless $exptype =~ /^short|long|once$/;
|
||
|
|
|
||
|
|
croak( "Invalid options: " . join( ", ", keys %opts ) ) if %opts;
|
||
|
|
|
||
|
|
my $udbh = LJ::get_cluster_master($u);
|
||
|
|
return undef unless $udbh;
|
||
|
|
|
||
|
|
# clean up any old, expired sessions they might have (lazy clean)
|
||
|
|
$u->do( "DELETE FROM sessions WHERE userid=? AND timeexpire < UNIX_TIMESTAMP()",
|
||
|
|
undef, $u->{userid} );
|
||
|
|
|
||
|
|
# FIXME: but this doesn't remove their memcached keys
|
||
|
|
|
||
|
|
my $expsec = LJ::Session->session_length($exptype);
|
||
|
|
my $timeexpire = time() + $expsec;
|
||
|
|
|
||
|
|
my $sess = {
|
||
|
|
auth => LJ::rand_chars(10),
|
||
|
|
exptype => $exptype,
|
||
|
|
ipfixed => $ipfixed,
|
||
|
|
timeexpire => $timeexpire,
|
||
|
|
};
|
||
|
|
|
||
|
|
my $id = LJ::alloc_user_counter( $u, 'S' );
|
||
|
|
return undef unless $id;
|
||
|
|
|
||
|
|
$u->record_login($id)
|
||
|
|
unless $nolog;
|
||
|
|
|
||
|
|
$u->do(
|
||
|
|
"REPLACE INTO sessions (userid, sessid, auth, exptype, "
|
||
|
|
. "timecreate, timeexpire, ipfixed) VALUES (?,?,?,?,UNIX_TIMESTAMP()," . "?,?)",
|
||
|
|
undef, $u->{'userid'}, $id, $sess->{'auth'}, $exptype, $timeexpire, $ipfixed
|
||
|
|
);
|
||
|
|
|
||
|
|
return undef if $u->err;
|
||
|
|
$sess->{'sessid'} = $id;
|
||
|
|
$sess->{'userid'} = $u->{'userid'};
|
||
|
|
|
||
|
|
# clean up old sessions
|
||
|
|
my $old =
|
||
|
|
$udbh->selectcol_arrayref( "SELECT sessid FROM sessions WHERE "
|
||
|
|
. "userid=$u->{'userid'} AND "
|
||
|
|
. "timeexpire < UNIX_TIMESTAMP()" );
|
||
|
|
$u->kill_sessions(@$old) if $old;
|
||
|
|
|
||
|
|
# mark account as being used
|
||
|
|
LJ::mark_user_active( $u, 'login' );
|
||
|
|
|
||
|
|
bless $sess;
|
||
|
|
return $u->{'_session'} = $sess;
|
||
|
|
}
|
||
|
|
|
||
|
|
############################################################################
|
||
|
|
# INSTANCE METHODS
|
||
|
|
############################################################################
|
||
|
|
|
||
|
|
# not stored in database, call this before calling to update cookie strings
|
||
|
|
sub set_flags {
|
||
|
|
my ( $sess, $flags ) = @_;
|
||
|
|
$sess->{flags} = $flags;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub flags {
|
||
|
|
my $sess = shift;
|
||
|
|
return $sess->{flags};
|
||
|
|
}
|
||
|
|
|
||
|
|
sub set_ipfixed {
|
||
|
|
my ( $sess, $ip ) = @_;
|
||
|
|
return $sess->_dbupdate( ipfixed => $ip );
|
||
|
|
}
|
||
|
|
|
||
|
|
sub set_exptype {
|
||
|
|
my ( $sess, $exptype ) = @_;
|
||
|
|
croak("Invalid exptype") unless $exptype =~ /^short|long|once$/;
|
||
|
|
return $sess->_dbupdate(
|
||
|
|
exptype => $exptype,
|
||
|
|
timeexpire => time() + LJ::Session->session_length($exptype)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
sub _dbupdate {
|
||
|
|
my ( $sess, %changes ) = @_;
|
||
|
|
my $u = $sess->owner;
|
||
|
|
|
||
|
|
my $n_userid = $sess->{userid} + 0;
|
||
|
|
my $n_sessid = $sess->{sessid} + 0;
|
||
|
|
|
||
|
|
my @sets;
|
||
|
|
my @values;
|
||
|
|
foreach my $k ( keys %changes ) {
|
||
|
|
push @sets, "$k=?";
|
||
|
|
push @values, $changes{$k};
|
||
|
|
}
|
||
|
|
|
||
|
|
my $rv = $u->do(
|
||
|
|
"UPDATE sessions SET "
|
||
|
|
. join( ", ", @sets )
|
||
|
|
. " WHERE userid=$n_userid AND sessid=$n_sessid",
|
||
|
|
undef, @values
|
||
|
|
);
|
||
|
|
if ( !$rv ) {
|
||
|
|
|
||
|
|
# FIXME: eventually use Error::Strict here on return
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
# update ourself, once db update succeeded
|
||
|
|
foreach my $k ( keys %changes ) {
|
||
|
|
$sess->{$k} = $changes{$k};
|
||
|
|
}
|
||
|
|
|
||
|
|
LJ::MemCache::delete( $sess->_memkey );
|
||
|
|
return 1;
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
# returns unix timestamp of expiration
|
||
|
|
sub expiration_time {
|
||
|
|
my $sess = shift;
|
||
|
|
|
||
|
|
# expiration time if we have it,
|
||
|
|
return $sess->{timeexpire} if $sess->{timeexpire};
|
||
|
|
|
||
|
|
$sess->{timeexpire} = time() + LJ::Session->session_length( $sess->{exptype} );
|
||
|
|
return $sess->{timeexpire};
|
||
|
|
}
|
||
|
|
|
||
|
|
# return format of the "ljloggedin" cookie.
|
||
|
|
sub loggedin_cookie_string {
|
||
|
|
my ($sess) = @_;
|
||
|
|
return "u$sess->{userid}:s$sess->{sessid}";
|
||
|
|
}
|
||
|
|
|
||
|
|
sub master_cookie_string {
|
||
|
|
my $sess = shift;
|
||
|
|
|
||
|
|
my $ver = VERSION;
|
||
|
|
my $cookie = "v$ver:" . "u$sess->{userid}:" . "s$sess->{sessid}:" . "a$sess->{auth}";
|
||
|
|
|
||
|
|
if ( $sess->{flags} ) {
|
||
|
|
$cookie .= ":f$sess->{flags}";
|
||
|
|
}
|
||
|
|
|
||
|
|
$cookie .= "//" . LJ::eurl( $LJ::COOKIE_GEN || "" );
|
||
|
|
return $cookie;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub domsess_cookie_string {
|
||
|
|
my ( $sess, $domcook ) = @_;
|
||
|
|
croak("No domain cookie provided") unless $domcook;
|
||
|
|
|
||
|
|
# compute a signed domain key
|
||
|
|
my ( $time, $key ) = LJ::get_secret();
|
||
|
|
my $sig = domsess_signature( $time, $sess, $domcook );
|
||
|
|
|
||
|
|
# the cookie
|
||
|
|
my $ver = VERSION;
|
||
|
|
my $value =
|
||
|
|
"v$ver:"
|
||
|
|
. "u$sess->{userid}:"
|
||
|
|
. "s$sess->{sessid}:"
|
||
|
|
. "t$time:"
|
||
|
|
. "g$sig//"
|
||
|
|
. LJ::eurl( $LJ::COOKIE_GEN || "" );
|
||
|
|
|
||
|
|
return $value;
|
||
|
|
}
|
||
|
|
|
||
|
|
# sets new ljmastersession cookie given the session object
|
||
|
|
sub update_master_cookie {
|
||
|
|
my ($sess) = @_;
|
||
|
|
|
||
|
|
my @expires;
|
||
|
|
if ( $sess->{exptype} eq 'long' ) {
|
||
|
|
push @expires, expires => $sess->expiration_time;
|
||
|
|
}
|
||
|
|
|
||
|
|
my $domain = $LJ::DOMAIN_WEB || $LJ::DOMAIN;
|
||
|
|
|
||
|
|
set_cookie(
|
||
|
|
ljmastersession => $sess->master_cookie_string,
|
||
|
|
domain => $domain,
|
||
|
|
path => '/',
|
||
|
|
http_only => 1,
|
||
|
|
@expires,
|
||
|
|
);
|
||
|
|
|
||
|
|
set_cookie(
|
||
|
|
ljloggedin => $sess->loggedin_cookie_string,
|
||
|
|
domain => $LJ::DOMAIN,
|
||
|
|
path => '/',
|
||
|
|
http_only => 1,
|
||
|
|
@expires,
|
||
|
|
);
|
||
|
|
|
||
|
|
$sess->owner->preload_props('schemepref');
|
||
|
|
|
||
|
|
if ( my $scheme = $sess->owner->prop('schemepref') ) {
|
||
|
|
set_cookie(
|
||
|
|
BMLschemepref => $scheme,
|
||
|
|
domain => $LJ::DOMAIN,
|
||
|
|
path => '/',
|
||
|
|
http_only => 1,
|
||
|
|
@expires,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
set_cookie(
|
||
|
|
BMLschemepref => "",
|
||
|
|
domain => $LJ::DOMAIN,
|
||
|
|
path => '/',
|
||
|
|
delete => 1
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub auth {
|
||
|
|
my $sess = shift;
|
||
|
|
return $sess->{auth};
|
||
|
|
}
|
||
|
|
|
||
|
|
# NOTE: do not store any references in the LJ::Session instances because of serialization
|
||
|
|
# and storage in memcache
|
||
|
|
sub owner {
|
||
|
|
my $sess = shift;
|
||
|
|
return LJ::load_userid( $sess->{userid} );
|
||
|
|
}
|
||
|
|
|
||
|
|
# instance method: has this session expired, or is it IP bound and
|
||
|
|
# bound to the wrong IP?
|
||
|
|
sub valid {
|
||
|
|
my $sess = shift;
|
||
|
|
my $now = time();
|
||
|
|
my $err = sub { 0; };
|
||
|
|
|
||
|
|
return $err->("Invalid auth") if $sess->{'timeexpire'} < $now;
|
||
|
|
|
||
|
|
if ( $sess->{'ipfixed'} && !$LJ::Session::OPT_IGNORE_IP ) {
|
||
|
|
my $remote_ip = LJ::get_remote_ip();
|
||
|
|
return $err->("Session wrong IP ($remote_ip != $sess->{ipfixed})")
|
||
|
|
if $sess->{'ipfixed'} ne $remote_ip;
|
||
|
|
}
|
||
|
|
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub id {
|
||
|
|
my $sess = shift;
|
||
|
|
return $sess->{sessid};
|
||
|
|
}
|
||
|
|
|
||
|
|
sub ipfixed {
|
||
|
|
my $sess = shift;
|
||
|
|
return $sess->{ipfixed};
|
||
|
|
}
|
||
|
|
|
||
|
|
sub exptype {
|
||
|
|
my $sess = shift;
|
||
|
|
return $sess->{exptype};
|
||
|
|
}
|
||
|
|
|
||
|
|
# end a session
|
||
|
|
sub destroy {
|
||
|
|
my $sess = shift;
|
||
|
|
my $id = $sess->id;
|
||
|
|
my $u = $sess->owner;
|
||
|
|
|
||
|
|
return LJ::Session->destroy_sessions( $u, $id );
|
||
|
|
}
|
||
|
|
|
||
|
|
# based on our type and current expiration length, update this cookie if we need to
|
||
|
|
sub try_renew {
|
||
|
|
my ( $sess, $cookies ) = @_;
|
||
|
|
|
||
|
|
# only renew long type cookies
|
||
|
|
return if $sess->{exptype} ne 'long';
|
||
|
|
|
||
|
|
# how long to live for
|
||
|
|
my $u = $sess->owner;
|
||
|
|
my $sess_length = LJ::Session->session_length( $sess->{exptype} );
|
||
|
|
my $now = time();
|
||
|
|
my $new_expire = $now + $sess_length;
|
||
|
|
|
||
|
|
# if there is a new session length to be set and the user's db writer is available,
|
||
|
|
# go ahead and set the new session expiration in the database. then only update the
|
||
|
|
# cookies if the database operation is successful
|
||
|
|
if ( $sess_length
|
||
|
|
&& $sess->{'timeexpire'} - $now < $sess_length / 2
|
||
|
|
&& $u->writer
|
||
|
|
&& $sess->_dbupdate( timeexpire => $new_expire ) )
|
||
|
|
{
|
||
|
|
$sess->update_master_cookie;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
############################################################################
|
||
|
|
# CLASS METHODS
|
||
|
|
############################################################################
|
||
|
|
|
||
|
|
# NOTE: internal function REQUIRES trusted input
|
||
|
|
sub helper_url {
|
||
|
|
my ( $class, $dest ) = @_;
|
||
|
|
|
||
|
|
return unless $dest;
|
||
|
|
|
||
|
|
my $u = LJ::get_remote();
|
||
|
|
unless ($u) {
|
||
|
|
LJ::Session->clear_master_cookie;
|
||
|
|
return $dest;
|
||
|
|
}
|
||
|
|
|
||
|
|
my $domcook = LJ::Session->domain_cookie($dest)
|
||
|
|
or return;
|
||
|
|
|
||
|
|
if ( $dest =~ m!^(https?://)([^/]*?)\.\Q$LJ::USER_DOMAIN\E/?([^/]*)! ) {
|
||
|
|
my $url = "$1$2.$LJ::USER_DOMAIN/";
|
||
|
|
if ( is_journal_subdomain($2) ) {
|
||
|
|
$url .= "$3/"
|
||
|
|
if $3 && ( $3 ne '/' ); # 'http://community.livejournal.com/name/__setdomsess'
|
||
|
|
}
|
||
|
|
|
||
|
|
my $sess = $u->session;
|
||
|
|
my $cookie = $sess->domsess_cookie_string($domcook);
|
||
|
|
return
|
||
|
|
$url
|
||
|
|
. "__setdomsess?dest="
|
||
|
|
. LJ::eurl($dest) . "&k="
|
||
|
|
. LJ::eurl($domcook) . "&v="
|
||
|
|
. LJ::eurl($cookie);
|
||
|
|
}
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
# given a URL (or none, for current url), what domain cookie represents this URL?
|
||
|
|
# return undef if not URL for a domain cookie, which means either bogus URL
|
||
|
|
# or the master cookies should be tried.
|
||
|
|
sub domain_cookie {
|
||
|
|
my ( $class, $url ) = @_;
|
||
|
|
my ( $subdomain, $user ) = LJ::Session->domain_journal($url);
|
||
|
|
|
||
|
|
# undef: not on a user-subdomain
|
||
|
|
return undef unless $subdomain;
|
||
|
|
|
||
|
|
# on a user subdomain, or shared subdomain
|
||
|
|
if ( $user ne "" ) {
|
||
|
|
$user =~ s/-/_/g; # URLs may be - or _, convert to _ which is what usernames contain
|
||
|
|
return "ljdomsess.$subdomain.$user";
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
return "ljdomsess.$subdomain";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# given an optional URL (by default, the current URL), what is the username
|
||
|
|
# of that URL?. undef if no user. in list context returns the ($subdomain, $user)
|
||
|
|
# where $user can be "" if $subdomain isn't, say, "community" or "users".
|
||
|
|
# in scalar context, userame is always the canonical username (no hypens/capitals)
|
||
|
|
sub domain_journal {
|
||
|
|
my ( $class, $url ) = @_;
|
||
|
|
|
||
|
|
$url ||= LJ::create_url( undef, keep_args => 1 );
|
||
|
|
return undef
|
||
|
|
unless $url =~ m!^https?://(.+?)(/.*)$!;
|
||
|
|
|
||
|
|
my ( $host, $path ) = ( $1, $2 );
|
||
|
|
$host = lc($host);
|
||
|
|
|
||
|
|
# don't return a domain cookie for the master domain
|
||
|
|
return undef if $host eq lc($LJ::DOMAIN_WEB) || $host eq lc($LJ::DOMAIN);
|
||
|
|
|
||
|
|
return undef
|
||
|
|
unless $host =~ m!^([-\w\.]{1,50})\.\Q$LJ::USER_DOMAIN\E$!;
|
||
|
|
|
||
|
|
my $subdomain = lc($1);
|
||
|
|
if ( is_journal_subdomain($subdomain) ) {
|
||
|
|
my $user = get_path_user($path);
|
||
|
|
return undef unless $user;
|
||
|
|
return wantarray ? ( $subdomain, $user ) : $user;
|
||
|
|
}
|
||
|
|
|
||
|
|
# where $subdomain is actually a username:
|
||
|
|
return wantarray ? ( $subdomain, "" ) : LJ::canonical_username($subdomain);
|
||
|
|
}
|
||
|
|
|
||
|
|
sub url_owner {
|
||
|
|
my ( $class, $url ) = @_;
|
||
|
|
$url ||= LJ::create_url( undef, keep_args => 1 );
|
||
|
|
my ( $subdomain, $user ) = LJ::Session->domain_journal($url);
|
||
|
|
$user = $subdomain if $user eq "";
|
||
|
|
return LJ::canonical_username($user);
|
||
|
|
}
|
||
|
|
|
||
|
|
# CLASS METHOD
|
||
|
|
# -- frontend to session_from_domain_cookie and session_from_master_cookie below
|
||
|
|
sub session_from_cookies {
|
||
|
|
my $class = shift;
|
||
|
|
my %getopts = @_;
|
||
|
|
|
||
|
|
my $r = DW::Request->get;
|
||
|
|
return undef unless $r;
|
||
|
|
|
||
|
|
my $sessobj;
|
||
|
|
|
||
|
|
my $domain_cookie = LJ::Session->domain_cookie;
|
||
|
|
if ($domain_cookie) {
|
||
|
|
|
||
|
|
# journal domain
|
||
|
|
$sessobj =
|
||
|
|
LJ::Session->session_from_domain_cookie( \%getopts, $r->cookie_multi($domain_cookie) );
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
# this is the master cookie at "www.livejournal.com" or "livejournal.com";
|
||
|
|
my @cookies = $r->cookie_multi('ljmastersession');
|
||
|
|
|
||
|
|
# but support old clients who are just sending an "ljsession" cookie which they got
|
||
|
|
# from LJ::Protocol's "generatesession" mode.
|
||
|
|
unless (@cookies) {
|
||
|
|
@cookies = $r->cookie_multi('ljsession');
|
||
|
|
$getopts{old_cookie} = 1;
|
||
|
|
}
|
||
|
|
$sessobj = LJ::Session->session_from_master_cookie( \%getopts, @cookies );
|
||
|
|
}
|
||
|
|
|
||
|
|
return $sessobj;
|
||
|
|
}
|
||
|
|
|
||
|
|
# CLASS METHOD
|
||
|
|
# -- but not called directly. usually called by LJ::Session->session_from_cookies above
|
||
|
|
sub session_from_domain_cookie {
|
||
|
|
my $class = shift;
|
||
|
|
my $opts = ref $_[0] ? shift() : {};
|
||
|
|
|
||
|
|
my $r = DW::Request->get;
|
||
|
|
|
||
|
|
# the logged-in cookie
|
||
|
|
my $li_cook = $r->cookie('ljloggedin');
|
||
|
|
return undef unless $li_cook;
|
||
|
|
|
||
|
|
my $no_session = sub {
|
||
|
|
my $reason = shift;
|
||
|
|
warn "No session found for domain cookie: $reason\n" if $LJ::IS_DEV_SERVER;
|
||
|
|
|
||
|
|
my $rr = $opts->{redirect_ref};
|
||
|
|
$$rr =
|
||
|
|
"$LJ::SITEROOT/misc/get_domain_session?return="
|
||
|
|
. LJ::eurl( LJ::create_url( undef, keep_args => 1 ) )
|
||
|
|
if $rr;
|
||
|
|
|
||
|
|
return undef;
|
||
|
|
};
|
||
|
|
|
||
|
|
my @cookies = grep { $_ } @_;
|
||
|
|
return $no_session->("no cookies") unless @cookies;
|
||
|
|
|
||
|
|
my $domcook = LJ::Session->domain_cookie;
|
||
|
|
|
||
|
|
foreach my $cookie (@cookies) {
|
||
|
|
my $sess = valid_domain_cookie( $domcook, $cookie->[0], $li_cook );
|
||
|
|
return $sess if $sess;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $no_session->("no valid cookie");
|
||
|
|
}
|
||
|
|
|
||
|
|
# CLASS METHOD
|
||
|
|
# -- but not called directly. usually called by LJ::Session->session_from_cookies above
|
||
|
|
# call: ( $opts?, @ljmastersession_cookie(s) )
|
||
|
|
# return value is LJ::Session object if we found one; else undef
|
||
|
|
# FIXME: document ops
|
||
|
|
sub session_from_master_cookie {
|
||
|
|
my $class = shift;
|
||
|
|
my $opts = ref $_[0] ? shift() : {};
|
||
|
|
my @cookies = grep { $_ } @_;
|
||
|
|
return undef unless @cookies;
|
||
|
|
|
||
|
|
my $r = DW::Request->get;
|
||
|
|
|
||
|
|
my $errs = delete $opts->{errlist} || [];
|
||
|
|
my $tried_fast = delete $opts->{tried_fast} || do { my $foo; \$foo; };
|
||
|
|
my $ignore_ip = delete $opts->{ignore_ip} ? 1 : 0;
|
||
|
|
my $old_cookie = delete $opts->{old_cookie} ? 1 : 0;
|
||
|
|
|
||
|
|
delete $opts->{redirect_ref}; # we don't use this
|
||
|
|
croak("Unknown options") if %$opts;
|
||
|
|
|
||
|
|
my $now = time();
|
||
|
|
|
||
|
|
# our return value
|
||
|
|
my $sess;
|
||
|
|
|
||
|
|
my $li_cook = $r->cookie('ljloggedin');
|
||
|
|
|
||
|
|
COOKIE:
|
||
|
|
foreach my $sessdata (@cookies) {
|
||
|
|
my ( $cookie, $gen ) = split( m!//!, $sessdata->[0] );
|
||
|
|
|
||
|
|
my ( $version, $userid, $sessid, $auth, $flags );
|
||
|
|
|
||
|
|
my $dest = {
|
||
|
|
v => \$version,
|
||
|
|
u => \$userid,
|
||
|
|
s => \$sessid,
|
||
|
|
a => \$auth,
|
||
|
|
f => \$flags,
|
||
|
|
};
|
||
|
|
|
||
|
|
my $bogus = 0;
|
||
|
|
foreach my $var ( split /:/, $cookie ) {
|
||
|
|
if ( $var =~ /^(\w)(.+)$/ && $dest->{$1} ) {
|
||
|
|
${ $dest->{$1} } = $2;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$bogus = 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# must do this first so they can't trick us
|
||
|
|
$$tried_fast = 1 if $flags && $flags =~ /\.FS\b/;
|
||
|
|
|
||
|
|
next COOKIE if $bogus;
|
||
|
|
|
||
|
|
next COOKIE unless valid_cookie_generation($gen);
|
||
|
|
|
||
|
|
my $err = sub {
|
||
|
|
$sess = undef;
|
||
|
|
push @$errs, "$sessdata: $_[0]";
|
||
|
|
};
|
||
|
|
|
||
|
|
# fail unless version matches current
|
||
|
|
unless ( $version == VERSION ) {
|
||
|
|
$err->("no ws auth");
|
||
|
|
next COOKIE;
|
||
|
|
}
|
||
|
|
|
||
|
|
my $u = LJ::load_userid($userid);
|
||
|
|
unless ($u) {
|
||
|
|
$err->("user doesn't exist");
|
||
|
|
next COOKIE;
|
||
|
|
}
|
||
|
|
|
||
|
|
# locked accounts can't be logged in
|
||
|
|
if ( $u->is_locked ) {
|
||
|
|
$err->("User account is locked.");
|
||
|
|
next COOKIE;
|
||
|
|
}
|
||
|
|
|
||
|
|
$sess = LJ::Session->instance( $u, $sessid );
|
||
|
|
|
||
|
|
unless ($sess) {
|
||
|
|
$err->("Couldn't find session");
|
||
|
|
next COOKIE;
|
||
|
|
}
|
||
|
|
|
||
|
|
unless ( $sess->{auth} eq $auth ) {
|
||
|
|
$err->("Invald auth");
|
||
|
|
next COOKIE;
|
||
|
|
}
|
||
|
|
|
||
|
|
unless ( $sess->valid ) {
|
||
|
|
$err->("expired or IP bound problems");
|
||
|
|
next COOKIE;
|
||
|
|
}
|
||
|
|
|
||
|
|
# make sure their ljloggedin cookie
|
||
|
|
unless ( $old_cookie || $sess->loggedin_cookie_string eq $li_cook ) {
|
||
|
|
$err->("loggedin cookie bogus");
|
||
|
|
next COOKIE;
|
||
|
|
}
|
||
|
|
|
||
|
|
last COOKIE;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $sess;
|
||
|
|
}
|
||
|
|
|
||
|
|
# class method
|
||
|
|
sub destroy_all_sessions {
|
||
|
|
my ( $class, $u ) = @_;
|
||
|
|
return 0 unless $u;
|
||
|
|
|
||
|
|
my $udbh = LJ::get_cluster_master($u)
|
||
|
|
or return 0;
|
||
|
|
|
||
|
|
my $sessions = $udbh->selectcol_arrayref( "SELECT sessid FROM sessions WHERE " . "userid=?",
|
||
|
|
undef, $u->{'userid'} );
|
||
|
|
|
||
|
|
return LJ::Session->destroy_sessions( $u, @$sessions ) if @$sessions;
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
# class method
|
||
|
|
sub destroy_sessions {
|
||
|
|
my ( $class, $u, @sessids ) = @_;
|
||
|
|
|
||
|
|
my $in = join( ',', map { $_ + 0 } @sessids );
|
||
|
|
return 1 unless $in;
|
||
|
|
my $userid = $u->{'userid'};
|
||
|
|
foreach (qw(sessions sessions_data)) {
|
||
|
|
$u->do( "DELETE FROM $_ WHERE userid=? AND " . "sessid IN ($in)", undef, $userid )
|
||
|
|
or return 0; # FIXME: use Error::Strict
|
||
|
|
}
|
||
|
|
foreach my $id (@sessids) {
|
||
|
|
$id += 0;
|
||
|
|
LJ::MemCache::delete( _memkey( $u, $id ) );
|
||
|
|
}
|
||
|
|
return 1;
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
sub clear_master_cookie {
|
||
|
|
my ($class) = @_;
|
||
|
|
|
||
|
|
my $domain = $LJ::DOMAIN_WEB || $LJ::DOMAIN;
|
||
|
|
|
||
|
|
set_cookie(
|
||
|
|
ljmastersession => "",
|
||
|
|
domain => $domain,
|
||
|
|
path => '/',
|
||
|
|
delete => 1
|
||
|
|
);
|
||
|
|
|
||
|
|
set_cookie(
|
||
|
|
ljloggedin => "",
|
||
|
|
domain => $LJ::DOMAIN,
|
||
|
|
path => '/',
|
||
|
|
delete => 1
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
# CLASS method for getting the length of a given session type in seconds
|
||
|
|
sub session_length {
|
||
|
|
my ( $class, $exptype ) = @_;
|
||
|
|
croak("Invalid exptype") unless $exptype =~ /^short|long|once$/;
|
||
|
|
|
||
|
|
return {
|
||
|
|
short => 60 * 60 * 24 * 1.5, # 1.5 days
|
||
|
|
long => 60 * 60 * 24 * 60, # 60 days
|
||
|
|
once => 60 * 60 * 2, # 2 hours
|
||
|
|
}->{$exptype};
|
||
|
|
}
|
||
|
|
|
||
|
|
# returns the URL to go to after setting the domain cookie
|
||
|
|
sub setdomsess_handler {
|
||
|
|
my ($class) = @_;
|
||
|
|
|
||
|
|
my $r = DW::Request->get;
|
||
|
|
|
||
|
|
my $get = $r->get_args;
|
||
|
|
|
||
|
|
my $dest = $get->{'dest'};
|
||
|
|
my $domcook = $get->{'k'};
|
||
|
|
my $cookie = $get->{'v'};
|
||
|
|
|
||
|
|
return "$LJ::SITEROOT" unless valid_destination($dest);
|
||
|
|
return $dest unless valid_domain_cookie( $domcook, $cookie, $r->cookie('ljloggedin') );
|
||
|
|
|
||
|
|
my $path = get_cookie_path($dest);
|
||
|
|
|
||
|
|
my $expires = $LJ::DOMSESS_EXPIRATION || 0; # session-cookie only
|
||
|
|
set_cookie(
|
||
|
|
$domcook => $cookie,
|
||
|
|
path => $path,
|
||
|
|
http_only => 1,
|
||
|
|
expires => $expires
|
||
|
|
);
|
||
|
|
|
||
|
|
# add in a trailing slash, if URL doesn't have at least two slashes.
|
||
|
|
# otherwise the path on the cookie above (which is like /community/)
|
||
|
|
# won't be caught when we bounce them to /community.
|
||
|
|
unless ( $dest =~ m!^https?://.+?/.+?/! || $path eq "/" ) {
|
||
|
|
|
||
|
|
# add a slash unless we can slip one in before the query parameters
|
||
|
|
$dest .= "/" unless $dest =~ s!\?!/?!;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $dest;
|
||
|
|
}
|
||
|
|
|
||
|
|
############################################################################
|
||
|
|
# UTIL FUNCTIONS
|
||
|
|
############################################################################
|
||
|
|
|
||
|
|
sub domsess_signature {
|
||
|
|
my ( $time, $sess, $domcook ) = @_;
|
||
|
|
|
||
|
|
my $u = $sess->owner;
|
||
|
|
my $secret = LJ::get_secret($time);
|
||
|
|
|
||
|
|
my $data = join( "-", $sess->{auth}, $domcook, $u->{userid}, $sess->{sessid}, $time );
|
||
|
|
my $sig = hmac_sha1_hex( $data, $secret );
|
||
|
|
return $sig;
|
||
|
|
}
|
||
|
|
|
||
|
|
# function or instance method.
|
||
|
|
# FIXME: update the documentation for memkeys
|
||
|
|
sub _memkey {
|
||
|
|
if ( @_ == 2 ) {
|
||
|
|
my ( $u, $sessid ) = @_;
|
||
|
|
$sessid += 0;
|
||
|
|
return [ $u->{'userid'}, "ljms:$u->{'userid'}:$sessid" ];
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
my $sess = shift;
|
||
|
|
return [ $sess->{'userid'}, "ljms:$sess->{'userid'}:$sess->{sessid}" ];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# FIXME: move this somewhere better
|
||
|
|
sub set_cookie {
|
||
|
|
my ( $key, $value, %opts ) = @_;
|
||
|
|
|
||
|
|
my $r = DW::Request->get;
|
||
|
|
return unless $r;
|
||
|
|
|
||
|
|
my $http_only = delete $opts{http_only};
|
||
|
|
my $domain = delete $opts{domain};
|
||
|
|
my $path = delete $opts{path};
|
||
|
|
my $expires = delete $opts{expires};
|
||
|
|
my $delete = delete $opts{delete};
|
||
|
|
croak( "Invalid cookie options: " . join( ", ", keys %opts ) ) if %opts;
|
||
|
|
|
||
|
|
# expires can be absolute or relative. this is gross or clever, your pick.
|
||
|
|
$expires += time() if $expires && $expires <= 1135217120;
|
||
|
|
|
||
|
|
# set expires to 5 seconds after 1970. definitely in the past.
|
||
|
|
# so cookie will be deleted.
|
||
|
|
$expires = 5 if $delete;
|
||
|
|
|
||
|
|
$r->add_cookie(
|
||
|
|
name => $key,
|
||
|
|
value => $value,
|
||
|
|
expires => $expires ? LJ::time_to_cookie($expires) : undef,
|
||
|
|
domain => $domain || undef,
|
||
|
|
path => $path || undef,
|
||
|
|
httponly => $http_only ? 1 : 0,
|
||
|
|
);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
# returns undef or a session, given a $domcook and its $val, as well
|
||
|
|
# as the current logged-in cookie $li_cook which says the master
|
||
|
|
# session's uid/sessid
|
||
|
|
sub valid_domain_cookie {
|
||
|
|
my ( $domcook, $val, $li_cook, $opts ) = @_;
|
||
|
|
$opts ||= {};
|
||
|
|
|
||
|
|
my ( $cookie, $gen ) = split m!//!, $val;
|
||
|
|
|
||
|
|
my ( $version, $uid, $sessid, $time, $sig, $flags );
|
||
|
|
my $dest = {
|
||
|
|
v => \$version,
|
||
|
|
u => \$uid,
|
||
|
|
s => \$sessid,
|
||
|
|
t => \$time,
|
||
|
|
g => \$sig,
|
||
|
|
f => \$flags,
|
||
|
|
};
|
||
|
|
|
||
|
|
my $bogus = 0;
|
||
|
|
foreach my $var ( split /:/, $cookie ) {
|
||
|
|
if ( $var =~ /^(\w)(.+)$/ && $dest->{$1} ) {
|
||
|
|
${ $dest->{$1} } = $2;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$bogus = 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
my $not_valid = sub {
|
||
|
|
my $reason = shift;
|
||
|
|
warn "Invalid domain cookie: $reason\n" if $LJ::IS_DEV_SERVER;
|
||
|
|
|
||
|
|
return undef;
|
||
|
|
};
|
||
|
|
|
||
|
|
return $not_valid->("bogus params") if $bogus;
|
||
|
|
return $not_valid->("wrong gen") unless valid_cookie_generation($gen);
|
||
|
|
return $not_valid->("wrong ver") if $version != VERSION;
|
||
|
|
|
||
|
|
# have to be relatively new. these shouldn't last longer than a day
|
||
|
|
# or so anyway.
|
||
|
|
unless ( $opts->{ignore_age} ) {
|
||
|
|
my $now = time();
|
||
|
|
return $not_valid->("old cookie") unless $time > $now - 86400 * 7;
|
||
|
|
}
|
||
|
|
|
||
|
|
my $u = LJ::load_userid($uid)
|
||
|
|
or return $not_valid->("no user $uid");
|
||
|
|
|
||
|
|
my $sess = $u->session($sessid)
|
||
|
|
or return $not_valid->("no session $sessid");
|
||
|
|
|
||
|
|
# the master session can't be expired or ip-bound to wrong IP
|
||
|
|
return $not_valid->("not valid") unless $sess->valid;
|
||
|
|
|
||
|
|
# the per-domain cookie has to match the session of the master cookie
|
||
|
|
unless ( $opts->{ignore_li_cook} ) {
|
||
|
|
my $sess_licook = $sess->loggedin_cookie_string;
|
||
|
|
return $not_valid->("li_cook mismatch. session=$sess_licook, user=$li_cook")
|
||
|
|
unless $sess_licook eq $li_cook;
|
||
|
|
}
|
||
|
|
|
||
|
|
my $correct_sig = domsess_signature( $time, $sess, $domcook );
|
||
|
|
return $not_valid->("signature wrong") unless $correct_sig eq $sig;
|
||
|
|
|
||
|
|
return $sess;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub valid_destination {
|
||
|
|
my $dest = shift;
|
||
|
|
return $dest =~ qr!^https?://[-\w\.]+\.\Q$LJ::USER_DOMAIN\E/!;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub valid_cookie_generation {
|
||
|
|
my $gen = shift || '';
|
||
|
|
my $dgen = LJ::durl($gen);
|
||
|
|
foreach my $okay ( $LJ::COOKIE_GEN, @LJ::COOKIE_GEN_OKAY ) {
|
||
|
|
$okay = '' unless defined $okay;
|
||
|
|
return 1 if $gen eq $okay;
|
||
|
|
return 1 if $dgen eq $okay;
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub is_journal_subdomain {
|
||
|
|
my ($subdomain) = @_;
|
||
|
|
return 0 unless defined $subdomain;
|
||
|
|
$subdomain = lc $subdomain;
|
||
|
|
|
||
|
|
my $func = $LJ::SUBDOMAIN_FUNCTION{$subdomain};
|
||
|
|
return $func && $func eq "journal" ? 1 : 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub get_cookie_path {
|
||
|
|
my ($dest) = @_;
|
||
|
|
my $path = '/'; # By default cookie path is root
|
||
|
|
|
||
|
|
# If it is not the master domain, include the username
|
||
|
|
|
||
|
|
if ( $dest && $dest =~ m!^https?://(.+?)(/.*)$! ) {
|
||
|
|
my ( $host, $url_path ) = ( lc($1), $2 );
|
||
|
|
my $path_user = get_path_user($url_path);
|
||
|
|
|
||
|
|
if (
|
||
|
|
$host =~ m!^([-\w\.]{1,50})\.\Q$LJ::USER_DOMAIN\E$!
|
||
|
|
&& is_journal_subdomain($1) # undef: not on a user-subdomain
|
||
|
|
&& $path_user
|
||
|
|
)
|
||
|
|
{
|
||
|
|
|
||
|
|
$path = '/' . $path_user . '/';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $path;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub get_path_user {
|
||
|
|
my ($path) = @_;
|
||
|
|
return unless $path =~ m!^/(\w{1,$LJ::USERNAME_MAXLENGTH})\b!;
|
||
|
|
return lc $1;
|
||
|
|
}
|
||
|
|
|
||
|
|
1;
|