# $Id: Comms.pm,v 0.21 2002/04/06 17:45:42 pcollins Exp $ package HTTP::DAV::Comms; use HTTP::DAV::Utils; use HTTP::DAV::Response; use LWP; use URI; $VERSION = sprintf("%d.%02d", q$Revision: 0.21 $ =~ /(\d+)\.(\d+)/); use strict; use vars qw($VERSION $DEBUG); { no warnings; BEGIN { *LWP::UserAgent::redirect_ok = sub { 0 } } } #### # Construct a new object and initialize it sub new { my $class = shift; my $self = bless {}, ref($class) || $class; #print Data::Dumper->Dump( [$self] , [ '$self' ] ); $self->_init(@_); return $self; } # Requires a reusable HTTP Agent. # and some default headers, like, the user agent sub _init { my ($self,@p) = @_; my ($headers,$useragent) = HTTP::DAV::Utils::rearrange( ['HEADERS','USERAGENT'], @p); # This is cached in this object here so that each http request # doesn't have to invoke a new useragent. $self->init_user_agent($useragent); $self->set_headers($headers); } sub init_user_agent { my($self,$useragent) = @_; if ( defined $useragent ) { $self->{_user_agent} = $useragent; } else { $self->{_user_agent} = HTTP::DAV::UserAgent->new; $self->set_agent("DAV.pm/v$HTTP::DAV::VERSION"); } } #### # GET/SET # Sets a User-Agent as specified by user or as the default sub set_agent { my ($self, $agent) = @_; $self->{_user_agent}->agent($agent); } sub set_header { my ($self,$var,$val) = @_; $self->set_headers() unless defined $self->{_headers}; $self->{_headers}->header($var,$val); } sub get_user_agent { $_[0]->{_user_agent}; } sub get_headers { $_[0]->{_headers}; } sub set_headers { my ($self,$headers) = @_; if ( defined $headers && ref($headers) eq "HTTP::Headers" ) { $headers = HTTP::DAV::Headers->clone( $headers ); } else { $headers = HTTP::DAV::Headers->new; } $self->{_headers} = $headers; } sub _set_last_request { $_[0]->{_last_request} = $_[1]; } sub _set_last_response { $_[0]->{_last_response} = $_[1]; } # Save the Server: header line into this object instance # We will want to use it later to workaround server bugs. # For instance mod_dav has a bug in the Destination: header # whereby it incorrectly throws "Bad Gateway" errors. # The only way we can munge around this is if the copy() routine # has some idea of the server it is talking to. # So this routine stores the "Server: Apache..." line into a host:port hash (i.e. localhost:443). # so $comms->_set_server_type( "host.org:443", "Apache/1.3.22 (Unix) DAV/1.0.2 ") # yields # %_server_type = { # "host.org:443" => "Apache/1.3.22 (Unix) DAV/1.0.2 SSL" # "host.org:80" => "Apache/1.3.22 (Unix) DAV/1.0.2 " # }; # Note that this is an instance hash NOT a class hash. # So each comms object will be learning independently. sub _set_server_type { $_[0]->{_server_type}{$_[1]} = $_[2]; } # $server = $comms->get_server_type( "host.org:443" ) sub get_server_type { $_[0]->{_server_type}{$_[1]} } # Returns an HTTP::Request object sub get_last_request { $_[0]->{_last_request}; } # Returns an HTTP::DAV::Response object sub get_last_response { $_[0]->{_last_response}; } #### # Ensure there is a Host: header based on the URL # sub do_http_request { my ($self, @p ) = @_; my ($method,$url,$newheaders,$content,$save_to,$callback_func,$chunk) = HTTP::DAV::Utils::rearrange( ['METHOD', ['URL','URI'], 'HEADERS', 'CONTENT', 'SAVE_TO', 'CALLBACK','CHUNK'],@p ); # Method management if (! defined $method || $method eq "" || $method !~ /^\w+$/ ) { die "Incorrect HTTP Method specified in do_http_request: \"$method\""; } $method = uc($method); # URL management my $url_obj; $url_obj = (ref($url) =~ /URI/)? $url : URI->new($url); die "Comms: Bad HTTP Url: \"$url_obj\"\n" if ($url_obj->scheme !~ /^http/ ); # If you see user:pass detail embedded in the URL. Then get it out. if ( $url_obj->userinfo ) { $self->{_user_agent}->credentials($url,undef, split(':',$url_obj->userinfo) ); } # Header management if ( $newheaders && ref($newheaders) !~ /Headers/ ){ die "Bad headers object: " . Data::Dumper->Dump( [$newheaders] , [ '$newheaders' ] ); } my $headers = HTTP::DAV::Headers->new(); $headers->add_headers( $self->{_headers} ); $headers->add_headers( $newheaders ); #$headers->header("Host", $url_obj->host); $headers->header("Host", $url_obj->host_port); my $length = ($content) ? length($content) : 0; $headers->header("Content-Length", $length); #print "HTTP HEADERS\n" . $self->get_headers->as_string . "\n\n"; # It would be good if, at this stage, we could prefill the # username and password values to prevent the client having # to submit 2 requests, submit->401, submit->200 # This is the same kind of username, password remembering # functionality that a browser performs. #@userpass = $self->{_user_agent}->get_basic_credentials(undef, $url); # Add a Content-type of text/xml if the body has header("Content-Type", "text/xml"); } #### # Do the HTTP call my $req = HTTP::Request->new( $method, $url_obj, $headers->to_http_headers, $content ); # It really bugs me, but libwww-perl doesn't honour this call. # I'll leave it here anyway for future compatibility. $req->protocol("HTTP/1.1"); my $resp; # If a callback is set and it is a ref to a function # then pass it through to LWP::UserAgent::request. # See man page of LWP for more details of callback. # callback is primarily used by DAV::get(); # if ( defined $save_to && $save_to ne "" ) { $resp = $self->{_user_agent}->request($req,$save_to); } elsif ( ref($callback_func) =~ /CODE/ ) { $resp = $self->{_user_agent}->request($req,$callback_func,$chunk); } else { $resp = $self->{_user_agent}->request($req); } # Redirect loop {{{ my $code = $resp->code; if ($code == &HTTP::Status::RC_MOVED_PERMANENTLY or $code == &HTTP::Status::RC_MOVED_TEMPORARILY) { # And then we update the URL based on the Location:-header. my($referral_uri) = $resp->header('Location'); { # Some servers erroneously return a relative URL for redirects, # so make it absolute if it not already is. local $URI::ABS_ALLOW_RELATIVE_SCHEME = 1; my $base = $resp->base; $referral_uri = $HTTP::URI_CLASS->new($referral_uri, $base) ->abs($base); } # Check for loop in the redirects my $count = 0; my $r = $resp; my $bad_loop=0; while ($r) { if (++$count > 13 || $r->request->url->as_string eq $referral_uri->as_string) { $resp->header("Client-Warning" => "Redirect loop detected"); #if ( $HTTP::DAV::DEBUG ) { # print "*** CLIENT AND SERVER STUCK IN REDIRECT LOOP OR MOVED PERMENANTLY. $count. BREAKING ***\n"; # print "*** " . $r->request->url->as_string . "***\n"; # print "*** " . $referral_uri->as_string . "***\n"; #} $bad_loop=1; last; } $r = $r->previous; } $resp = $self->do_http_request ( -method => $method, -url => $referral_uri, -headers => $newheaders, -content => $content, -saveto => $save_to, -callback => $callback_func, -chunk => $chunk, ) unless $bad_loop; } # }}} if ( $HTTP::DAV::DEBUG > 1 ) { no warnings; #open(DEBUG, ">&STDOUT") || die ("Can't open STDERR");; open(DEBUG, ">>/tmp/perldav_debug.txt"); print DEBUG "\n" . "-"x70 . "\n"; print DEBUG localtime() . "\n"; print DEBUG "$method REQUEST>>\n" . $req->as_string(); if ( $resp->headers->header('Content-Type') =~ /xml/ ) { my $body = $resp->as_string(); #$body =~ s/>\n*/>\n/g; print DEBUG "$method XML RESPONSE>>$body\n"; #} elsif ( $resp->headers->header('Content-Type') =~ /text.html/ ) { #require HTML::TreeBuilder; #require HTML::FormatText; #my $tree = HTML::TreeBuilder->new->parse($resp->content()); #my $formatter = HTML::FormatText->new(leftmargin => 0); #print DEBUG "$method RESPONSE (HTML)>>\n" . $resp->headers->as_string(); #print DEBUG $formatter->format($tree); } else { print DEBUG "$method RESPONSE>>\n" . $resp->as_string(); } close DEBUG; } #### # Copy the HTTP:Response into a HTTP::DAV::Response. It specifically # knows details about DAV Status Codes and their associated # messages. my $dav_resp = HTTP::DAV::Response->clone_http_resp($resp); $dav_resp->set_message( $resp->code ); #### # Save the req and resp objects as the "last used" $self->_set_last_request ($req); $self->_set_last_response($dav_resp); $self->_set_server_type($url_obj->host_port, $dav_resp->headers->header("Server")); return $dav_resp; } sub credentials { my($self, @p) = @_; my ($user,$pass,$url,$realm) = HTTP::DAV::Utils::rearrange( ['USER', 'PASS','URL','REALM'], @p); $self->{_user_agent}->credentials($url,$realm,$user,$pass); } ########################################################################### # We make our own specialization of LWP::UserAgent # called HTTP::DAV::UserAgent. # The variations allow us to have various levels of protection. # Where the user hasn't specified what Realm to use we pass the # userpass combo to all realms of that host # Also this UserAgent remembers a user on the next request. # The standard UserAgent doesn't. { package HTTP::DAV::UserAgent; use strict; use vars qw(@ISA); @ISA = qw(LWP::UserAgent); #require LWP::UserAgent; sub new { my $self = LWP::UserAgent::new(@_); $self->agent("lwp-request/$HTTP::DAV::VERSION"); $self; } sub credentials { my($self, $netloc, $realm,$user,$pass) = @_; $realm = "default" unless $realm; if ($netloc) { $netloc = "http://$netloc" unless $netloc=~/^http/; my $uri = URI->new($netloc); $netloc = $uri->host_port; } else { $netloc = "default"; } { no warnings; print "Setting auth details for $netloc, $realm to $user,$pass\n" if $HTTP::DAV::DEBUG > 2; } @{ $self->{'basic_authentication'}{$netloc}{$realm}}= ($user, $pass); } sub get_basic_credentials { my($self, $realm, $uri) = @_; $uri = HTTP::DAV::Utils::make_uri($uri); my $netloc = $uri->host_port; my $userpass; { no warnings; # SHUTUP with your silly warnings. $userpass= $self->{'basic_authentication'}{$netloc}{$realm} || $self->{'basic_authentication'}{default}{$realm} || $self->{'basic_authentication'}{$netloc}{default} || []; print "Using user/pass combo: @$userpass. For $realm, $uri\n" if $HTTP::DAV::DEBUG > 2; } return @$userpass; } } ########################################################################### # We make our own special version of HTTP::Headers # called HTTP::DAV::Headers. This is because we want to add # a new method called add_headers { package HTTP::DAV::Headers; use strict; use vars qw(@ISA); @ISA = qw( HTTP::Headers ); require HTTP::Headers; # $dav_headers = HTTP::DAV::Headers->clone( $http_headers ); sub to_http_headers { my ($self) = @_; my %clone = %{$self}; bless { %clone }, "HTTP::Headers"; } sub clone { my ($class,$headers) = @_; my %clone = %{$headers}; bless { %clone }, ref($class) || $class; } sub add_headers { my ($self,$headers) = @_; return unless (defined $headers && ref($headers) =~ /Headers/ ); #print "About to add headers!!\n"; #print Data::Dumper->Dump( [$headers] , [ '$headers' ] ); foreach my $key ( sort keys %$headers ) { $self->header( $key, $headers->{$key} ); } } } 1;