line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package AnyEvent::UA::Req; |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
sub new { |
4
|
0
|
|
|
0
|
|
|
my $pk = shift; |
5
|
0
|
|
|
|
|
|
my ($method,$uri) = (shift,shift); |
6
|
0
|
|
|
|
|
|
my $self = bless {@_}, $pk; |
7
|
0
|
|
|
|
|
|
$self->{method} = $method; |
8
|
0
|
|
|
|
|
|
$self->{uri} = $uri; |
9
|
0
|
|
|
|
|
|
$self; |
10
|
|
|
|
|
|
|
} |
11
|
|
|
|
|
|
|
|
12
|
|
|
|
|
|
|
sub error { |
13
|
0
|
|
|
0
|
|
|
my $self = shift; |
14
|
0
|
|
|
|
|
|
$self->{cb}( undef, HTTP::Easy::Headers->new->HTTP(@_) ); |
15
|
0
|
|
|
|
|
|
%$self = (); |
16
|
|
|
|
|
|
|
} |
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
package AnyEvent::UA::Con; |
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
sub new { |
21
|
0
|
|
|
0
|
|
|
my $pk = shift; |
22
|
0
|
|
|
|
|
|
my $self = bless {@_}, $pk; |
23
|
0
|
|
|
|
|
|
$self; |
24
|
|
|
|
|
|
|
} |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
package AnyEvent::UA; |
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
#use strict; |
29
|
|
|
|
|
|
|
#use warnings; |
30
|
1
|
|
|
1
|
|
26196
|
use common::sense; |
|
1
|
|
|
|
|
10
|
|
|
1
|
|
|
|
|
7
|
|
31
|
|
|
|
|
|
|
|
32
|
1
|
|
|
1
|
|
879
|
use AE; |
|
1
|
|
|
|
|
16080
|
|
|
1
|
|
|
|
|
37
|
|
33
|
1
|
|
|
1
|
|
1199
|
use AnyEvent::DNS; |
|
1
|
|
|
|
|
41417
|
|
|
1
|
|
|
|
|
47
|
|
34
|
1
|
|
|
1
|
|
14
|
use AnyEvent::Socket; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
114
|
|
35
|
1
|
|
|
1
|
|
1266
|
use AnyEvent::Handle; |
|
1
|
|
|
|
|
9242
|
|
|
1
|
|
|
|
|
41
|
|
36
|
1
|
|
|
1
|
|
427
|
use HTTP::Easy::Headers; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
use HTTP::Easy::Cookies; |
38
|
|
|
|
|
|
|
use Scalar::Util 'weaken'; |
39
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
=head1 NAME |
42
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
AnyEvent::UA - HTTP/1.1 UserAgent using AnyEvent |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
=cut |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
our $VERSION = '0.01_01';#$VERSION = eval($VERSION); |
48
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
=head1 SYNOPSIS |
50
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
Full docs to be done, so just an example. |
52
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
my $ua = AnyEvent::UA->new(); |
54
|
|
|
|
|
|
|
$ua->req(GET => 'HTTP://www.google.ru:80', cb => sub { |
55
|
|
|
|
|
|
|
my ($body,$headers) = @_; |
56
|
|
|
|
|
|
|
}); |
57
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
=head1 DESCRIPTION |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
This module is alpha quality. It was not tested perfectly. Use it on your own risk. Interfaces and implementation may be changed. |
61
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
=cut |
63
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
sub new { |
65
|
|
|
|
|
|
|
my $pk = shift; |
66
|
|
|
|
|
|
|
my $self = bless {}, $pk; |
67
|
|
|
|
|
|
|
my %args = @_; |
68
|
|
|
|
|
|
|
$self->{headers} = { |
69
|
|
|
|
|
|
|
# 'accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', |
70
|
|
|
|
|
|
|
# 'user-agent' => 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.10) Gecko/2009042315 Firefox/3.0.10', |
71
|
|
|
|
|
|
|
'referer' => undef, |
72
|
|
|
|
|
|
|
# 'accept-language' => 'ru,en-us;q=0.8,en;q=0.5,ru-ru;q=0.3', |
73
|
|
|
|
|
|
|
# 'accept-encoding' => 'gzip', |
74
|
|
|
|
|
|
|
'accept-charset' => 'utf-8,windows-1251;q=0.7,*;q=0.7', |
75
|
|
|
|
|
|
|
'connection' => 'keep-alive', |
76
|
|
|
|
|
|
|
%{ $args{headers} || {} }, |
77
|
|
|
|
|
|
|
}; |
78
|
|
|
|
|
|
|
$self->{cv} = $args{cv} || AE::cv; |
79
|
|
|
|
|
|
|
$self->{cookie} //= HTTP::Easy::Cookies->new(); |
80
|
|
|
|
|
|
|
#$self->{auth} = {}; |
81
|
|
|
|
|
|
|
#$self->{requests} = []; |
82
|
|
|
|
|
|
|
#$self->{domain} = $args{domain} || '.odnoklassniki.ru'; |
83
|
|
|
|
|
|
|
$self->{debug} = $args{debug} // 1; |
84
|
|
|
|
|
|
|
$self->{proxy} = $args{proxy} if exists $args{proxy}; |
85
|
|
|
|
|
|
|
$self; |
86
|
|
|
|
|
|
|
} |
87
|
|
|
|
|
|
|
|
88
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
our $TIMEOUT = 10; |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
sub connect : method { |
92
|
|
|
|
|
|
|
my ($self,$host,$port,%args) = @_; |
93
|
|
|
|
|
|
|
# TODO: |
94
|
|
|
|
|
|
|
# * slots (max-open-con) |
95
|
|
|
|
|
|
|
# * single resolve queue |
96
|
|
|
|
|
|
|
$self->{cv}->begin; |
97
|
|
|
|
|
|
|
my %state; |
98
|
|
|
|
|
|
|
my $con = sub { |
99
|
|
|
|
|
|
|
if (my $ra = shift) { |
100
|
|
|
|
|
|
|
warn "ready to con $ra $port"; |
101
|
|
|
|
|
|
|
$state{connect} = tcp_connect $ra,$port,sub { |
102
|
|
|
|
|
|
|
my $fh = shift; |
103
|
|
|
|
|
|
|
@_ = (); |
104
|
|
|
|
|
|
|
if( $fh ) { |
105
|
|
|
|
|
|
|
$args{cb}($fh); |
106
|
|
|
|
|
|
|
} else { |
107
|
|
|
|
|
|
|
$args{cb}(undef,"$!"); |
108
|
|
|
|
|
|
|
} |
109
|
|
|
|
|
|
|
%state = (); |
110
|
|
|
|
|
|
|
},$args{on_prepare} || sub { $args{timeout} || $TIMEOUT }; |
111
|
|
|
|
|
|
|
} else { |
112
|
|
|
|
|
|
|
$args{cb}(undef,@_); |
113
|
|
|
|
|
|
|
$self->{cv}->end; |
114
|
|
|
|
|
|
|
} |
115
|
|
|
|
|
|
|
}; |
116
|
|
|
|
|
|
|
my $ip = $self->{dns}{$host}; |
117
|
|
|
|
|
|
|
if ($ip) { |
118
|
|
|
|
|
|
|
push @$ip, my $ra = shift @$ip; |
119
|
|
|
|
|
|
|
$con->($ra); |
120
|
|
|
|
|
|
|
} else { |
121
|
|
|
|
|
|
|
AnyEvent::DNS::a $host, sub { |
122
|
|
|
|
|
|
|
if (@_) { |
123
|
|
|
|
|
|
|
$self->{dns}{$host} = [@_]; |
124
|
|
|
|
|
|
|
$con->($_[-1]); |
125
|
|
|
|
|
|
|
} else { |
126
|
|
|
|
|
|
|
$con->(undef, "$!"); |
127
|
|
|
|
|
|
|
} |
128
|
|
|
|
|
|
|
}; |
129
|
|
|
|
|
|
|
} |
130
|
|
|
|
|
|
|
return defined wantarray ? AnyEvent::Util::guard { %state = (); } : undef; |
131
|
|
|
|
|
|
|
} |
132
|
|
|
|
|
|
|
|
133
|
|
|
|
|
|
|
our $qr_nl = qr{\015?\012}o; |
134
|
|
|
|
|
|
|
our $qr_nlnl = qr{(?
|
135
|
|
|
|
|
|
|
our $MAX_RECURSE = 10; |
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
sub decode_uri { |
138
|
|
|
|
|
|
|
my $self = shift; |
139
|
|
|
|
|
|
|
my $uri = shift; |
140
|
|
|
|
|
|
|
my $port = { http => 80, https => 443, }->{ lc $uri->scheme } or return; |
141
|
|
|
|
|
|
|
my $realport = $uri->port; |
142
|
|
|
|
|
|
|
my $host = lc $uri->host; |
143
|
|
|
|
|
|
|
#warn "$host : $port"; |
144
|
|
|
|
|
|
|
my $host_header = $port != $realport ? "$host:$realport" : $host; |
145
|
|
|
|
|
|
|
my $proxy; |
146
|
|
|
|
|
|
|
my ($rhost, $rport, $rscheme, $rpath); # request host, port, path |
147
|
|
|
|
|
|
|
if ($proxy) { |
148
|
|
|
|
|
|
|
($rpath, $rhost, $rport, $rscheme) = ("$uri", @$proxy); |
149
|
|
|
|
|
|
|
$rscheme = "http" unless defined $rscheme; |
150
|
|
|
|
|
|
|
# don't support https requests over https-proxy transport, |
151
|
|
|
|
|
|
|
# can't be done with tls as spec'ed, unless you double-encrypt. |
152
|
|
|
|
|
|
|
$rscheme = "http" if $uri->scheme eq "https" && $rscheme eq "https"; |
153
|
|
|
|
|
|
|
} else { |
154
|
|
|
|
|
|
|
($rhost, $rport, $rscheme, $rpath) = ($host,$realport,$uri->scheme,$uri->path); |
155
|
|
|
|
|
|
|
} |
156
|
|
|
|
|
|
|
return ($rhost, $rport, $rscheme, $rpath, $host_header); |
157
|
|
|
|
|
|
|
} |
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
sub req { |
160
|
|
|
|
|
|
|
my $self = shift; |
161
|
|
|
|
|
|
|
my ($method, $uri, %args) = @_; |
162
|
|
|
|
|
|
|
use URI; |
163
|
|
|
|
|
|
|
$uri = URI->new($uri) unless ref $uri; |
164
|
|
|
|
|
|
|
$uri->path('/') unless length $uri->path; |
165
|
|
|
|
|
|
|
my %state; |
166
|
|
|
|
|
|
|
my $e = sub { my ($code,$mess) = @_; %state = (); $args{cb}(undef, { Status => $code, Reason => $mess, URL => $uri }); }; |
167
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
my ($host, $port, $scheme, $path, $host_header) = $self->decode_uri($uri) |
169
|
|
|
|
|
|
|
or return $e->(599); |
170
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
my $headers = HTTP::Easy::Headers->new( { %{$self->{headers}}, host => $host_header } ); |
172
|
|
|
|
|
|
|
warn "($host, $port, $scheme, $path) \n"; |
173
|
|
|
|
|
|
|
my $conkey = "$host:$port"; |
174
|
|
|
|
|
|
|
my $id; |
175
|
|
|
|
|
|
|
if (exists $self->{ka}{$conkey}) { |
176
|
|
|
|
|
|
|
$id = $self->{ka}{$conkey}; |
177
|
|
|
|
|
|
|
warn "Have KA for $conkey: $self->{con}{ $id }"; |
178
|
|
|
|
|
|
|
push @{ $self->{con}{ $id }{r} }, AnyEvent::UA::Req->new( |
179
|
|
|
|
|
|
|
$method, $uri, %args, headers => $headers, |
180
|
|
|
|
|
|
|
); |
181
|
|
|
|
|
|
|
#$self->rr($h, $method, $uri, %args, path => $path, headers => $headers); |
182
|
|
|
|
|
|
|
$self->rr2($id); |
183
|
|
|
|
|
|
|
return; |
184
|
|
|
|
|
|
|
} else { |
185
|
|
|
|
|
|
|
# TODO: |
186
|
|
|
|
|
|
|
# push req to r, connect, handle all |
187
|
|
|
|
|
|
|
{ |
188
|
|
|
|
|
|
|
weaken( my $this = $self ); |
189
|
|
|
|
|
|
|
my $con = { |
190
|
|
|
|
|
|
|
host => $host, |
191
|
|
|
|
|
|
|
port => $port, |
192
|
|
|
|
|
|
|
r => [], |
193
|
|
|
|
|
|
|
# TODO |
194
|
|
|
|
|
|
|
# $self->{keep_alive} ? ( |
195
|
|
|
|
|
|
|
# ka => AE::timer 300,0,sub { |
196
|
|
|
|
|
|
|
# $self or return; |
197
|
|
|
|
|
|
|
# delete $self->{con}{$id}; |
198
|
|
|
|
|
|
|
# }, |
199
|
|
|
|
|
|
|
# ) : (), |
200
|
|
|
|
|
|
|
}; |
201
|
|
|
|
|
|
|
$id = int $con; |
202
|
|
|
|
|
|
|
$con->{id} = $id; |
203
|
|
|
|
|
|
|
$self->{con}{$id} = $con; |
204
|
|
|
|
|
|
|
$self->{ka}{$conkey} = $id; |
205
|
|
|
|
|
|
|
$con->{close} = sub { |
206
|
|
|
|
|
|
|
$this or return; |
207
|
|
|
|
|
|
|
exists $this->{con}{$id} or return; |
208
|
|
|
|
|
|
|
for(@{ $this->{con}{$id}{r} }) { |
209
|
|
|
|
|
|
|
$_->error(599,$_[0]); |
210
|
|
|
|
|
|
|
} |
211
|
|
|
|
|
|
|
delete $this->{con}{$id}; |
212
|
|
|
|
|
|
|
}; |
213
|
|
|
|
|
|
|
} |
214
|
|
|
|
|
|
|
push @{ $self->{con}{ $id }{r} }, AnyEvent::UA::Req->new( |
215
|
|
|
|
|
|
|
$method, $uri, %args, headers => $headers, |
216
|
|
|
|
|
|
|
); |
217
|
|
|
|
|
|
|
} |
218
|
|
|
|
|
|
|
my $timeout = $args{timeout} || $TIMEOUT; |
219
|
|
|
|
|
|
|
my $proxy = $args{proxy};# || $PROXY; |
220
|
|
|
|
|
|
|
$state{connect} = |
221
|
|
|
|
|
|
|
$self->connect( |
222
|
|
|
|
|
|
|
$host, $port, |
223
|
|
|
|
|
|
|
timeout => $timeout, |
224
|
|
|
|
|
|
|
on_prepare => $args{on_prepare}, |
225
|
|
|
|
|
|
|
cb => sub { |
226
|
|
|
|
|
|
|
if (my $fh = shift) { |
227
|
|
|
|
|
|
|
warn "connected 1"; |
228
|
|
|
|
|
|
|
return unless delete $state{connect}; |
229
|
|
|
|
|
|
|
warn "connected 2. id = $id"; |
230
|
|
|
|
|
|
|
my $h = AnyEvent::Handle->new( |
231
|
|
|
|
|
|
|
fh => $fh, |
232
|
|
|
|
|
|
|
timeout => $timeout, |
233
|
|
|
|
|
|
|
peername => $host, |
234
|
|
|
|
|
|
|
on_eof => sub { warn "EOF"; delete($self->{con}{$id})->{close}("Unexpected end-of-file") }, |
235
|
|
|
|
|
|
|
on_error => sub { warn "ERR @_"; delete($self->{con}{$id})->{close}( $_[2]); }, |
236
|
|
|
|
|
|
|
#tls_ctx => $arg{tls_ctx}, |
237
|
|
|
|
|
|
|
); |
238
|
|
|
|
|
|
|
$self->{con}{$id}{h} = $h; |
239
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
# TODO: limit KA conns |
241
|
|
|
|
|
|
|
# (re-)configure handle |
242
|
|
|
|
|
|
|
my $request = sub { |
243
|
|
|
|
|
|
|
# Connection initially established |
244
|
|
|
|
|
|
|
#$self->rr($h, $method, $uri, %args, path => $path, headers => $headers); |
245
|
|
|
|
|
|
|
$self->rr2($id); |
246
|
|
|
|
|
|
|
};#END $request |
247
|
|
|
|
|
|
|
# now handle proxy-CONNECT method |
248
|
|
|
|
|
|
|
$h->starttls ("connect") if $scheme eq "https"; |
249
|
|
|
|
|
|
|
if ($proxy and $scheme eq "https") { |
250
|
|
|
|
|
|
|
my $peer = (my $uhost = $uri->host).':'.$uri->port; |
251
|
|
|
|
|
|
|
$h->push_write ("CONNECT $peer HTTP/1.0\015\012Host: $uhost\015\012\015\012"); |
252
|
|
|
|
|
|
|
$h->push_read (line => $qr_nlnl, sub { |
253
|
|
|
|
|
|
|
$_[1] =~ /^HTTP\/([0-9\.]+) \s+ ([0-9]{3}) (?: \s+ ([^\015\012]*) )?/ix |
254
|
|
|
|
|
|
|
or return $e->(599, "Invalid proxy connect response ($_[1])"); |
255
|
|
|
|
|
|
|
if ($2 == 200) { |
256
|
|
|
|
|
|
|
$path = $uri->path; |
257
|
|
|
|
|
|
|
$self->{con}{$id}{type} = 'raw'; |
258
|
|
|
|
|
|
|
$self->rr2($id); |
259
|
|
|
|
|
|
|
} else { |
260
|
|
|
|
|
|
|
return $e->($2,$3); |
261
|
|
|
|
|
|
|
} |
262
|
|
|
|
|
|
|
}); |
263
|
|
|
|
|
|
|
} else { |
264
|
|
|
|
|
|
|
$h->starttls ("connect") if $scheme eq "https" && !exists $state{handle}{tls}; |
265
|
|
|
|
|
|
|
$self->rr2($id); |
266
|
|
|
|
|
|
|
} |
267
|
|
|
|
|
|
|
} else { |
268
|
|
|
|
|
|
|
warn "Got error @_"; |
269
|
|
|
|
|
|
|
return $e->(599,@_); |
270
|
|
|
|
|
|
|
} |
271
|
|
|
|
|
|
|
} |
272
|
|
|
|
|
|
|
); |
273
|
|
|
|
|
|
|
return; |
274
|
|
|
|
|
|
|
} |
275
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
sub rr2 { |
277
|
|
|
|
|
|
|
my $self = shift; |
278
|
|
|
|
|
|
|
my $id = shift; |
279
|
|
|
|
|
|
|
return warn "no such connection $id" unless exists $self->{con}{$id}; |
280
|
|
|
|
|
|
|
my $con = $self->{con}{$id}; |
281
|
|
|
|
|
|
|
return warn ("Not connected yet"), unless $con->{h}; |
282
|
|
|
|
|
|
|
return warn ("No more requests for $id"), $con->{h}->timeout(undef) unless @{ $con->{r} }; |
283
|
|
|
|
|
|
|
#while (@{ $con->{r} }) { |
284
|
|
|
|
|
|
|
my $r = shift @{ $con->{r} }; |
285
|
|
|
|
|
|
|
warn "Run request $r->{method} $r->{uri} over $con->{id}"; |
286
|
|
|
|
|
|
|
if ($con->{type} eq 'proxy') { |
287
|
|
|
|
|
|
|
return $r->error(599, "Proxy not implemented"); |
288
|
|
|
|
|
|
|
} else { |
289
|
|
|
|
|
|
|
my $path = $r->{uri}->path_query; |
290
|
|
|
|
|
|
|
$con->{h}->push_write ( |
291
|
|
|
|
|
|
|
"$r->{method} $path HTTP/1.1\015\012" . |
292
|
|
|
|
|
|
|
$r->{headers}->encode . "\015\012" . |
293
|
|
|
|
|
|
|
(delete $r->{body}) |
294
|
|
|
|
|
|
|
); |
295
|
|
|
|
|
|
|
$con->{h}->push_read (line => $qr_nl, sub { |
296
|
|
|
|
|
|
|
#return unless exists $self->{con}{$id}; |
297
|
|
|
|
|
|
|
$_[1] =~ /^HTTP\/([0-9\.]+) \s+ ([0-9]{3}) (?: \s+ ([^\015\012]*) )?/ixo |
298
|
|
|
|
|
|
|
or return $r->error( 599, "Invalid server response ($_[1])" ); |
299
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
my $status = $2;my $reason = $3;my $http_version = $1; |
301
|
|
|
|
|
|
|
|
302
|
|
|
|
|
|
|
# headers, could be optimized a bit |
303
|
|
|
|
|
|
|
$con->{h}->unshift_read (line => $qr_nlnl, sub { |
304
|
|
|
|
|
|
|
my $uri = $r->{uri}; |
305
|
|
|
|
|
|
|
my $method = $r->{method}; |
306
|
|
|
|
|
|
|
my $hdr = HTTP::Easy::Headers->decode($_[1], base => $uri); |
307
|
|
|
|
|
|
|
$hdr->{Status} = $status; |
308
|
|
|
|
|
|
|
$hdr->{Reason} = $reason; |
309
|
|
|
|
|
|
|
# TODO: check correctness? |
310
|
|
|
|
|
|
|
# or return $r->error(599, "Garbled response headers"); |
311
|
|
|
|
|
|
|
$self->{cookie}->decode($hdr->{"set-cookie"}, host => $uri->host) if exists $hdr->{"set-cookie"}; |
312
|
|
|
|
|
|
|
$self->{cookie}->decode($hdr->{"set-cookie2"}, host => $uri->host) if exists $hdr->{"set-cookie2"}; |
313
|
|
|
|
|
|
|
$self->{cookie}->decode($hdr->{"set-cookie3"}, host => $uri->host) if exists $hdr->{"set-cookie3"}; |
314
|
|
|
|
|
|
|
|
315
|
|
|
|
|
|
|
my $redirect; |
316
|
|
|
|
|
|
|
my $recurse = 0;# TODO: exists $args{recurse} ? delete $args{recurse} : $MAX_RECURSE; |
317
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
if ($recurse) { |
319
|
|
|
|
|
|
|
if ($status =~ /^30[12]$/ and $method ne "POST") { |
320
|
|
|
|
|
|
|
# apparently, mozilla et al. just change POST to GET here |
321
|
|
|
|
|
|
|
# more research is needed before we do the same |
322
|
|
|
|
|
|
|
$redirect = 1; |
323
|
|
|
|
|
|
|
} |
324
|
|
|
|
|
|
|
elsif ($status == 303) { |
325
|
|
|
|
|
|
|
# even http/1.1 is unclear on how to mutate the method |
326
|
|
|
|
|
|
|
$method = "GET" unless $method eq "HEAD"; |
327
|
|
|
|
|
|
|
$redirect = 1; |
328
|
|
|
|
|
|
|
} |
329
|
|
|
|
|
|
|
elsif ($status == 307 and $method =~ /^(?:GET|HEAD)$/) { |
330
|
|
|
|
|
|
|
$redirect = 1; |
331
|
|
|
|
|
|
|
} |
332
|
|
|
|
|
|
|
} |
333
|
|
|
|
|
|
|
|
334
|
|
|
|
|
|
|
my $finish = sub { |
335
|
|
|
|
|
|
|
#$con->destroy if $con; |
336
|
|
|
|
|
|
|
#%state = (); |
337
|
|
|
|
|
|
|
|
338
|
|
|
|
|
|
|
if ($redirect && exists $hdr->{location}) { |
339
|
|
|
|
|
|
|
# we ignore any errors, as it is very common to receive |
340
|
|
|
|
|
|
|
# Content-Length != 0 but no actual body |
341
|
|
|
|
|
|
|
# we also access %hdr, as $_[1] might be an erro |
342
|
|
|
|
|
|
|
#http_request ($method => $hdr{location}, %arg, recurse => $recurse - 1, $cb); |
343
|
|
|
|
|
|
|
warn "Redirect => $hdr->{location}"; |
344
|
|
|
|
|
|
|
} else { |
345
|
|
|
|
|
|
|
if (exists $_[1]{'content-encoding'}) { |
346
|
|
|
|
|
|
|
if (lc($_[1]{'content-encoding'}) =~ /^(?:x-)?gzip$/) { |
347
|
|
|
|
|
|
|
eval{ |
348
|
|
|
|
|
|
|
my $def = Compress::Zlib::memGunzip($_[0]); |
349
|
|
|
|
|
|
|
if (defined $def) { |
350
|
|
|
|
|
|
|
$_[0] = $def; |
351
|
|
|
|
|
|
|
#warn "Page deflated from $hdr->{'content-encoding'}" if $self->{debug}; |
352
|
|
|
|
|
|
|
1; |
353
|
|
|
|
|
|
|
} else { 0 } |
354
|
|
|
|
|
|
|
} or do { |
355
|
|
|
|
|
|
|
warn "Deflate failed: $@"; |
356
|
|
|
|
|
|
|
} |
357
|
|
|
|
|
|
|
} else { |
358
|
|
|
|
|
|
|
warn "Unsupported content-encoding method: $_[1]{'content-encoding'}"; |
359
|
|
|
|
|
|
|
} |
360
|
|
|
|
|
|
|
} |
361
|
|
|
|
|
|
|
$r->{cb}($_[0], $_[1]); |
362
|
|
|
|
|
|
|
} |
363
|
|
|
|
|
|
|
$self->rr2($id); |
364
|
|
|
|
|
|
|
}; |
365
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
my $len = $hdr->{"content-length"}; |
367
|
|
|
|
|
|
|
|
368
|
|
|
|
|
|
|
# if (!$redirect && $args{on_header} && !$args{on_header}($hdr)) { |
369
|
|
|
|
|
|
|
# $finish->(undef, { Status => 598, Reason => "Request cancelled by on_header", URL => $uri }); |
370
|
|
|
|
|
|
|
# } |
371
|
|
|
|
|
|
|
# elsif ( |
372
|
|
|
|
|
|
|
if( |
373
|
|
|
|
|
|
|
$status =~ /^(?:1..|[23]04)$/ |
374
|
|
|
|
|
|
|
or $method eq "HEAD" |
375
|
|
|
|
|
|
|
or (defined $len && !$len) |
376
|
|
|
|
|
|
|
) { |
377
|
|
|
|
|
|
|
# no body |
378
|
|
|
|
|
|
|
$finish->("", $hdr); |
379
|
|
|
|
|
|
|
} |
380
|
|
|
|
|
|
|
else { |
381
|
|
|
|
|
|
|
#warn dumper $hdr,$self->{cookie}; |
382
|
|
|
|
|
|
|
if (lc $hdr->{'transfer-encoding'} eq 'chunked') { |
383
|
|
|
|
|
|
|
my $body = ''; |
384
|
|
|
|
|
|
|
my $get_chunk;$get_chunk = sub { |
385
|
|
|
|
|
|
|
$con->{h}->unshift_read( regex => qr{([a-f0-9]{1,32})(?:[\011\040]+[^\012]{0,255})?\015?\012}o,sub { |
386
|
|
|
|
|
|
|
my $chunk = hex($1); |
387
|
|
|
|
|
|
|
if ($chunk > 0) { |
388
|
|
|
|
|
|
|
$get_chunk->(); |
389
|
|
|
|
|
|
|
#warn "need chunk $chunk"; |
390
|
|
|
|
|
|
|
$_[0]->unshift_read(chunk => $chunk, sub { |
391
|
|
|
|
|
|
|
$body .= $_[1]; |
392
|
|
|
|
|
|
|
}); |
393
|
|
|
|
|
|
|
} else { |
394
|
|
|
|
|
|
|
undef $get_chunk; |
395
|
|
|
|
|
|
|
#warn "Got all chunks, read trailer"; |
396
|
|
|
|
|
|
|
$_[0]->unshift_read(line => $qr_nlnl, sub { |
397
|
|
|
|
|
|
|
#warn "Got trailer $_[1]"; |
398
|
|
|
|
|
|
|
$finish->($body,$hdr); |
399
|
|
|
|
|
|
|
}); |
400
|
|
|
|
|
|
|
} |
401
|
|
|
|
|
|
|
}); |
402
|
|
|
|
|
|
|
}; |
403
|
|
|
|
|
|
|
$get_chunk->(); |
404
|
|
|
|
|
|
|
} else { |
405
|
|
|
|
|
|
|
$_[0]->on_eof (undef); |
406
|
|
|
|
|
|
|
if ($len) { |
407
|
|
|
|
|
|
|
warn "ready for body (+$len)"; |
408
|
|
|
|
|
|
|
$_[0]->on_error (sub { $finish->(undef, $hdr->HTTP(599,$_[2])) }); |
409
|
|
|
|
|
|
|
$_[0]->unshift_read(chunk => $len, sub { |
410
|
|
|
|
|
|
|
$finish->($_[1],$hdr); |
411
|
|
|
|
|
|
|
}); |
412
|
|
|
|
|
|
|
} else { |
413
|
|
|
|
|
|
|
warn "ready for body until eof"; |
414
|
|
|
|
|
|
|
$_[0]->on_error (sub { |
415
|
|
|
|
|
|
|
$! == Errno::EPIPE || !$! |
416
|
|
|
|
|
|
|
? $finish->(delete $_[0]{rbuf}, $hdr) |
417
|
|
|
|
|
|
|
: $finish->(undef, $hdr->HTTP(599,$_[2])); |
418
|
|
|
|
|
|
|
}); |
419
|
|
|
|
|
|
|
$_[0]->on_read (sub { }); |
420
|
|
|
|
|
|
|
} |
421
|
|
|
|
|
|
|
} |
422
|
|
|
|
|
|
|
|
423
|
|
|
|
|
|
|
} |
424
|
|
|
|
|
|
|
}); |
425
|
|
|
|
|
|
|
}); |
426
|
|
|
|
|
|
|
|
427
|
|
|
|
|
|
|
} |
428
|
|
|
|
|
|
|
#} |
429
|
|
|
|
|
|
|
} |
430
|
|
|
|
|
|
|
|
431
|
|
|
|
|
|
|
sub rr { # request/response |
432
|
|
|
|
|
|
|
my $self = shift; |
433
|
|
|
|
|
|
|
my $con = shift; |
434
|
|
|
|
|
|
|
my ($method, $uri, %args) = @_;@_ = (); |
435
|
|
|
|
|
|
|
my $e = sub { my ($code,$mess) = @_; undef $con; $args{cb}(undef, { Status => $code, Reason => $mess, URL => $uri }); }; |
436
|
|
|
|
|
|
|
my $recurse = exists $args{recurse} ? delete $args{recurse} : $MAX_RECURSE; |
437
|
|
|
|
|
|
|
warn "Run request $method $uri"; |
438
|
|
|
|
|
|
|
|
439
|
|
|
|
|
|
|
# send request |
440
|
|
|
|
|
|
|
$con->push_write ( |
441
|
|
|
|
|
|
|
"$method $args{path} HTTP/1.1\015\012" |
442
|
|
|
|
|
|
|
. $args{headers}->encode |
443
|
|
|
|
|
|
|
. "\015\012" |
444
|
|
|
|
|
|
|
. (delete $args{body}) |
445
|
|
|
|
|
|
|
); |
446
|
|
|
|
|
|
|
if ($args{body_cb}) { |
447
|
|
|
|
|
|
|
my $written = 0; |
448
|
|
|
|
|
|
|
my $need = $args{headers}{"content-length"}; |
449
|
|
|
|
|
|
|
$con->on_drain(sub { |
450
|
|
|
|
|
|
|
$args{body_cb}(sub { |
451
|
|
|
|
|
|
|
shift if @_ and length $_[0] == 0; |
452
|
|
|
|
|
|
|
use bytes; |
453
|
|
|
|
|
|
|
if (@_) { |
454
|
|
|
|
|
|
|
my $chunk = shift; |
455
|
|
|
|
|
|
|
my $left = $need - $written; |
456
|
|
|
|
|
|
|
$written += ( my $length = length $chunk ); |
457
|
|
|
|
|
|
|
#warn "Written chunk=$length. now have written=$written and left=".($need - $written); |
458
|
|
|
|
|
|
|
if ($written >= $need) { |
459
|
|
|
|
|
|
|
if ($written > $need) { |
460
|
|
|
|
|
|
|
$chunk = substr($chunk,0,$left); |
461
|
|
|
|
|
|
|
warn "got more data $written, than content-length $need, truncated at @{[ (caller)[1,2] ]}\n"; |
462
|
|
|
|
|
|
|
} |
463
|
|
|
|
|
|
|
$con->on_drain(undef); |
464
|
|
|
|
|
|
|
undef $args{body_cb}; |
465
|
|
|
|
|
|
|
} |
466
|
|
|
|
|
|
|
$con->push_write($chunk); |
467
|
|
|
|
|
|
|
} else { |
468
|
|
|
|
|
|
|
$con->on_drain(undef); |
469
|
|
|
|
|
|
|
undef $args{body_cb}; |
470
|
|
|
|
|
|
|
if ($written < $need) { |
471
|
|
|
|
|
|
|
return $e->(599, "Insufficient ".($need-$written)." bytes data from body_cb. need $need, got $written"); |
472
|
|
|
|
|
|
|
} |
473
|
|
|
|
|
|
|
} |
474
|
|
|
|
|
|
|
}); |
475
|
|
|
|
|
|
|
}); |
476
|
|
|
|
|
|
|
# TODO |
477
|
|
|
|
|
|
|
#%state or return; |
478
|
|
|
|
|
|
|
} |
479
|
|
|
|
|
|
|
|
480
|
|
|
|
|
|
|
delete $args{headers}; |
481
|
|
|
|
|
|
|
|
482
|
|
|
|
|
|
|
# status line |
483
|
|
|
|
|
|
|
$con->push_read (line => $qr_nl, sub { |
484
|
|
|
|
|
|
|
$_[1] =~ /^HTTP\/([0-9\.]+) \s+ ([0-9]{3}) (?: \s+ ([^\015\012]*) )?/ixo |
485
|
|
|
|
|
|
|
or return $e->(599, "Invalid server response ($_[1])"); |
486
|
|
|
|
|
|
|
|
487
|
|
|
|
|
|
|
my $status = $2;my $reason = $3;my $http_version = $1; |
488
|
|
|
|
|
|
|
|
489
|
|
|
|
|
|
|
# headers, could be optimized a bit |
490
|
|
|
|
|
|
|
$con->unshift_read (line => $qr_nlnl, sub { |
491
|
|
|
|
|
|
|
my $hdr = HTTP::Easy::Headers->decode($_[1], base => $uri); |
492
|
|
|
|
|
|
|
$self->{cookie}->decode($hdr->{"set-cookie"}, host => $uri->host) if exists $hdr->{"set-cookie"}; |
493
|
|
|
|
|
|
|
$self->{cookie}->decode($hdr->{"set-cookie2"}, host => $uri->host) if exists $hdr->{"set-cookie2"}; |
494
|
|
|
|
|
|
|
$self->{cookie}->decode($hdr->{"set-cookie3"}, host => $uri->host) if exists $hdr->{"set-cookie3"}; |
495
|
|
|
|
|
|
|
# TODO: check correctness? |
496
|
|
|
|
|
|
|
# or return (%state = (), $cb->(undef, { Status => 599, Reason => "Garbled response headers", URL => $url })); |
497
|
|
|
|
|
|
|
|
498
|
|
|
|
|
|
|
my $redirect; |
499
|
|
|
|
|
|
|
|
500
|
|
|
|
|
|
|
if ($recurse) { |
501
|
|
|
|
|
|
|
if ($status =~ /^30[12]$/ and $method ne "POST") { |
502
|
|
|
|
|
|
|
# apparently, mozilla et al. just change POST to GET here |
503
|
|
|
|
|
|
|
# more research is needed before we do the same |
504
|
|
|
|
|
|
|
$redirect = 1; |
505
|
|
|
|
|
|
|
} |
506
|
|
|
|
|
|
|
elsif ($status == 303) { |
507
|
|
|
|
|
|
|
# even http/1.1 is unclear on how to mutate the method |
508
|
|
|
|
|
|
|
$method = "GET" unless $method eq "HEAD"; |
509
|
|
|
|
|
|
|
$redirect = 1; |
510
|
|
|
|
|
|
|
} |
511
|
|
|
|
|
|
|
elsif ($status == 307 and $method =~ /^(?:GET|HEAD)$/) { |
512
|
|
|
|
|
|
|
$redirect = 1; |
513
|
|
|
|
|
|
|
} |
514
|
|
|
|
|
|
|
} |
515
|
|
|
|
|
|
|
|
516
|
|
|
|
|
|
|
my $finish = sub { |
517
|
|
|
|
|
|
|
#$con->destroy if $con; |
518
|
|
|
|
|
|
|
#%state = (); |
519
|
|
|
|
|
|
|
|
520
|
|
|
|
|
|
|
# set-cookie processing |
521
|
|
|
|
|
|
|
$self->{cookie}->decode($_[1]{"set-cookie"}, host => $uri->host); |
522
|
|
|
|
|
|
|
#$DEBUG_RECV->($_[1]{URL},$_[0],$_[1]) if defined $DEBUG_RECV; |
523
|
|
|
|
|
|
|
|
524
|
|
|
|
|
|
|
if ($redirect && exists $hdr->{location}) { |
525
|
|
|
|
|
|
|
# we ignore any errors, as it is very common to receive |
526
|
|
|
|
|
|
|
# Content-Length != 0 but no actual body |
527
|
|
|
|
|
|
|
# we also access %hdr, as $_[1] might be an erro |
528
|
|
|
|
|
|
|
#http_request ($method => $hdr{location}, %arg, recurse => $recurse - 1, $cb); |
529
|
|
|
|
|
|
|
warn "Redirect => $hdr->{location}"; |
530
|
|
|
|
|
|
|
} else { |
531
|
|
|
|
|
|
|
warn "OK"; |
532
|
|
|
|
|
|
|
$args{cb}($_[0], $_[1]); |
533
|
|
|
|
|
|
|
} |
534
|
|
|
|
|
|
|
}; |
535
|
|
|
|
|
|
|
|
536
|
|
|
|
|
|
|
my $len = $hdr->{"content-length"}; |
537
|
|
|
|
|
|
|
|
538
|
|
|
|
|
|
|
if (!$redirect && $args{on_header} && !$args{on_header}($hdr)) { |
539
|
|
|
|
|
|
|
$finish->(undef, { Status => 598, Reason => "Request cancelled by on_header", URL => $uri }); |
540
|
|
|
|
|
|
|
} |
541
|
|
|
|
|
|
|
elsif ( |
542
|
|
|
|
|
|
|
$status =~ /^(?:1..|[23]04)$/ |
543
|
|
|
|
|
|
|
or $method eq "HEAD" |
544
|
|
|
|
|
|
|
or (defined $len && !$len) |
545
|
|
|
|
|
|
|
) { |
546
|
|
|
|
|
|
|
# no body |
547
|
|
|
|
|
|
|
$finish->("", $hdr); |
548
|
|
|
|
|
|
|
} |
549
|
|
|
|
|
|
|
else { |
550
|
|
|
|
|
|
|
#warn dumper $hdr,$self->{cookie}; |
551
|
|
|
|
|
|
|
if (lc $hdr->{'transfer-encoding'} eq 'chunked') { |
552
|
|
|
|
|
|
|
my $body = ''; |
553
|
|
|
|
|
|
|
my $get_chunk;$get_chunk = sub { |
554
|
|
|
|
|
|
|
$con->unshift_read( regex => qr{([a-f0-9]{1,255})\015?\012},sub { |
555
|
|
|
|
|
|
|
my $chunk = hex($1);@_ = (); |
556
|
|
|
|
|
|
|
if ($chunk > 0) { |
557
|
|
|
|
|
|
|
#warn "need chunk $chunk"; |
558
|
|
|
|
|
|
|
$get_chunk->(); |
559
|
|
|
|
|
|
|
$con->unshift_read(chunk => $chunk, sub { |
560
|
|
|
|
|
|
|
$body .= $_[1]; |
561
|
|
|
|
|
|
|
}); |
562
|
|
|
|
|
|
|
} else { |
563
|
|
|
|
|
|
|
undef $get_chunk; |
564
|
|
|
|
|
|
|
warn "Got all chunks"; |
565
|
|
|
|
|
|
|
$finish->($body,$hdr); |
566
|
|
|
|
|
|
|
} |
567
|
|
|
|
|
|
|
}); |
568
|
|
|
|
|
|
|
}; |
569
|
|
|
|
|
|
|
$get_chunk->(); |
570
|
|
|
|
|
|
|
} else { |
571
|
|
|
|
|
|
|
$_[0]->on_eof (undef); |
572
|
|
|
|
|
|
|
if ($len) { |
573
|
|
|
|
|
|
|
warn "ready for body (+$len)"; |
574
|
|
|
|
|
|
|
$_[0]->on_error (sub { $finish->(undef, { Status => 599, Reason => $_[2], URL => $uri }) }); |
575
|
|
|
|
|
|
|
$_[0]->unshift_read(chunk => $len, sub { |
576
|
|
|
|
|
|
|
$finish->($_[1],$hdr); |
577
|
|
|
|
|
|
|
}); |
578
|
|
|
|
|
|
|
} else { |
579
|
|
|
|
|
|
|
warn "ready for body until eof"; |
580
|
|
|
|
|
|
|
$_[0]->on_error (sub { |
581
|
|
|
|
|
|
|
$! == Errno::EPIPE || !$! |
582
|
|
|
|
|
|
|
? $finish->(delete $_[0]{rbuf}, $hdr) |
583
|
|
|
|
|
|
|
: $finish->(undef, { Status => 599, Reason => $_[2], URL => $uri }); |
584
|
|
|
|
|
|
|
}); |
585
|
|
|
|
|
|
|
$_[0]->on_read (sub { }); |
586
|
|
|
|
|
|
|
} |
587
|
|
|
|
|
|
|
} |
588
|
|
|
|
|
|
|
|
589
|
|
|
|
|
|
|
} |
590
|
|
|
|
|
|
|
}); |
591
|
|
|
|
|
|
|
}); |
592
|
|
|
|
|
|
|
|
593
|
|
|
|
|
|
|
} |
594
|
|
|
|
|
|
|
|
595
|
|
|
|
|
|
|
sub http_request; |
596
|
|
|
|
|
|
|
sub req1 { |
597
|
|
|
|
|
|
|
my $self = shift; |
598
|
|
|
|
|
|
|
my %args = @_; |
599
|
|
|
|
|
|
|
$self->{cv}->begin; |
600
|
|
|
|
|
|
|
http_request + |
601
|
|
|
|
|
|
|
( $args{form} ? 'POST' : 'GET') => "$args{uri}", |
602
|
|
|
|
|
|
|
$args{form} ? ( |
603
|
|
|
|
|
|
|
body => _postdata(@{ $args{form} }), |
604
|
|
|
|
|
|
|
) : (), |
605
|
|
|
|
|
|
|
headers => { |
606
|
|
|
|
|
|
|
%{ $self->{headers} }, |
607
|
|
|
|
|
|
|
$args{form} ? ( 'content-type' => 'application/x-www-form-urlencoded' ) : (), |
608
|
|
|
|
|
|
|
%{ $args{headers} || {} } |
609
|
|
|
|
|
|
|
}, |
610
|
|
|
|
|
|
|
cookie_jar => $self->{cookie}, |
611
|
|
|
|
|
|
|
timeout => 10, |
612
|
|
|
|
|
|
|
$self->next_proxy(), |
613
|
|
|
|
|
|
|
cb => sub { |
614
|
|
|
|
|
|
|
push @{$self->{requests}}, join(' ',$_[1]{Status}, ':', ($args{form} ? 'POST' : 'GET'), $args{uri} ); |
615
|
|
|
|
|
|
|
#$self->{requests}++; |
616
|
|
|
|
|
|
|
if( my $cookies = $_[1]{'set-cookie'} ) { |
617
|
|
|
|
|
|
|
local $self->{uri} = URI->new($_[1]{URL}); |
618
|
|
|
|
|
|
|
$self->_parse_cookies($cookies); |
619
|
|
|
|
|
|
|
} |
620
|
|
|
|
|
|
|
if (exists $args{raw}) { |
621
|
|
|
|
|
|
|
$args{raw}(@_); |
622
|
|
|
|
|
|
|
} else { |
623
|
|
|
|
|
|
|
my ($body,$hdr) = @_; |
624
|
|
|
|
|
|
|
if ($hdr->{Status} =~ /^(200|302)$/) { |
625
|
|
|
|
|
|
|
$self->{uri} = URI->new($hdr->{URL}); |
626
|
|
|
|
|
|
|
$self->{page} = $self->getpage; |
627
|
|
|
|
|
|
|
if (exists $hdr->{'content-encoding'}) { |
628
|
|
|
|
|
|
|
if (lc($hdr->{'content-encoding'}) eq 'gzip') { |
629
|
|
|
|
|
|
|
eval{ |
630
|
|
|
|
|
|
|
my $def = Compress::Zlib::memGunzip($body); |
631
|
|
|
|
|
|
|
if (defined $def) { |
632
|
|
|
|
|
|
|
$body = $def; |
633
|
|
|
|
|
|
|
#warn "Page deflated from $hdr->{'content-encoding'}" if $self->{debug}; |
634
|
|
|
|
|
|
|
1; |
635
|
|
|
|
|
|
|
} else { 0 } |
636
|
|
|
|
|
|
|
} or do { |
637
|
|
|
|
|
|
|
warn "Deflate failed: $@"; |
638
|
|
|
|
|
|
|
} |
639
|
|
|
|
|
|
|
} else { |
640
|
|
|
|
|
|
|
warn "Unsupported content-encoding method: $hdr->{'content-encoding'}"; |
641
|
|
|
|
|
|
|
} |
642
|
|
|
|
|
|
|
} |
643
|
|
|
|
|
|
|
warn "Req $self->{uri} / $self->{page}\n"._postdata(@{ $args{form} })."\n ok" if $self->{debug}; |
644
|
|
|
|
|
|
|
$args{cb}( { body => $body, head => $hdr } ); |
645
|
|
|
|
|
|
|
} else { |
646
|
|
|
|
|
|
|
$args{cb}(undef, "req($hdr->{URL}) failed: $hdr->{Status}: $hdr->{Reason}"); |
647
|
|
|
|
|
|
|
} |
648
|
|
|
|
|
|
|
} |
649
|
|
|
|
|
|
|
$self->{cv}->end; |
650
|
|
|
|
|
|
|
}, |
651
|
|
|
|
|
|
|
; |
652
|
|
|
|
|
|
|
return; |
653
|
|
|
|
|
|
|
} |
654
|
|
|
|
|
|
|
|
655
|
|
|
|
|
|
|
=head1 AUTHOR |
656
|
|
|
|
|
|
|
|
657
|
|
|
|
|
|
|
Mons Anderson, C<< >> |
658
|
|
|
|
|
|
|
|
659
|
|
|
|
|
|
|
=head1 ACKNOWLEDGEMENTS |
660
|
|
|
|
|
|
|
|
661
|
|
|
|
|
|
|
Many parts of this module was derived from L |
662
|
|
|
|
|
|
|
|
663
|
|
|
|
|
|
|
=head1 LICENSE |
664
|
|
|
|
|
|
|
|
665
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify it |
666
|
|
|
|
|
|
|
under the terms of either: the GNU General Public License as published |
667
|
|
|
|
|
|
|
by the Free Software Foundation; or the Artistic License. |
668
|
|
|
|
|
|
|
|
669
|
|
|
|
|
|
|
=cut |
670
|
|
|
|
|
|
|
|
671
|
|
|
|
|
|
|
1; # End of AnyEvent::UA |