File Coverage

blib/lib/WWW/Suffit/UserAgent.pm
Criterion Covered Total %
statement 33 241 13.6
branch 0 70 0.0
condition 0 83 0.0
subroutine 11 33 33.3
pod 21 21 100.0
total 65 448 14.5


line stmt bran cond sub pod time code
1             package WWW::Suffit::UserAgent;
2 2     2   139568 use warnings;
  2         27  
  2         97  
3 2     2   13 use strict;
  2         4  
  2         56  
4 2     2   1346 use utf8;
  2         74  
  2         12  
5              
6             =encoding utf-8
7              
8             =head1 NAME
9              
10             WWW::Suffit::UserAgent - Suffit API user agent library
11              
12             =head1 VERSION
13              
14             Version 1.00
15              
16             =head1 SYNOPSIS
17              
18             use WWW::Suffit::UserAgent;
19              
20             my $clinet = WWW::Suffit::UserAgent->new(
21             url => "https://localhost",
22             username => "username", # optional
23             password => "password", # optional
24             max_redirects => 2, # Default: 10
25             connect_timeout => 3, # Default: 10 sec
26             inactivity_timeout => 5, # Default: 30 sec
27             request_timeout => 10, # Default: 5 min (300 sec)
28             );
29             my $status = $client->check();
30              
31             if ($status) {
32             print STDOUT $client->res->body;
33             } else {
34             print STDERR $clinet->error;
35             }
36              
37             =head1 DESCRIPTION
38              
39             Suffit API user agent library
40              
41             =head2 new
42              
43             my $clinet = WWW::Suffit::UserAgent->new(
44             url => "https://localhost",
45             username => "username", # optional
46             password => "password", # optional
47             max_redirects => 2, # Default: 10
48             connect_timeout => 3, # Default: 10 sec
49             inactivity_timeout => 5, # Default: 30 sec
50             request_timeout => 10, # Default: 5 min (300 sec)
51             );
52              
53             Returns the client instance
54              
55             =over 8
56              
57             =item B
58              
59             Sets the authentication scheme. HTTP Authentication Schemes: Bearer, Basic, ApiKey
60              
61             Default: ApiKey (use token header)
62              
63             =item B
64              
65             Enables ask username and password from terminal
66              
67             =item B
68              
69             Maximum number of redirects the user agent will follow before it fails. Default - 10
70              
71             =item B
72              
73             Default password for basic authentication
74              
75             =item B<*timeout>
76              
77             Timeout for connections, requests and inactivity periods in seconds.
78              
79             =item B
80              
81             The Mojo UserAgent object
82              
83             =item B
84              
85             Full URL of the WEB Server
86              
87             =item B
88              
89             Default username for basic authentication
90              
91             =back
92              
93             =head1 METHODS
94              
95             List of the User Agent interface methods
96              
97             =head2 cleanup
98              
99             $client->cleanup;
100              
101             Cleanup all variable data in object and returns client object
102              
103             =head2 code
104              
105             my $code = $clinet->code;
106             $client = $clinet->code(200);
107              
108             Returns HTTP code of the response
109              
110             =head2 credentials
111              
112             my $userinfo = $client->credentials(1);
113              
114             Gets credentials for User Agent
115              
116             =head2 error
117              
118             print $clinet->error;
119             $clinet = $clinet->error("My error");
120              
121             Returns error string
122              
123             =head2 path2url
124              
125             # For url = http://localhost:8695/api
126             my $url_str = $client->path2url("/foo/bar");
127             # http://localhost:8695/api/foo/bar
128              
129             Merges path to tail of url
130              
131             # For url = http://localhost:8695/api
132             my $url_str = $client->path2url("/foo/bar", 1);
133             # http://localhost:8695/foo/bar
134              
135             Sets path to url
136              
137             =head2 private_key
138              
139             $clinet = $clinet->private_key("---- BEGIN ... END -----");
140             my $private_key = $client->private_key;
141              
142             Sets or returns RSA private key
143              
144             =head2 public_key
145              
146             $clinet = $clinet->public_key("---- BEGIN ... END -----");
147             my $public_key = $client->public_key;
148              
149             Sets or returns RSA public key
150              
151             =head2 proxy
152              
153             my $proxy = $client->proxy;
154             $client->proxy('http://47.88.62.42:80');
155              
156             Get or set proxy
157              
158             =head2 req
159              
160             my $request = $clinet->req;
161              
162             Returns Mojo::Message::Request object
163              
164             =head2 request
165              
166             my $json = $clinet->request("METHOD", "PATH", ...ATTRIBUTES...);
167              
168             Send request
169              
170             =head2 res
171              
172             my $response = $clinet->res;
173              
174             Returns Mojo::Message::Response object
175              
176             =head2 status
177              
178             my $status = $clinet->status;
179             $clinet = $clinet->status(1);
180              
181             Returns object status value. 0 - Error; 1 - Ok
182              
183             =head2 str2url
184              
185             # http://localhost/api -> http://localhost/api/foo/bar
186             my $url = $self->str2url("foo/bar");
187              
188             # http://localhost/api -> http://localhost/foo/bar
189             my $url = $self->str2url("/foo/bar");
190              
191             # http://localhost/api/baz -> http://localhost/api/baz
192             my $url = $self->str2url("http://localhost/api/baz");
193              
194             Returns URL from specified sting
195              
196             =head2 token
197              
198             $clinet = $clinet->token("abc123...fcd");
199             my $token = $client->token;
200              
201             Returns token
202              
203             =head2 trace
204              
205             my $trace = $client->trace;
206             print $client->trace("New trace record");
207              
208             Gets trace stack or pushes new trace record to trace stack
209              
210             =head2 tx
211              
212             my $status = $clinet->tx($tx);
213              
214             Works with Mojo::Transaction object, interface with it
215              
216             =head2 tx_string
217              
218             print $client->tx_string;
219              
220             Retruns transaction status string
221              
222             =head2 ua
223              
224             my $ua = $clinet->ua;
225              
226             Returns Mojo::UserAgent object
227              
228             =head2 url
229              
230             my $url_object = $clinet->url;
231              
232             Returns Mojo::URL object
233              
234             =head1 API METHODS
235              
236             List of predefined the Suffit API methods
237              
238             =head2 check
239              
240             my $status = $client->check;
241             my $status = $client->check( URLorPath );
242              
243             Returns check-status of server. 0 - Error; 1 - Ok
244              
245             =head1 HTTP BASIC AUTHORIZATION
246              
247             For pass HTTP Basic Authorization with ask user credentials from console use follow code:
248              
249             my $client = WWW::Suffit::UserAgent->new(
250             ask_credentials => 1,
251             auth_scheme => 'Basic',
252             # ...
253             );
254              
255             ... and without ask:
256              
257             my $client = WWW::Suffit::UserAgent->new(
258             username => 'test',
259             password => 'test',
260             # ...
261             );
262              
263             You can also use credentials in the userinfo part of a base URL:
264              
265             my $client = WWW::Suffit::UserAgent->new(
266             url => 'https://test:test@localhost',
267             # ...
268             )
269              
270             =head1 TLS CLIENT CERTIFICATES
271              
272             $client->ua->cert('tls.crt')->key('tls.key')->ca('ca.crt');
273              
274             See L, L, L and L
275              
276             =head1 PROXY
277              
278             In constructor:
279              
280             my $client = WWW::Suffit::UserAgent->new(
281             proxy => 'http://47.88.62.42:80',
282             # ...
283             );
284              
285             Before request:
286              
287             my $status = $client
288             ->proxy('http://47.88.62.42:80')
289             ->request(GET => $client->str2url('http://ifconfig.io/all.json'));
290              
291             # Socks5
292             my $status = $client
293             ->proxy('socks://socks:socks@192.168.201.129:1080')
294             ->request(GET => $client->str2url('http://ifconfig.io/all.json'));
295              
296             Directly:
297              
298             $client->ua->proxy
299             ->http('http://47.88.62.42:80')
300             ->https('http://188.125.173.185:8080');
301              
302             my $status = $client
303             ->proxy('http://47.88.62.42:80')
304             #->proxy('socks://socks:socks@192.168.201.129:1080')
305             ->request(GET => $client->str2url('http://ifconfig.io/all.json'));
306              
307             =head1 DEPENDENCIES
308              
309             L, L
310              
311             =head1 TO DO
312              
313             See C file
314              
315             =head1 SEE ALSO
316              
317             L
318              
319             =head1 AUTHOR
320              
321             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
322              
323             =head1 COPYRIGHT
324              
325             Copyright (C) 1998-2023 D&D Corporation. All Rights Reserved
326              
327             =head1 LICENSE
328              
329             This program is free software; you can redistribute it and/or
330             modify it under the same terms as Perl itself.
331              
332             See C file and L
333              
334             =cut
335              
336             our $VERSION = '1.00';
337              
338 2     2   1316 use Mojo::UserAgent;
  2         935309  
  2         19  
339 2     2   101 use Mojo::UserAgent::Proxy;
  2         7  
  2         16  
340 2     2   54 use Mojo::Asset::File;
  2         5  
  2         10  
341 2     2   55 use Mojo::URL;
  2         8  
  2         9  
342 2     2   77 use Mojo::Util qw/steady_time b64_encode/;
  2         4  
  2         116  
343              
344 2     2   1073 use WWW::Suffit::Const qw/ DEFAULT_URL TOKEN_HEADER_NAME /;
  2         2371  
  2         175  
345 2     2   965 use WWW::Suffit::Util qw/ fbytes fduration /;
  2         4046  
  2         180  
346              
347             use constant {
348 2         5344 MAX_REDIRECTS => 10,
349             CONNECT_TIMEOUT => 10,
350             INACTIVITY_TIMEOUT => 30,
351             REQUEST_TIMEOUT => 180,
352             TRANSACTION_MASK => "%s %s >>> %s %s [%s in %s%s]", # GET /info >>> 200 OK [1.04 KB in 0.0242 seconds (43.1 KB/sec)]
353             CONTENT_TYPE => 'application/json',
354             REALM => 'Restricted zone',
355 2     2   16 };
  2         4  
356              
357             sub new {
358 0     0 1   my $class = shift;
359 0           my %args = @_;
360              
361             # General
362 0           $args{status} = 1; # Boolean status: 0 - error, 1 - ok
363 0           $args{error} = ""; # Error string (message) or HTTP Error message
364 0           $args{code} = 0; # HTTP Error code (integer) or error code string value (default is integer)
365              
366             # Base URL & URL Prefix
367 0   0       $args{url} = Mojo::URL->new($args{url} || DEFAULT_URL); # base url
368 0   0       $args{prefix} = $args{url}->path->to_string // ''; $args{prefix} =~ s/\/+$//;
  0            
369              
370             # HTTP Basic Authorization credentials
371 0           $args{credentials} = "";
372 0   0       $args{auth_scheme} ||= "";
373 0   0       $args{username} //= $args{url}->username // '';
      0        
374 0   0       $args{password} //= $args{url}->password // '';
      0        
375 0   0       $args{ask_credentials} ||= 0;
376              
377             # API/Access/Session token
378 0   0       $args{token} //= "";
379 0   0       $args{token_name} ||= TOKEN_HEADER_NAME;
380              
381             # Security
382 0   0       $args{public_key} //= "";
383 0   0       $args{private_key} //= "";
384              
385             # Proxy string
386 0   0       $args{proxy} //= "";
387              
388             # Transaction (tx)
389 0           $args{trace} = []; # trace pool
390 0           $args{tx_string} = "";
391 0           $args{tx_time} = 0;
392 0           $args{req} = undef;
393 0           $args{res} = undef;
394              
395             # User Agent
396 0           my $ua = $args{ua};
397 0 0         unless ($ua) {
398             # Create the instance
399             $ua = Mojo::UserAgent->new(
400             max_redirects => $args{max_redirects} || MAX_REDIRECTS,
401             connect_timeout => $args{connect_timeout} || CONNECT_TIMEOUT,
402             inactivity_timeout => $args{inactivity_timeout} || INACTIVITY_TIMEOUT,
403             request_timeout => $args{request_timeout} || REQUEST_TIMEOUT,
404 0   0       insecure => $args{insecure} || 0,
      0        
      0        
      0        
      0        
405             );
406 0           $ua->transactor->name(sprintf("%s/%s", __PACKAGE__, __PACKAGE__->VERSION));
407              
408             # Set proxy
409 0           my $proxy = Mojo::UserAgent::Proxy->new;
410 0 0         $ua->proxy($proxy->http($args{proxy})->https($args{proxy})) if $args{proxy};
411              
412 0           $args{ua} = $ua;
413             }
414              
415 0           my $self = bless {%args}, $class;
416 0           return $self;
417             }
418              
419             ## INTERFACE METHODS
420              
421             sub error {
422 0     0 1   my $self = shift;
423 0           my $e = shift;
424 0 0         if (defined $e) {
425 0           $self->{error} = $e;
426 0           return $self;
427             }
428 0           return $self->{error};
429             }
430             sub status {
431 0     0 1   my $self = shift;
432 0           my $s = shift;
433 0 0         if (defined $s) {
434 0           $self->{status} = $s;
435 0           return $self;
436             }
437 0           return $self->{status};
438             }
439             sub code {
440 0     0 1   my $self = shift;
441 0           my $c = shift;
442 0 0         if (defined $c) {
443 0           $self->{code} = $c;
444 0           return $self;
445             }
446 0           return $self->{code};
447             }
448             sub trace {
449 0     0 1   my $self = shift;
450 0           my $v = shift;
451 0 0         if (defined($v)) {
452 0           my $a = $self->{trace};
453 0           push @$a, $v;
454 0           return $v;
455             }
456 0   0       my $trace = $self->{trace} || [];
457 0           return join("\n",@$trace);
458             }
459             sub token {
460 0     0 1   my $self = shift;
461 0           my $t = shift;
462 0 0         if (defined $t) {
463 0           $self->{token} = $t;
464 0           return $self;
465             }
466 0           return $self->{token};
467             }
468             sub public_key {
469 0     0 1   my $self = shift;
470 0           my $k = shift;
471 0 0         if (defined $k) {
472 0           $self->{public_key} = $k;
473 0           return $self;
474             }
475 0           return $self->{public_key};
476             }
477             sub private_key {
478 0     0 1   my $self = shift;
479 0           my $k = shift;
480 0 0         if (defined $k) {
481 0           $self->{private_key} = $k;
482 0           return $self;
483             }
484 0           return $self->{private_key};
485             }
486             sub proxy {
487 0     0 1   my $self = shift;
488 0           my $p = shift;
489 0 0         return $self->{proxy} unless defined $p;
490 0           $self->{proxy} = $p;
491              
492             # Set proxy
493 0 0         $self->ua->proxy->http($p)->https($p) if length $p;
494              
495 0           return $self;
496             }
497             sub cleanup {
498 0     0 1   my $self = shift;
499 0           $self->{status} = 1;
500 0           $self->{error} = "";
501 0           $self->{code} = 0;
502 0           $self->{tx_string} = "";
503 0           undef $self->{req};
504 0           $self->{req} = undef;
505 0           undef $self->{res};
506 0           $self->{res} = undef;
507 0           undef $self->{trace};
508 0           $self->{trace} = [];
509 0           return $self;
510             }
511             sub req {
512 0     0 1   my $self = shift;
513 0           return $self->{req};
514             }
515             sub res {
516 0     0 1   my $self = shift;
517 0           return $self->{res};
518             }
519             sub url {
520 0     0 1   my $self = shift;
521 0           return $self->{url};
522             }
523             sub tx_string {
524 0     0 1   my $self = shift;
525 0   0       return $self->{tx_string} // '';
526             }
527             sub path2url {
528 0     0 1   my $self = shift;
529 0   0       my $p = shift // "/";
530 0           my $r = shift; # Is root, no use preffix
531 0           my $url = $self->url->clone;
532 0 0         my $path = $r ? $p : sprintf("%s/%s", $self->{prefix}, $p);
533 0           $path =~ s/\/{2,}/\//g;
534 0           return $url->path_query($path)->to_string;
535             }
536             sub str2url {
537 0     0 1   my $self = shift;
538 0   0       my $str = shift // "";
539 0 0         if ($str =~ /^https?\:\/\//) { # url (http/https)
    0          
    0          
540 0           return $str;
541             } elsif ($str =~ /^\//) { # absolute path (started from root, e.g.: /foo/bar)
542 0           return $self->path2url($str, 1);
543             } elsif (length $str) { # relative path (started from tail of base url, e.g.: foo/bar)
544 0           return $self->path2url($str);
545             }
546 0           return $self->url->clone->to_string;
547             }
548             sub ua {
549 0     0 1   my $self = shift;
550 0           return $self->{ua};
551             }
552             sub tx {
553 0     0 1   my $self = shift;
554 0           my $tx = shift;
555              
556             # Check Error
557 0           my $err = $tx->error;
558 0 0 0       unless (!$err || $err->{code}) {
559 0           $self->error($err->{message});
560 0           $self->status(0);
561             }
562 0   0       $self->code($tx->res->code || "000");
563 0 0         $self->status($tx->res->is_success ? 1 : 0);
564 0 0 0       $self->error($tx->res->json("/error") || $tx->res->json("/message") || $err->{message} || "Unknown transaction error" )
      0        
565             if $tx->res->is_error && !$self->error;
566              
567             # Transaction string
568 0   0       my $length = $tx->res->body_size || 0;
569 0   0       my $rtime = $self->{tx_time} // 0;
570             $self->{tx_string} = sprintf(TRANSACTION_MASK,
571             $tx->req->method, $tx->req->url->to_abs, # Method & URL
572 0 0 0       $self->code, $tx->res->message || $err->{message} || "Unknown error", # Line
573             fbytes($length), # Length
574             fduration($rtime), # Duration
575             $rtime ? sprintf(" (%s/sec)", fbytes($length/$rtime)) : "",
576             );
577              
578             # Tracing
579 0           $self->trace($self->{tx_string});
580 0           my $req_hdrs = $tx->req->headers->to_string;
581 0 0         if ($req_hdrs) {
582 0           $self->trace(join("\n", map {$_ = "> $_"} split(/\n/, $req_hdrs)));
  0            
583 0           $self->trace(">");
584             }
585 0           my $res_hdrs = $tx->res->headers->to_string;
586 0 0         if ($res_hdrs) {
587 0           $self->trace(join("\n", map {$_ = "< $_"} split(/\n/, $res_hdrs)));
  0            
588 0           $self->trace("<");
589             }
590              
591             # Request And Response
592 0           $self->{req} = $tx->req;
593 0           $self->{res} = $tx->res;
594              
595 0           return $self->status;
596             }
597             sub request {
598 0     0 1   my $self = shift;
599 0           my $meth = shift;
600 0           my $_url = shift;
601 0           my @params = @_;
602 0           $self->cleanup(); # Cleanup first
603              
604             # Set URL
605 0 0         my $url = $_url ? Mojo::URL->new("$_url") : $self->url->clone;
606 0           my $credentials = $self->credentials(0); # No ask!
607 0 0         $url->userinfo($credentials) if $credentials; # + credentials
608              
609             # Request
610 0           my $start_time = steady_time() * 1;
611 0           my $tx = $self->ua->build_tx($meth, $url, @params); # Create transaction (tx)
612 0           $self->_set_authorization_header($tx);
613 0           my $res_tx = $self->ua->start($tx); # Run it!
614 0           $self->{tx_time} = sprintf("%.*f",4, steady_time()*1 - $start_time) * 1;
615 0           my $status = $self->tx($res_tx); # Validate!);
616              
617             # Auth required? - for Basic scheme set credentials to URL
618 0 0 0       if (!$status && $self->{ask_credentials} && ($self->code == 401) && lc($self->{auth_scheme}) eq 'basic') {
      0        
      0        
619 0           $self->cleanup();
620 0           $credentials = $self->credentials(1); # Ask!;
621 0 0         $url->userinfo($credentials) if $credentials;
622              
623             # Request
624 0           $tx = $self->ua->build_tx($meth, $url, @params); # Create transaction (tx)
625 0           $self->_set_authorization_header($tx);
626 0           $res_tx = $self->ua->start($tx); # Run it!
627 0           $self->{tx_time} = sprintf("%.*f",4, steady_time()*1 - $start_time) * 1;
628 0           $status = $self->tx($res_tx); # Validate!;
629             }
630              
631 0           return $status;
632             }
633             sub credentials {
634 0     0 1   my $self = shift;
635 0 0         my $ask = shift(@_) ? 1 : 0;
636              
637             # Return predefined credentials
638 0 0         return $self->{credentials} if $self->{credentials};
639              
640             # Return predefined credentials if username and password are specified
641 0 0 0       if (length($self->{username}) && length($self->{password})) {
642 0           $self->{credentials} = sprintf("%s:%s", $self->{username}, $self->{password});
643 0           return $self->{credentials};
644             }
645              
646             # Prompt if ask flag is true and has terminal
647 0 0 0       if ($ask && -t STDIN) {
648 0           my ($username, $password);
649 0           printf STDERR "Enter username for %s at %s: ", REALM, $self->url->host_port;
650 0           $username = ;
651 0           chomp($username);
652 0 0         if (length($username)) {
653 0           print STDERR "Password: ";
654 0           system("stty -echo");
655 0           $password = ;
656 0           system("stty echo");
657 0           print STDERR "\n"; # because we disabled echo
658 0           chomp($password);
659 0           $self->{username} = $username;
660 0           $self->{password} = $password;
661             } else {
662 0           return "";
663             }
664 0           $self->{credentials} = sprintf("%s:%s", $username, $password);
665 0           return $self->{credentials};
666             }
667              
668 0           return "";
669             }
670              
671             ## SUFFIT API COMMON METHODS
672              
673             sub check {
674 0     0 1   my $self = shift;
675 0   0       my $url = shift // ''; # URL or String (api)
676 0           return $self->request(HEAD => $self->str2url($url));
677             }
678              
679             ## INTERNAL METHODS
680              
681             sub _set_authorization_header {
682 0     0     my $self = shift;
683 0           my $tx = shift;
684 0           my $scheme = lc($self->{auth_scheme});
685 0           my $header_name = 'Authorization';
686 0           my $header_value = '';
687              
688             # HTTP Authentication Schemes: https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml
689 0 0         if ($scheme eq 'bearer') { # Bearer [RFC6750]
    0          
    0          
690 0 0         $header_value = sprintf('Bearer %s', $self->token) if $self->token;
691             } elsif ($scheme eq 'basic') { # Basic [RFC7617]
692             $header_value = sprintf('Basic %s',
693             b64_encode(sprintf('%s:%s',
694             $self->{username} // 'anonymous',
695 0   0       $self->{password} // ''
      0        
696             ), '')
697             );
698             } elsif ($self->token) { # Oops! Use custom header
699 0           $tx->req->headers->header($self->{token_name}, $self->token);
700 0           return $self->token;
701             } else {
702 0           return undef;
703             }
704              
705             # Set header
706 0 0         $tx->req->headers->header($header_name, $header_value) if $header_value;
707 0           return $header_value;
708             }
709              
710             1;
711              
712             __END__