root/trunk/extlib/Net/OpenID/Association.pm @ 3531

Revision 3531, 7.4 kB (checked in by fumiakiy, 9 months ago)

Merged sockfish to trunk. "svn merge -r3114:3527 http://code.sixapart.com/svn/movabletype/branches/sockfish/ ."

Line 
1use strict;
2use Carp ();
3
4############################################################################
5package Net::OpenID::Association;
6use fields (
7            'server',    # author-identity identity server endpoint
8            'secret',    # the secret for this association
9            'handle',    # the 255-character-max ASCII printable handle (33-126)
10            'expiry',    # unixtime, adjusted, of when this association expires
11            'type',      # association type
12            );
13
14use Storable ();
15use Digest::SHA1 qw(sha1);
16
17sub new {
18    my Net::OpenID::Association $self = shift;
19    $self = fields::new( $self ) unless ref $self;
20    my %opts = @_;
21    for my $f (qw( server secret handle expiry type )) {
22        $self->{$f} = delete $opts{$f};
23    }
24    Carp::croak("unknown options: " . join(", ", keys %opts)) if %opts;
25    return $self;
26}
27
28sub handle {
29    my $self = shift;
30    die if @_;
31    $self->{'handle'};
32}
33
34sub secret {
35    my $self = shift;
36    die if @_;
37    $self->{'secret'};
38}
39
40sub type {
41    my $self = shift;
42    die if @_;
43    $self->{'type'};
44}
45
46sub server {
47    my Net::OpenID::Association $self = shift;
48    Carp::croak("Too many parameters") if @_;
49    return $self->{server};
50}
51
52sub expired {
53    my Net::OpenID::Association $self = shift;
54    return time() > $self->{'expiry'};
55}
56
57sub usable {
58    my Net::OpenID::Association $self = shift;
59    return 0 unless $self->{'handle'} =~ /^[\x21-\x7e]{1,255}$/;
60    return 0 unless $self->{'expiry'} =~ /^\d+$/;
61    return 0 unless $self->{'secret'};
62    return 0 if $self->expired;
63    return 1;
64}
65
66
67# return a handle for an identity server, or undef if
68# no local storage/cache is available, in which case the caller
69# goes into dumb consumer mode.  will do a POST and allocate
70# a new assoc_handle if none is found, or has expired
71sub server_assoc {
72    my ($csr, $server, $force_reassociate, %opts) = @_;
73
74    my $protocol_version = delete $opts{protocol_version} || 1;
75    Carp::croak("unknown options: " . join(", ", keys %opts)) if %opts;
76
77    # closure to return undef (dumb consumer mode) and log why
78    my $dumb = sub {
79        $csr->_debug("server_assoc: dumb mode: $_[0]");
80        return undef;
81    };
82
83    my $cache = $csr->cache;
84    return $dumb->("no_cache") unless $cache;
85
86    unless ($force_reassociate) {
87        # try first from cached association handle
88        if (my $handle = $cache->get("shandle:$server")) {
89            my $assoc = handle_assoc($csr, $server, $handle);
90
91            if ($assoc && $assoc->usable) {
92                $csr->_debug("Found association from cache (handle=$handle)");
93                return $assoc;
94            }
95        }
96    }
97
98    # make a new association
99    my $dh = _default_dh();
100
101    my %post = (
102                "openid.mode" => "associate",
103                "openid.assoc_type" => "HMAC-SHA1",
104                "openid.session_type" => "DH-SHA1",
105                "openid.dh_consumer_public" => OpenID::util::bi2arg($dh->pub_key),
106                );
107
108    if ($protocol_version == 2) {
109        $post{"openid.ns"} = OpenID::util::version_2_namespace();
110    }
111
112    my $req = HTTP::Request->new(POST => $server);
113    $req->header("Content-Type" => "application/x-www-form-urlencoded");
114    $req->content(join("&", map { "$_=" . OpenID::util::eurl($post{$_}) } keys %post));
115
116    $csr->_debug("Associate mode request: " . $req->content);
117
118    my $ua  = $csr->ua;
119    my $res = $ua->request($req);
120
121    # uh, some failure, let's go into dumb mode?
122    return $dumb->("http_failure_no_associate") unless $res && $res->is_success;
123
124    my $recv_time = time();
125    my $content = $res->content;
126    my %args = OpenID::util::parse_keyvalue($content);
127    $csr->_debug("Response to associate mode: [$content] parsed = " . join(",", %args));
128
129    return $dumb->("unknown_assoc_type") unless $args{'assoc_type'} eq "HMAC-SHA1";
130
131    my $stype = $args{'session_type'};
132    return $dumb->("unknown_session_type") if $stype && $stype ne "DH-SHA1";
133
134    # protocol version 1.1
135    my $expires_in = $args{'expires_in'};
136
137    # protocol version 1.0 (DEPRECATED)
138    if (! $expires_in) {
139        if (my $issued = OpenID::util::w3c_to_time($args{'issued'})) {
140            my $expiry = OpenID::util::w3c_to_time($args{'expiry'});
141            my $replace_after = OpenID::util::w3c_to_time($args{'replace_after'});
142
143            # seconds ahead (positive) or behind (negative) the server is
144            $expires_in = ($replace_after || $expiry) - $issued;
145        }
146    }
147
148    # between 1 second and 2 years
149    return $dumb->("bogus_expires_in") unless $expires_in > 0 && $expires_in < 63072000;
150
151    my $ahandle = $args{'assoc_handle'};
152
153    my $secret;
154    if ($stype ne "DH-SHA1") {
155        $secret = OpenID::util::d64($args{'mac_key'});
156    } else {
157        my $server_pub = OpenID::util::arg2bi($args{'dh_server_public'});
158        my $dh_sec = $dh->compute_secret($server_pub);
159        $secret = OpenID::util::d64($args{'enc_mac_key'}) ^ sha1(OpenID::util::bi2bytes($dh_sec));
160    }
161    return $dumb->("secret_not_20_bytes") unless length($secret) == 20;
162
163    my %assoc = (
164                 handle => $ahandle,
165                 server => $server,
166                 secret => $secret,
167                 type   => $args{'assoc_type'},
168                 expiry => $recv_time + $expires_in,
169                 );
170
171    my $assoc = Net::OpenID::Association->new( %assoc );
172    return $dumb->("assoc_undef") unless $assoc;
173
174    $cache->set("hassoc:$server:$ahandle", Storable::freeze(\%assoc));
175    $cache->set("shandle:$server", $ahandle);
176
177    # now we test that the cache object given to us actually works.  if it
178    # doesn't, it'll also fail later, making the verify fail, so let's
179    # go into stateless (dumb mode) earlier if we can detect this.
180    $cache->get("shandle:$server")
181        or return $dumb->("cache_broken");
182
183    return $assoc;
184}
185
186# returns association, or undef if it can't be found
187sub handle_assoc {
188    my ($csr, $server, $handle) = @_;
189
190    # closure to return undef (dumb consumer mode) and log why
191    my $dumb = sub {
192        $csr->_debug("handle_assoc: dumb mode: $_[0]");
193        return undef;
194    };
195
196    return $dumb->("no_handle") unless $handle;
197
198    my $cache = $csr->cache;
199    return $dumb->("no_cache") unless $cache;
200
201    my $frozen = $cache->get("hassoc:$server:$handle");
202    return $dumb->("not_in_cache") unless $frozen;
203
204    my $param = eval { Storable::thaw($frozen) };
205    return $dumb->("not_a_hashref") unless ref $param eq "HASH";
206
207    return Net::OpenID::Association->new( %$param );
208}
209
210sub invalidate_handle {
211    my ($csr, $server, $handle) = @_;
212    my $cache = $csr->cache
213        or return;
214    $cache->set("hassoc:$server:$handle", "");
215}
216
217sub _default_dh {
218    my $dh = Crypt::DH->new;
219    $dh->p("155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443");
220    $dh->g("2");
221    $dh->generate_keys;
222    return $dh;
223}
224
225
226
2271;
228
229__END__
230
231=head1 NAME
232
233Net::OpenID::Association - a relationship with an identity server
234
235=head1 DESCRIPTION
236
237Internal class.
238
239=head1 COPYRIGHT, WARRANTY, AUTHOR
240
241See L<Net::OpenID::Consumer> for author, copyrignt and licensing information.
242
243=head1 SEE ALSO
244
245L<Net::OpenID::Consumer>
246
247L<Net::OpenID::VerifiedIdentity>
248
249L<Net::OpenID::Server>
250
251Website:  L<http://www.danga.com/openid/>
252
Note: See TracBrowser for help on using the browser.