File Coverage

blib/lib/LightTCP/SSLclient.pm
Criterion Covered Total %
statement 167 470 35.5
branch 48 222 21.6
condition 29 123 23.5
subroutine 41 60 68.3
pod 28 32 87.5
total 313 907 34.5


line stmt bran cond sub pod time code
1             package LightTCP::SSLclient;
2              
3 6     6   860668 use strict;
  6         14  
  6         257  
4 6     6   32 use warnings;
  6         16  
  6         362  
5 6     6   5948 use IO::Socket::SSL;
  6         842640  
  6         63  
6 6     6   1472 use IO::Socket::INET;
  6         16  
  6         87  
7 6     6   7493 use MIME::Base64 qw(encode_base64);
  6         5978  
  6         515  
8 6     6   4030 use URI;
  6         56519  
  6         415  
9              
10             our $VERSION = '1.06';
11              
12 6     6   56 use base 'Exporter';
  6         13  
  6         1433  
13              
14             our @EXPORT_OK = qw(
15             ECONNECT
16             EREQUEST
17             ERESPONSE
18             ETIMEOUT
19             ESSL
20             );
21              
22             our %EXPORT_TAGS = (
23             errors => [qw(ECONNECT EREQUEST ERESPONSE ETIMEOUT ESSL)],
24             );
25              
26             use constant {
27 6         55439 ECONNECT => 1,
28             EREQUEST => 2,
29             ERESPONSE => 3,
30             ETIMEOUT => 4,
31             ESSL => 5,
32 6     6   64 };
  6         19  
33              
34             sub new {
35 31     31 0 1436919 my ($class, %opts) = @_;
36             my $self = {
37             timeout => $opts{timeout} // 10,
38             insecure => $opts{insecure} // 0,
39             cert => $opts{cert} // undef,
40             verbose => $opts{verbose} // 0,
41             user_agent => $opts{user_agent} // 'LightTCP::SSLclient/'.$VERSION,
42             ssl_protocols => $opts{ssl_protocols} // ['TLSv1.2', 'TLSv1.3'],
43             ssl_ciphers => $opts{ssl_ciphers} // 'HIGH:!aNULL:!MD5:!RC4',
44             keep_alive => $opts{keep_alive} // 0,
45             buffer_size => $opts{buffer_size} // 8192,
46             max_redirects => $opts{max_redirects} // 5,
47 31   100     1340 follow_redirects => $opts{follow_redirects} // 1,
      100        
      50        
      100        
      66        
      100        
      100        
      100        
      100        
      100        
      100        
48             _socket => undef,
49             _connected => 0,
50             _target_host => undef,
51             _target_port => undef,
52             _proxy => undef,
53             _proxy_auth => undef,
54             _buffer => '',
55             _redirect_count=> 0,
56             _redirect_history => [],
57             };
58 31         94 bless $self, $class;
59 31         187 return $self;
60             }
61              
62             sub _parse_proxy_address {
63 2     2   5 my ($proxy) = @_;
64              
65 2 50       11 if ($proxy =~ /^\[(.+)\]:(\d+)$/) {
66 0         0 return ($1, $2);
67             }
68              
69 2 50       19 if ($proxy =~ /^([^:]+):(\d+)$/) {
70 0         0 return ($1, $2);
71             }
72              
73 2 50       14 if ($proxy =~ /^([^:]+)$/) {
74 0         0 return ($1, 8080);
75             }
76              
77 2         8 return (undef, undef);
78             }
79              
80             sub _sanitize_credentials {
81 0     0   0 my ($str) = @_;
82 0 0       0 return '' unless defined $str;
83 0         0 $str =~ s/Basic\s+[A-Za-z0-9+\/=]+/Basic [REDACTED]/gi;
84 0         0 return $str;
85             }
86              
87             sub _read_line_from_socket {
88 0     0   0 my ($self, $socket, $timeout, $buffer_ref) = @_;
89 0   0     0 $$buffer_ref //= '';
90 0         0 return _read_until_delimiter($self, $socket, $buffer_ref, $timeout, "\n");
91             }
92              
93             sub _read_until_delimiter {
94 0     0   0 my ($self, $socket, $buffer_ref, $timeout, $delim) = @_;
95 0         0 my $delim_len = length($delim);
96              
97 0         0 eval {
98 0     0   0 local $SIG{ALRM} = sub { die "timeout\n" };
  0         0  
99 0         0 alarm $timeout;
100              
101 0         0 while ($$buffer_ref !~ /\Q$delim\E/) {
102 0         0 my $read;
103 0         0 my $res = sysread($socket, $read, $self->{buffer_size});
104 0 0       0 if (!defined $res) {
105 0         0 alarm 0;
106 0 0 0     0 return undef if $!{EAGAIN} || $!{EWOULDBLOCK};
107 0         0 return undef;
108             }
109 0 0       0 last if $res == 0;
110 0         0 $$buffer_ref .= $read;
111             }
112              
113 0         0 alarm 0;
114             };
115 0 0       0 if ($@) {
116 0         0 return undef;
117             }
118              
119 0 0       0 if ($$buffer_ref =~ /^([^\Q$delim\E]*)\Q$delim\E(.*)$/s) {
120 0         0 $$buffer_ref = $2;
121 0         0 return $1 . $delim;
122             }
123 0         0 return undef;
124             }
125              
126             sub _read_exact_bytes {
127 0     0   0 my ($socket, $bytes, $timeout) = @_;
128 0         0 my $result = '';
129 0         0 my $buf;
130              
131 0         0 eval {
132 0     0   0 local $SIG{ALRM} = sub { die "timeout\n" };
  0         0  
133 0         0 alarm $timeout;
134              
135 0         0 while (length($result) < $bytes) {
136 0         0 my $read = sysread($socket, $buf, $bytes - length($result));
137 0 0 0     0 last unless defined $read && $read > 0;
138 0         0 $result .= $buf;
139             }
140              
141 0         0 alarm 0;
142             };
143 0 0       0 if ($@) {
144 0         0 return substr($result, 0, length($result));
145             }
146 0         0 return $result;
147             }
148              
149             sub _hex_dump {
150 0     0   0 my ($data) = @_;
151 0         0 return join('', map { sprintf('%02x', ord($_)) } split(//, $data));
  0         0  
152             }
153              
154             sub connect {
155 2     2 1 17 my ($self, $target_host, $target_port, $proxy, $proxy_auth) = @_;
156              
157 2         7 my $timeout = $self->{timeout};
158 2         7 my $socket;
159              
160             my @debug;
161 2         0 my @errors;
162 2 100       9 push(@debug, "# === LightTCP::SSLclient::connect ===\n") if $self->{verbose};
163              
164 2         7 $self->{_target_host} = $target_host;
165 2         4 $self->{_target_port} = $target_port;
166 2         5 $self->{_proxy} = $proxy;
167 2         5 $self->{_proxy_auth} = $proxy_auth;
168 2         5 $self->{_buffer} = '';
169              
170             my %ssl_opts = (
171             SSL_verifycn_scheme => 'http',
172             SSL_verifycn_name => $target_host,
173             SSL_hostname => $target_host,
174             Timeout => $timeout,
175             SSL_protocols => $self->{ssl_protocols},
176             SSL_cipher_list => $self->{ssl_ciphers},
177 2         16 );
178              
179 2 50       8 if ($self->{insecure}) {
180 0         0 $ssl_opts{SSL_verify_mode} = IO::Socket::SSL::SSL_VERIFY_NONE;
181             } else {
182 2         6 $ssl_opts{SSL_verify_mode} = IO::Socket::SSL::SSL_VERIFY_PEER;
183             }
184              
185 2 0 33     9 if ($self->{cert} && -f $self->{cert}.'.key' && -f $self->{cert}.'.crt') {
      33        
186 0         0 $ssl_opts{SSL_key_file} = $self->{cert}.'.key';
187 0         0 $ssl_opts{SSL_cert_file} = $self->{cert}.'.crt';
188             }
189              
190 2 100       7 if ($self->{verbose}) {
191 1         4 push(@debug, "# === ssl_opts ===\n");
192 1         8 foreach my $k (sort keys %ssl_opts) {
193 7 50       20 next if $k =~ /SSL_(key|cert)_file/;
194 7         23 push(@debug, "- $k = $ssl_opts{$k}\n");
195             }
196             }
197              
198 2 50       8 if ($proxy) {
199 2         8 my ($proxy_host, $proxy_port) = _parse_proxy_address($proxy);
200 2 50 33     9 unless ($proxy_host && $proxy_port) {
201 2         6 push(@errors, "- ERROR: Invalid proxy address format: $proxy\n");
202 2         17 return (0, \@errors, \@debug, ECONNECT);
203             }
204              
205 0 0       0 push(@debug, "# Connecting to proxy $proxy_host:$proxy_port...\n") if $self->{verbose};
206 0         0 $socket = IO::Socket::INET->new(
207             PeerAddr => $proxy_host,
208             PeerPort => $proxy_port,
209             Proto => 'tcp',
210             Timeout => $timeout,
211             );
212 0 0       0 unless ($socket) {
213 0         0 push(@errors, "- ERROR: Cannot connect to proxy: $!\n");
214 0         0 return (0, \@errors, \@debug, ECONNECT);
215             }
216              
217 0         0 my $connect_req = "CONNECT $target_host:$target_port HTTP/1.1\r\n";
218 0         0 $connect_req .= "Host: $target_host:$target_port\r\n";
219 0 0       0 if ($proxy_auth) {
220 0         0 my $encoded = encode_base64($proxy_auth, '');
221 0         0 $connect_req .= "Proxy-Authorization: Basic $encoded\r\n";
222 0 0       0 push(@debug, "- Proxy-Authorization: " . _sanitize_credentials("Basic $encoded") . "\n") if $self->{verbose};
223             }
224 0         0 $connect_req .= "\r\n";
225 0         0 print $socket $connect_req;
226              
227 0         0 my $proxy_resp = _read_line_from_socket($self, $socket, $timeout, \$self->{_buffer});
228 0 0       0 unless ($proxy_resp) {
229 0         0 push(@errors, "- ERROR: Failed to read proxy response: $!\n");
230 0 0       0 $socket->close() if $socket;
231 0         0 return (0, \@errors, \@debug, ECONNECT);
232             }
233              
234 0 0       0 unless ($proxy_resp =~ /^HTTP\/1\.[01]\s+200\b/i) {
235 0 0       0 $socket->close() if $socket;
236 0         0 push(@errors, "- ERROR: Proxy CONNECT failed:\n$proxy_resp");
237 0         0 return (0, \@errors, \@debug, ECONNECT);
238             }
239 0 0       0 push(@debug, "- Proxy tunnel established.\n") if $self->{verbose};
240              
241 0 0       0 if (IO::Socket::SSL->start_SSL($socket, %ssl_opts)) {
242 0         0 $socket->timeout($timeout);
243 0 0       0 push(@debug, "- SSL connect established.\n") if $self->{verbose};
244 0         0 $self->{_socket} = $socket;
245 0         0 $self->{_connected} = 1;
246 0         0 return (1, \@errors, \@debug, 0);
247             } else {
248 0         0 push(@errors, "- ERROR: SSL connect failed: $SSL_ERROR\n");
249 0         0 return (0, \@errors, \@debug, ESSL);
250             }
251             } else {
252 0 0       0 push(@debug, "# Connecting to $target_host:$target_port...\n") if $self->{verbose};
253 0         0 $socket = IO::Socket::SSL->new(
254             PeerHost => $target_host,
255             PeerPort => $target_port,
256             %ssl_opts,
257             );
258 0 0       0 if ($socket) {
259 0         0 $socket->timeout($timeout);
260 0 0       0 push(@debug, "- Direct SSL connect established.\n") if $self->{verbose};
261 0         0 $self->{_socket} = $socket;
262 0         0 $self->{_connected} = 1;
263 0         0 return (1, \@errors, \@debug, 0);
264             } else {
265 0         0 push(@errors, "- ERROR: Direct SSL connection failed: $SSL_ERROR\n");
266 0         0 return (0, \@errors, \@debug, ESSL);
267             }
268             }
269             }
270              
271             sub reconnect {
272 0     0 1 0 my ($self) = @_;
273 0 0 0     0 return 0 unless $self->{_target_host} && $self->{_target_port};
274 0         0 $self->close();
275             my ($ok, $err, $dbg, $code) = $self->connect(
276             $self->{_target_host},
277             $self->{_target_port},
278             $self->{_proxy},
279             $self->{_proxy_auth},
280 0         0 );
281 0         0 return $ok;
282             }
283              
284             sub request {
285 3     3 1 21 my ($self, $method, $path, %opts) = @_;
286              
287 3         25 my $socket = $self->{_socket};
288 3         7 my @debug;
289             my @errors;
290              
291 3 100       13 push(@debug, "# === LightTCP::SSLclient::request ===\n") if $self->{verbose};
292 3 100       12 push(@debug, "- Sending: $method $path HTTP/1.1\n") if $self->{verbose};
293              
294 3 50       10 unless ($socket) {
295 3         8 push(@errors, "- ERROR: No connection established\n");
296 3         17 return (0, \@errors, \@debug, EREQUEST);
297             }
298              
299 0   0     0 my $host = $opts{host} // '';
300 0   0     0 my $payload = $opts{payload} // '';
301 0   0     0 my $ph = $opts{headers} // {};
302              
303 0 0       0 my $length = defined $payload ? length($payload) : 0;
304              
305 0   0     0 $ph->{'Host'} ||= $host;
306 0   0     0 $ph->{'Accept'} ||= '*/*';
307 0   0     0 $ph->{'User-Agent'} ||= $self->{user_agent};
308 0 0 0     0 $ph->{'Connection'} ||= $self->{keep_alive} ? 'keep-alive' : 'close';
309              
310 0         0 my $timeout = $self->{timeout};
311 0         0 my $send_ok = 1;
312              
313 0         0 eval {
314 0     0   0 local $SIG{ALRM} = sub { die "timeout\n" };
  0         0  
315 0         0 alarm $timeout;
316              
317 0 0       0 unless (print $socket "$method $path HTTP/1.1\r\n") {
318 0         0 push(@errors, "- ERROR: Failed to send request line: $!\n");
319 0         0 $send_ok = 0;
320             }
321              
322 0 0       0 if ($send_ok) {
323 0 0       0 push(@debug, "# === Headers: ===\n") if $self->{verbose};
324 0         0 foreach my $key (sort keys %$ph) {
325 0   0     0 my $val = $ph->{$key} // '';
326 0         0 $val =~ s/[\r\n]+//g;
327 0         0 $val =~ s/^\s+|\s+$//g;
328 0 0       0 push(@debug, "- $key: $val\n") if $self->{verbose};
329 0 0       0 unless (print $socket "$key: $val\r\n") {
330 0         0 push(@errors, "- ERROR: Failed to send header '$key': $!\n");
331 0         0 $send_ok = 0;
332 0         0 last;
333             }
334             }
335             }
336              
337 0 0 0     0 if ($send_ok && $length > 0) {
338 0 0       0 unless (print $socket "Content-Length: $length\r\n") {
339 0         0 push(@errors, "- ERROR: Failed to send Content-Length: $!\n");
340 0         0 $send_ok = 0;
341             }
342             }
343              
344 0 0       0 if ($send_ok) {
345 0 0       0 unless (print $socket "\r\n") {
346 0         0 push(@errors, "- ERROR: Failed to send header terminator: $!\n");
347 0         0 $send_ok = 0;
348             }
349             }
350              
351 0 0 0     0 if ($send_ok && defined $payload && $length > 0) {
      0        
352 0 0       0 unless (print $socket $payload) {
353 0         0 push(@errors, "- ERROR: Failed to send payload (connection broken): $!\n");
354 0         0 $send_ok = 0;
355             }
356             }
357              
358 0         0 alarm 0;
359             };
360 0 0       0 if ($@) {
361 0         0 push(@errors, "- ERROR: Timeout during request send\n");
362 0         0 return (0, \@errors, \@debug, ETIMEOUT);
363             }
364              
365 0 0       0 unless ($send_ok) {
366 0         0 return (0, \@errors, \@debug, EREQUEST);
367             }
368 0         0 return (1, \@errors, \@debug, 0);
369             }
370              
371             sub request_with_redirects {
372 0     0 1 0 my ($self, $method, $path, %opts) = @_;
373              
374 0         0 $self->{_redirect_count} = 0;
375 0         0 $self->{_redirect_history} = [];
376              
377 0         0 return $self->_do_request_with_redirects($method, $path, %opts);
378             }
379              
380             sub _do_request_with_redirects {
381 0     0   0 my ($self, $method, $path, %opts) = @_;
382              
383 0         0 my @errors;
384             my @debug;
385 0         0 my ($ok, $req_errors, $req_debug, $error_code) = $self->request($method, $path, %opts);
386 0 0       0 push(@errors, @$req_errors) if $req_errors;
387 0 0       0 push(@debug, @$req_debug) if $req_debug;
388 0 0       0 return (undef, undef, undef, undef, \@errors, \@debug, $error_code, []) unless $ok;
389              
390 0         0 my ($code, $state, $headers, $body, $resp_errors, $resp_debug, $resp_code) = $self->response();
391 0 0       0 push(@errors, @$resp_errors) if $resp_errors;
392 0 0       0 push(@debug, @$resp_debug) if $resp_debug;
393 0 0       0 return ($code, $state, $headers, $body, \@errors, \@debug, $resp_code, $self->{_redirect_history}) unless $code;
394              
395 0 0 0     0 if ($self->{follow_redirects} && $code >= 300 && $code < 400 && $headers && $headers->{'location'}) {
      0        
      0        
      0        
396 0 0       0 return ($code, $state, $headers, $body, $resp_errors, $resp_debug, $resp_code, $self->{_redirect_history}) if $self->{_redirect_count} >= $self->{max_redirects};
397              
398 0         0 my $location = $headers->{'location'};
399              
400 0         0 push(@{$self->{_redirect_history}}, {
  0         0  
401             from => "$method $path",
402             to => $location,
403             code => $code,
404             });
405              
406 0         0 $self->{_redirect_count}++;
407              
408 0         0 my ($new_method, $new_path, $new_host, $new_opts) = $self->_resolve_redirect($method, $path, $location, %opts);
409              
410 0         0 my $debug_msg = "- Following redirect ($code): $method $path -> $new_method $new_path\n";
411 0 0       0 push(@$resp_debug, $debug_msg) if $self->{verbose};
412              
413 0         0 return $self->_do_request_with_redirects($new_method, $new_path, %$new_opts);
414             }
415              
416 0         0 return ($code, $state, $headers, $body, $resp_errors, $resp_debug, $resp_code, $self->{_redirect_history});
417             }
418              
419             sub _resolve_redirect {
420 0     0   0 my ($self, $method, $path, $location, %opts) = @_;
421              
422 0         0 my $uri = URI->new($location);
423 0         0 my $new_method = $method;
424 0   0     0 my $new_path = $uri->path_query || '/';
425 0   0     0 my $new_host = $opts{host} // '';
426              
427 0 0       0 if ($uri->scheme) {
    0          
428 0 0       0 if ($uri->scheme ne 'https') {
429 0         0 return ($method, $path, '', \%opts);
430             }
431 0         0 $new_host = $uri->host;
432 0   0     0 $new_path = $uri->path_query || '/';
433 0 0 0     0 if ($uri->port && $uri->port != 443) {
434 0         0 $new_host = $uri->host . ':' . $uri->port;
435             }
436             } elsif ($location =~ m{^/}) {
437 0         0 $new_path = $location;
438             } else {
439 0         0 $new_path = $self->_resolve_relative_path($path, $location);
440             }
441              
442 0         0 my %new_opts = %opts;
443 0         0 $new_opts{host} = $new_host;
444              
445 0 0       0 if ($location =~ /^HTTP\/1\.[01] (30[1278])\b/i) {
446 0         0 my $code = $1;
447 0 0 0     0 if ($code eq '301' || $code eq '302') {
448 0 0       0 if ($method eq 'POST') {
449 0         0 $new_method = 'GET';
450 0         0 $new_opts{payload} = undef;
451             }
452             }
453             }
454              
455 0         0 return ($new_method, $new_path, $new_host, \%new_opts);
456             }
457              
458             sub _resolve_relative_path {
459 3     3   16 my ($self, $current_path, $relative) = @_;
460              
461 3 50       16 return $relative if $relative =~ m{^/};
462              
463 3         25 $current_path =~ s{/[^/]*$}{};
464 3 50       11 $current_path = '/' unless length $current_path;
465              
466 3         23 my $result = $current_path;
467 3 50       12 $result .= '/' unless $result =~ m{/$};
468 3         9 $result .= $relative;
469              
470 3         12 $result =~ s{[^/]+/\.\./}{}g;
471 3         7 $result =~ s{/\./}{}g;
472 3         7 $result =~ s{//+}{/};
473              
474 3         20 return $result;
475             }
476              
477             sub response {
478 3     3 1 4177 my ($self) = @_;
479              
480 3         16 my $socket = $self->{_socket};
481 3         9 my @resp_debug;
482             my @resp_errors;
483              
484 3 100       28 push(@resp_debug, "# === LightTCP::SSLclient::response ===\n") if $self->{verbose};
485              
486 3 50       13 unless ($socket) {
487 3         8 push(@resp_errors, "- ERROR: No connection established\n");
488 3         18 return (undef, undef, undef, undef, \@resp_errors, \@resp_debug, ERESPONSE);
489             }
490              
491 0   0     0 my $timeout = $socket->timeout // 15;
492              
493 0         0 my $headers_raw = '';
494 0         0 my $buf;
495 0         0 my $buf_size = $self->{buffer_size};
496 0         0 eval {
497 0     0   0 local $SIG{ALRM} = sub { die "timeout\n" };
  0         0  
498 0         0 alarm $timeout;
499 0         0 while (sysread($socket, $buf, $buf_size)) {
500 0         0 $headers_raw .= $buf;
501 0 0       0 last if $headers_raw =~ /\r\n\r\n|\n\n/;
502             }
503 0         0 alarm 0
504             };
505 0 0       0 if ($@) {
506 0         0 push(@resp_errors, "- ERROR: Timeout while reading response headers\n");
507 0         0 return (undef, undef, undef, undef, \@resp_errors, \@resp_debug, ETIMEOUT);
508             }
509              
510 0 0       0 unless ($headers_raw =~ /^HTTP\/1\.[01]\s+(\d{3})\s+(.*?)\r?\n/i) {
511 0         0 push(@resp_errors, "- ERROR: INVALID RESPONSE (no valid status line)\n");
512 0         0 return (undef, undef, undef, undef, \@resp_errors, \@resp_debug, ERESPONSE);
513             }
514 0         0 my $code = $1;
515 0         0 my $state = $2;
516 0 0       0 push(@resp_debug, "- $code $state\n") if $self->{verbose};
517              
518 0 0       0 my ($headers_part, $initial_body) = $headers_raw =~ /^(.*?)\r\n\r\n(.*)$/s
519             ? ($1, $2)
520             : ($headers_raw, '');
521              
522 0         0 my @lines = split /\r?\n/, $headers_part;
523 0         0 shift @lines;
524              
525 0         0 my %hdr;
526 0         0 my $current_key = '';
527 0         0 for my $line (@lines) {
528 0 0       0 if ($line =~ /^\s+(.+)/) {
    0          
529 0 0       0 $hdr{$current_key} .= " $1" if $current_key;
530             } elsif ($line =~ /^([^:]+):\s*(.*)/) {
531 0         0 $current_key = lc $1;
532 0         0 $hdr{$current_key} = $2;
533             }
534             }
535              
536 0         0 my $body = $initial_body;
537 0   0     0 my $chunked = (lc($hdr{'transfer-encoding'} || '') eq 'chunked');
538 0 0       0 my $content_length = $hdr{'content-length'} ? int($hdr{'content-length'}) : undef;
539              
540 0 0 0     0 if ($chunked) {
    0          
541 0         0 $body = $self->_read_chunked_with_timeout($body, $timeout, \@resp_errors, \@resp_debug);
542             } elsif (defined $content_length && $content_length > length($body)) {
543 0         0 $body = $self->_read_exact_bytes_with_timeout($body, $content_length - length($body), $timeout, \@resp_errors, \@resp_debug);
544             } else {
545 0         0 $body = $self->_read_until_eof_with_timeout($body, $timeout, \@resp_errors, \@resp_debug);
546             }
547              
548 0         0 return ($code, $state, \%hdr, $body, \@resp_errors, \@resp_debug, 0);
549             }
550              
551             sub _read_exact_bytes_with_timeout {
552 0     0   0 my ($self, $initial, $remaining, $timeout, $perrs, $pdebug) = @_;
553 0         0 my $body = $initial;
554 0         0 my $buf;
555 0         0 my $socket = $self->{_socket};
556 0         0 my $buf_size = $self->{buffer_size};
557              
558 0         0 eval {
559 0     0   0 local $SIG{ALRM} = sub { die "timeout\n" };
  0         0  
560 0         0 alarm $timeout;
561 0         0 while ($remaining > 0) {
562 0 0       0 my $read = sysread($socket, $buf, ($remaining > $buf_size ? $buf_size : $remaining));
563 0 0 0     0 last unless defined $read && $read > 0;
564 0         0 $body .= $buf;
565 0         0 $remaining -= $read;
566             }
567 0         0 alarm 0
568             };
569 0 0       0 if ($@) {
570 0         0 push(@$perrs, "- WARNING: Timeout during Content-Length body read (incomplete body)\n");
571             }
572 0         0 return $body;
573             }
574              
575             sub _read_until_eof_with_timeout {
576 0     0   0 my ($self, $initial, $timeout, $perrs, $pdebug) = @_;
577 0         0 my $body = $initial;
578 0         0 my $buf;
579 0         0 my $socket = $self->{_socket};
580 0         0 my $buf_size = $self->{buffer_size};
581              
582 0         0 eval {
583 0     0   0 local $SIG{ALRM} = sub { die "timeout\n" };
  0         0  
584 0         0 alarm $timeout;
585 0         0 while (sysread($socket, $buf, $buf_size)) {
586 0         0 $body .= $buf;
587             }
588 0         0 alarm 0
589             };
590 0 0       0 if ($@) {
591 0         0 push(@$perrs, "- WARNING: Timeout during EOF body read (connection may have stalled)\n");
592             }
593 0         0 return $body;
594             }
595              
596             sub _read_chunked_with_timeout {
597 0     0   0 my ($self, $initial, $timeout, $perrs, $pdebug) = @_;
598 0         0 my $body = $initial;
599 0         0 my $buf;
600 0         0 my $socket = $self->{_socket};
601 0         0 my $buffer = $self->{_buffer};
602 0         0 my $buf_size = $self->{buffer_size};
603              
604 0         0 eval {
605 0     0   0 local $SIG{ALRM} = sub { die "timeout\n" };
  0         0  
606 0         0 alarm $timeout;
607              
608 0         0 while (1) {
609 0         0 my $chunk_line = _read_until_delimiter($self, $socket, $buffer, $timeout, "\n");
610 0 0 0     0 last unless defined $chunk_line && length($chunk_line);
611              
612 0         0 $chunk_line =~ s/\r?\n$//;
613 0         0 my $chunk_size = hex($chunk_line);
614 0 0       0 push(@$pdebug, "- Chunk size: $chunk_size\n") if $self->{verbose};
615              
616 0 0       0 last if $chunk_size == 0;
617              
618 0         0 my $read_so_far = 0;
619 0         0 while ($read_so_far < $chunk_size) {
620 0         0 my $need = $chunk_size - $read_so_far;
621 0 0       0 my $read = sysread($socket, $buf, ($need > $buf_size ? $buf_size : $need));
622 0 0 0     0 unless (defined $read && $read > 0) {
623 0         0 push(@$perrs, "- WARNING: Failed to read chunk data\n");
624 0         0 last;
625             }
626 0         0 $body .= $buf;
627 0         0 $read_so_far += $read;
628             }
629              
630 0         0 my $trailing = _read_exact_bytes($socket, 2, $timeout);
631 0 0 0     0 unless ($trailing eq "\r\n" || $trailing eq "\n") {
632 0         0 push(@$perrs, "- WARNING: Invalid chunk trailer, expected CRLF, got: " . _hex_dump($trailing) . "\n");
633             }
634             }
635 0         0 alarm 0
636             };
637 0 0       0 if ($@) {
638 0         0 push(@$perrs, "- WARNING: Timeout during chunked transfer (incomplete body)\n");
639             }
640 0         0 $self->{_buffer} = $buffer;
641 0         0 return $body;
642             }
643              
644             sub fingerprint_read {
645 5     5 1 9287 my ($self, $dir, $host, $port) = @_;
646 5         18 my $file = "$dir/$host.$port";
647 5 100       236 return '' unless -f $file;
648              
649 3 50       162 open my $fh, '<', $file or return '';
650 3         102 my $fp = <$fh>;
651 3         42 close $fh;
652 3 50       18 chomp $fp if defined $fp;
653 3 50       69 $fp =~ s/^\s+|\s+$//g if defined $fp;
654 3   50     60 return $fp // '';
655             }
656              
657             sub fingerprint_save {
658 8     8 1 2314 my ($self, $dir, $host, $port, $fingerprint, $save) = @_;
659 8 100       42 my $suffix = $save ? '' : '.new';
660 8         95 my $file = "$dir/$host.$port$suffix";
661 8         53 my @errors;
662             my @debug;
663 8 100       249 mkdir $dir unless -d $dir;
664 8 100       1236 open my $fh, '>', $file or do {
665 2         26 push(@errors, "- WARNING: Cannot save fingerprint to \"$file\": $!\n");
666 2         24 return (1, \@errors, \@debug, EREQUEST);
667             };
668 6         149 print $fh "$fingerprint\n";
669 6         330 close $fh;
670 6 100       55 push(@debug, "- Saved fingerprint to: $file\n") if $self->{verbose};
671 6 100       19 if ($save) {
672 2 50       112 unlink("$file.new") if -f "$file.new";
673 2         30 return (1, \@errors, \@debug, 0);
674             }
675 4         47 return (1, \@errors, \@debug, 0);
676             }
677              
678             sub DESTROY {
679 31     31   71206 my ($self) = @_;
680 31 100       310 $self->close() if $self->{_connected};
681             }
682              
683             sub socket {
684 1     1 1 8 my ($self) = @_;
685 1         7 return $self->{_socket};
686             }
687              
688             sub is_connected {
689 4     4 1 13 my ($self) = @_;
690 4         23 return $self->{_connected};
691             }
692              
693             sub close {
694 3     3 1 13 my ($self) = @_;
695 3 50       15 if ($self->{_socket}) {
696 0         0 $self->{_socket}->close();
697 0         0 $self->{_socket} = undef;
698 0         0 $self->{_connected} = 0;
699             }
700 3         8 $self->{_buffer} = '';
701 3         19 return 1;
702             }
703              
704             sub set_cert {
705 1     1 1 5 my ($self, $cert) = @_;
706 1         4 $self->{cert} = $cert;
707 1         5 return $self->{cert};
708             }
709              
710             sub set_timeout {
711 1     1 1 10 my ($self, $timeout) = @_;
712 1   50     6 $self->{timeout} = $timeout // 10;
713 1 50       5 $self->{_socket}->timeout($self->{timeout}) if $self->{_socket};
714 1         6 return $self->{timeout};
715             }
716              
717             sub set_insecure {
718 2     2 1 16 my ($self, $insecure) = @_;
719 2 100       10 $self->{insecure} = $insecure ? 1 : 0;
720 2         11 return $self->{insecure};
721             }
722              
723             sub set_keep_alive {
724 2     2 1 9 my ($self, $keep_alive) = @_;
725 2 50       12 $self->{keep_alive} = $keep_alive ? 1 : 0;
726 2         9 return $self->{keep_alive};
727             }
728              
729             sub get_timeout {
730 4     4 1 752 my ($self) = @_;
731 4         22 return $self->{timeout};
732             }
733              
734             sub get_user_agent {
735 2     2 1 6 my ($self) = @_;
736 2         8 return $self->{user_agent};
737             }
738              
739             sub is_verbose {
740 3     3 0 7 my ($self) = @_;
741 3         12 return $self->{verbose};
742             }
743              
744             sub get_cert {
745 1     1 1 3 my ($self) = @_;
746 1         5 return $self->{cert};
747             }
748              
749             sub get_insecure {
750 1     1 1 4 my ($self) = @_;
751 1         4 return $self->{insecure};
752             }
753              
754             sub get_ssl_protocols {
755 1     1 1 4 my ($self) = @_;
756 1         9 return $self->{ssl_protocols};
757             }
758              
759             sub get_ssl_ciphers {
760 1     1 1 3 my ($self) = @_;
761 1         6 return $self->{ssl_ciphers};
762             }
763              
764             sub get_keep_alive {
765 7     7 1 28 my ($self) = @_;
766 7         36 return $self->{keep_alive};
767             }
768              
769             sub is_keep_alive {
770 1     1 0 5 my ($self) = @_;
771 1         6 return $self->{keep_alive};
772             }
773              
774             sub get_buffer_size {
775 7     7 1 27 my ($self) = @_;
776 7         31 return $self->{buffer_size};
777             }
778              
779             sub set_buffer_size {
780 2     2 0 8 my ($self, $size) = @_;
781 2 100 66     17 $self->{buffer_size} = defined $size && $size > 0 ? $size : 8192;
782 2         10 return $self->{buffer_size};
783             }
784              
785             sub get_max_redirects {
786 4     4 1 20 my ($self) = @_;
787 4         22 return $self->{max_redirects};
788             }
789              
790             sub get_follow_redirects {
791 5     5 1 15 my ($self) = @_;
792 5         26 return $self->{follow_redirects};
793             }
794              
795             sub set_max_redirects {
796 1     1 1 4 my ($self, $max) = @_;
797 1   50     5 $self->{max_redirects} = $max // 5;
798 1         4 return $self->{max_redirects};
799             }
800              
801             sub set_follow_redirects {
802 1     1 1 7 my ($self, $follow) = @_;
803 1 50       8 $self->{follow_redirects} = $follow ? 1 : 0;
804 1         3 return $self->{follow_redirects};
805             }
806              
807             sub get_redirect_count {
808 1     1 1 3 my ($self) = @_;
809 1         12 return $self->{_redirect_count};
810             }
811              
812             sub get_redirect_history {
813 1     1 1 11 my ($self) = @_;
814 1         3 return $self->{_redirect_history};
815             }
816              
817             1;
818              
819             __END__