File Coverage

blib/lib/EMDIS/ECS.pm
Criterion Covered Total %
statement 146 649 22.5
branch 43 458 9.3
condition 6 102 5.8
subroutine 39 55 70.9
pod 0 37 0.0
total 234 1301 17.9


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w
2             #
3             # Copyright (C) 2002-2026 National Marrow Donor Program. All rights reserved.
4             #
5             # For a description of this module, please refer to the POD documentation
6             # embedded at the bottom of the file (e.g. perldoc EMDIS::ECS).
7              
8             package EMDIS::ECS;
9              
10 6     6   22002 use Authen::SASL qw(Perl);
  6         8447  
  6         44  
11 6     6   44905 use CPAN::Version;
  6         9233  
  6         268  
12 6     6   56 use Fcntl qw(:DEFAULT :flock);
  6         12  
  6         2700  
13 6     6   60 use File::Basename;
  6         19  
  6         488  
14 6     6   2613 use File::Copy;
  6         41445  
  6         487  
15 6     6   46 use File::Spec::Functions qw(catfile);
  6         11  
  6         335  
16 6     6   5797 use File::Temp qw(tempfile);
  6         148815  
  6         555  
17 6     6   3029 use IO::File;
  6         6773  
  6         1359  
18 6     6   50 use IO::Handle;
  6         11  
  6         293  
19 6     6   3210 use IPC::Open2;
  6         26166  
  6         458  
20 6     6   2908 use MIME::Base64;
  6         4623  
  6         484  
21 6     6   3773 use Net::SMTP;
  6         849789  
  6         740  
22 6     6   61 use strict;
  6         13  
  6         311  
23 6         1044 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS
24             $ECS_CFG $ECS_NODE_TBL $FILEMODE @LOG_LEVEL
25 6     6   33 $configured $pidfile $cmd_output $pid_saved);
  6         13  
26              
27             # load OS specific modules at compile time, in BEGIN block
28             BEGIN
29             {
30 6 50   6   1079 if( $^O =~ /MSWin32/ )
31             {
32             # Win32 only modules
33 0         0 eval "require Win32::Process";
34             }
35             }
36              
37             # module/package version
38             $VERSION = '0.48';
39              
40             # file creation mode (octal, a la chmod)
41             $FILEMODE = 0660;
42              
43             # subclass Exporter and define Exporter set up
44             require Exporter;
45             @ISA = qw(Exporter);
46             @EXPORT = (); # items exported by default
47             @EXPORT_OK = (); # items exported by request
48             %EXPORT_TAGS = ( # tags for groups of items
49             ALL => [ qw($ECS_CFG $ECS_NODE_TBL $FILEMODE $VERSION
50             load_ecs_config delete_old_files dequote ecs_is_configured
51             log log_debug log_info log_warn log_error log_fatal
52             copy_to_dir move_to_dir read_ecs_message_id
53             send_admin_email send_amqp_message send_ecs_message
54             send_email send_encrypted_message format_datetime
55             format_doc_filename
56             format_msg_filename openpgp_decrypt openpgp_encrypt
57             pgp2_decrypt pgp2_encrypt check_pid save_pid
58             timelimit_cmd remove_pidfile trim valid_encr_typ EOL
59             is_yes is_no get_oauth_token) ] );
60             Exporter::export_ok_tags('ALL'); # use tag handling fn to define EXPORT_OK
61              
62             BEGIN {
63 6     6   33 $configured = ''; # boolean indicates whether ECS has been configured
64 6         21 @LOG_LEVEL = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'FATAL');
65 6         64371 $pid_saved = '';
66             }
67              
68             # ----------------------------------------------------------------------
69             # Return platform specific end-of-line string
70             sub EOL
71             {
72 0 0   0 0 0 return "\r\n" if $^O =~ /MSWin32/;
73 0         0 return "\n";
74             }
75              
76             # ----------------------------------------------------------------------
77             # test for YES or TRUE
78             sub is_yes
79             {
80 95     95 0 233 my $val = shift;
81 95 50       228 return 0 if not defined $val;
82 95 100 100     667 return 1 if $val =~ /^\s*YES\s*$/io or $val =~ /^\s*TRUE\s*$/io;
83 71         380 return 0;
84             }
85              
86             # ----------------------------------------------------------------------
87             # test for NO or FALSE
88             sub is_no
89             {
90 32     32 0 71 my $val = shift;
91 32 50       76 return 0 if not defined $val;
92 32 100 100     339 return 1 if $val =~ /^\s*NO\s*$/io or $val =~ /^\s*FALSE\s*$/io;
93 7         31 return 0;
94             }
95              
96             # ----------------------------------------------------------------------
97             # Load ECS configuration into global variables.
98             # returns empty string if successful or error message if error encountered
99             sub load_ecs_config
100             {
101 0     0 0 0 my $cfg_file = shift;
102              
103 0         0 require EMDIS::ECS::Config;
104 0         0 my $cfg = new EMDIS::ECS::Config($cfg_file);
105 0 0       0 return "Unable to load ECS configuration ($cfg_file): $cfg"
106             unless ref $cfg;
107              
108 0         0 require EMDIS::ECS::LockedHash;
109 0         0 my $node_tbl = new EMDIS::ECS::LockedHash($cfg->NODE_TBL, $cfg->NODE_TBL_LCK);
110 0 0       0 return "Unable to open ECS node_tbl (" . $cfg->NODE_TBL .
111             "): $node_tbl"
112             unless ref $node_tbl;
113              
114 0         0 $pidfile = catfile($cfg->ECS_DAT_DIR, basename($0) . ".pid");
115              
116             # assign values to global config variables
117 0         0 $ECS_CFG = $cfg;
118 0         0 $ECS_NODE_TBL = $node_tbl;
119 0         0 $configured = 1;
120              
121             # successful
122 0         0 return '';
123             }
124              
125             # ----------------------------------------------------------------------
126             # delete old files (mtime < cutoff time) from specified directory
127             # no recursion
128             sub delete_old_files
129             {
130 0     0 0 0 my $dirname = shift;
131 0         0 my $cutoff_time = shift;
132              
133 0 0       0 if(! -d $dirname)
134             {
135 0         0 warn "Not a directory name: $dirname";
136 0         0 return;
137             }
138 0 0       0 if($cutoff_time !~ /^\d+$/)
139             {
140 0         0 warn "Cutoff time not numeric: $cutoff_time";
141 0         0 return;
142             }
143 0         0 opendir DELDIR, $dirname;
144 0         0 my @names = readdir DELDIR;
145 0         0 closedir DELDIR;
146 0         0 foreach my $name (@names)
147             {
148 0         0 my $filename = catfile($dirname, $name);
149 0 0       0 next unless -f $filename;
150             # delete file if mtime < $cutoff_time
151 0         0 my @stat = stat $filename;
152 0 0       0 if($stat[9] < $cutoff_time)
153             {
154 0 0       0 unlink $filename
155             or warn "Unable to delete file: $filename";
156             }
157             }
158             }
159              
160             # ----------------------------------------------------------------------
161             # Return string value with enclosing single or double quotes removed.
162             sub dequote {
163 12     12 0 320724 my $str = shift;
164 12 50       33 return if not defined $str;
165 12 100       65 if($str =~ /^"(.*)"$/) {
    100          
166 5         15 $str = $1;
167             }
168             elsif($str =~ /^'(.*)'$/) {
169 1         5 $str = $1;
170             }
171 12         51 return $str;
172             }
173              
174             # ----------------------------------------------------------------------
175             # Return boolean indicating whether ECS has been configured.
176             sub ecs_is_configured {
177 64     64 0 394 return $configured;
178             }
179              
180             # ----------------------------------------------------------------------
181             # Write message to ECS log file. Takes two arguments: a level which is
182             # used to classify logged messages and the text to be logged.
183             # Push an aditional email to admin if the error is encountering
184             # the MAIL_LEVEL.
185             # Returns empty string if successful or error message if error encountered.
186             sub log {
187 6 50   6 0 39 if(not ecs_is_configured()) {
188 6         46 my $warning = "EMDIS::ECS::log(): ECS has not been configured.";
189 6         76 warn "$warning\n";
190 6         131 return $warning;
191             }
192 0         0 my $cfg = $ECS_CFG;
193 0         0 my $level = shift;
194 0 0 0     0 $level = '1' if (not defined $level) or
      0        
195             ($level < 0) or ($level > $#LOG_LEVEL);
196 0 0 0     0 return if $level < $cfg->LOG_LEVEL && ! $cfg->ECS_DEBUG; # check log-level
197 0         0 my $text = join("\n ", @_);
198 0 0       0 $text = '' if not defined $text;
199 0         0 my $timestamp = localtime;
200 0         0 my $origin = $0;
201              
202 0         0 my $setmode = not -e $cfg->LOG_FILE;
203 0 0       0 open LOG, ">>" . $cfg->LOG_FILE or do {
204 0         0 warn "Error within ECS library: $! " . $cfg->LOG_FILE;
205 0         0 return;
206             };
207 0         0 print LOG join("|",$timestamp,$origin,$LOG_LEVEL[$level],$text),"\n";
208 0         0 close LOG;
209 0 0       0 chmod $FILEMODE, $cfg->LOG_FILE if $setmode;
210 0 0       0 if ( $level >= $cfg->MAIL_LEVEL )
211             {
212 0         0 send_admin_email (join("|",$timestamp,$origin,$LOG_LEVEL[$level],$text));
213             }
214 0         0 return '';
215             }
216             # logging subroutines for specific logging levels
217 1     1 0 4 sub log_debug { return &log(0, @_); }
218 1     1 0 5 sub log_info { return &log(1, @_); }
219 1     1 0 4 sub log_warn { return &log(2, @_); }
220 1     1 0 4 sub log_error { return &log(3, @_); }
221 1     1 0 5 sub log_fatal { return &log(4, @_); }
222              
223             # ----------------------------------------------------------------------
224             # Copy file to specified directory. If necessary, rename file to avoid
225             # filename collision.
226             # Returns empty string if successful or error message if error encountered.
227             sub copy_to_dir {
228 0     0 0 0 my $filename = shift;
229 0         0 my $targetdir = shift;
230 0         0 my $err;
231              
232 0 0       0 return "file not found: $filename" unless -f $filename;
233 0 0       0 return "directory not found: $targetdir" unless -d $targetdir;
234              
235             # do some fancy footwork to avoid name collision in target dir,
236             # then copy file
237 0         0 my $basename = basename($filename);
238 0         0 my $template = $basename;
239 0         0 my $suffix = '';
240 0 0       0 if($basename =~ /^(\d{8}_\d{6}_(.+_)?).{4}(\..{3})$/) {
241 0         0 $template = "$1XXXX";
242 0         0 $suffix = $3;
243             }
244             else {
245 0         0 $template .= '_XXXX';
246             }
247 0         0 my ($fh, $tempfilename) = tempfile($template,
248             DIR => $targetdir,
249             SUFFIX => $suffix);
250 0 0       0 return "unable to open tempfile in directory $targetdir: $!"
251             unless $fh;
252 0 0       0 $err = "unable to copy $filename to $tempfilename: $!"
253             unless copy($filename, $fh);
254 0         0 close $fh;
255 0         0 chmod $FILEMODE, $tempfilename;
256 0         0 return $err;
257             }
258              
259             # ----------------------------------------------------------------------
260             # Move file to specified directory. If necessary, rename file to avoid
261             # filename collision.
262             # Returns empty string if successful or error message if error encountered.
263             sub move_to_dir {
264 0     0 0 0 my $filename = shift;
265 0         0 my $targetdir = shift;
266              
267 0         0 my $err = copy_to_dir($filename, $targetdir);
268 0 0       0 unlink $filename unless $err;
269 0         0 return $err;
270             }
271              
272             # ----------------------------------------------------------------------
273             # Execute AUTHN_OAUTH_TOKEN_CMD to get OAuth access token
274             # Returns two-element list: ($err, $access_token)
275             sub get_oauth_token {
276 0     0 0 0 my $timelimit = shift;
277 0         0 my $token_cmd = shift;
278 0         0 my $desc = shift;
279              
280             # execute command
281 0         0 my $err = timelimit_cmd($timelimit, $token_cmd);
282 0 0       0 return("EMDIS::ECS::get_oauth_token(): $desc command execution failed: $err", undef)
283             if $err;
284              
285             # get command output from module-level variable
286 0 0       0 my $access_token = $EMDIS::ECS::cmd_output
287             or return("EMDIS::ECS::get_oauth_token(): $desc command returned no access token", undef);
288              
289 0         0 chomp $access_token; # remove any trailing EOL, just in case
290 0         0 return('', $access_token);
291             }
292              
293             # ----------------------------------------------------------------------
294             # Read ECS message id from specified file. File is presumed to be in the
295             # format of an email message; message id is comprised of node_id and seq_num,
296             # with optional $part_num and $num_parts or DOC suffix.
297             # Returns empty array if unable to retrieve ECS message id from file.
298             sub read_ecs_message_id
299             {
300 10     10 0 2130 my $filename = shift;
301              
302 10 100       48 return "EMDIS::ECS::read_ecs_message_id(): ECS has not been configured."
303             unless ecs_is_configured();
304 9         73 my $mail_mrk = $ECS_CFG->MAIL_MRK;
305              
306 9         65 my $fh = new IO::File;
307 9 50       360 return () unless $fh->open("< $filename");
308 9         885 while(<$fh>) {
309 11 100       183 /^Subject:.*$mail_mrk:(\S+?):(\d+):(\d+)\/(\d+)\s*$/io and do {
310 1         46 return ($1,$2,$3,$4,0);
311             };
312 10 100       136 /^Subject:.*$mail_mrk:(\S+?):(\d+)\s*$/io and do {
313 4         133 return ($1,$2,1,1,0);
314             };
315 6 100       84 /^Subject:.*$mail_mrk:(\S+?):(\d+):DOC\s*$/io and do {
316 1         30 return ($1,$2,1,1,1);
317             };
318 5 100       70 /^Subject:.*$mail_mrk:(\S+)\s*$/io and do {
319 1         37 return ($1,undef,undef,undef,0);
320             };
321 4 100       23 /^$/ and last; # blank line marks end of mail headers
322             }
323 2         37 close $fh;
324 2         17 return (); # return empty array
325             }
326              
327             # ----------------------------------------------------------------------
328             # Send email to administrator and also archive the email message in the
329             # mboxes/out directory. Takes one or more arguments: the body lines to
330             # be emailed.
331             # Returns empty string if successful or error message if error encountered.
332             # Also logs error if error encountered.
333             sub send_admin_email {
334              
335 1     1 0 3 my $err = '';
336 1 50       3 $err = "EMDIS::ECS::send_admin_email(): ECS has not been configured."
337             unless ecs_is_configured();
338 1         3 my $cfg = $ECS_CFG;
339              
340             # record message contents in 'out' file
341 1 50       3 if(not $err) {
342 0         0 my $template = format_datetime(time, '%04d%02d%02d_%02d%02d%02d_XXXX');
343 0         0 my ($fh, $filename) = tempfile($template,
344             DIR => $cfg->ECS_MBX_OUT_DIR,
345             SUFFIX => '.msg');
346 0 0       0 $err = "EMDIS::ECS::send_admin_email(): unable to create 'out' file"
347             unless $fh;
348 0 0       0 if($fh) {
349 0         0 print $fh @_;
350 0         0 close $fh;
351 0         0 chmod $FILEMODE, $filename;
352             }
353             }
354              
355 1 50       4 if(not $err)
356             {
357 0         0 my @recipients = split /,/, $cfg->ADM_ADDR;
358 0         0 foreach my $recipient (@recipients)
359             {
360 0         0 $err = send_email($recipient, '[' . $cfg->MAIL_MRK . '] ECS Error',
361             undef, "Origin: $0\n", @_);
362              
363 0 0 0     0 log_error("Unable to send admin email to $recipient: $err")
364             if $err and $_[$#_] !~ /Unable to send admin email/iso;
365             }
366             }
367              
368 1         20 return $err;
369             }
370              
371             # ----------------------------------------------------------------------
372             # Send ECS email message.
373             # Returns empty string if successful or error message if error encountered.
374             sub send_ecs_message {
375 1     1 0 4 my $node_id = shift;
376 1         2 my $seq_num = shift;
377             # @_ now contains message body
378              
379             # initialize
380 1 50       3 return "EMDIS::ECS::send_ecs_message(): ECS has not been configured."
381             unless ecs_is_configured();
382 0         0 my $cfg = $ECS_CFG;
383 0         0 my $node_tbl = $ECS_NODE_TBL;
384 0         0 my $err = '';
385              
386             # do some validation
387 0         0 my ($hub_rcv, $hub_snd);
388 0 0 0     0 if($seq_num && !$node_id) {
389             # parse FML to determing $node_id:
390             # do some cursory validation, extract HUB_RCV and HUB_SND
391 0         0 my $fml = join('', @_);
392 0 0       0 return "EMDIS::ECS::send_ecs_message(): message does not contain valid FML"
393             unless $fml =~ /^.+:.+;/s;
394 0 0       0 if($fml =~ /HUB_RCV\s*=\s*([^,;]+)/is) { # presumes [^,;] in HUB_RCV
395 0         0 $hub_rcv = dequote(trim($1));
396             }
397             else {
398 0         0 return "EMDIS::ECS::send_ecs_message(): message does not specify " .
399             "HUB_RCV";
400             }
401 0 0       0 if($fml =~ /HUB_SND\s*=\s*([^,;]+)/is) { # presumes [^,;] in HUB_SND
402 0         0 $hub_snd = dequote(trim($1));
403             }
404             else {
405 0         0 return "EMDIS::ECS::send_ecs_message(): message does not specify " .
406             "HUB_SND";
407             }
408 0 0       0 return "EMDIS::ECS::send_ecs_message(): HUB_SND is incorrect: $hub_snd"
409             unless $hub_snd eq $ECS_CFG->THIS_NODE;
410 0 0       0 $node_id = $hub_rcv unless $node_id;
411 0 0       0 return "EMDIS::ECS::send_ecs_message(): node_id ($node_id) and FML " .
412             "HUB_RCV ($hub_rcv) do not match"
413             unless $node_id eq $hub_rcv;
414             }
415              
416             # look up specified node in node_tbl
417 0         0 my $was_locked = $node_tbl->LOCK;
418 0 0       0 if(not $was_locked) {
419 0 0       0 $node_tbl->lock() # lock node_tbl if needed
420             or return "EMDIS::ECS::send_ecs_message(): unable to lock node_tbl: " .
421             $node_tbl->ERROR;
422             }
423 0         0 my $node = $node_tbl->read($node_id);
424 0 0       0 if(not $node) {
425 0 0       0 $node_tbl->unlock() unless $was_locked; # unlock node_tbl if needed
426 0         0 return "EMDIS::ECS::send_ecs_message(): node not found: " . $node_id;
427             }
428 0 0       0 if(not $node->{addr}) {
429 0 0       0 $node_tbl->unlock() unless $was_locked; # unlock node_tbl if needed
430 0         0 return "EMDIS::ECS::send_ecs_message(): addr not defined for node: $node_id";
431             }
432 0 0       0 if($seq_num =~ /auto/i) {
433             # automatically get next sequence number
434 0         0 $node->{out_seq}++;
435 0         0 $seq_num = $node->{out_seq};
436             }
437              
438 0         0 my $subject = $cfg->MAIL_MRK . ':' . $cfg->THIS_NODE;
439 0 0       0 $subject .= ":$seq_num" if $seq_num;
440              
441 0         0 my $filename;
442              
443             # if not meta-message, copy to mboxes/out_NODE subdirectory
444 0 0       0 if($seq_num) {
445 0         0 $filename = format_msg_filename($node_id,$seq_num);
446             # create directory if it doesn't already exist
447 0         0 my $dirname = dirname($filename);
448 0 0       0 mkdir $dirname unless -e $dirname;
449             }
450             else {
451             # if meta-message, copy to mboxes/out subdirectory
452 0         0 $filename = sprintf("%s_%s_%s.msg",
453             $cfg->THIS_NODE, $node_id, "META");
454 0         0 my $dirname = $cfg->ECS_MBX_OUT_DIR;
455             # create directory if it doesn't already exist
456 0 0       0 mkdir $dirname unless -e $dirname;
457 0         0 $filename = catfile($dirname, $filename);
458             }
459              
460             # don't overwrite $filename file if it already exists
461 0         0 my $fh;
462 0 0       0 if(-e $filename) {
463 0         0 my $template = $filename . "_XXXX";
464 0         0 ($fh, $filename) = tempfile($template);
465 0 0       0 return "EMDIS::ECS::send_ecs_message(): unable to open _XXXX file: " .
466             "$filename"
467             unless $fh;
468             }
469             else {
470 0         0 $fh = new IO::File;
471 0 0       0 return "EMDIS::ECS::send_ecs_message(): unable to open file: " .
472             "$filename"
473             unless $fh->open("> $filename");
474             }
475              
476 0         0 $fh->print("Subject: $subject\n");
477 0         0 $fh->print("To: $node->{addr}\n");
478 0         0 $fh->print("From: " . $cfg->SMTP_FROM . "\n\n");
479 0         0 $fh->print(@_);
480 0         0 $fh->close();
481 0         0 chmod $FILEMODE, $filename;
482              
483 0 0 0     0 if ( $err ) {
    0          
484 0         0 $err = "EMDIS::ECS::send_ecs_message(): unable to update node $node_id: $err";
485             }
486             elsif ( not $seq_num and ($node->{encr_meta} !~ /true/i) ) {
487             # if indicated, don't encrypt meta-message
488 0 0 0     0 if(is_yes($cfg->ENABLE_AMQP) and exists $node->{amqp_addr_meta} and $node->{amqp_addr_meta}) {
    0 0        
489             # send meta-message via AMQP (if indicated by node config)
490             $err = send_amqp_message(
491             $node->{amqp_addr_meta},
492 0         0 $subject,
493             $node,
494             undef,
495             @_);
496             }
497             elsif(is_yes($node->{amqp_only})) {
498             $err = "EMDIS::ECS::send_ecs_message(): unable to send " .
499             "email META message to node " . $node->{node} .
500 0         0 ": amqp_only selected.";
501             }
502             else {
503 0         0 $err = send_email($node->{addr}, $subject, undef, @_);
504             }
505             }
506             else {
507             # otherwise, send encrypted message
508             $err = send_encrypted_message(
509             $node->{encr_typ},
510             $node->{addr_r},
511             $node->{addr},
512             $node->{encr_out_keyid},
513             $node->{encr_out_passphrase},
514 0         0 $node,
515             $subject,
516             undef,
517             @_);
518             }
519              
520 0 0       0 if ( ! $err ) {
521             # update node last_out, possibly out_seq
522 0         0 $node->{last_out} = time();
523 0 0       0 $err = $node_tbl->ERROR
524             unless $node_tbl->write($node_id, $node);
525             }
526             $node_tbl->unlock() # unlock node_tbl
527 0 0       0 unless $was_locked;
528              
529 0         0 return $err;
530             }
531              
532             # ----------------------------------------------------------------------
533             # Send email message. Takes four or more arguments: the recipient,
534             # subject line, custom headers (hash ref), and body lines to be emailed.
535             # Returns empty string if successful or error message if error encountered.
536             sub send_email {
537 1     1 0 3 my $recipient = shift;
538 1         2 my $subject = shift;
539 1         2 my $custom_headers = shift;
540             # @_ now contains message body
541              
542 1 50       17 return "EMDIS::ECS::send_email(): ECS has not been configured."
543             unless ecs_is_configured();
544 0         0 my $cfg = $ECS_CFG;
545              
546 0 0 0     0 return "EMDIS::ECS::send_email(): custom_headers must be undef or HASH ref (found " .
547             ref($custom_headers) . ")"
548             if defined $custom_headers and not 'HASH' eq ref $custom_headers;
549              
550 0         0 my $smtp;
551 0 0 0     0 if(is_yes($cfg->SMTP_USE_SSL) or is_yes($cfg->SMTP_USE_STARTTLS)) {
552 0 0       0 return "To use SSL or TLS please install Net::SMTP with version >= 3.05"
553             if CPAN::Version->vlt($Net::SMTP::VERSION, '3.05');
554             }
555 0 0       0 if(is_yes($cfg->SMTP_USE_SSL)) {
556 0 0       0 $smtp = Net::SMTP->new($cfg->SMTP_HOST,
557             Hello => $cfg->SMTP_DOMAIN,
558             Timeout => $cfg->SMTP_TIMEOUT,
559             Debug => $cfg->SMTP_DEBUG,
560             Port => $cfg->SMTP_PORT,
561             SSL => 1)
562             or return "Unable to open SMTP connection to " .
563             $cfg->SMTP_HOST . ": $@";
564             }
565             else {
566 0 0       0 $smtp = Net::SMTP->new($cfg->SMTP_HOST,
567             Hello => $cfg->SMTP_DOMAIN,
568             Timeout => $cfg->SMTP_TIMEOUT,
569             Debug => $cfg->SMTP_DEBUG,
570             Port => $cfg->SMTP_PORT)
571             or return "Unable to open SMTP connection to " .
572             $cfg->SMTP_HOST . ": $@";
573 0 0       0 if(is_yes($cfg->SMTP_USE_STARTTLS)) {
574 0 0       0 if(not $smtp->starttls()) {
575 0         0 my $err = "STARTTLS failed: " . $smtp->message();
576 0         0 $smtp->quit();
577 0         0 return $err;
578             }
579             }
580             }
581 0 0       0 if($cfg->SMTP_OAUTH_TOKEN_CMD) {
582 0 0 0     0 if(!is_yes($ECS_CFG->SMTP_USE_SSL) and !is_yes($ECS_CFG->SMTP_USE_STARTTLS)) {
583 0         0 $smtp->quit();
584 0         0 return "Unable to use SMTP SASL OAuth authentication without SSL/TLS.";
585             }
586              
587 0 0       0 return "To use SASL authentication mechanisms XOAUTH2 or OAUTHBEARER " .
588             "please install Authen::SASL::Perl with version >= 2.1800"
589             if CPAN::Version->vlt($Authen::SASL::Perl::VERSION, '2.1800');
590              
591             # using SASL XOAUTH2 or OAUTHBEARER authentication
592 0         0 my ($err, $access_token) = get_oauth_token(
593             $cfg->SMTP_OAUTH_TOKEN_CMD_TIMELIMIT,
594             $cfg->SMTP_OAUTH_TOKEN_CMD,
595             'SMTP_OAUTH_TOKEN_CMD');
596 0 0       0 if($err) {
597 0         0 $smtp->quit();
598 0         0 return "Unable to get access token: $err";
599             }
600              
601 0         0 my $sasl = Authen::SASL->new(
602             mechanism => $ECS_CFG->SMTP_OAUTH_SASL_MECHANISM,
603             callback => {
604             user => $ECS_CFG->SMTP_USERNAME,
605             pass => $access_token,
606             }
607             );
608 0 0       0 if(not $sasl) {
609 0         0 $smtp->quit();
610 0         0 return("Unable to construct Authen::SASL object (SMTP).", undef);
611             }
612              
613 0 0       0 if(not $smtp->auth($sasl)) {
614 0         0 my $err = $smtp->message();
615 0         0 $smtp->quit();
616 0         0 return "Unable to authenticate via SASL OAuth method to " . $cfg->SMTP_DOMAIN .
617             " SMTP server: $err";
618             }
619             }
620             else {
621             # using username/password authentication
622 0 0 0     0 if($cfg->SMTP_USERNAME and $cfg->SMTP_PASSWORD) {
623 0 0       0 if(not $smtp->auth($cfg->SMTP_USERNAME, $cfg->SMTP_PASSWORD)) {
624 0         0 my $err = "Unable to authenticate with " . $cfg->SMTP_DOMAIN .
625             " SMTP server as user " . $cfg->SMTP_USERNAME . ": " .
626             $smtp->message();
627 0         0 $smtp->quit();
628 0         0 return $err;
629             }
630             }
631             }
632 0 0       0 $smtp->mail($cfg->SMTP_FROM)
633             or return "Unable to initiate sending of email message.";
634 0 0       0 $smtp->to($recipient)
635             or return "Unable to define email recipient.";
636 0 0       0 $smtp->data()
637             or return "Unable to start sending of email data.";
638 0 0       0 if(defined $custom_headers)
639             {
640 0         0 for my $key (keys %$custom_headers)
641             {
642 0         0 my $value = $custom_headers->{$key};
643 0 0       0 $smtp->datasend("$key: $value\n")
644             or return "Unable to send email data.";
645             }
646             }
647 0         0 my $from_addr = $cfg->SMTP_FROM;
648 0 0       0 $smtp->datasend("From: $from_addr\n")
649             or return "Unable to send email data.";
650 0 0       0 $smtp->datasend("Subject: $subject\n")
651             or return "Unable to send email data.";
652 0 0       0 $smtp->datasend("To: $recipient\n")
653             or return "Unable to send email data.";
654 0 0       0 if($cfg->ADM_ADDR =~ /\b$recipient\b/) {
655             # set reply-to header when sending mail to admin
656 0 0       0 $smtp->datasend("Reply-To: $recipient\n")
657             or return "Unable to send email data.";
658             }
659 0 0       0 $smtp->datasend("MIME-Version: 1.0\n")
660             or return "Unable to send email data.";
661 0 0       0 $smtp->datasend("Content-Type: text/plain\n")
662             or return "Unable to send email data.";
663 0 0       0 $smtp->datasend("Content-Transfer-Encoding: 7bit\n")
664             or return "Unable to send email data.";
665 0 0       0 $smtp->datasend("\n")
666             or return "Unable to send email data.";
667 0 0       0 $smtp->datasend(@_)
668             or return "Unable to send email data.";
669 0 0       0 $smtp->dataend()
670             or return "Unable to end sending of email data.";
671 0 0       0 $smtp->quit()
672             or return "Unable to close the SMTP connection.";
673 0         0 return ''; # successful
674             }
675              
676             # ----------------------------------------------------------------------
677             # Send AMQP message. AMQP analog for send_email(). Takes five or more
678             # arguments: the AMQP address (queue name), subject line, node_info
679             # (hash ref), custom properties (hash ref), and body lines to be
680             # emailed. Returns empty string if successful or error message if
681             # error encountered.
682             sub send_amqp_message {
683 0     0 0 0 my $amqp_addr = shift;
684 0         0 my $subject = shift;
685 0         0 my $node = shift;
686 0         0 my $custom_properties = shift;
687             # @_ now contains message body
688              
689 0 0       0 if(not defined $amqp_addr) {
690 0         0 return 'send_amqp_message(): Missing amqp_addr (required).';
691             }
692              
693 0 0       0 if(not defined $subject) {
694 0         0 return 'send_amqp_message(): Missing subject (required).';
695             }
696              
697 0 0       0 if(not defined $node) {
    0          
698 0         0 return 'send_amqp_message(): Missing node details (required).';
699             }
700             elsif(not 'HASH' eq ref $node) {
701 0 0       0 return 'send_amqp_message(): unexpected node details; expected HASH ref, found ' .
702             (ref $custom_properties ? ref $custom_properties . ' ref' : '(non reference)');
703             }
704              
705 0 0 0     0 if(defined $custom_properties and not 'HASH' eq ref $custom_properties) {
706 0 0       0 return 'send_amqp_message(): unexpected custom_properties value; expected undef or HASH ref, found ' .
707             (ref $custom_properties ? ref $custom_properties . ' ref' : '(non reference)');
708             }
709              
710 0 0 0     0 if ((exists $node->{node_disabled}) and is_yes($node->{node_disabled}) )
711             {
712             return('send_amqp_message(): node_disabled is set for node ' .
713 0         0 $node->{node} . '. Message not sent.');
714 0         0 next;
715             }
716              
717             # default send_opts
718             my $send_opts = {
719             'amqp_broker_url' => $ECS_CFG->AMQP_BROKER_URL,
720             'amqp_cmd_send' => $ECS_CFG->AMQP_CMD_SEND,
721             # 'amqp_content_type' => 'text/plain',
722             'amqp_debug_level' => $ECS_CFG->AMQP_DEBUG_LEVEL,
723             # 'amqp_encoding' => 'utf-8',
724             'amqp_password' => (exists $ECS_CFG->{AMQP_PASSWORD} ? $ECS_CFG->AMQP_PASSWORD : ''),
725             'amqp_sslcert' => (exists $ECS_CFG->{AMQP_SSLCERT} ? $ECS_CFG->AMQP_SSLCERT : ''),
726             'amqp_sslkey' => (exists $ECS_CFG->{AMQP_SSLKEY} ? $ECS_CFG->AMQP_SSLKEY : ''),
727             'amqp_sslpass' => (exists $ECS_CFG->{AMQP_SSLPASS} ? $ECS_CFG->AMQP_SSLPASS : ''),
728             'amqp_truststore' => (exists $ECS_CFG->{AMQP_TRUSTSTORE} ? $ECS_CFG->AMQP_TRUSTSTORE : ''),
729             'amqp_username' => (exists $ECS_CFG->{AMQP_USERNAME} ? $ECS_CFG->AMQP_USERNAME : ''),
730 0 0       0 'amqp_vhost' => (exists $ECS_CFG->{AMQP_VHOST} ? $ECS_CFG->AMQP_VHOST : '')
    0          
    0          
    0          
    0          
    0          
    0          
731             };
732              
733             # override default send_opts with node-specific opts (where indicated)
734 0         0 foreach my $opt (keys %$send_opts) {
735             $send_opts->{$opt} = $node->{$opt}
736 0 0       0 if exists $node->{$opt};
737             }
738              
739             # default send_props
740 0         0 my $mail_mrk = $ECS_CFG->MAIL_MRK;
741 0         0 my $hub_snd = '';
742 0         0 my $seq_num = '';
743 0 0       0 if($subject =~ /$mail_mrk:(\S+?):(\d+):(\d+)\/(\d+)\s*$/io) {
    0          
    0          
    0          
744 0         0 $hub_snd = $1;
745 0         0 $seq_num = "$2:$3/$4";
746             }
747             elsif($subject =~ /$mail_mrk:(\S+?):(\d+)\s*$/io) {
748 0         0 $hub_snd = $1;
749 0         0 $seq_num = $2;
750             }
751             elsif($subject =~ /$mail_mrk:(\S+?):(\d+):DOC\s*$/io) {
752 0         0 $hub_snd = $1;
753 0         0 $seq_num = $2;
754             }
755             elsif($subject =~ /$mail_mrk:(\S+)\s*$/io) {
756 0         0 $hub_snd = $1;
757             }
758             # sanity check
759 0 0       0 if($ECS_CFG->THIS_NODE ne $hub_snd) {
760 0         0 return "send_amqp_message(): hub_snd ($hub_snd) ne THIS_NODE (" . $ECS_CFG->THIS_NODE . ")";
761             }
762             my $send_props = {
763             'x-emdis-hub-snd' => $ECS_CFG->THIS_NODE,
764 0 0       0 'x-emdis-hub-rcv' => ($node->{node} ? $node->{node} : ''),
    0          
765             'x-emdis-sequential-number' => ($seq_num ? $seq_num : '')
766             };
767              
768             # add custom properties to send_props (where indicated)
769 0 0       0 if(defined $custom_properties) {
770 0         0 foreach my $prop (keys %$custom_properties) {
771 0         0 $send_props->{$prop} = $custom_properties->{$prop};
772             }
773             }
774              
775             # construct AMQP send command
776             my $cmd = sprintf('%s --inputfile - --debug %s --address %s --broker %s ' .
777             '--subject %s',
778             $send_opts->{amqp_cmd_send},
779             $send_opts->{amqp_debug_level},
780             $amqp_addr,
781             $send_opts->{amqp_broker_url},
782 0         0 $subject);
783             $cmd .= sprintf(' --type %s', $send_opts->{amqp_content_type})
784 0 0       0 if $send_opts->{amqp_content_type};
785             $cmd .= sprintf(' --encoding %s', $send_opts->{amqp_encoding})
786 0 0       0 if $send_opts->{amqp_encoding};
787             $cmd .= sprintf(' --vhost %s', $send_opts->{amqp_vhost})
788 0 0       0 if $send_opts->{amqp_vhost};
789             $cmd .= sprintf(' --truststore %s', $send_opts->{amqp_truststore})
790 0 0       0 if $send_opts->{amqp_truststore};
791             $cmd .= sprintf(' --sslcert %s --sslkey %s',
792             $send_opts->{amqp_sslcert},
793             $send_opts->{amqp_sslkey})
794 0 0 0     0 if $send_opts->{amqp_sslcert} and $send_opts->{amqp_sslkey};
795             $cmd .= sprintf(' --username %s', $send_opts->{amqp_username})
796 0 0       0 if $send_opts->{amqp_username};
797 0         0 foreach my $prop (keys %$send_props) {
798             $cmd .= sprintf(' --property %s=%s', $prop, $send_props->{$prop})
799 0 0       0 if $send_props->{$prop};
800             }
801              
802             # set environment variables containing passwords:
803             # ECS_AMQP_PASSWORD and ECS_AMQP_SSLPASS
804             $ENV{ECS_AMQP_PASSWORD} = $send_opts->{amqp_password}
805 0 0       0 if $send_opts->{amqp_password};
806             $ENV{ECS_AMQP_SSLPASS} = $send_opts->{amqp_sslpass}
807 0 0       0 if $send_opts->{amqp_sslpass};
808              
809             # execute command to send AMQP message
810 0 0       0 print ": AMQP send command: $cmd\n"
811             if $ECS_CFG->ECS_DEBUG > 0;
812 0         0 my $err = timelimit_cmd($ECS_CFG->AMQP_SEND_TIMELIMIT, $cmd, join('', @_));
813 0 0       0 if($err) {
814 0         0 return "send_amqp_message(): unable to send AMQP message to $amqp_addr: $err";
815             }
816              
817 0         0 return '';
818             }
819              
820             # ----------------------------------------------------------------------
821             # Send encrypted email message.
822             # Returns empty string if successful or error message if error encountered.
823             sub send_encrypted_message
824             {
825 1     1 0 3 my $encr_typ = shift;
826 1         3 my $encr_recip = shift;
827 1         2 my $recipient = shift;
828 1         4 my $encr_out_keyid = shift;
829 1         3 my $encr_out_passphrase = shift;
830 1         3 my $node = shift;
831 1         2 my $subject = shift;
832 1         2 my $custom_headers = shift;
833             # @_ now contains message body
834              
835 1 50       3 return "EMDIS::ECS::send_encrypted_message(): ECS has not been configured."
836             unless ecs_is_configured();
837 0         0 my $cfg = $ECS_CFG;
838              
839             # compose template for name of temp file
840 0         0 my $template = format_datetime(time, '%04d%02d%02d_%02d%02d%02d_XXXX');
841              
842             # write message body to temp file
843 0         0 my ($fh, $filename) = tempfile($template,
844             DIR => $cfg->ECS_TMP_DIR,
845             SUFFIX => '.tmp');
846 0 0       0 return "EMDIS::ECS::send_encrypted_message(): unable to create temporary file"
847             unless $fh;
848 0         0 print $fh @_;
849 0         0 close $fh;
850 0         0 chmod $FILEMODE, $filename;
851            
852             # create file containing encrypted message
853 0         0 my $encr_filename = "$filename.pgp";
854 0         0 my $result = '';
855 0         0 for ($encr_typ) {
856 0 0       0 /PGP2/i and do {
857 0         0 $result = pgp2_encrypt($filename, $encr_filename, $encr_recip,
858             $encr_out_keyid, $encr_out_passphrase);
859 0         0 last;
860             };
861 0 0       0 /OpenPGP/i and do {
862 0         0 $result = openpgp_encrypt($filename, $encr_filename, $encr_recip,
863             $encr_out_keyid, $encr_out_passphrase);
864 0         0 last;
865             };
866 0         0 $result = "unrecognized encr_typ: $encr_typ";
867             }
868              
869             # delete first temp file
870 0         0 unlink $filename;
871              
872             # check for error
873 0 0       0 return "EMDIS::ECS::send_encrypted_message(): $result" if $result;
874              
875             # read contents of encrypted file
876 0         0 $fh = new IO::File;
877 0 0       0 return "EMDIS::ECS::send_encrypted_message(): unable to open file: " .
878             "$encr_filename"
879             unless $fh->open("< $encr_filename");
880 0         0 my @body = $fh->getlines();
881 0         0 $fh->close();
882              
883             # delete encrypted (temp) file
884 0         0 unlink $encr_filename;
885              
886 0 0       0 if(is_yes($cfg->ENABLE_AMQP)) {
887             # send message via AMQP, if indicated by node config
888 0         0 my $amqp_addr = '';
889 0 0       0 if($subject =~ /^[^:]+:[^:]+$/io) {
    0          
    0          
    0          
890             return "EMDIS::ECS::send_encrypted_message(): unable to send " .
891             "AMQP META message to node " . $node->{node} . ": amqp_only " .
892             "selected, but amqp_addr_meta not configured."
893 0 0 0     0 if not $node->{amqp_addr_meta} and is_yes($node->{amqp_only});
894 0         0 $amqp_addr = $node->{amqp_addr_meta};
895             }
896             elsif($subject =~ /^[^:]+:[^:]+:[0123456789]+:DOC/io) {
897             return "EMDIS::ECS::send_encrypted_message(): unable to send " .
898             "AMQP document to node " . $node->{node} . ": amqp_only " .
899             "selected, but amqp_addr_doc not configured."
900 0 0 0     0 if not $node->{amqp_addr_doc} and is_yes($node->{amqp_only});
901 0         0 $amqp_addr = $node->{amqp_addr_doc};
902             }
903             elsif($subject =~ /^[^:]+:[^:]+:[0123456789]+/io) {
904             return "EMDIS::ECS::send_encrypted_message(): unable to send " .
905             "AMQP regular message to node " . $node->{node} . ": amqp_only " .
906             "selected, but amqp_addr_msg not configured."
907 0 0 0     0 if not $node->{amqp_addr_msg} and is_yes($node->{amqp_only});
908 0         0 $amqp_addr = $node->{amqp_addr_msg};
909             }
910             elsif(is_yes($node->{amqp_only})) {
911             return "EMDIS::ECS::send_encrypted_message(): unable to send " .
912 0         0 "via AMQP to node " . $node->{node} . ": amqp_only selected, " .
913             "but unable to determine amqp_addr from Subject: $subject"
914             }
915 0 0       0 if($amqp_addr) {
916 0         0 return send_amqp_message(
917             $amqp_addr,
918             $subject,
919             $node,
920             $custom_headers,
921             @body);
922             }
923             }
924              
925 0 0       0 if(is_yes($node->{amqp_only})) {
926             return "EMDIS::ECS::send_encrypted_message(): unable to send " .
927 0         0 "via email to node " . $node->{node} . ": amqp_only selected"
928             }
929              
930 0 0 0     0 if($node->{amqp_addr_meta} or $node->{amqp_addr_msg} or $node->{amqp_addr_doc}) {
      0        
931             # print debug message if AMQP is only partially configured for recipient node
932             print " EMDIS::ECS::send_encrypted_message(): sending via " .
933 0 0       0 "email (not AMQP) to node " . $node->{node} . ": $subject\n"
934             if $cfg->ECS_DEBUG > 0;
935             }
936              
937             # send message via email
938 0         0 return send_email($recipient, $subject, $custom_headers, @body);
939             }
940              
941             # ----------------------------------------------------------------------
942             # Format a datetime value
943             sub format_datetime
944             {
945 2     2 0 6 my $datetime = shift;
946 2         5 my $format = shift;
947 2 100       8 $format = '%04d-%02d-%02d %02d:%02d:%02d'
948             unless defined $format;
949 2         37 my ($seconds, $minutes, $hours, $mday, $month, $year, $wday, $yday,
950             $isdst) = localtime($datetime);
951 2         32 return sprintf($format, $year + 1900, $month + 1, $mday,
952             $hours, $minutes, $seconds);
953             }
954              
955             # ----------------------------------------------------------------------
956             # Format filename for document.
957             sub format_doc_filename
958             {
959 0 0   0 0 0 return "EMDIS::ECS::format_doc_filename(): ECS has not been configured."
960             unless ecs_is_configured();
961 0         0 my $cfg = $ECS_CFG;
962 0         0 my $node_id = shift;
963 0         0 my $seq_num = shift;
964 0         0 my $template = sprintf("%s_%s_d%010d",
965             $cfg->THIS_NODE, $node_id, $seq_num);
966 0         0 my $dirname = $cfg->ECS_MBX_OUT_DIR . "_$node_id";
967 0         0 return catfile($dirname, "$template.doc");
968             }
969              
970             # ----------------------------------------------------------------------
971             # Format filename for regular message.
972             sub format_msg_filename
973             {
974 1 50   1 0 4 return "EMDIS::ECS::format_msg_filename(): ECS has not been configured."
975             unless ecs_is_configured();
976 0         0 my $cfg = $ECS_CFG;
977 0         0 my $node_id = shift;
978 0         0 my $seq_num = shift;
979 0         0 my $template = sprintf("%s_%s_%010d",
980             $cfg->THIS_NODE, $node_id, $seq_num);
981 0         0 my $dirname = $cfg->ECS_MBX_OUT_DIR . "_$node_id";
982 0         0 return catfile($dirname, "$template.msg");
983             }
984              
985             # ----------------------------------------------------------------------
986             # Use OpenPGP (GnuPG) to decrypt a file.
987             # Returns empty string if successful or error message if error encountered.
988             sub openpgp_decrypt
989             {
990 1     1 0 3 my $input_filename = shift;
991 1         2 my $output_filename = shift;
992 1         2 my $required_signature = shift;
993 1         3 my $encr_out_passphrase = shift;
994              
995             # initialize
996 1 50       3 return "EMDIS::ECS::openpgp_decrypt(): ECS has not been configured."
997             unless ecs_is_configured();
998 0         0 my $cfg = $ECS_CFG;
999              
1000             # compose command
1001 0         0 my $cmd = $cfg->OPENPGP_CMD_DECRYPT;
1002 0         0 $cmd =~ s/__INPUT__/$input_filename/g;
1003 0         0 $cmd =~ s/__OUTPUT__/$output_filename/g;
1004 0 0       0 print " openpgp_decrypt() command: $cmd\n"
1005             if $cfg->ECS_DEBUG > 0;
1006              
1007             # set GNUPGHOME environment variable
1008 0         0 $ENV{GNUPGHOME} = $cfg->GPG_HOMEDIR;
1009              
1010             # attempt to execute command
1011 0 0 0     0 my $result = timelimit_cmd($cfg->T_MSG_PROC, $cmd,
      0        
1012             (defined $encr_out_passphrase and 0 < length $encr_out_passphrase) ?
1013             $encr_out_passphrase :
1014             (defined $cfg->GPG_PASSPHRASE and 0 < length $cfg->GPG_PASSPHRASE ?
1015             $cfg->GPG_PASSPHRASE : undef));
1016 0 0       0 $result = "EMDIS::ECS::openpgp_decrypt(): $result" if $result;
1017              
1018             # check signature, if indicated
1019 0 0 0     0 if(defined($required_signature) and not $result) {
1020 0 0       0 if($cmd_output !~ /Good signature from[^\n]+$required_signature/is) {
1021 0         0 $result = "EMDIS::ECS::openpgp_decrypt(): required signature not " .
1022             "present: $required_signature";
1023             }
1024             }
1025              
1026 0         0 return $result;
1027             }
1028              
1029             # ----------------------------------------------------------------------
1030             # Use OpenPGP (GnuPG) to encrypt a file.
1031             # Returns empty string if successful or error message if error encountered.
1032             sub openpgp_encrypt
1033             {
1034 1     1 0 2 my $input_filename = shift;
1035 1         3 my $output_filename = shift;
1036 1         2 my $recipient = shift;
1037 1         3 my $encr_out_keyid = shift;
1038 1         22 my $encr_out_passphrase = shift;
1039              
1040             # initialize
1041 1 50       5 return "EMDIS::ECS::openpgp_encrypt(): ECS has not been configured."
1042             unless ecs_is_configured();
1043 0         0 my $cfg = $ECS_CFG;
1044              
1045             # compose command
1046 0 0 0     0 my $keyid = (defined $encr_out_keyid and 0 < length $encr_out_keyid) ?
1047             $encr_out_keyid : $cfg->GPG_KEYID;
1048 0         0 my $cmd = $cfg->OPENPGP_CMD_ENCRYPT;
1049 0         0 $cmd =~ s/__INPUT__/$input_filename/g;
1050 0         0 $cmd =~ s/__OUTPUT__/$output_filename/g;
1051 0         0 $cmd =~ s/__RECIPIENT__/$recipient/g;
1052 0         0 $cmd =~ s/__SELF__/$keyid/g;
1053 0 0       0 print " openpgp_encrypt() command: $cmd\n"
1054             if $cfg->ECS_DEBUG > 0;
1055              
1056             # set GNUPGHOME environment variable
1057 0         0 $ENV{GNUPGHOME} = $cfg->GPG_HOMEDIR;
1058              
1059             # attempt to execute command
1060 0 0 0     0 my $result = timelimit_cmd($cfg->T_MSG_PROC, $cmd,
      0        
1061             (defined $encr_out_passphrase and 0 < length $encr_out_passphrase) ?
1062             $encr_out_passphrase :
1063             (defined $cfg->GPG_PASSPHRASE and 0 < length $cfg->GPG_PASSPHRASE ?
1064             $cfg->GPG_PASSPHRASE : undef));
1065 0 0       0 $result = "EMDIS::ECS::openpgp_encrypt(): $result" if $result;
1066 0         0 return $result;
1067             }
1068              
1069             # ----------------------------------------------------------------------
1070             # Use PGP2 (PGP) to decrypt a file.
1071             # Returns empty string if successful or error message if error encountered.
1072             sub pgp2_decrypt
1073             {
1074 1     1 0 3 my $input_filename = shift;
1075 1         3 my $output_filename = shift;
1076 1         2 my $required_signature = shift;
1077 1         2 my $encr_out_passphrase = shift;
1078              
1079             # initialize
1080 1 50       4 return "EMDIS::ECS::pgp2_decrypt(): ECS has not been configured."
1081             unless ecs_is_configured();
1082 0         0 my $cfg = $ECS_CFG;
1083              
1084             # compose command
1085 0         0 my $cmd = $cfg->PGP2_CMD_DECRYPT;
1086 0         0 $cmd =~ s/__INPUT__/$input_filename/g;
1087 0         0 $cmd =~ s/__OUTPUT__/$output_filename/g;
1088 0 0       0 print " pgp2_decrypt() command: $cmd\n"
1089             if $cfg->ECS_DEBUG > 0;
1090              
1091             # set PGPPATH and PGPPASS environment variables
1092 0         0 $ENV{PGPPATH} = $cfg->PGP_HOMEDIR;
1093 0 0 0     0 my $passphrase = (defined $encr_out_passphrase and 0 < length $encr_out_passphrase) ?
1094             $encr_out_passphrase : $cfg->PGP_PASSPHRASE;
1095 0         0 $ENV{PGPPASS} = $passphrase;
1096              
1097             # attempt to execute command - pipe passphrase to cmd, to support usage of gpg1 in place of pgp2
1098 0         0 my $result = timelimit_cmd($cfg->T_MSG_PROC, $cmd, $passphrase);
1099 0 0       0 $result = '' if($result =~ /^Status 0x0100/); # ignore exit value = 1
1100 0 0       0 $result = "EMDIS::ECS::pgp2_decrypt(): $result" if $result;
1101              
1102             # check signature, if indicated
1103 0 0 0     0 if(defined($required_signature) and not $result) {
1104 0 0       0 if($cmd_output !~ /Good signature from[^\n]+$required_signature/is) {
1105 0         0 $result = "EMDIS::ECS::pgp2_decrypt(): required signature not " .
1106             "present: $required_signature";
1107             }
1108             }
1109              
1110 0         0 return $result;
1111             }
1112              
1113             # ----------------------------------------------------------------------
1114             # Use PGP to encrypt a file.
1115             # Returns empty string if successful or error message if error encountered.
1116             sub pgp2_encrypt
1117             {
1118 1     1 0 2 my $input_filename = shift;
1119 1         3 my $output_filename = shift;
1120 1         2 my $recipient = shift;
1121 1         14 my $encr_out_keyid = shift;
1122 1         4 my $encr_out_passphrase = shift;
1123              
1124             # initialize
1125 1 50       3 return "EMDIS::ECS::pgp2_encrypt(): ECS has not been configured."
1126             unless ecs_is_configured();
1127 0         0 my $cfg = $ECS_CFG;
1128              
1129             # compose command
1130 0 0 0     0 my $keyid = (defined $encr_out_keyid and 0 < length $encr_out_keyid) ?
1131             $encr_out_keyid : $cfg->PGP_KEYID;
1132 0         0 my $cmd = $cfg->PGP2_CMD_ENCRYPT;
1133 0         0 $cmd =~ s/__INPUT__/$input_filename/g;
1134 0         0 $cmd =~ s/__OUTPUT__/$output_filename/g;
1135 0         0 $cmd =~ s/__RECIPIENT__/$recipient/g;
1136 0         0 $cmd =~ s/__SELF__/$keyid/g;
1137 0 0       0 print " pgp2_encrypt() command: $cmd\n"
1138             if $cfg->ECS_DEBUG > 0;
1139              
1140             # set PGPPATH and PGPPASS environment variables
1141 0         0 $ENV{PGPPATH} = $cfg->PGP_HOMEDIR;
1142 0 0 0     0 my $passphrase = (defined $encr_out_passphrase and 0 < length $encr_out_passphrase) ?
1143             $encr_out_passphrase : $cfg->PGP_PASSPHRASE;
1144 0         0 $ENV{PGPPASS} = $passphrase;
1145            
1146             # attempt to execute command - pipe passphrase to cmd, to support usage of gpg1 in place of pgp2
1147 0         0 my $result = timelimit_cmd($cfg->T_MSG_PROC, $cmd, $passphrase);
1148 0 0       0 $result = "EMDIS::ECS::pgp2_encrypt(): $result" if $result;
1149 0         0 return $result;
1150             }
1151              
1152             # ----------------------------------------------------------------------
1153             # Check whether another copy of the program is already running.
1154             # If so, this one dies.
1155             sub check_pid
1156             {
1157 0 0   0 0 0 die "EMDIS::ECS::check_pid(): ECS has not been configured."
1158             unless ecs_is_configured();
1159              
1160 0 0       0 if(open PIDFILE, $pidfile) {
1161 0         0 my $pid = ;
1162 0         0 $pid =~ s/\s+//g;
1163 0 0       0 die "Error: $0 is already running (pid $pid).\n"
1164             if kill(0, $pid);
1165 0         0 close PIDFILE;
1166             }
1167              
1168 0         0 save_pid();
1169             }
1170              
1171             # ----------------------------------------------------------------------
1172             # Update PID file.
1173             sub save_pid
1174             {
1175 0 0   0 0 0 die "EMDIS::ECS::save_pid(): ECS has not been configured."
1176             unless ecs_is_configured();
1177              
1178 0         0 open PIDFILE, ">$pidfile";
1179 0         0 print PIDFILE "$$\n";
1180 0         0 close PIDFILE;
1181 0         0 chmod $FILEMODE, $pidfile;
1182 0         0 $pid_saved = 1;
1183             }
1184              
1185             # ----------------------------------------------------------------------
1186             # Select the Win32 or Unix version of timelimit_cmd
1187             sub timelimit_cmd
1188             {
1189 0 0   0 0 0 $^O =~ /MSWin32/ ? timelimit_cmd_win32(@_) : timelimit_cmd_unix(@_);
1190             }
1191              
1192              
1193              
1194             # Returns empty string if successful or error message if error encountered.
1195             sub timelimit_cmd_win32
1196             {
1197 0     0 0 0 my $timelimit = shift;
1198 0         0 my $cmd = shift;
1199 0         0 my $input_data = shift;
1200 0         0 my $cfg = $ECS_CFG;
1201 0         0 my @msgs = ();
1202 0         0 my $result = "";
1203 0         0 my ($ProcessObj, $rc, $appname, $cmdline);
1204              
1205             # reset module-level variable containing command output
1206 0         0 $cmd_output = '';
1207              
1208 0         0 pipe(READ, WRITE);
1209 0         0 select(WRITE);
1210 0         0 $| = 1;
1211 0         0 select(STDOUT);
1212 0 0       0 open(OLDIN, "< &STDIN") || die "Can not save STDIN\n";
1213 0 0       0 open(STDIN, "< &READ") || die "Can not redirect STDIN\n";
1214              
1215 0 0       0 open(OLDOUT, ">&STDOUT") || die "Can not save STDOUT\n";
1216 0 0       0 open(STDOUT, ">$$.txt" ) || die( "Unable to redirect STDOUT ");
1217              
1218 0 0       0 open(OLDERR, ">&STDERR" ) || die "Can not redirect STDERR\n";
1219 0 0       0 open(STDERR, ">&STDOUT" ) || die( "Unable to dup STDOUT to STDERR" );
1220              
1221 0         0 select(STDERR);
1222 0         0 $| = 1;
1223 0         0 select(STDIN);
1224 0         0 $| = 1;
1225 0         0 select(STDOUT);
1226              
1227 0 0       0 if(! defined $input_data) { $input_data = ""; }
  0         0  
1228              
1229             # compute $appname and $cmdline
1230 0         0 $cmd =~ /\s*(\S+)\s*(.*)/;
1231 0         0 $appname = $1;
1232 0         0 $cmdline = "$1 $2";
1233             # if applicable, append .exe or .bat extension to $appname
1234 0 0       0 if(-x "$appname.exe")
    0          
1235             {
1236 0         0 $appname = "$appname.exe";
1237             }
1238             elsif(-x "$appname.bat")
1239             {
1240 0         0 $appname = "$appname.bat";
1241             }
1242            
1243 0 0       0 print "\n: Running External Command" .
1244             "\nappname=" . $appname .
1245             "\ncmdline=" . $cmdline .
1246             # "\nSTDIN=" . $input_data . # (don't print out PGP passphrase)
1247             "\nTimelimit=" . $timelimit . "\n"
1248             if $cfg->ECS_DEBUG > 0;
1249              
1250 0         0 $rc = Win32::Process::Create(
1251             $ProcessObj,
1252             $appname,
1253             $cmdline,
1254             1,
1255             Win32::Process::constant('NORMAL_PRIORITY_CLASS'),
1256             ".");
1257              
1258 0 0       0 if ($rc) {
1259 0 0       0 print ": PID = " . $ProcessObj->GetProcessID() . "\n"
1260             if $cfg->ECS_DEBUG > 0;
1261             }
1262             else {
1263 0         0 my $winMsg = Win32::FormatMessage(Win32::GetLastError());
1264 0 0       0 if (defined $winMsg) {
1265 0         0 $result = $winMsg;
1266             } else {
1267 0 0       0 print ": Windows error\n"
1268             if $cfg->ECS_DEBUG > 0;
1269 0         0 $result = "Windows error";
1270             }
1271             }
1272              
1273 0 0       0 if($rc)
1274             {
1275 0         0 print WRITE "$input_data\n";
1276 0         0 close(WRITE);
1277              
1278 0 0       0 print ": Waiting\n"
1279             if $cfg->ECS_DEBUG > 0;
1280 0         0 $rc = $ProcessObj->Wait($timelimit * 1000);
1281              
1282             # Check for return code
1283 0 0       0 if ($rc ) {
1284 0         0 my $ret;
1285 0         0 $ProcessObj->GetExitCode($ret);
1286 0 0       0 print ": Process OK ($ret)\n\n"
1287             if $cfg->ECS_DEBUG > 0;
1288             } else {
1289 0         0 Win32::Process::KillProcess($ProcessObj->GetProcessID(), 0);
1290 0 0       0 print ": Process Timeout\n\n"
1291             if $cfg->ECS_DEBUG > 0;
1292 0         0 $result = "Process Timeout";
1293             }
1294             }
1295              
1296             # Restore STDIN, STDOUT, STDERR
1297 0         0 open(STDIN, "<&OLDIN");
1298 0         0 open(STDOUT, ">&OLDOUT" );
1299 0         0 open(STDERR, ">&OLDERR" );
1300              
1301 0         0 if(0)
1302             {
1303             # just leave these hanging until next time around ...
1304             # (avoid potential deadlock waiting for child process to end)
1305             close(READ);
1306             close(OLDIN);
1307             close(OLDOUT);
1308             close(OLDERR);
1309             }
1310              
1311              
1312 0 0       0 if(open FILETEMP, "< $$.txt")
1313             {
1314 0         0 @msgs = ;
1315 0         0 close FILETEMP;
1316 0         0 unlink "$$.txt";
1317 0         0 print "\n======== EXTERNAL BEGIN =============\n";
1318 0         0 print @msgs;
1319 0         0 print "========= EXTERNAL END ==============\n";
1320             }
1321              
1322             # set module-level variable containing command output
1323 0 0       0 if($#msgs >= 0) { $cmd_output = join('', @msgs); }
  0         0  
1324 0         0 else { $cmd_output = ''; }
1325              
1326 0         0 return $result;
1327             }
1328              
1329              
1330             # ----------------------------------------------------------------------
1331             # Unix version
1332             # Execute specified command, with time limit and optional input data.
1333             # Returns empty string if successful or error message if error encountered.
1334             sub timelimit_cmd_unix
1335             {
1336 0     0 0 0 my $timelimit = shift;
1337 0         0 my $cmd = shift;
1338 0         0 my $input_data = shift;
1339              
1340             # reset module-level variable containing command output
1341 0         0 $cmd_output = '';
1342              
1343             # initialize
1344 0         0 my ($reader, $writer) = (IO::Handle->new, IO::Handle->new);
1345 0         0 my ($pid, @msgs, $status);
1346 0         0 my $result = '';
1347              
1348             # set up "local" SIG_PIPE and SIG_ALRM handlers
1349             # (Note: not using "local $SIG{PIPE}" because it ignores die())
1350 0         0 my $broken_pipe = '';
1351 0         0 my $oldsigpipe = $SIG{PIPE};
1352 0     0   0 $SIG{PIPE} = sub { $broken_pipe = 1; };
  0         0  
1353 0         0 my $oldsigalrm = $SIG{ALRM};
1354             $SIG{ALRM} = sub {
1355 0     0   0 die "timeout - $timelimit second processing time limit exceeded\n";
1356 0         0 };
1357              
1358             # use eval {}; to enforce time limit (see Perl Cookbook, 16.21)
1359 0         0 eval {
1360 0         0 alarm($timelimit); # set time limit
1361 0         0 $broken_pipe = '';
1362 0         0 $pid = open2($reader, $writer, $cmd);
1363 0 0       0 print $writer $input_data if defined $input_data;
1364 0         0 close $writer;
1365 0         0 @msgs = $reader->getlines();
1366 0         0 close $reader;
1367 0         0 waitpid $pid, 0;
1368 0         0 $status = $?;
1369 0 0       0 die "broken pipe\n" if $broken_pipe;
1370 0         0 alarm(0);
1371             };
1372 0 0       0 if($@) {
    0          
1373 0         0 alarm(0);
1374             # detect runaway child from open2() fork/exec
1375 0 0 0     0 die "runaway child, probably caused by bad command\n"
1376             if (not defined $pid) and ($@ =~ /^open2/);
1377             # construct error message
1378 0         0 chomp $@;
1379 0         0 $result = "$@: $cmd\n";
1380             }
1381             elsif ($status) {
1382 0         0 my $exit_value = $status >> 8;
1383 0         0 my $signal_num = $status & 127;
1384 0         0 my $dumped_core = $status & 128;
1385             # construct error message
1386 0 0       0 $result = sprintf("Status 0x%04x (exit %d%s%s)",
    0          
1387             $status, $exit_value,
1388             ($signal_num ? ", signal $signal_num" : ''),
1389             ($dumped_core ? ', core dumped' : ''));
1390             }
1391 0 0       0 $writer->close if $writer->opened;
1392 0 0       0 $reader->close if $reader->opened;
1393 0 0       0 if(defined $oldsigpipe) { $SIG{PIPE} = $oldsigpipe; }
  0         0  
1394 0         0 else { delete $SIG{PIPE}; }
1395 0 0       0 if(defined $oldsigalrm) { $SIG{ALRM} = $oldsigalrm; }
  0         0  
1396 0         0 else { delete $SIG{ALRM}; }
1397 0 0 0     0 $result .= "\n----------\n" . join("", @msgs) if($result and $#msgs >= 0);
1398             # set module-level variable containing command output
1399 0 0       0 if($#msgs >= 0) { $cmd_output = join('', @msgs); }
  0         0  
1400 0         0 else { $cmd_output = ''; }
1401 0         0 return $result;
1402             }
1403              
1404             # ----------------------------------------------------------------------
1405             # Unlink PID file.
1406             sub remove_pidfile
1407             {
1408 0 0   0 0 0 unlink $pidfile if $pidfile;
1409             }
1410              
1411             # ----------------------------------------------------------------------
1412             # Return string value with leading and trailing whitespace trimmed off.
1413             sub trim {
1414 11     11 0 35 my $str = shift;
1415 11 50       27 return if not defined $str;
1416 11         43 $str =~ s/^\s+//;
1417 11         41 $str =~ s/\s+$//;
1418 11         47 return $str;
1419             }
1420              
1421             # ----------------------------------------------------------------------
1422             # Return boolean indicating whether specified encr_typ is valid.
1423             sub valid_encr_typ
1424             {
1425 4     4 0 9 my $encr_typ = shift;
1426 4         9 for ($encr_typ) {
1427 4 100       33 /PGP2/i and return 1;
1428 2 50       16 /OpenPGP/i and return 1;
1429             }
1430 0           return '';
1431             }
1432              
1433             1;
1434              
1435             __DATA__