File Coverage

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