File Coverage

blib/lib/MToken.pm
Criterion Covered Total %
statement 108 236 45.7
branch 0 68 0.0
condition 0 41 0.0
subroutine 36 54 66.6
pod 12 12 100.0
total 156 411 37.9


line stmt bran cond sub pod time code
1             package MToken; # $Id: MToken.pm 116 2021-10-12 15:17:49Z minus $
2 1     1   71535 use strict;
  1         8  
  1         29  
3 1     1   671 use utf8;
  1         15  
  1         5  
4              
5             =encoding utf-8
6              
7             =head1 NAME
8              
9             MToken - Tokens processing system (Security)
10              
11             =head1 VERSION
12              
13             Version 1.04
14              
15             =head1 SYNOPSIS
16              
17             use MToken;
18              
19             =head1 DESCRIPTION
20              
21             Tokens processing system (Security)
22              
23             =head2 client
24              
25             my $client = $mt->client;
26              
27             Returns the Mojo client (user agent) instance
28              
29             =head2 execmd
30              
31             my %exest = $self->execmd("command", "arg1", "arg2", "argn");
32              
33             Performs execute system commands and returns hash:
34              
35             =over 8
36              
37             =item command
38              
39             The command line
40              
41             =item status
42              
43             The status of operation. 1 - no errors; 0 - error
44              
45             =item exitval
46              
47             The exitval value
48              
49             =item error
50              
51             The error message
52              
53             =item output
54              
55             The data from program
56              
57             =back
58              
59             =head2 get_fingerprint
60              
61             Returns the fingerprint from local config or ask it
62              
63             =head2 get_gpgbin
64              
65             Returns the GNUPG path from local config
66              
67             =head2 get_manifest
68              
69             Returns manifest of current token
70              
71             =head2 get_name
72              
73             Returns name of current token
74              
75             =head2 get_opensslbin
76              
77             Returns the OpenSSL path from local config
78              
79             =head2 get_server_url
80              
81             Returns SERVER_URL from local config
82              
83             =head2 lconfig
84              
85             my $lconfig = $mt->lconfig;
86              
87             Returns local config instance
88              
89             =head2 raise
90              
91             return $mt->raise("Red message");
92              
93             Sends message to STDERR and returns 0
94              
95             =head2 store
96              
97             my $store = $mt->store;
98              
99             Returns the Store instance (Database)
100              
101             =head1 HISTORY
102              
103             See C file
104              
105             =head1 DEPENDENCIES
106              
107             C, C
108              
109             =head1 TO DO
110              
111             See C file
112              
113             =head1 BUGS
114              
115             * none noted
116              
117             =head1 SEE ALSO
118              
119             L, L
120              
121             =head1 AUTHOR
122              
123             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
124              
125             =head1 COPYRIGHT
126              
127             Copyright (C) 1998-2021 D&D Corporation. All Rights Reserved
128              
129             =head1 LICENSE
130              
131             This program is free software; you can redistribute it and/or
132             modify it under the same terms as Perl itself.
133              
134             See C file and L
135              
136             =cut
137              
138 1     1   54 use vars qw/ $VERSION /;
  1         2  
  1         80  
139             $VERSION = "1.04";
140              
141 1     1   7 use feature qw/say/;
  1         2  
  1         110  
142 1     1   7 use Carp;
  1         1  
  1         57  
143 1     1   609 use Encode; # Encode::_utf8_on();
  1         10772  
  1         65  
144 1     1   511 use Encode::Locale;
  1         4181  
  1         54  
145              
146 1     1   1793 use Archive::Tar;
  1         155615  
  1         75  
147 1     1   9 use Cwd qw/getcwd/;
  1         2  
  1         47  
148 1     1   6 use Digest::MD5 qw/md5_hex/;
  1         1  
  1         66  
149 1     1   647 use ExtUtils::Manifest qw/maniread/;
  1         11784  
  1         64  
150 1     1   8 use File::Spec;
  1         2  
  1         19  
151 1     1   1264 use File::HomeDir;
  1         8804  
  1         58  
152 1     1   14 use File::Find;
  1         2  
  1         45  
153 1     1   517 use File::stat qw//;
  1         7375  
  1         30  
154 1     1   6 use List::Util qw/uniq/;
  1         2  
  1         94  
155 1     1   644 use POSIX qw//;
  1         6846  
  1         30  
156 1     1   611 use Text::SimpleTable;
  1         2601  
  1         33  
157 1     1   1275 use URI;
  1         8622  
  1         50  
158              
159 1     1   783 use Mojo::File qw/path/;
  1         185217  
  1         77  
160 1     1   11 use Mojo::Util qw/tablify steady_time/;
  1         2  
  1         97  
161 1     1   495 use Mojo::Date qw//;
  1         4784  
  1         45  
162 1     1   523 use Mojo::Server::Prefork;
  1         295683  
  1         17  
163              
164 1     1   812 use CTK::Skel;
  1         104442  
  1         52  
165 1     1   13 use CTK::Util qw/preparedir which dtf tz_diff isTrueFlag rundir sharedir sharedstatedir/;
  1         3  
  1         123  
166 1     1   656 use CTK::UtilXS qw/wipe/;
  1         1483  
  1         135  
167 1     1   11 use CTK::TFVals qw/ :ALL /;
  1         3  
  1         251  
168 1     1   8 use CTK::ConfGenUtil;
  1         3  
  1         70  
169              
170 1     1   593 use MToken::Const;
  1         3  
  1         155  
171 1     1   667 use MToken::Util qw/explain sha1sum red green yellow cyan blue magenta yep nope skip wow filesize/;
  1         3  
  1         149  
172 1     1   440 use MToken::Config;
  1         4  
  1         41  
173 1     1   735 use MToken::Store;
  1         3  
  1         48  
174 1     1   709 use MToken::Server;
  1         5  
  1         12  
175 1     1   817 use MToken::Client;
  1         4  
  1         54  
176              
177 1     1   13 use base qw/ CTK::App /;
  1         2  
  1         832  
178              
179             use constant {
180 1         13463 ERROR_NO_TOKEN => "No token selected. Please use --datadir option or change the current directory to Your token device",
181 1     1   7872 };
  1         3  
182              
183             __PACKAGE__->register_handler(
184             handler => "test",
185             description => "MToken testing (internal use only)",
186             code => sub {
187             ### CODE:
188             my ($self, $meta, @arguments) = @_;
189              
190             #say tablify [['foo', 'bar'], ['yadaffffgff', 'yada'], ['baz', 'yada']];
191              
192             #my $fingerprint = $self->get_fingerprint;
193             #say explain(\%exest);
194             #my @strings = split("\n", $exest{output});
195             #say explain(\@strings);
196              
197             return 1;
198             });
199              
200             __PACKAGE__->register_handler(
201             handler => "version",
202             description => sprintf("%s Version", PROJECTNAME),
203             code => sub {
204             ### CODE:
205             my ($self, $meta, @arguments) = @_;
206             printf("%s/%s\n", PROJECTNAME, $self->VERSION);
207             return 1;
208             });
209              
210             __PACKAGE__->register_handler(
211             handler => "status",
212             description => "Get status information",
213             code => sub {
214             ### CODE:
215             my ($self, $meta, @arguments) = @_;
216              
217             printf("Version : %s\n", $self->VERSION);
218             printf("Data dir : %s\n", $self->datadir);
219             printf("Temp dir : %s\n", $self->tempdir);
220             printf("Global config : %s\n", $self->conf("loadstatus") ? $self->configfile : yellow("not loaded"));
221             $self->debug(explain($self->config)) if $self->conf("loadstatus") && $self->verbosemode;
222             printf("Local config : %s\n", $self->lconfig->is_loaded ? green("loaded") : red("not loaded"));
223             $self->debug(explain($self->lconfig)) if $self->lconfig->is_loaded && $self->verbosemode;
224              
225             # Return if no token selected
226             return 1 unless $self->lconfig->is_loaded;
227              
228             # Database
229              
230             my $store = $self->store;
231             printf("DB DSN : %s\n", $store->dsn);
232             printf("DB status : %s\n", $store->status ? green("ok") : red($store->error || sprintf("Store (%s): Unknown error", $store->dsn)));
233             if ($store->file) {
234             my $s = filesize($store->file) || 0;
235             printf("DB size : %s\n", $store->status ? sprintf("%s (%d bytes)", _fbytes($s), $s) : yellow("unknown"));
236             printf("DB modified : %s\n", $store->status ? _fdate(File::stat::stat($store->file)->mtime || 0) : yellow("unknown"));
237             }
238             printf("Stored files : %s\n", $store->status ? $store->count || 0 : yellow("unknown"));
239              
240             # Server
241             my $client = $self->client;
242             $client->check(); # Check
243             printf("Server URL : %s\n", $client->url ? $client->url->to_string : yellow("unknown"));
244             printf("Server status : %s\n", $client->status ? green("ok") : red($client->error));
245             $self->debug($client->trace);
246              
247             # Get info from server
248             if ($client->status) {
249             if ($client->info($self->get_name)) {
250             my $files = array($client->res->json("/files"));
251              
252             # Init table
253             my $tbl = Text::SimpleTable->new(
254             [24, 'TARBALL FILE'],
255             [10, 'FILE SIZE'],
256             [25, 'MAKE TIME'],
257             );
258             my $i = 0;
259             my $tz = tz_diff();
260             # Table caption
261             foreach my $row (@$files) {
262             $i++;
263             $tbl->row(
264             $row->{filename} || "noname",
265             _fbytes($row->{size} || 0),
266             dtf(DATETIME_FORMAT . " " . $tz, $row->{mtime} || 0),
267             );
268             }
269             # Show table
270             if ($i) {
271             print $tbl->draw();
272             say cyan("total %d file(s)", $i);
273             } else {
274             say yellow("No data found on server");
275             }
276             } else {
277             say red($client->error);
278             $self->debug($client->trace);
279             }
280             }
281              
282             return 1;
283             });
284              
285             __PACKAGE__->register_handler(
286             handler => "init",
287             description => "Initialize token",
288             code => sub {
289             ### CODE:
290             my ($self, $meta, @arguments) = @_;
291             my $tkn = shift(@arguments);
292             my $dir = $self->datadir || getcwd(); # Destination directory
293              
294             # Prepare DataDir if specified
295             if ($self->option("datadir")) {
296             unless (preparedir($dir)) {
297             $self->error(sprintf("Can't prepare directory %s", $dir));
298             return 0;
299             }
300             }
301              
302             # Project name
303             $tkn ||= $self->cli_prompt('Token name:', $self->prefix);
304             $tkn = lc($tkn);
305             $tkn =~ s/\s+//g;
306             $tkn =~ s/[^a-z0-9]//g;
307             $tkn ||= $self->prefix;
308             if ($tkn =~ /^\d/) {
309             $self->error("The token name must not begin with numbers. Choose another name consisting mainly of letters of the Latin alphabet");
310             return 0;
311             }
312              
313             printf("Initializing token \"%s\"...\n", $tkn);
314              
315             # Initialize local configuration for device
316             $self->{lconfig} = MToken::Config->new(
317             file => File::Spec->catfile($dir, DIR_PRIVATE, DEVICE_CONF_FILE),
318             name => $tkn,
319             );
320             #say(explain($self->lconfig));
321             my %before = $self->lconfig->getall;
322              
323              
324             # Ask OpenSSL
325             my $opensslbin = $self->cli_prompt('OpenSSL program:', $self->lconfig->get("opensslbin") ||
326             $self->conf("opensslbin") || which(OPENSSLBIN) || OPENSSLBIN);
327             unless ($opensslbin) {
328             return $self->raise("Program openssl not found. Please install it and try again later");
329             } else {
330             my $cmd = [$opensslbin, "version"];
331             my $err = "";
332             my $out = CTK::Util::execute( $cmd, undef, \$err );
333             if ($err) {
334             say cyan("#", join(" ", @$cmd));
335             say STDERR red($err);
336             }
337             return $self->raise("Program openssl not found. Please install it and try again later") unless $out;
338             unless ($out =~ /^OpenSSL\s+[1-9]\.[0-9]/m) {
339             say STDERR yellow("OpenSSL Version is not correctly. May be some problems");
340             say cyan($out) if $self->verbosemode;
341             }
342             }
343             $self->lconfig->set(opensslbin => $opensslbin);
344              
345             # Ask GnuPG
346             my $gpgbin = $self->cli_prompt('GnuPG (gpg) program:', $self->lconfig->get("ogpgbin") ||
347             $self->conf("gpgbin") || which(GPGBIN) || GPGBIN);
348             unless ($gpgbin) {
349             return $self->raise("Program GnuPG (gpg) not found. Please install it and try again later");
350             } else {
351             my $cmd = [$gpgbin, "--version"];
352             my $err = "";
353             my $out = CTK::Util::execute( $cmd, undef, \$err );
354             if ($err) {
355             say cyan("#", join(" ", @$cmd));
356             say STDERR red($err);
357             }
358             return $self->raise("Program GnuPG (gpg) not found. Please install it and try again later") unless $out;
359             unless ($out =~ /^gpg\s+\(GnuPG\)\s+[2-9]\.[0-9]/m) {
360             say STDERR yellow("GnuPG Version is not correctly. May be some problems");
361             say cyan($out) if $self->verbosemode;
362             }
363             }
364             $self->lconfig->set(gpgbin => $gpgbin);
365              
366             # Ask fingerprint
367             my $fingerprint = $self->get_fingerprint;
368             $self->lconfig->set(fingerprint => $fingerprint) if $fingerprint;
369              
370             # Server URL (server_url)
371             my $default_url = _get_default_url($tkn);
372             my $server_url = $self->cli_prompt('Server URL:', MToken::Util::hide_pasword($self->lconfig->get("server_url")
373             || $self->conf("server_url") || $default_url, 1));
374             my $uri = URI->new( $server_url );
375             my $url = $uri->canonical->as_string;
376              
377             # Server credentials
378             if ($self->cli_prompt('Ask the credentials interactively (Recommended, t. It\'s safer)?:','yes') =~ /^\s*y/i) {
379             $uri->userinfo(undef);
380             } else {
381             my ($server_user, $server_password) = MToken::Util::parse_credentials($uri);
382             unless ($server_user) { # User
383             $server_user = $self->cli_prompt('Server user:', "anonymous") // "";
384             $server_user =~ s/%/%25/g;
385             $server_user =~ s/:/%3A/g;
386             }
387             unless ($server_password) { # Password
388             system("stty -echo") unless IS_MSWIN;
389             $server_password = $self->cli_prompt('Server password:', "none") // "";
390             $server_password =~ s/%/%25/g;
391             system("stty echo") unless IS_MSWIN;
392             print STDERR "\n"; # because we disabled echo
393             $server_password = "" if $server_password eq "none";
394             }
395             $uri->userinfo(sprintf("%s:%s", $server_user, $server_password));
396             $url = $uri->canonical->as_string;
397             wow("Full server URL: %s", MToken::Util::hide_pasword($url));
398             }
399             $self->lconfig->set(server_url => $url);
400              
401             # Hash Diff and Save
402             my %after = $self->lconfig->getall;
403             if (_hashmd5(%before) eq _hashmd5(%after)) {
404             skip("Nothing changed in current configuration data");
405             } elsif ($self->cli_prompt('Are you sure you want to save all changes to local configuration file?:','yes') =~ /^\s*y/i) {
406             if ($self->lconfig->save) {
407             yep("File \"%s\" successfully saved", $self->lconfig->{local_config_file});
408             } else {
409             return $self->raise("Can't save file \"%s\"", $self->lconfig->{local_config_file});
410             }
411             }
412              
413             # Skeleton
414             my $skel = CTK::Skel->new (
415             -name => $tkn,
416             -root => $dir,
417             -skels => {
418             device => 'MToken::DeviceSkel',
419             },
420             -debug => $self->debugmode,
421             );
422             #say("Skel object: ", explain($skel));
423              
424             # Ask
425             return skip("Aborted") unless $self->cli_prompt("Are you sure you want to build token $tkn to \"$dir\"?:",'no') =~ /^\s*y/i;
426              
427             # Build
428             my %vars = (
429             PACKAGE => __PACKAGE__,
430             VERSION => $self->VERSION, MTOKEN_VERSION => $self->VERSION,
431             TOKEN => $tkn, TOKEN_NAME => $tkn,
432             SERVER_URL => $url,
433             );
434             return $self->raise("Can't build the token to \"%s\" directory", $dir)
435             unless $skel->build("device", $dir, {%vars});
436              
437             # Database (store)
438             my $store = $self->store(do_init => 1);
439             return $self->raise($store->error ? $store->error : sprintf("Store (%s): Unknown error", $store->dsn))
440             unless $store->status;
441             #say(explain($store));
442              
443             # Ok
444             return yep("Done");
445             });
446              
447             __PACKAGE__->register_handler(
448             handler => "add",
449             description => "Add file(s) to token",
450             code => sub {
451             ### CODE:
452             my ($self, $meta, @arguments) = @_;
453             unless ($self->lconfig->is_loaded) {
454             $self->error(ERROR_NO_TOKEN);
455             return 0;
456             }
457              
458             # Database
459             my $store = $self->store;
460             unless ($store->status) {
461             $self->error($store->error || sprintf("Store (%s): Unknown error", $store->dsn));
462             return 0;
463             }
464              
465             # Input files
466             my @in_files = uniq(_expand_wildcards(@arguments));
467             unless (scalar(@in_files)) {
468             $self->error("No input file(s) specified");
469             return 0;
470             }
471              
472             # Get Fingerprint
473             my $fingerprint = $self->get_fingerprint;
474             unless ($fingerprint) {
475             $self->error("No fingerprint specified");
476             return 0;
477             }
478              
479             # Processing every file
480             foreach my $in_file (@in_files) {
481             my $in_file_path = path($in_file);
482             $in_file = $in_file_path->to_abs->to_string;
483              
484             # Check input file first
485             unless ($in_file && -f $in_file) {
486             skip("Can't load file %s", $in_file);
487             next;
488             }
489              
490             # Get file info
491             my $fname = $in_file_path->basename();
492             my $size = filesize($in_file_path->to_string);
493             my $mtime = File::stat::stat($in_file_path->to_string)->mtime;
494             my $sha1 = sha1sum($in_file);
495             #say explain([$fname, $size, $mtime, $sha1]);
496              
497             # Get info from DB
498             my %db_info = $store->get($fname);
499             unless ($store->status) {
500             $self->raise($store->error);
501             next;
502             }
503             if ($db_info{id}) {
504             unless ($self->option("force") || $self->cli_prompt('The file '.$in_file.' already exists in token. Are you sure you want to update it file?:','yes') =~ /^\s*y/i) {
505             skip("Skip file %s", $in_file);
506             next;
507             }
508             }
509              
510             # Ask subject
511             my $subject = $self->option("force")
512             ? $db_info{subject}
513             : decode(locale => $self->cli_prompt('Subject (commas, slash or backslash is as line delimiter):', encode(locale => $db_info{subject} || "")));
514              
515             # Ask tags
516             my $tags = $self->option("force")
517             ? $db_info{tags}
518             : decode(locale => $self->cli_prompt('Tags (commas or spaces are tag delimiter):', encode(locale => $db_info{tags} || "")));
519              
520              
521             # New filename
522             my $out_file = File::Spec->catfile($self->tempdir, sprintf("%s.gpg", $fname));
523             #say $out_file;
524              
525             # Encrypt file to tempdir
526             my %exest = $self->execmd($self->get_gpgbin, "--encrypt", "--armor", "--quiet", "--recipient", $fingerprint, "--output", $out_file, $in_file);
527             unless ($exest{status} && -f $out_file) {
528             $self->raise("Can't encrypt file %s", $in_file);
529             next;
530             }
531              
532             # Get path object
533             my $out_file_path = path($out_file);
534              
535             # Add/Set new record
536             my @sarg = (
537             file => $fname,
538             size => $size,
539             mtime => $mtime,
540             checksum => $sha1,
541             tags => $tags,
542             subject => $subject,
543             content => $out_file_path->slurp,
544             );
545             my $sts = $db_info{id} ? $store->set(id => $db_info{id}, @sarg) : $store->add(@sarg);
546             unless ($sts) {
547             $out_file_path->remove;
548             $self->raise($store->error);
549             next;
550             }
551              
552             # Remove output file
553             $out_file_path->remove;
554              
555             # Remove source file (if set the remove option)
556             if ($self->option("remove")) {
557             if ($self->option("force") || $self->cli_prompt('Are you sure you want to remove file '.$in_file.'?:','no') =~ /^\s*y/i) {
558             wipe($in_file);
559             $in_file_path->remove;
560             }
561             }
562              
563             # Ok
564             yep("File %s successfully added", $in_file);
565             }
566              
567             # Ok
568             return yep("Done");
569              
570             return 1;
571             });
572              
573             __PACKAGE__->register_handler(
574             handler => "list",
575             description => "Files list on token",
576             code => sub {
577             ### CODE:
578             my ($self, $meta, @arguments) = @_;
579             unless ($self->lconfig->is_loaded) {
580             $self->error(ERROR_NO_TOKEN);
581             return 0;
582             }
583              
584             # Database
585             my $store = $self->store;
586             unless ($store->status) {
587             $self->error($store->error || sprintf("Store (%s): Unknown error", $store->dsn));
588             return 0;
589             }
590              
591             # Get info from DB
592             my $page = $self->option("page") || 1;
593             my $cnt = $store->count || 0;
594             my $of = ($cnt - $cnt % RECORDS_PER_PAGE)/RECORDS_PER_PAGE + 1;
595             $page = $of if $page > $of;
596             say cyan("File list of \"%s\"", $self->get_name);
597              
598             my @table = $store->getall(($page - 1) * RECORDS_PER_PAGE, RECORDS_PER_PAGE); # offset, row_count
599             unless ($store->status) {
600             $self->error($store->error);
601             return 0;
602             }
603              
604             # Init table
605             my $tbl_hdrs = [(
606             [SCREENWIDTH() - 54, 'FILE/SUBJECT'],
607             [21, 'TAGS'],
608             [10, 'SIZE, B'],
609             [10, 'MTIME'],
610             )];
611             my $tbl = Text::SimpleTable->new(@$tbl_hdrs);
612              
613             # Show table
614             my $i = 0;
615             my $c = scalar(@table);
616             foreach my $row (@table) {
617             $i++;
618             #$tbl->row("Test.txt\nTest document", "foo, bar, baz", 1024, "2020-12-12\n12:12:12");
619             $tbl->row(
620             sprintf("%s\n %s%s",
621             $row->[1] || "noname",
622             encode(locale => $row->[6] || ''),
623             "", #($c > $i ? "\n" : ""),
624             ),
625             encode(locale => $row->[5] || '-'),
626             _fbytes($row->[2] || 0),
627             sprintf("%s\n %s",
628             dtf(DATE_FORMAT, $row->[3] || 0),
629             dtf(TIME_FORMAT, $row->[3] || 0),
630             ),
631             );
632             #$tbl->hr if $c > $i;
633             }
634              
635             # Show table
636             print $tbl->draw();
637             say cyan("total %d file(s); page %d of %d", $store->count || 0, $page, $of);
638              
639             return 1;
640             });
641              
642             __PACKAGE__->register_handler(
643             handler => "info",
644             description => "Get file/database information",
645             code => sub {
646             ### CODE:
647             my ($self, $meta, @arguments) = @_;
648             unless ($self->lconfig->is_loaded) {
649             $self->error(ERROR_NO_TOKEN);
650             return 0;
651             }
652              
653             # Database
654             my $store = $self->store;
655             unless ($store->status) {
656             $self->error($store->error || sprintf("Store (%s): Unknown error", $store->dsn));
657             return 0;
658             }
659              
660             # Input file
661             my $filename = shift @arguments;
662             if ($filename) {
663             my %data = $store->get($filename);
664             unless ($store->status) {
665             $self->error($store->error || sprintf("Store (%s): Unknown error", $store->dsn));
666             return 0;
667             }
668              
669             # Show table
670             say tablify([
671             ['Filename :', $filename],
672             ['Id :', $data{id} || 0],
673             ['Size :', sprintf("%s (%d bytes)", _fbytes($data{size} || 0), $data{size} || 0)],
674             ['MTime :', _fdate($data{mtime} || 0)],
675             ['Checksum :', $data{checksum} || ""],
676             ['Tags :', encode(locale => $data{tags} || "")],
677             ]);
678             say cyan(encode(locale => $data{subject} || "none")), "\n";
679             say $data{content} || "" if $self->verbosemode;
680             } else {
681             my $count = $store->count || 0;
682             unless ($store->status) {
683             $self->error($store->error || sprintf("Store (%s): Unknown error", $store->dsn));
684             return 0;
685             }
686             my $dbfile = $store->{file};
687             my $dbfile_size = ($dbfile && -e $dbfile) ? filesize($dbfile) || 0 : 0;
688             say tablify([
689             ['DSN :', $store->dsn || ""],
690             ['Files in DB :', $count || 0],
691             ($dbfile ? (
692             ['DB File :', $dbfile],
693             ['DB File size :', sprintf("%s (%d bytes)", _fbytes($dbfile_size), $dbfile_size)],
694             ) : ()),
695             ]);
696             }
697              
698             return 1;
699             });
700              
701             __PACKAGE__->register_handler(
702             handler => "get",
703             description => "Get (extract) file from token to disk",
704             code => sub {
705             ### CODE:
706             my ($self, $meta, @arguments) = @_;
707             unless ($self->lconfig->is_loaded) {
708             $self->error(ERROR_NO_TOKEN);
709             return 0;
710             }
711              
712             # Database
713             my $store = $self->store;
714             unless ($store->status) {
715             $self->error($store->error || sprintf("Store (%s): Unknown error", $store->dsn));
716             return 0;
717             }
718              
719             # Input file
720             my $filename = shift @arguments;
721             unless ($filename) {
722             $self->error("No input file specified");
723             return 0;
724             }
725              
726             # Get data from database
727             my %data = $store->get($filename);
728             unless ($store->status) {
729             $self->error($store->error || sprintf("Store (%s): Unknown error", $store->dsn));
730             return 0;
731             }
732              
733             # Get file names
734             my $enc_file_path = path($self->tempdir, sprintf("%s.gpg", $filename));
735             my $dec_file_path = path($self->option("output") || File::Spec->catfile(getcwd(), $filename));
736             #say explain({enc_file_path => $enc_file_path->to_string, dec_file_path => $dec_file_path->to_string});
737              
738             # Write file content on disk (spurt, spew; see also Module::Build::Base::_spew)
739             $enc_file_path->spurt($data{content} || "");
740             unless (filesize($enc_file_path->to_string)) {
741             $self->error(sprintf("Can't load empty file %s", $enc_file_path->to_string));
742             return 0;
743             }
744              
745             # Decrypt file to tempdir
746             # gpg -d -q -o $bname $1
747             my $out_file = $dec_file_path->to_string;
748             my %exest = $self->execmd($self->get_gpgbin, "--decrypt", "--quiet", "--output", $out_file, $enc_file_path->to_string);
749             unless ($exest{status} && -e $out_file) {
750             $self->error(sprintf("Can't decrypt file %s", $enc_file_path->to_string));
751             my $newfile = $enc_file_path->copy_to(sprintf("%s.gpg", $out_file));
752             say magenta("The encrypted file has been stored to %s", $newfile->to_string) if filesize($newfile->to_string);
753             return 0;
754             }
755              
756             # Check size
757             my $nsize = filesize($dec_file_path->to_string) || 0;
758             unless ($nsize == ($data{size} || 0)) {
759             $self->error(sprintf("File size mismatch (%s). Expected %d, got %d", $out_file, $nsize, $data{size} || 0));
760             return 0;
761             }
762              
763             # Check sha1
764             my $sha1 = sha1sum($out_file);
765             unless ($sha1 eq ($data{checksum} || "~")) {
766             $self->error(sprintf("File checksum mismatch (%s)", $out_file));
767             return 0;
768             }
769              
770             # Change utime
771             if ($data{mtime}) {
772             utime(time(), $data{mtime}, $out_file) || skip("Couldn't touch %s: %s", $out_file, $!);
773             }
774              
775             # Remove temp file
776             $enc_file_path->remove;
777              
778             # Ok
779             yep("File %s successfully extracted", $out_file);
780             say cyan(encode(locale => $data{subject} || "none")), "\n";
781              
782             return 1;
783             });
784              
785             __PACKAGE__->register_handler(
786             handler => "show",
787             description => "Extract and print file from token to STDOUT",
788             code => sub {
789             ### CODE:
790             my ($self, $meta, @arguments) = @_;
791             unless ($self->lconfig->is_loaded) {
792             $self->error(ERROR_NO_TOKEN);
793             return 0;
794             }
795              
796             # Database
797             my $store = $self->store;
798             unless ($store->status) {
799             $self->error($store->error || sprintf("Store (%s): Unknown error", $store->dsn));
800             return 0;
801             }
802              
803             # Input file
804             my $filename = shift @arguments;
805             unless ($filename) {
806             $self->error("No input file specified");
807             return 0;
808             }
809              
810             # Get data from database
811             my %data = $store->get($filename);
812             unless ($store->status) {
813             $self->error($store->error || sprintf("Store (%s): Unknown error", $store->dsn));
814             return 0;
815             }
816              
817             # Show raw file
818             if ($self->option("raw")) {
819             say $data{content} || "";
820             return 1;
821             }
822              
823             # Get file names
824             my $enc_file_path = path($self->tempdir, sprintf("%s.gpg", $filename));
825             my $dec_file_path = path($self->tempdir, $filename);
826              
827             # Write file content on disk (spurt, spew; see also Module::Build::Base::_spew)
828             my $in_file = $enc_file_path->to_string;
829             $enc_file_path->spurt($data{content} || "");
830             unless (filesize($enc_file_path->to_string)) {
831             $self->error(sprintf("Can't load empty file %s", $in_file));
832             return 0;
833             }
834              
835             # Decrypt file to tempdir
836             # gpg -d -q -o $bname $1
837             my $out_file = $dec_file_path->to_string;
838             my %exest = $self->execmd($self->get_gpgbin, "--decrypt", "--quiet", "--output", $out_file, $in_file);
839             unless ($exest{status} && -e $out_file) {
840             $self->error(sprintf("Can't decrypt file %s", $in_file));
841             say $data{content} || "";
842             return 0;
843             }
844              
845             # Check size
846             my $nsize = filesize($dec_file_path->to_string) || 0;
847             unless ($nsize && $nsize == ($data{size} || 0)) {
848             $self->error(sprintf("File size mismatch (%s). Expected %d, got %d", $out_file, $nsize, $data{size} || 0));
849             return 0;
850             }
851              
852             # Check sha1
853             my $sha1 = sha1sum($out_file);
854             unless ($sha1 eq ($data{checksum} || "~")) {
855             $self->error(sprintf("File checksum mismatch (%s)", $out_file));
856             return 0;
857             }
858              
859             # Check text or binary file (-T)
860             if (-B $out_file) {
861             say STDERR yellow("File %s is binary!\nPlease use the \"get\" command for extract it as file", $out_file);
862             say STDERR cyan(encode(locale => $data{subject} || "none")), "\n";
863             } else {
864             say $dec_file_path->slurp;
865             }
866              
867             # Remove temp files
868             $enc_file_path->remove;
869             wipe($out_file);
870             $dec_file_path->remove;
871              
872             return 1;
873             });
874              
875             __PACKAGE__->register_handler(
876             handler => "del",
877             description => "Delete file from token",
878             code => sub {
879             ### CODE:
880             my ($self, $meta, @arguments) = @_;
881             unless ($self->lconfig->is_loaded) {
882             $self->error(ERROR_NO_TOKEN);
883             return 0;
884             }
885              
886             # Database
887             my $store = $self->store;
888             unless ($store->status) {
889             $self->error($store->error || sprintf("Store (%s): Unknown error", $store->dsn));
890             return 0;
891             }
892              
893             # Input file
894             my $filename = shift @arguments;
895             unless ($filename) {
896             $self->error("No input file specified");
897             return 0;
898             }
899              
900             # Get data from database
901             my %data = $store->get($filename);
902             unless ($store->status) {
903             $self->error($store->error || sprintf("Store (%s): Unknown error", $store->dsn));
904             return 0;
905             }
906             unless ($data{id}) {
907             $self->error("File not found");
908             return 0;
909             }
910              
911             # Delete file
912             if ($self->option("force") || $self->cli_prompt('Are you sure you want to remove file '.$filename .'?:','no') =~ /^\s*y/i) {
913             $store->del($filename) or do {
914             $self->error($store->error);
915             return 0;
916             };
917             } else {
918             return skip("Aborted. Skip file %s", $filename);
919             }
920              
921             # Ok
922             return yep("File %s successfully deleted", $filename);
923             });
924              
925             __PACKAGE__->register_handler(
926             handler => "genkey",
927             description => "Generate",
928             code => sub {
929             ### CODE:
930             my ($self, $meta, @arguments) = @_;
931              
932             # Output file
933             my $filename = shift(@arguments) || $self->option("output");
934             my $path = $filename
935             ? path($filename)
936             : $self->lconfig->is_loaded
937             ? path($self->datadir, DIR_PRIVATE, RND_KEY_FILE)
938             : path(getcwd(), RND_KEY_FILE);
939             my $file_out = $path->to_string;
940             if (-e $file_out) {
941             unless ($self->option("force") ||
942             $self->cli_prompt('File '.$file_out.' already exists. Are you sure you want to replace this file?:','no') =~ /^\s*y/i) {
943             return skip("Aborted. Skip file %s", $file_out);
944             }
945             }
946              
947             # Get size
948             my $size = $self->option("size") ||
949             int(rand(MToken::Const::KEYMAXSIZE - MToken::Const::KEYMINSIZE)) + MToken::Const::KEYMINSIZE;
950              
951             my %exest = $self->execmd($self->get_opensslbin, "rand", "-out", $file_out, $size);
952             unless ($exest{status} && -e $file_out) {
953             $self->error(sprintf("Can't generate rand key file %s", $file_out));
954             return 0;
955             }
956             say cyan($exest{output}) if $exest{output};
957              
958             # Ok
959             return yep("File %s successfully generated", $file_out);
960             });
961              
962             __PACKAGE__->register_handler(
963             handler => "server",
964             description => "MToken HTTP server",
965             code => sub {
966             ### CODE:
967             my ($self, $meta, @arguments) = @_;
968              
969             # Dash k
970             my $dash_k = shift(@arguments) || "status";
971             unless (grep {$_ eq $dash_k} qw/start status stop restart reload/) {
972             $self->error("Incorrect LSB command! Please use start, status, stop, restart or reload");
973             return 0;
974             }
975              
976             # Get permisions by names
977             my $uid = $>; # Effect. UID
978             my $gid = $); # Effect. GID
979             if (IS_ROOT) {
980             $uid = getpwnam(USERNAME) || do {
981             $self->error("getpwnam failed - $!");
982             return 0;
983             };
984             $gid = getgrnam(GROUPNAME) || do {
985             $self->error("getgrnam failed - $!\n");
986             return 0;
987             };
988             }
989              
990             # Prepare DataDir if not specified
991             unless ($self->option("datadir")) {
992             if (IS_ROOT) { # /var/lib/mtoken
993             $self->datadir(File::Spec->catdir(sharedstatedir(), PROJECTNAMEL));
994             } else { #~/.local/share/mtoken/www
995             $self->datadir(File::Spec->catdir(File::HomeDir->my_data(), PROJECTNAMEL, "www"));
996             }
997             unless (-e $self->datadir) {
998             unless (preparedir($self->datadir)) {
999             $self->error(sprintf("Can't prepare directory %s", $self->datadir));
1000             return 0;
1001             }
1002             }
1003             # Set permisions (GID and UID) for work directory
1004             chown($uid, $gid, $self->datadir) if IS_ROOT && File::stat::stat($self->datadir)->uid != $uid;
1005             }
1006              
1007             # Prepare tempdir
1008             $self->tempdir(File::Spec->catdir(File::Spec->tmpdir(), PROJECTNAMEL));
1009             unless (preparedir( $self->tempdir, 0777 )) {
1010             $self->error(sprintf("Can't prepare temp directory: %s", $self->tempdir));
1011             return 0;
1012             }
1013             chown($uid, $gid, $self->tempdir) if IS_ROOT && File::stat::stat($self->tempdir)->uid != $uid;
1014             $self->debug(sprintf("Temp dir: %s", $self->tempdir));
1015              
1016             # Prepare log directory
1017             if (IS_ROOT) {
1018             my $logdir = $self->logdir;
1019             unless (preparedir( $logdir, 0777 )) {
1020             $self->error(sprintf("Can't prepare log directory: %s", $logdir));
1021             return 0;
1022             }
1023             # Set permisions (GID and UID) for log directory
1024             chown($uid, $gid, $logdir) if File::stat::stat($logdir)->uid != $uid;
1025             $self->debug(sprintf("Log dir: %s", $self->logdir));
1026             } else {
1027             $self->logfile(File::Spec->catfile($self->tempdir(), sprintf("%s.log", PROJECTNAMEL)));
1028             $self->debug(sprintf("Log file: %s", $self->logfile));
1029             }
1030              
1031             # Prepare pid directory and file
1032             my $piddir = IS_ROOT ? File::Spec->catdir( rundir(), PROJECTNAMEL) : $self->tempdir();
1033             my $pidfile = File::Spec->catfile($piddir, sprintf("%s.pid", PROJECTNAMEL));
1034             unless (preparedir($piddir)) {
1035             $self->error(sprintf("Can't prepare pid directory: %s", $piddir));
1036             return 0;
1037             }
1038             # Set permisions (GID and UID) for pid directory
1039             chown($uid, $gid, $piddir) if IS_ROOT && File::stat::stat($piddir)->uid != $uid;
1040             $self->debug(sprintf("Pid file: %s", $pidfile));
1041              
1042             # Hypnotoad variables
1043             my $upgrade = 0;
1044             my $reload = 0;
1045             my $upgrade_timeout = UPGRADE_TIMEOUT;
1046              
1047             # Mojolicious Application
1048             my $app = MToken::Server->new(ctk => $self);
1049             $app->attr(ctk => sub { $self }); # has ctk => sub { CTKx->instance->ctk };
1050             my $prefork = Mojo::Server::Prefork->new( app => $app ); # app => $self
1051             $prefork->pid_file($pidfile);
1052              
1053             # Hypnotoad Pre-fork settings
1054             $prefork->max_clients(tv2int(value($self->conf("clients")))) if defined $self->conf("clients");
1055             $prefork->max_requests(tv2int(value($self->conf("requests")))) if defined $self->conf("requests");
1056             $prefork->accepts(tv2int(value($self->conf("accepts")))) if defined $self->conf("accepts");
1057             $prefork->spare(tv2int(value($self->conf("spare")))) if defined $self->conf("spare");
1058             $prefork->workers(tv2int(value($self->conf("workers")))) if defined $self->conf("workers");
1059              
1060             # Make Listen
1061             my $cfg_listen = value($self->conf("listen"));
1062             my $tls_on = isTrueFlag(value($self->conf("tls")));
1063             my $listen = $tls_on ? "https://" : "http://";
1064             if ($cfg_listen) {
1065             $listen .= $cfg_listen;
1066             } else {
1067             $listen .= sprintf("%s:%d",
1068             value($self->conf("listenaddr")) || SERVER_LISTEN_ADDR,
1069             tv2int16(value($self->conf("listenport"))) || SERVER_LISTEN_PORT,
1070             );
1071             }
1072             my $_resolve_cf = sub {
1073             my $f = shift;
1074             return $f if File::Spec->file_name_is_absolute($f);
1075             return File::Spec->catfile($self->root, $f);
1076             };
1077             if ($tls_on) {
1078             my @p = ();
1079             foreach my $k (qw/ciphers version/) {
1080             my $v = value($self->conf("tls_$k")) // '';
1081             next unless length $v;
1082             push @p, sprintf("%s=%s", $k, $v);
1083             }
1084             foreach my $k (qw/ca cert key/) {
1085             my $v = value($self->conf("tls_$k")) // '';
1086             next unless length $v;
1087             push @p, sprintf("%s=%s", $k, $_resolve_cf->($v));
1088             }
1089             push @p, sprintf("%s=%s", "verify", value($self->conf("tls_verify")) || '0x00')
1090             if value($self->conf("tls_verify"));
1091             $listen .= sprintf("?%s", join('&', @p));
1092             }
1093             $prefork->listen([$listen]);
1094              
1095             # Working with Dash k
1096             if ($dash_k eq 'start') {
1097             if (my $pid = $prefork->check_pid()) {
1098             say "Already running $pid";
1099             return 1;
1100             }
1101             } elsif ($dash_k eq 'stop') {
1102             if (my $pid = $prefork->check_pid()) {
1103             kill 'QUIT', $pid;
1104             say "Stopping $pid";
1105             } else {
1106             say "Not running";
1107             }
1108             return 1;
1109             } elsif ($dash_k eq 'restart') {
1110             if (my $pid = $prefork->check_pid()) {
1111             $upgrade ||= steady_time;
1112             kill 'QUIT', $pid;
1113             my $up = $upgrade_timeout;
1114             while (kill 0, $pid) {
1115             $up--;
1116             sleep 1;
1117             }
1118             die("Can't stop $pid") if $up <= 0;
1119             say "Stopping $pid";
1120             $upgrade = 0;
1121             }
1122             } elsif ($dash_k eq 'reload') {
1123             my $pid = $prefork->check_pid();
1124             if ($pid) {
1125             # Start hot deployment
1126             kill 'USR2', $pid;
1127             say "Reloading $pid";
1128             return 1;
1129             }
1130             say "Not running";
1131             } else { # status
1132             if (my $pid = $prefork->check_pid()) {
1133             say "Running $pid";
1134             } else {
1135             say "Not running";
1136             }
1137             return 1;
1138             }
1139              
1140             #
1141             # LSB start
1142             #
1143              
1144             # This is a production server
1145             $ENV{MOJO_MODE} ||= 'production';
1146              
1147             # Listen USR2 (reload)
1148             $SIG{USR2} = sub { $upgrade ||= steady_time };
1149              
1150             # Set hooks
1151             #$prefork->on(spawn => sub () { # Spawn (start worker)
1152             # my $self = shift; # Prefork object
1153             # my $pid = shift;
1154             # #say "Spawn (start) $pid";
1155             # $self->app->log->debug("Spawn (start) $pid");
1156             #});
1157             $prefork->on(wait => sub { # Manage (every 1 sec)
1158             my $self = shift; # Prefork object
1159              
1160             # Upgrade
1161             if ($upgrade) {
1162             #$self->app->log->debug(">>> " . $self->healthy() || '?');
1163             unless ($reload) {
1164             $reload = 1; # Off next reloading
1165             if ($self->app->reload()) {
1166             $reload = 0;
1167             $upgrade = 0;
1168             return;
1169             }
1170             }
1171              
1172             # Timeout
1173             if (($upgrade + $upgrade_timeout) <= steady_time()) {
1174             kill 'KILL', $$;
1175             $upgrade = 0;
1176             }
1177             }
1178             });
1179             #$prefork->on(reap => sub { # Cleanup (Emitted when a child process exited)
1180             # my $self = shift; # Prefork object
1181             # my $pid = shift;
1182             # #say "Reap (cleanup) $pid";
1183             # $self->app->log->debug("Reap (cleanup) $pid");
1184             #});
1185             $prefork->on(finish => sub { # Finish
1186             my $self = shift; # Prefork object
1187             my $graceful = shift;
1188             $self->app->log->debug($graceful ? 'Graceful server shutdown' : 'Server shutdown');
1189             });
1190              
1191             # Set GID and UID
1192             if (IS_ROOT) {
1193             if (defined($gid)) {
1194             POSIX::setgid($gid) || do {
1195             $self->error("setgid $gid failed - $!");
1196             return 0;
1197             };
1198             $) = "$gid $gid"; # this calls setgroups
1199             if (!($( eq "$gid $gid" && $) eq "$gid $gid")) { # just to be sure
1200             $self->error("detected strange gid");
1201             return 0;
1202             }
1203             }
1204             if (defined($uid)) {
1205             POSIX::setuid($uid) || do {
1206             $self->error("setuid $uid failed - $!");
1207             return 0;
1208             };
1209             if (!($< == $uid && $> == $uid)) { # just to be sure
1210             $self->error("detected strange uid");
1211             return 0;
1212             }
1213             }
1214             }
1215              
1216              
1217             # Daemonize
1218             $prefork->daemonize() unless $self->debugmode();
1219              
1220             # Running
1221             say "Running";
1222             $prefork->run();
1223              
1224             #my $fingerprint = $self->get_fingerprint;
1225             #say explain(\%exest);
1226             #my @strings = split("\n", $exest{output});
1227             #say explain(\@strings);
1228              
1229             return 1;
1230             });
1231              
1232             __PACKAGE__->register_handler(
1233             handler => "commit",
1234             description => "Send tarball to server (backup)",
1235             code => sub {
1236             ### CODE:
1237             my ($self, $meta, @arguments) = @_;
1238             unless ($self->lconfig->is_loaded) {
1239             $self->error(ERROR_NO_TOKEN);
1240             return 0;
1241             }
1242              
1243             # Check client
1244             unless ($self->client->check) {
1245             $self->error($self->client->error);
1246             $self->debug($self->client->trace);
1247             return 0;
1248             }
1249              
1250             # Get Fingerprint
1251             my $fingerprint = $self->get_fingerprint;
1252             unless ($fingerprint) {
1253             $self->error("No fingerprint specified");
1254             return 0;
1255             }
1256              
1257             # Get Manifest
1258             my $manifest = $self->get_manifest; # file=>full_path
1259              
1260             # Get file for tarball making
1261             my $tmp_dir = $self->debugmode ? File::Spec->catdir(File::Spec->tmpdir(), "mtoken") : $self->tempdir;
1262             my $tarball_name = dtf(TARBALL_FORMAT, time());
1263             my $tarball_arch_name = sprintf("%s.tgz", $tarball_name =~ m/(.+?)\.tkn/ ? $1 : $tarball_name);
1264             my $tarball_path = path($tmp_dir, $tarball_name);
1265             my $tarball_arch_path = path($tmp_dir, $tarball_arch_name);
1266              
1267             # make_tarball
1268             my $curdir = path(getcwd())->to_abs->to_string;
1269             my $newdir = path($self->datadir)->to_abs->to_string;
1270             chdir $newdir;
1271             my $tar = Archive::Tar->new;
1272             $tar->add_files(keys(%$manifest));
1273             for my $f ($tar->get_files) {
1274             $f->mode($f->mode & ~022); # chmod go-w
1275             }
1276             $tar->write($tarball_arch_path->to_string, 1);
1277             chdir $curdir;
1278              
1279             # Encrypt file to tempdir
1280             my %exest = $self->execmd($self->get_gpgbin, "--encrypt", "--quiet", "--recipient", $fingerprint, "--output",
1281             $tarball_path->to_string, $tarball_arch_path->to_string);
1282             unless ($exest{status} && -f $tarball_path->to_string) {
1283             $self->error(sprintf("Can't encrypt file %s: %s", $tarball_arch_path->to_string, $exest{error}));
1284             return 0;
1285             }
1286             $tarball_arch_path->remove;
1287              
1288             # Upload (PUT method)
1289             my $status = $self->client->upload($self->get_name, $tarball_path->to_string); # "C20211009T090718.tkn"
1290             #say magenta($tarball_path->to_string);
1291             #say explain($self->client->req->content);
1292             #$self->debug($self->client->trace);
1293             #$self->debug($self->client->res->body);
1294             if ($status) {
1295             $tarball_path->remove;
1296             } else {
1297             $self->error($self->client->error);
1298             $self->debug($self->client->trace);
1299             return 0;
1300             }
1301              
1302             # Ok
1303             return yep("Done");
1304             });
1305              
1306             __PACKAGE__->register_handler(
1307             handler => "update",
1308             description => "Get tarball from server (restore)",
1309             code => sub {
1310             ### CODE:
1311             my ($self, $meta, @arguments) = @_;
1312             unless ($self->lconfig->is_loaded) {
1313             $self->error(ERROR_NO_TOKEN);
1314             return 0;
1315             }
1316              
1317             # Check client & get filelist
1318             unless ($self->client->info($self->get_name)) {
1319             $self->error($self->client->error);
1320             $self->debug($self->client->trace);
1321             return 0;
1322             }
1323              
1324             # Get file for tarball
1325             my $tarball_name = shift @arguments;
1326             if ($tarball_name) {
1327             unless ($tarball_name =~ TARBALL_PATTERN) {
1328             $self->error("Incorrect tarball name");
1329             return 0;
1330             }
1331             } else {
1332             my $files = array($self->client->res->json("/files"));
1333             my @tmp = sort {$a->{mtime} <=> $b->{mtime}} @$files;
1334             $tarball_name = value(pop(@tmp), "filename");
1335             unless ($tarball_name && $tarball_name =~ TARBALL_PATTERN) {
1336             $self->error("Tarball not found");
1337             return 0;
1338             }
1339             }
1340              
1341             # Get paths
1342             my $tmp_dir = $self->debugmode ? File::Spec->catdir(File::Spec->tmpdir(), "mtoken") : $self->tempdir;
1343             my $tarball_pfx = $tarball_name =~ m/(.+?)\.tkn/ ? $1 : $tarball_name;
1344             my $tarball_path = path($tmp_dir, $tarball_name);
1345             my $archive_path = path($tmp_dir, sprintf("%s.tgz", $tarball_pfx));
1346             my $tarball_dir = path($tmp_dir, $tarball_pfx)->make_path;
1347              
1348             # Download file
1349             unless ($self->client->download($self->get_name => $tarball_path->to_string)) {
1350             $self->error($self->client->error);
1351             $self->debug($self->client->trace);
1352             return 0;
1353             }
1354              
1355             # Get Last_Modified from headers
1356             my $lm = $self->client->res->headers->last_modified;
1357             my $last_modified = $lm ? Mojo::Date->new($lm)->epoch : 0;
1358              
1359             # Check mtime
1360             if ($self->store->status) {
1361             my $db_mtime = $self->store->file ? File::stat::stat($self->store->file)->mtime || 0 : 0;
1362             if ($last_modified && $db_mtime && $db_mtime > $last_modified) { # Conflict
1363             say yellow("%s: conflict detected", $tarball_name);
1364             say yellow(" Tarball created: %s", _fdate($last_modified));
1365             say yellow(" Token modified: %s", _fdate($db_mtime));
1366             say yellow("The current token was changed later than the one in the repository.");
1367             unless ($self->option("force") ||
1368             $self->cli_prompt('Are you sure you want to revert to an earlier state of the token?:','no') =~ /^\s*y/i) {
1369             return skip("Aborted");
1370             }
1371             }
1372             }
1373              
1374             # Decrypt file
1375             unless (-e $archive_path->to_string) {
1376             my %exest = $self->execmd($self->get_gpgbin, "--decrypt", "--quiet", "--output", $archive_path->to_string, $tarball_path->to_string);
1377             unless ($exest{status} && -e $archive_path->to_string) {
1378             $self->error(sprintf("Can't decrypt file %s: %s", $tarball_path->to_string, $exest{error}));
1379             return 0;
1380             }
1381             $tarball_path->remove;
1382             }
1383              
1384             # Store to selected file or directory
1385             if ($self->option("output") || $self->option("outdir")) {
1386             my $file_out = $self->option("output");
1387             $file_out = File::Spec->catfile($self->option("outdir"), sprintf("%s.tgz", $tarball_pfx))
1388             if !$file_out && -d $self->option("outdir");
1389             return skip("Incorrect output file %s. File already exists", $tarball_name) if -e $file_out;
1390             $archive_path->move_to($file_out);
1391             return nope("Can't download %s tarbal", $tarball_name) unless -f $file_out;
1392             return yep("Tarbal %s successfully downloaded as archive to %s", $tarball_name, $file_out);
1393             }
1394              
1395             # Extract files from archive
1396             my $tar = Archive::Tar->new;
1397             $tar->read($archive_path->to_abs->to_string);
1398             $tar->setcwd($archive_path->to_string);
1399             foreach my $file ($tar->list_files()) {
1400             $tar->extract_file($file, path($tarball_dir->to_string, $file)->to_string);
1401             }
1402             #say explain(\@files);
1403              
1404             # Install files
1405             find({
1406             no_chdir => 1,
1407             wanted => sub {
1408             return if -d;
1409             my $src = path($_);
1410             my $dst = path($_)->to_rel($tarball_dir->to_string);
1411             #say blue("%s -> %s", $src->to_string, $dst->to_string);
1412             $src->move_to(File::Spec->catfile($self->datadir, $dst->to_string));
1413             }}, $tarball_dir->to_string);
1414              
1415             # Ok
1416             return yep("Done");
1417             });
1418              
1419             __PACKAGE__->register_handler(
1420             handler => "revoke",
1421             description => "Revoke tarball from server (delete)",
1422             code => sub {
1423             ### CODE:
1424             my ($self, $meta, @arguments) = @_;
1425             unless ($self->lconfig->is_loaded) {
1426             $self->error(ERROR_NO_TOKEN);
1427             return 0;
1428             }
1429              
1430             # Check client & get filelist
1431             unless ($self->client->info($self->get_name)) {
1432             $self->error($self->client->error);
1433             $self->debug($self->client->trace);
1434             return 0;
1435             }
1436              
1437             # Get file for tarball
1438             my $tarball_name = shift @arguments;
1439             if ($tarball_name) {
1440             unless ($tarball_name =~ TARBALL_PATTERN) {
1441             $self->error("Incorrect tarball name");
1442             return 0;
1443             }
1444             } else {
1445             my $files = array($self->client->res->json("/files"));
1446             my @tmp = sort {$a->{mtime} <=> $b->{mtime}} @$files;
1447             $tarball_name = value(pop(@tmp), "filename");
1448             unless ($tarball_name && $tarball_name =~ TARBALL_PATTERN) {
1449             $self->error("Tarball not found");
1450             return 0;
1451             }
1452             }
1453              
1454             # Delete
1455             unless ($self->client->remove($self->get_name, $tarball_name)) {
1456             $self->error($self->client->error);
1457             $self->debug($self->client->trace);
1458             return 0;
1459             }
1460              
1461             # Ok
1462             return yep("Done");
1463             });
1464              
1465             __PACKAGE__->register_handler(
1466             handler => "clean",
1467             description => "Clean temporary files",
1468             code => sub {
1469             ### CODE:
1470             my ($self, $meta, @arguments) = @_;
1471             unless ($self->lconfig->is_loaded) {
1472             $self->error(ERROR_NO_TOKEN);
1473             return 0;
1474             }
1475              
1476             # Temp directory
1477             my $tmp_dir = File::Spec->catdir(File::Spec->tmpdir(), "mtoken");
1478             find({
1479             no_chdir => 1,
1480             wanted => sub {
1481             return if -d;
1482             return if /\.pid$/;
1483             my $file = path($_);
1484              
1485             # Remove
1486             $file->remove;
1487             say magenta("Remove file %s", $file->to_string) if $self->verbosemode;
1488             }}, $tmp_dir);
1489              
1490             # Private directory
1491             my $priv_dir = File::Spec->catfile($self->datadir, DIR_PRIVATE);
1492             find({
1493             no_chdir => 1,
1494             wanted => sub {
1495             return if -d;
1496             return unless /\.tmp$/;
1497             my $file = path($_);
1498              
1499             # Remove
1500             $file->remove;
1501             say magenta("Remove file %s", $file->to_string) if $self->verbosemode;
1502             }}, $priv_dir);
1503              
1504             # Ok
1505             return yep("Done");
1506             });
1507              
1508             sub again {
1509 0     0 1   my $self = shift;
1510 0           $self->SUPER::again(); # CTK::App again first!!
1511              
1512             # Device & Local configuration
1513 0           $self->{lconfig} = MToken::Config->new(file => File::Spec->catfile($self->datadir, DIR_PRIVATE, DEVICE_CONF_FILE));
1514              
1515             # Store conf
1516 0   0       my $store_conf = $self->{lconfig}->get("store") || $self->config("store") || {};
1517 0 0         $store_conf = {} unless is_hash($store_conf);
1518 0           $self->{store_conf} = {%$store_conf};
1519 0           $self->{store} = undef;
1520             #$self->debug(_explain($store));
1521              
1522             # Client instance
1523 0 0         $self->{client} = MToken::Client->new(
    0          
1524             url => $self->lconfig->is_loaded ? $self->get_server_url : undef,
1525             insecure => $self->option("insecure"),
1526             max_redirects => $self->conf("maxredirects"),
1527             connect_timeout => $self->conf("connecttimeout"),
1528             inactivity_timeout => $self->conf("inactivitytimeout"),
1529             request_timeout => $self->conf("requesttimeout"),
1530             pwcache => File::Spec->catfile($self->datadir, DIR_PRIVATE, PWCACHE_FILE),
1531             $self->option("insecure") ? (pwcache_ttl => 0) : (),
1532             );
1533              
1534 0           return $self; # CTK requires!
1535             }
1536             sub raise {
1537 0     0 1   my $self = shift;
1538 0           say STDERR red(@_);
1539 0           return 0;
1540             }
1541             sub store {
1542 0     0 1   my $self = shift;
1543 0           my %store_args = (@_);
1544 0 0         if (is_void(\%store_args)) {
1545 0 0         return $self->{store} if defined $self->{store}; # Already initialized
1546 0           my $sconf = $self->{store_conf};
1547 0           %store_args = %$sconf;
1548 0 0         $store_args{do_init} = 1 if $self->lconfig->is_loaded;
1549             }
1550              
1551             # Leazy initializing
1552             $store_args{file} = File::Spec->catfile($self->datadir, DIR_PRIVATE, DB_FILE)
1553 0 0 0       unless ($store_args{file} || $store_args{dsn});
1554 0           $self->{store} = MToken::Store->new(%store_args);
1555              
1556 0           return $self->{store};
1557             }
1558             sub lconfig {
1559 0     0 1   my $self = shift;
1560             return $self->{lconfig}
1561 0           }
1562             sub client {
1563 0     0 1   my $self = shift;
1564             return $self->{client}
1565 0           }
1566             sub execmd {
1567 0     0 1   my $self = shift;
1568 0           my @cmd = (@_);
1569 0           my $scmd = join(" ", @cmd);
1570 0           my $error;
1571              
1572             # Run command
1573 0           my $exe_err = '';
1574 0           my $exe_out = CTK::Util::execute([@cmd], undef, \$exe_err);
1575 0           my $stt = $? >> 8;
1576 0 0         my $exe_stt = $stt ? 0 : 1;
1577 0 0 0       chomp($exe_out) if defined($exe_out) && length($exe_out);
1578 0 0 0       if (!$exe_stt && $exe_err) {
    0          
1579 0           chomp($exe_err);
1580 0           say cyan("#", $scmd);
1581 0           $error = $exe_err;
1582 0           say STDERR red($error);
1583             } elsif ($stt) {
1584 0           say cyan("#", $scmd);
1585 0           $error = sprintf("Exitval=%d", $stt);
1586 0           say STDERR red($error);
1587             }
1588              
1589             return (
1590 0           command => $scmd,
1591             status => $exe_stt,
1592             exitval => $stt,
1593             error => $error,
1594             output => $exe_out,
1595             );
1596             }
1597             sub get_name {
1598 0     0 1   my $self = shift;
1599 0           $self->lconfig->{name};
1600             }
1601             sub get_opensslbin {
1602 0     0 1   my $self = shift;
1603 0   0       return $self->lconfig->get("opensslbin") || $self->conf("opensslbin") || which(OPENSSLBIN) || OPENSSLBIN;
1604             }
1605             sub get_gpgbin {
1606 0     0 1   my $self = shift;
1607 0   0       return $self->lconfig->get("gpgbin") || $self->conf("gpgbin") || which(GPGBIN) || GPGBIN;
1608             }
1609             sub get_server_url {
1610 0     0 1   my $self = shift;
1611 0   0       return $self->lconfig->get("server_url") || $self->conf("server_url") || SERVER_URL;
1612             }
1613             sub get_fingerprint {
1614 0     0 1   my $self = shift;
1615 0   0       my $fingerprint_cfg = $self->lconfig->get("fingerprint") || $self->conf("fingerprint") || "";
1616 0           my $fingerprint = "";
1617 0           my %exest = ();
1618              
1619             # Get public keys info
1620 0 0         unless ($self->option("force")) {
1621 0           %exest = $self->execmd($self->get_gpgbin, "--list-keys");
1622 0 0         if ($exest{status}) {
1623 0   0       say blue($exest{output} || "no keys found");
1624             }
1625             }
1626              
1627             # Get public keys fingerprints
1628 0           %exest = $self->execmd($self->get_gpgbin, "--list-keys", "--with-colons");
1629 0 0 0       if ($exest{status} && $exest{output}) {
1630 0 0         my @fingerprints = map {$_ = uc($1) if /\:([0-9a-f]{16,40})\:/i } grep { /fpr/ } split("\n", $exest{output});
  0            
  0            
1631 0   0       my $fingerprint_default = $fingerprint_cfg || $fingerprints[0] || 'none';
1632 0           while (1) {
1633 0 0         if ($self->option("force")) {
1634 0           $fingerprint = $fingerprint_default;
1635 0 0         $fingerprint = "" if $fingerprint =~ /^\s*n/i;
1636 0           last;
1637             }
1638 0           $fingerprint = uc($self->cli_prompt('Please provide the fingerprint of recipient:', $fingerprint_default));
1639 0 0         unless (grep {$_ eq $fingerprint} @fingerprints) {
  0            
1640 0 0         if ($fingerprint =~ /^\s*n/i) {
1641 0           $fingerprint = "";
1642 0           last;
1643             }
1644 0           say yellow("Fingerprint not found! Type \"n\" to skip");
1645 0           next;
1646             }
1647 0           last;
1648             }
1649             } else {
1650 0 0 0       $fingerprint = $self->option("force")
      0        
1651             ? $fingerprint_cfg || "none"
1652             : uc($self->cli_prompt('Please provide the fingerprint of recipient:', $fingerprint_cfg || "none"));
1653 0 0         if ($fingerprint =~ /^\s*n/i) {
    0          
1654 0           $fingerprint = "";
1655             } elsif (!_fingerprint_check($fingerprint)) {
1656 0           say yellow("Fingerprint is incorrect!");
1657 0           $fingerprint = "";
1658             }
1659             }
1660 0 0         say cyan("Fingerprint: %s", $fingerprint) if $fingerprint;
1661              
1662 0           return $fingerprint;
1663             }
1664             sub get_manifest {
1665 0     0 1   my $self = shift;
1666 0           my $manifile = File::Spec->catfile($self->datadir, DIR_PRIVATE, DEVICE_MANIFEST_FILE);
1667 0 0         return {} unless -e $manifile;
1668 0           my $manifest = maniread($manifile);
1669 0           my $dir = path($self->datadir)->to_abs->to_string;
1670 0           while (my ($k, $v) = each %$manifest) {
1671 0           $manifest->{$k} = path($dir, $k)->to_string;
1672 0 0         delete $manifest->{$k} unless -e $manifest->{$k};
1673             }
1674 0           return $manifest;
1675             }
1676              
1677              
1678             sub _get_default_url {
1679 0   0 0     my $name = shift || PROJECTNAMEL;
1680 0           my $uri = URI->new( DEFAULT_URL );
1681 0           $uri->scheme('https');
1682 0           $uri->host(HOSTNAME);
1683 0           $uri->port(SERVER_LISTEN_PORT);
1684             #$uri->path(join("/", "mtoken", $name)); # Disabled!
1685 0           return $uri->canonical->as_string;
1686             }
1687             sub _hashmd5 {
1688 0     0     my %h = @_ ;
1689 0           my $s = "";
1690 0           foreach my $k (sort {$a cmp $b} (keys(%h))) { $s .= uv2null($h{$k}) }
  0            
  0            
1691 0 0         return "" unless $s;
1692 0           return md5_hex($s);
1693             }
1694             sub _expand_wildcards {
1695 0     0     my @files = (@_);
1696             # Original in package ExtUtils::Command
1697 0 0         @files = map(/[*?]/o ? glob($_) : $_, @files);
1698 0           return (@files);
1699             }
1700             sub _fingerprint_check {
1701 0   0 0     my $fpr = shift || '';
1702 0           my $l = length($fpr);
1703 0 0 0       return 0 unless $l == 40 or $l == 16; # Fingerprint or KeyID
1704 0 0         return 1 if $fpr =~ /^[0-9a-f]+$/i;
1705 0           return 0;
1706             }
1707             sub _fbytes {
1708 0     0     my $n = int(shift);
1709 0 0         if ($n >= 1024 ** 3) {
    0          
    0          
1710 0           return sprintf "%.3g GB", $n / (1024 ** 3);
1711             } elsif ($n >= 1024 ** 2) {
1712 0           return sprintf "%.3g MB", $n / (1024.0 * 1024);
1713             } elsif ($n >= 1024) {
1714 0           return sprintf "%.3g KB", $n / 1024.0;
1715             } else {
1716 0           return "$n B";
1717             }
1718             }
1719             sub _fdate {
1720 0   0 0     my $d = shift || 0;
1721 0   0       my $g = shift || 0;
1722 0 0         return "unknown" unless $d;
1723 0 0         return dtf(DATETIME_GMT_FORMAT, $d, 1) if $g;
1724 0           return dtf(DATETIME_FORMAT . " " . tz_diff(), $d);
1725             }
1726              
1727             1;
1728              
1729             __END__