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