package Net::SSL;
use strict;
use vars qw(@ISA $VERSION $NEW_ARGS);
use MIME::Base64;
use Socket;
use Carp;
require IO::Socket;
@ISA=qw(IO::Socket::INET);
my %REAL; # private to this package only
my $DEFAULT_VERSION = '23';
my $CRLF = "\015\012";
require Crypt::SSLeay;
$VERSION = '2.77';
sub _default_context
{
require Crypt::SSLeay::MainContext;
Crypt::SSLeay::MainContext::main_ctx(@_);
}
sub new {
my($class, %arg) = @_;
local $NEW_ARGS = \%arg;
$class->SUPER::new(%arg);
}
sub DESTROY {
my $self = shift;
delete $REAL{$self};
local $@;
eval { $self->SUPER::DESTROY; };
}
sub configure
{
my($self, $arg) = @_;
my $ssl_version = delete $arg->{SSL_Version} ||
$ENV{HTTPS_VERSION} || $DEFAULT_VERSION;
my $ssl_debug = delete $arg->{SSL_Debug} || $ENV{HTTPS_DEBUG} || 0;
my $ctx = delete $arg->{SSL_Context} || _default_context($ssl_version);
*$self->{'ssl_ctx'} = $ctx;
*$self->{'ssl_version'} = $ssl_version;
*$self->{'ssl_debug'} = $ssl_debug;
*$self->{'ssl_arg'} = $arg;
*$self->{'ssl_peer_addr'} = $arg->{PeerAddr};
*$self->{'ssl_peer_port'} = $arg->{PeerPort};
*$self->{'ssl_new_arg'} = $NEW_ARGS;
*$self->{'ssl_peer_verify'} = 0;
## Crypt::SSLeay must also aware the SSL Proxy before calling
## $socket->configure($args). Because the $sock->configure() will
## die when failed to resolve the destination server IP address,
## whatever the SSL proxy is used or not!
## - dqbai, 2003-05-10
if (my $proxy = $self->proxy) {
my ($host, $port) = split(':',$proxy);
$port || die("no port given for proxy server $proxy");
$arg->{PeerAddr} = $host;
$arg->{PeerPort} = $port;
}
$self->SUPER::configure($arg);
}
# override to make sure there is really a timeout
sub timeout {
shift->SUPER::timeout || 60;
}
sub connect {
my $self = shift;
# configure certs on connect() time, so we can throw an undef
# and have LWP understand the error
eval { $self->configure_certs(); };
if($@) {
$@ = "configure certs failed: $@, $!";
$self->die_with_error($@);
}
# finished, update set_verify status
if(my $rv = *$self->{'ssl_ctx'}->set_verify()) {
*$self->{'ssl_peer_verify'} = $rv;
}
if ($self->proxy) {
# don't die() in connect, just return undef and set $@
my $proxy_connect = eval { $self->proxy_connect_helper(@_); };
if(! $proxy_connect || $@) {
$@ = "proxy connect failed: $@; $!";
die $@;
}
} else {
*$self->{io_socket_peername}=@_ == 1 ? $_[0] : IO::Socket::sockaddr_in(@_);
if(!$self->SUPER::connect(@_)) {
# better to die than return here
$@ = "Connect failed: $@; $!";
die $@;
}
}
# print "ssl_version ".*$self->{ssl_version}."\n";
my $debug = *$self->{'ssl_debug'} || 0;
my $ssl = Crypt::SSLeay::Conn->new(*$self->{'ssl_ctx'}, $debug, $self);
my $arg = *$self->{ssl_arg};
my $new_arg = *$self->{ssl_new_arg};
$arg->{SSL_Debug} = $debug;
eval {
local $SIG{ALRM} = sub { $self->die_with_error("SSL connect timeout") };
# timeout / 2 because we have 3 possible connects here
alarm_ok() && alarm($self->timeout / 2);
my $rv;
{
local $SIG{PIPE} = \¨
$rv = eval { $ssl->connect; };
}
if ($rv <= 0) {
alarm_ok() && alarm(0);
$ssl = undef;
my %args = (%$new_arg, %$arg);
if(*$self->{ssl_version} == 23) {
$args{SSL_Version} = 3;
# the new connect might itself be overridden with a REAL SSL
my $new_ssl = Net::SSL->new(%args);
$REAL{$self} = $REAL{$new_ssl} || $new_ssl;
return $REAL{$self};
} elsif(*$self->{ssl_version} == 3) {
# $self->die_with_error("SSL negotiation failed");
$args{SSL_Version} = 2;
my $new_ssl = Net::SSL->new(%args);
$REAL{$self} = $new_ssl;
return $new_ssl;
} else {
# don't die, but do set $@, and return undef
eval { $self->die_with_error("SSL negotiation failed") };
$@ = "$@; $!";
die $@;
}
}
alarm_ok() && alarm(0);
};
# odd error in eval {} block, maybe alarm outside the evals
if($@) {
$! = "$@; $!";
die $@;
}
# successful SSL connection gets stored
*$self->{'ssl_ssl'} = $ssl;
$self;
}
sub accept
{
die "NYI";
}
# Delegate these calls to the Crypt::SSLeay::Conn object
sub get_peer_certificate {
my $self = shift;
$self = $REAL{$self} || $self;
*$self->{'ssl_ssl'}->get_peer_certificate(@_);
}
sub get_peer_verify {
my $self = shift;
$self = $REAL{$self} || $self;
*$self->{'ssl_peer_verify'};
}
sub get_shared_ciphers {
my $self = shift;
$self = $REAL{$self} || $self;
*$self->{'ssl_ssl'}->get_shared_ciphers(@_);
}
sub get_cipher {
my $self = shift;
$self = $REAL{$self} || $self;
*$self->{'ssl_ssl'}->get_cipher(@_);
}
#sub get_peer_certificate { *{shift()}->{'ssl_ssl'}->get_peer_certificate(@_) }
#sub get_shared_ciphers { *{shift()}->{'ssl_ssl'}->get_shared_ciphers(@_) }
#sub get_cipher { *{shift()}->{'ssl_ssl'}->get_cipher(@_) }
sub ssl_context
{
my $self = shift;
$self = $REAL{$self} || $self;
*$self->{'ssl_ctx'};
}
sub die_with_error
{
my $self=shift;
my $reason=shift;
my $errs='';
while(my $err=Crypt::SSLeay::Err::get_error_string()) {
$errs.=" | " if $errs ne '';
$errs.=$err;
}
die "$reason: $errs";
}
sub alarm_ok() {
$^O ne 'MSWin32';
}
sub read
{
my $self = shift;
$self = $REAL{$self} || $self;
local $SIG{__DIE__} = \&Carp::confess;
local $SIG{ALRM} = sub { $self->die_with_error("SSL read timeout") };
alarm_ok() && alarm($self->timeout);
my $n=*$self->{'ssl_ssl'}->read(@_);
$self->die_with_error("read failed") if !defined $n;
alarm_ok() && alarm(0);
$n;
}
sub write
{
my $self = shift;
$self = $REAL{$self} || $self;
my $n=*$self->{'ssl_ssl'}->write(@_);
$self->die_with_error("write failed") if !defined $n;
$n;
}
*sysread = \&read;
*syswrite = \&write;
sub print
{
my $self = shift;
$self = $REAL{$self} || $self;
# should we care about $, and $\??
# I think it is too expensive...
$self->write(join("", @_));
}
sub printf
{
my $self = shift;
$self = $REAL{$self} || $self;
my $fmt = shift;
$self->write(sprintf($fmt, @_));
}
sub getchunk
{
my $self = shift;
$self = $REAL{$self} || $self;
my $buf = ''; # warnings
my $n = $self->read($buf, 32*1024);
return unless defined $n;
$buf;
}
# In order to implement these we will need to add a buffer in $self.
# Is it worth it?
sub getc { shift->_unimpl("getc"); }
sub ungetc { shift->_unimpl("ungetc"); }
#sub getline { shift->_unimpl("getline"); }
# This is really inefficient, but we only use it for reading the proxy response
# so that does not really matter.
sub getline {
my $self = shift;
$self = $REAL{$self} || $self;
my $val="";
my $buf;
do {
$self->SUPER::recv($buf, 1);
$val = $val . $buf;
} until ($buf eq "\n");
$val;
}
sub getlines { shift->_unimpl("getlines"); }
# XXX: no way to disable <$sock>?? (tied handle perhaps?)
sub _unimpl
{
my($self, $meth) = @_;
die "$meth not implemented for Net::SSL sockets";
}
sub get_lwp_object {
my $self = shift;
my $lwp_object;
my $i = 0;
while(1) {
package DB;
my @stack = caller($i++);
last unless @stack;
my @stack_args = @DB::args;
my $stack_object = $stack_args[0] || next;
ref($stack_object) || next;
if($stack_object->isa('LWP::UserAgent')) {
$lwp_object = $stack_object;
last;
}
}
$lwp_object;
}
sub proxy_connect_helper {
my $self = shift;
my $proxy = $self->proxy;
my ($host, $port) = split(':',$proxy);
my $conn_ok = 0;
my $need_auth = 0;
my $auth_basic = 0;
my $realm = "";
my $length = 0;
my $line = "<noline>";
my $lwp_object = $self->get_lwp_object;
my $iaddr = gethostbyname($host);
$iaddr || die("can't resolve proxy server name: $host, $!");
$port || die("no port given for proxy server $proxy");
$self->SUPER::connect($port, $iaddr)
|| die("proxy connect to $host:$port failed: $!");
my($peer_port, $peer_addr) = (*$self->{ssl_peer_port}, *$self->{ssl_peer_addr});
$peer_port || die("no peer port given");
$peer_addr || die("no peer addr given");
my $connect_string;
if ($ENV{"HTTPS_PROXY_USERNAME"} || $ENV{"HTTPS_PROXY_PASSWORD"}) {
my $user = $ENV{"HTTPS_PROXY_USERNAME"};
my $pass = $ENV{"HTTPS_PROXY_PASSWORD"};
my $credentials = encode_base64("$user:$pass", "");
$connect_string = join($CRLF,
"CONNECT $peer_addr:$peer_port HTTP/1.0",
"Proxy-authorization: Basic $credentials"
);
}else{
$connect_string = "CONNECT $peer_addr:$peer_port HTTP/1.0";
}
$connect_string .= $CRLF;
if($lwp_object && $lwp_object->agent) {
$connect_string .= "User-Agent: ".$lwp_object->agent.$CRLF;
}
$connect_string .= $CRLF;
$self->SUPER::send($connect_string);
my $header;
my $n = $self->SUPER::sysread($header, 8192);
if($header =~ /HTTP\/\d+\.\d+\s+200\s+/is) {
$conn_ok = 1;
}
unless ($conn_ok) {
die("PROXY ERROR HEADER, could be non-SSL URL:\n$header");
}
$conn_ok;
}
# code adapted from LWP::UserAgent, with $ua->env_proxy API
sub proxy {
# don't iterate through %ENV for speed
my $proxy_server;
for ('HTTPS_PROXY', 'https_proxy') {
$proxy_server = $ENV{$_};
last if $proxy_server;
}
return unless $proxy_server;
$proxy_server =~ s|^https?://||i;
$proxy_server;
}
sub configure_certs {
my $self = shift;
my $ctx = *$self->{ssl_ctx};
my $count = 0;
for ('HTTPS_PKCS12_FILE', 'HTTPS_CERT_FILE', 'HTTPS_KEY_FILE') {
my $file = $ENV{$_};
if($file) {
(-e $file) or die("$file file does not exist: $!");
$count++;
if (/PKCS12/) {
$count++;
$ctx->use_pkcs12_file($file ,$ENV{'HTTPS_PKCS12_PASSWORD'}) || die("failed to load $file: $!");
last;
} elsif (/CERT/) {
$ctx->use_certificate_file($file ,1) || die("failed to load $file: $!");
} elsif (/KEY/) {
$ctx->use_PrivateKey_file($file, 1) || die("failed to load $file: $!");
} else {
die("setting $_ not supported");
}
}
}
# if both configs are set, then verify them
if (($count == 2)) {
if (! $ctx->check_private_key) {
die("Private key and certificate do not match");
}
}
$count; # number of successful cert loads/checks
}
1;