File Coverage

blib/lib/HTTP/Session2/ClientStore2.pm
Criterion Covered Total %
statement 110 116 94.8
branch 12 20 60.0
condition 3 3 100.0
subroutine 23 24 95.8
pod 1 7 14.2
total 149 170 87.6


line stmt bran cond sub pod time code
1             package HTTP::Session2::ClientStore2;
2 4     4   167084 use strict;
  4         57  
  4         92  
3 4     4   15 use warnings;
  4         6  
  4         107  
4 4     4   1342 use utf8;
  4         36  
  4         16  
5 4     4   129 use 5.008_001;
  4         11  
6              
7 4     4   1342 use Cookie::Baker ();
  4         11776  
  4         71  
8 4     4   1930 use Storable ();
  4         9946  
  4         91  
9 4     4   1527 use MIME::Base64 ();
  4         2026  
  4         86  
10 4     4   1458 use Digest::HMAC ();
  4         1939  
  4         96  
11 4     4   1413 use HTTP::Session2::Expired;
  4         12  
  4         187  
12 4     4   1561 use HTTP::Session2::Random;
  4         10  
  4         108  
13 4     4   1469 use Data::MessagePack;
  4         3301  
  4         91  
14 4     4   1408 use Crypt::CBC;
  4         11838  
  4         103  
15              
16 4     4   20 use Mouse;
  4         6  
  4         19  
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   1930 no Mouse;
  4         6  
  4         15  
51              
52             # HMAC timing attack
53             sub _compare {
54 3     3   71 my ( $s1, $s2 ) = @_;
55              
56 3 50       8 return unless defined $s2;
57 3 50       10 return if length $s1 != length $s2;
58 3         4 my $r = 0;
59 3         10 for my $i ( 0 .. length($s1) - 1 ) {
60 240         313 $r |= ord( substr $s1, $i ) ^ ord( substr $s2, $i );
61             }
62              
63 3         12 return $r == 0;
64             }
65              
66             sub sig {
67 10     10 0 21 my($self, $b64) = @_;
68 10 50       39 $self->secret or die "Missing secret. ABORT";
69 10         44 Digest::HMAC::hmac_hex($b64, $self->secret, $self->hmac_function);
70             }
71              
72             sub load_session {
73 11     11 0 18 my $self = shift;
74              
75             # Load from cookie.
76 11         56 my $cookies = Cookie::Baker::crush_cookie($self->env->{HTTP_COOKIE});
77 11         420 my $session_cookie = $cookies->{$self->session_cookie->{name}};
78 11 100       47 if (defined $session_cookie) {
79 7         10 my $textified = $session_cookie;
80 7         19 my $encrypted = MIME::Base64::decode_base64url($textified);
81 7         73 my $serialized_and_sig = eval { $self->cipher->decrypt($encrypted) };
  7         29  
82 7 100       1436 if ($@) {
83 4         97 warn $@;
84 4         26 return;
85             }
86 3         11 my ($sig, $serialized) = $self->_deserialize($serialized_and_sig);
87 3 50       10 _compare($self->sig($serialized), $sig) or do {
88 0         0 return;
89             };
90 3         8 my ($time, $id, $data) = $self->_deserialize($serialized);
91              
92 3 50       11 if (defined $self->ignore_old) {
93 0 0       0 if ($time < $self->ignore_old()) {
94 0         0 return;
95             }
96             }
97              
98 3         8 $self->{id} = $id;
99 3         6 $self->{_data} = $data;
100 3         14 return 1;
101             }
102             }
103              
104             sub create_session {
105 6     6 0 11 my $self = shift;
106              
107 6         20 $self->{id} = HTTP::Session2::Random::generate_session_id();
108 6         20 $self->{_data} = +{};
109             }
110              
111             sub regenerate_id {
112 1     1 0 513 my ($self) = @_;
113              
114             # Load original session first.
115 1         3 $self->load_session();
116              
117             # Create new session.
118 1         4 $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 515 my $self = shift;
130              
131             # Load original session first.
132 1         4 $self->load_session();
133              
134             # Rebless to expired object.
135 1         5 bless $self, 'HTTP::Session2::Expired';
136              
137 1         2 return;
138             }
139              
140             sub finalize {
141 10     10 0 41 my ($self) = @_;
142              
143 10 100 100     72 return () unless $self->necessary_to_send || $self->is_dirty;
144              
145 7         32 my @cookies;
146              
147             # Finalize session cookie
148             {
149 7         39 my %cookie = %{$self->session_cookie};
  7         42  
150 7         17 my $name = delete $cookie{name};
151 7         26 my $value = $self->_serialize($self->id, $self->_data);
152 7         40 push @cookies, $name => +{
153             %cookie,
154             value => $value,
155             };
156             }
157              
158             # Finalize XSRF cookie
159             {
160 7         28 my %cookie = %{$self->xsrf_cookie};
  7         12  
  7         10  
  7         46  
161 7         17 my $name = delete $cookie{name};
162 7         27 push @cookies, $name => +{
163             %cookie,
164             value => $self->id,
165             };
166             }
167              
168 7         20 return @cookies;
169             }
170              
171             sub _serialize {
172 7     7   13 my ($self, $id, $data) = @_;
173              
174 7         27 my $serialized = $self->serializer->([time(), $id, $data]);
175 7         32 my $sig = $self->sig($serialized);
176 7         175 my $joined = $self->serializer->([$sig, $serialized]);
177 7         30 my $encrypted = $self->cipher->encrypt($joined);
178 7         5381 $encrypted = MIME::Base64::encode_base64url($encrypted);
179 7         111 return $encrypted;
180             }
181              
182             sub _deserialize {
183 6     6   14 my ($self, $serialized) = @_;
184              
185 6         9 my @data;
186 6         7 eval {
187 6         10 @data = @{$self->deserializer->($serialized)};
  6         17  
188             };
189 6 50       18 if ($@) {
190 0         0 warn "Can't deserialize session data. It seems that the browser sent strange or incompatible cookies.";
191             }
192              
193 6         18 @data;
194             }
195              
196             1;
197             __END__