📄 cookies.pm
字号:
package HTTP::Cookies;
# Based on draft-ietf-http-state-man-mec-08.txt and
# http://www.netscape.com/newsref/std/cookie_spec.html
use strict;
use HTTP::Date qw(str2time time2str);
use HTTP::Headers::Util qw(split_header_words join_header_words);
use LWP::Debug ();
use vars qw($VERSION);
$VERSION = sprintf("%d.%02d", q$Revision: 1.5 $ =~ /(\d+)\.(\d+)/);
=head1 NAME
HTTP::Cookies - Cookie storage and management
=head1 SYNOPSIS
use HTTP::Cookies;
$cookie_jar = HTTP::Cookies->new;
$cookie_jar->add_cookie_header($request);
$cookie_jar->extract_cookies($response);
=head1 DESCRIPTION
Cookies are a general mechanism which server side connections can use
to both store and retrieve information on the client side of the
connection. For more information about cookies referrer to
<URL:http://www.netscape.com/newsref/std/cookie_spec.html> and
<URL:http://www.cookiecentral.com/>. This module also implements the
new style cookies as described in I<draft-ietf-http-state-man-mec-08.txt>.
The two variants of cookies is supposed to be able to coexist happily.
Instances of the class I<HTTP::Cookies> are able to store a collection
of Set-Cookie2: and Set-Cookie:-headers and is able to use this
information to initialize Cookie-headers in I<HTTP::Request> objects.
The state of the I<HTTP::Cookies> can be saved and restored from
files.
=head1 METHODS
The following methods are provided:
=over 4
=cut
# A HTTP::Cookies object is a hash. The main attribute is the
# COOKIES 3 level hash: $self->{COOKIES}{$domain}{$path}{$key}.
=item $cookie_jar = HTTP::Cookies->new;
The constructor. Takes hash style parameters. The following
parameters are recognized:
file: name of the file to restore and save cookies to
autosave: should we save during destruction (bool)
ignore_discard: save even cookies that are requested to be discarded (bool)
Future parameters might include (not yet implemented):
max_cookies 300
max_cookies_per_domain 20
max_cookie_size 4096
no_cookies list of domain names that we never return cookies to
=cut
sub new
{
my $class = shift;
my $self = bless {
COOKIES => {},
}, $class;
my %cnf = @_;
for (keys %cnf) {
$self->{lc($_)} = $cnf{$_};
}
$self->load;
$self;
}
=item $cookie_jar->add_cookie_header($request);
The add_cookie_header() method will set the appropriate Cookie:-header
for the I<HTTP::Request> object given as argument. The $request must
have a valid url() attribute before this method is called.
=cut
sub add_cookie_header
{
my $self = shift;
my $request = shift || return;
my $url = $request->url;
my $domain = $url->host;
$domain = "$domain.local" unless $domain =~ /\./;
my $secure_request = ($url->scheme eq "https");
my $req_path = $url->epath;
my $req_port = $url->port;
my $now = time();
$self->_normalize_path($req_path) if $req_path =~ /%/;
my @cval; # cookie values for the "Cookie" header
my $set_ver;
while (($domain =~ tr/././) >= 2 || # must be at least 2 dots
$domain =~ /\.local$/)
{
LWP::Debug::debug("Checking $domain for cookies");
my $cookies = $self->{COOKIES}{$domain};
next unless $cookies;
# Want to add cookies corresponding to the most specific paths
# first (i.e. longest path first)
my $path;
for $path (sort {length($b) <=> length($a) } keys %$cookies) {
LWP::Debug::debug("- checking cookie path=$path");
if (index($req_path, $path) != 0) {
LWP::Debug::debug(" path $path:$req_path does not fit");
next;
}
my($key,$array);
while (($key,$array) = each %{$cookies->{$path}}) {
my($version,$val,$port,$path_spec,$secure,$expires) = @$array;
LWP::Debug::debug(" - checking cookie $key=$val");
if ($secure && !$secure_request) {
LWP::Debug::debug(" not a secure requests");
next;
}
if ($expires && $expires < $now) {
LWP::Debug::debug(" expired");
next;
}
if ($port) {
my $found;
if ($port =~ s/^_//) {
# The correponding Set-Cookie attribute was empty
$found++ if $port eq $req_port;
$port = "";
} else {
my $p;
for $p (split(/,/, $port)) {
$found++, last if $p eq $req_port;
}
}
unless ($found) {
LWP::Debug::debug(" port $port:$req_port does not fit");
next;
}
}
LWP::Debug::debug(" it's a match");
# set version number of cookie header.
# XXX: What should it be if multiple matching
# Set-Cookie headers have different versions themselves
if (!$set_ver++) {
if ($version >= 1) {
push(@cval, "\$Version=$version");
} else {
$request->header(Cookie2 => "\$Version=1");
}
}
# do we need to quote the value
if ($val =~ /\W/ && $version) {
$val =~ s/([\\\"])/\\$1/g;
$val = qq("$val");
}
# and finally remember this cookie
push(@cval, "$key=$val");
if ($version >= 1) {
push(@cval, qq(\$Path="$path")) if $path_spec;
push(@cval, qq(\$Domain="$domain")) if $domain =~ /^\./;
if (defined $port) {
my $p = '$Port';
$p .= qq(="$port") if length $port;
push(@cval, $p);
}
}
}
}
} continue {
# Try with a more general domain: www.sol.no ==> .sol.no
$domain =~ s/^\.?[^.]*//;
}
$request->header(Cookie => join("; ", @cval)) if @cval;
$request;
}
=item $cookie_jar->extract_cookies($response);
The extract_cookies() method will look for Set-Cookie: and
Set-Cookie2:-headers in the I<HTTP::Response> object passed as
argument. If some of these headers are found they are used to update
the state of the $cookie_jar.
=cut
sub extract_cookies
{
my $self = shift;
my $response = shift || return;
my @set = split_header_words($response->_header("Set-Cookie2"));
my $netscape_cookies;
unless (@set) {
@set = $response->_header("Set-Cookie");
return $response unless @set;
$netscape_cookies++;
}
my $url = $response->request->url;
my $req_host = $url->host;
$req_host = "$req_host.local" unless $req_host =~ /\./;
my $req_port = $url->port;
my $req_path = $url->epath;
$self->_normalize_path($req_path) if $req_path =~ /%/;
if ($netscape_cookies) {
# The old Netscape cookie format for Set-Cookie
# http://www.netscape.com/newsref/std/cookie_spec.html
# can for instance contain an unquoted "," in the expires
# field, so we have to use this ad-hoc parser.
my $now = time();
my @old = @set;
@set = ();
my $set;
for $set (@old) {
my @cur;
my $param;
my $expires;
for $param (split(/\s*;\s*/, $set)) {
my($k,$v) = split(/\s*=\s*/, $param, 2);
#print "$k => $v\n";
my $lc = lc($k);
if ($lc eq "expires") {
push(@cur, "Max-Age" => str2time($v) - $now);
$expires++;
} else {
push(@cur, $k => $v);
}
}
push(@cur, "Port" => $req_port);
push(@cur, "Discard" => undef) unless $expires;
push(@cur, "Version" => 0);
push(@set, \@cur);
}
}
SET_COOKIE:
for my $set (@set) {
next unless @$set >= 2;
my $key = shift @$set;
my $val = shift @$set;
LWP::Debug::debug("Set cookie $key => $val");
my %hash;
while (@$set) {
my $k = shift @$set;
my $v = shift @$set;
my $lc = lc($k);
# don't loose case distinction for unknown fields
$k = $lc if $lc =~ /^(?:discard|domain|max-age|
path|port|secure|version)$/x;
if ($k eq "discard" || $k eq "secure") {
$v = 1 unless defined $v;
}
next if exists $hash{$k}; # only first value is signigicant
$hash{$k} = $v;
};
my %orig_hash = %hash;
my $version = delete $hash{version};
$version = 1 unless defined($version);
my $discard = delete $hash{discard};
my $secure = delete $hash{secure};
my $maxage = delete $hash{'max-age'};
# Check domain
my $domain = delete $hash{domain};
if (defined($domain) && $domain ne $req_host) {
if ($domain !~ /\./ && $domain ne "local") {
LWP::Debug::debug("Domain $domain contains no dot");
next SET_COOKIE;
}
$domain = ".$domain" unless $domain =~ /^\./;
if ($domain =~ /\.\d+$/) {
LWP::Debug::debug("IP-address $domain illeagal as domain");
next SET_COOKIE;
}
my $len = length($domain);
unless (substr($req_host, -$len) eq $domain) {
LWP::Debug::debug("Domain $domain does not match host $req_host");
next SET_COOKIE;
}
my $hostpre = substr($req_host, 0, length($req_host) - $len);
if ($hostpre =~ /\./ && !$netscape_cookies) {
LWP::Debug::debug("Host prefix contain a dot: $hostpre => $domain");
next SET_COOKIE;
}
} else {
$domain = $req_host;
}
my $path = delete $hash{path};
my $path_spec;
if (defined $path) {
$path_spec++;
$self->_normalize_path($path) if $path =~ /%/;
if (!$netscape_cookies &&
substr($req_path, 0, length($path)) ne $path) {
LWP::Debug::debug("Path $path is not a prefix of $req_path");
next SET_COOKIE;
}
} else {
$path = $req_path;
$path =~ s,/[^/]*$,,;
$path = "/" unless length($path);
}
my $port;
if (exists $hash{port}) {
$port = delete $hash{port};
if (defined $port) {
$port =~ s/\s+//g;
my $found;
for my $p (split(/,/, $port)) {
unless ($p =~ /^\d+$/) {
LWP::Debug::debug("Bad port $port (not numeric)");
next SET_COOKIE;
}
$found++ if $p eq $req_port;
}
unless ($found) {
LWP::Debug::debug("Request port ($req_port) not found in $port");
next SET_COOKIE;
}
} else {
$port = "_$req_port";
}
}
$self->set_cookie($version,$key,$val,$path,$domain,$port,$path_spec,$secure,$maxage,$discard, \%hash)
if $self->set_cookie_ok(\%orig_hash);
}
$response;
}
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -