File Coverage

blib/lib/EMDIS/ECS.pm
Criterion Covered Total %
statement 140 612 22.8
branch 43 440 9.7
condition 6 99 6.0
subroutine 37 52 71.1
pod 0 36 0.0
total 226 1239 18.2


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