LightTCP-SSLclient
view release on metacpan or search on metacpan
lib/LightTCP/SSLclient.pm view on Meta::CPAN
package LightTCP::SSLclient;
use strict;
use warnings;
use IO::Socket::SSL;
use IO::Socket::INET;
use MIME::Base64 qw(encode_base64);
use URI;
our $VERSION = '1.06';
use base 'Exporter';
our @EXPORT_OK = qw(
ECONNECT
EREQUEST
ERESPONSE
ETIMEOUT
ESSL
);
our %EXPORT_TAGS = (
errors => [qw(ECONNECT EREQUEST ERESPONSE ETIMEOUT ESSL)],
);
use constant {
ECONNECT => 1,
EREQUEST => 2,
ERESPONSE => 3,
ETIMEOUT => 4,
ESSL => 5,
};
sub new {
my ($class, %opts) = @_;
my $self = {
timeout => $opts{timeout} // 10,
insecure => $opts{insecure} // 0,
cert => $opts{cert} // undef,
verbose => $opts{verbose} // 0,
user_agent => $opts{user_agent} // 'LightTCP::SSLclient/'.$VERSION,
ssl_protocols => $opts{ssl_protocols} // ['TLSv1.2', 'TLSv1.3'],
ssl_ciphers => $opts{ssl_ciphers} // 'HIGH:!aNULL:!MD5:!RC4',
keep_alive => $opts{keep_alive} // 0,
buffer_size => $opts{buffer_size} // 8192,
max_redirects => $opts{max_redirects} // 5,
follow_redirects => $opts{follow_redirects} // 1,
_socket => undef,
_connected => 0,
_target_host => undef,
_target_port => undef,
_proxy => undef,
_proxy_auth => undef,
_buffer => '',
_redirect_count=> 0,
_redirect_history => [],
};
bless $self, $class;
return $self;
}
sub _parse_proxy_address {
my ($proxy) = @_;
if ($proxy =~ /^\[(.+)\]:(\d+)$/) {
lib/LightTCP/SSLclient.pm view on Meta::CPAN
$$buffer_ref = $2;
return $1 . $delim;
}
return undef;
}
sub _read_exact_bytes {
my ($socket, $bytes, $timeout) = @_;
my $result = '';
my $buf;
eval {
local $SIG{ALRM} = sub { die "timeout\n" };
alarm $timeout;
while (length($result) < $bytes) {
my $read = sysread($socket, $buf, $bytes - length($result));
last unless defined $read && $read > 0;
$result .= $buf;
}
alarm 0;
};
if ($@) {
return substr($result, 0, length($result));
}
return $result;
}
sub _hex_dump {
my ($data) = @_;
return join('', map { sprintf('%02x', ord($_)) } split(//, $data));
}
sub connect {
my ($self, $target_host, $target_port, $proxy, $proxy_auth) = @_;
my $timeout = $self->{timeout};
my $socket;
my @debug;
my @errors;
push(@debug, "# === LightTCP::SSLclient::connect ===\n") if $self->{verbose};
$self->{_target_host} = $target_host;
$self->{_target_port} = $target_port;
$self->{_proxy} = $proxy;
$self->{_proxy_auth} = $proxy_auth;
$self->{_buffer} = '';
my %ssl_opts = (
SSL_verifycn_scheme => 'http',
SSL_verifycn_name => $target_host,
SSL_hostname => $target_host,
Timeout => $timeout,
SSL_protocols => $self->{ssl_protocols},
SSL_cipher_list => $self->{ssl_ciphers},
);
if ($self->{insecure}) {
$ssl_opts{SSL_verify_mode} = IO::Socket::SSL::SSL_VERIFY_NONE;
} else {
$ssl_opts{SSL_verify_mode} = IO::Socket::SSL::SSL_VERIFY_PEER;
}
if ($self->{cert} && -f $self->{cert}.'.key' && -f $self->{cert}.'.crt') {
$ssl_opts{SSL_key_file} = $self->{cert}.'.key';
$ssl_opts{SSL_cert_file} = $self->{cert}.'.crt';
}
if ($self->{verbose}) {
push(@debug, "# === ssl_opts ===\n");
foreach my $k (sort keys %ssl_opts) {
next if $k =~ /SSL_(key|cert)_file/;
push(@debug, "- $k = $ssl_opts{$k}\n");
}
}
if ($proxy) {
my ($proxy_host, $proxy_port) = _parse_proxy_address($proxy);
unless ($proxy_host && $proxy_port) {
push(@errors, "- ERROR: Invalid proxy address format: $proxy\n");
return (0, \@errors, \@debug, ECONNECT);
}
push(@debug, "# Connecting to proxy $proxy_host:$proxy_port...\n") if $self->{verbose};
$socket = IO::Socket::INET->new(
PeerAddr => $proxy_host,
PeerPort => $proxy_port,
Proto => 'tcp',
Timeout => $timeout,
);
unless ($socket) {
push(@errors, "- ERROR: Cannot connect to proxy: $!\n");
return (0, \@errors, \@debug, ECONNECT);
}
my $connect_req = "CONNECT $target_host:$target_port HTTP/1.1\r\n";
$connect_req .= "Host: $target_host:$target_port\r\n";
if ($proxy_auth) {
my $encoded = encode_base64($proxy_auth, '');
$connect_req .= "Proxy-Authorization: Basic $encoded\r\n";
push(@debug, "- Proxy-Authorization: " . _sanitize_credentials("Basic $encoded") . "\n") if $self->{verbose};
}
$connect_req .= "\r\n";
print $socket $connect_req;
my $proxy_resp = _read_line_from_socket($self, $socket, $timeout, \$self->{_buffer});
unless ($proxy_resp) {
push(@errors, "- ERROR: Failed to read proxy response: $!\n");
$socket->close() if $socket;
return (0, \@errors, \@debug, ECONNECT);
}
unless ($proxy_resp =~ /^HTTP\/1\.[01]\s+200\b/i) {
$socket->close() if $socket;
push(@errors, "- ERROR: Proxy CONNECT failed:\n$proxy_resp");
return (0, \@errors, \@debug, ECONNECT);
}
push(@debug, "- Proxy tunnel established.\n") if $self->{verbose};
if (IO::Socket::SSL->start_SSL($socket, %ssl_opts)) {
$socket->timeout($timeout);
push(@debug, "- SSL connect established.\n") if $self->{verbose};
$self->{_socket} = $socket;
$self->{_connected} = 1;
return (1, \@errors, \@debug, 0);
} else {
push(@errors, "- ERROR: SSL connect failed: $SSL_ERROR\n");
return (0, \@errors, \@debug, ESSL);
}
} else {
push(@debug, "# Connecting to $target_host:$target_port...\n") if $self->{verbose};
$socket = IO::Socket::SSL->new(
PeerHost => $target_host,
PeerPort => $target_port,
%ssl_opts,
);
if ($socket) {
$socket->timeout($timeout);
push(@debug, "- Direct SSL connect established.\n") if $self->{verbose};
$self->{_socket} = $socket;
$self->{_connected} = 1;
return (1, \@errors, \@debug, 0);
} else {
push(@errors, "- ERROR: Direct SSL connection failed: $SSL_ERROR\n");
return (0, \@errors, \@debug, ESSL);
}
}
}
sub reconnect {
my ($self) = @_;
return 0 unless $self->{_target_host} && $self->{_target_port};
$self->close();
my ($ok, $err, $dbg, $code) = $self->connect(
$self->{_target_host},
$self->{_target_port},
$self->{_proxy},
$self->{_proxy_auth},
);
return $ok;
}
sub request {
my ($self, $method, $path, %opts) = @_;
my $socket = $self->{_socket};
my @debug;
my @errors;
push(@debug, "# === LightTCP::SSLclient::request ===\n") if $self->{verbose};
push(@debug, "- Sending: $method $path HTTP/1.1\n") if $self->{verbose};
unless ($socket) {
push(@errors, "- ERROR: No connection established\n");
return (0, \@errors, \@debug, EREQUEST);
}
my $host = $opts{host} // '';
my $payload = $opts{payload} // '';
my $ph = $opts{headers} // {};
my $length = defined $payload ? length($payload) : 0;
$ph->{'Host'} ||= $host;
$ph->{'Accept'} ||= '*/*';
$ph->{'User-Agent'} ||= $self->{user_agent};
$ph->{'Connection'} ||= $self->{keep_alive} ? 'keep-alive' : 'close';
my $timeout = $self->{timeout};
my $send_ok = 1;
eval {
( run in 0.974 second using v1.01-cache-2.11-cpan-39bf76dae61 )