File Coverage

blib/lib/HTTP/Session2/ClientStore2.pm
Criterion Covered Total %
statement 90 117 76.9
branch 6 20 30.0
condition 3 3 100.0
subroutine 21 24 87.5
pod 1 7 14.2
total 121 171 70.7


line stmt bran cond sub pod time code
1             package HTTP::Session2::ClientStore2;
2 4     4   171788 use strict;
  4         12  
  4         760  
3 4     4   24 use warnings;
  4         9  
  4         133  
4 4     4   7857 use utf8;
  4         45  
  4         30  
5 4     4   534 use 5.008_001;
  4         14  
  4         184  
6              
7 4     4   4583 use Cookie::Baker ();
  4         29510  
  4         107  
8 4     4   5441 use Storable ();
  4         20450  
  4         118  
9 4     4   4625 use MIME::Base64 ();
  4         3353  
  4         247  
10 4     4   3586 use Digest::HMAC ();
  4         2964  
  4         85  
11 4     4   2849 use HTTP::Session2::Expired;
  4         16  
  4         148  
12 4     4   2662 use HTTP::Session2::Random;
  4         14  
  4         167  
13 4     4   9717 use Data::MessagePack;
  4         17313  
  4         162  
14 4     4   3338 use Crypt::CBC;
  4         18497  
  4         148  
15              
16 4     4   43 use Mouse;
  4         8  
  4         113  
17              
18             extends 'HTTP::Session2::Base';
19              
20             our $MESSAGE_PACK = Data::MessagePack->new();
21              
22             has serializer => (
23             is => 'ro',
24             default => sub {
25             sub {
26             $MESSAGE_PACK->pack($_[0]);
27             }
28             },
29             );
30              
31             has deserializer => (
32             is => 'ro',
33             default => sub {
34             sub {
35             $MESSAGE_PACK->unpack($_[0])
36             }
37             },
38             );
39              
40             has cipher => (
41             is => 'ro',
42             isa => 'Crypt::CBC',
43             required => 1,
44             );
45              
46             has ignore_old => (
47             is => 'ro',
48             );
49              
50 4     4   2494 no Mouse;
  4         11  
  4         24  
51              
52             # HMAC timing attack
53             sub _compare {
54 0     0   0 my ( $s1, $s2 ) = @_;
55              
56 0 0       0 return unless defined $s2;
57 0 0       0 return if length $s1 != length $s2;
58 0         0 my $r = 0;
59 0         0 for my $i ( 0 .. length($s1) - 1 ) {
60 0         0 $r |= ord( substr $s1, $i ) ^ ord( substr $s2, $i );
61             }
62              
63 0         0 return $r == 0;
64             }
65              
66             sub sig {
67 3     3 0 8 my($self, $b64) = @_;
68 3 50       20 $self->secret or die "Missing secret. ABORT";
69 3         26 Digest::HMAC::hmac_hex($b64, $self->secret, $self->hmac_function);
70             }
71              
72             sub load_session {
73 5     5 0 7 my $self = shift;
74              
75             # Load from cookie.
76 5         33 my $cookies = Cookie::Baker::crush_cookie($self->env->{HTTP_COOKIE});
77 5         232 my $session_cookie = $cookies->{$self->session_cookie->{name}};
78 5 100       23 if (defined $session_cookie) {
79 4         7 my $textified = $session_cookie;
80 4         15 my $encrypted = MIME::Base64::decode_base64url($textified);
81 4         72 my $serialized_and_sig = eval { $self->cipher->decrypt($encrypted) };
  4         31  
82 4 50       1083 if ($@) {
83 4         108 warn $@;
84 4         24 return;
85             }
86 0         0 my ($sig, $serialized) = $self->_deserialize($serialized_and_sig);
87 0 0       0 _compare($self->sig($serialized), $sig) or do {
88 0         0 return;
89             };
90 0         0 my ($time, $id, $data) = $self->_deserialize($serialized);
91              
92 0 0       0 if (defined $self->ignore_old) {
93 0 0       0 if ($time < $self->ignore_old()) {
94 0         0 return;
95             }
96             }
97              
98 0         0 $self->{id} = $id;
99 0         0 $self->{_data} = $data;
100 0         0 return 1;
101             }
102             }
103              
104             sub create_session {
105 3     3 0 7 my $self = shift;
106              
107 3         11 $self->{id} = HTTP::Session2::Random::generate_session_id();
108 3         12 $self->{_data} = +{};
109             }
110              
111             sub regenerate_id {
112 1     1 0 415 my ($self) = @_;
113              
114             # Load original session first.
115 1         4 $self->load_session();
116              
117             # Create new session.
118 1         6 $self->{id} = HTTP::Session2::Random::generate_session_id();
119 1         5 $self->is_dirty(1);
120 1         3 $self->necessary_to_send(1);
121             }
122              
123             sub xsrf_token {
124 0     0 1 0 my $self = shift;
125 0         0 return $self->id;
126             }
127              
128             sub expire {
129 1     1 0 455 my $self = shift;
130              
131             # Load original session first.
132 1         4 $self->load_session();
133              
134             # Rebless to expired object.
135 1         4 bless $self, 'HTTP::Session2::Expired';
136              
137 1         2 return;
138             }
139              
140             sub finalize {
141 4     4 0 6 my ($self) = @_;
142              
143 4 100 100     41 return () unless $self->necessary_to_send || $self->is_dirty;
144              
145 3         4 my @cookies;
146              
147             # Finalize session cookie
148             {
149 3         5 my %cookie = %{$self->session_cookie};
  3         5  
  3         19  
150 3         9 my $name = delete $cookie{name};
151 3         14 my $value = $self->_serialize($self->id, $self->_data);
152 3         25 push @cookies, $name => +{
153             %cookie,
154             value => $value,
155             };
156             }
157              
158             # Finalize XSRF cookie
159             {
160 3         6 my %cookie = %{$self->xsrf_cookie};
  3         5  
  3         30  
161 3         9 my $name = delete $cookie{name};
162 3         14 push @cookies, $name => +{
163             %cookie,
164             value => $self->id,
165             };
166             }
167              
168 3         15 return @cookies;
169             }
170              
171             sub _serialize {
172 3     3   7 my ($self, $id, $data) = @_;
173              
174 3         20 my $serialized = $self->serializer->([time(), $id, $data]);
175 3         13 my $sig = $self->sig($serialized);
176 3         128 my $joined = $self->serializer->([$sig, $serialized]);
177 3         19 my $encrypted = $self->cipher->encrypt($joined);
178 3         3834 $encrypted = MIME::Base64::encode_base64url($encrypted);
179 3         48 return $encrypted;
180             }
181              
182             sub _deserialize {
183 0     0     my ($self, $serialized) = @_;
184              
185 0           my @data;
186 0           eval {
187 0           @data = @{$self->deserializer->($serialized)};
  0            
188             };
189 0 0         if ($@) {
190 0           warn "Can't deserialize session data. It seems that the browser sent strange or incompatible cookies.";
191             }
192              
193 0           @data;
194             }
195              
196             1;
197             __END__