configuration file patch (for multiple keys)

contributed by Xavier Perseguers
requires AppConfig::File

allows signing of multiple domains with each domain having its own
key file, as specified in a configuration file
Index: dkfilter/scripts/dkfilter.out
===================================================================
--- dkfilter.orig/scripts/dkfilter.out
+++ dkfilter/scripts/dkfilter.out
@@ -18,6 +18,9 @@
 #   GNU General Public License for more details.
 #
 # Written by Bennett Todd <bet@rahul.net>
+# Support for multiple keys and configuration file added by
+#         Xavier Perseguers <xavier.perseguers@a3.epfl.ch>
+
 
 use strict;
 use warnings;
@@ -26,6 +29,7 @@ use Getopt::Long;
 use Pod::Usage;
 use IO::File;
 use Sys::Syslog;
+use AppConfig::File;
 
 use DKMessage;
 use MySmtpServer;
@@ -35,11 +39,8 @@ my $hostname = hostname;
 
 my $reject_fail = 0;  # not actually used in this filter
 my $reject_error = 0;
-my $keyfile;
-my $selector;
-my $domain_arg;
-my $method = "simple";
 my $headers = 0;
+my $configfile;
 my $setuser;
 my $setgroup;
 my $daemonize;
@@ -50,10 +51,7 @@ GetOptions(
 		"reject-fail" => \$reject_fail,
 		"reject-error" => \$reject_error,
 		"hostname=s" => \$hostname,
-		"keyfile=s" => \$keyfile,
-		"selector=s" => \$selector,
-		"domain=s" => \$domain_arg,
-		"method=s" => \$method,
+		"configfile=s" => \$configfile,
 		"headers" => \$headers,
 		"user=s" => \$setuser,
 		"group=s" => \$setgroup,
@@ -73,32 +71,88 @@ unless (defined($srcport) and defined($d
 	pod2usage("Error: source or destination port is missing");
 }
 
-unless (defined $keyfile)
+unless (defined $configfile)
 {
-	pod2usage("Error: no keyfile specified");
+	pod2usage("Error: no configfile specified");
 }
-unless (-r $keyfile)
+unless (-r $configfile)
 {
-	pod2usage("Error: cannot read keyfile $keyfile");
+	pod2usage("Error: cannot read configfile $configfile");
+}
+
+# Parse the configuration file
+
+my $state = AppConfig::State->new();
+
+$state->define(
+	'domain_name', {
+		ARGCOUNT => AppConfig::ARGCOUNT_LIST
+	},
+	'domain_selector', {
+		ARGCOUNT => AppConfig::ARGCOUNT_LIST
+	},
+	'domain_method', {
+		ARGCOUNT => AppConfig::ARGCOUNT_LIST,
+		VALIDATE => \&check_method
+	},
+	'domain_private_key', {
+		ARGCOUNT => AppConfig::ARGCOUNT_LIST,
+		VALIDATE => \&check_private_key
+	}
+);
+
+sub check_method {
+	my $var   = shift;
+	my $val   = shift;
+
+	return ($val eq "simple" || $val eq "nofws") ? 1 : 0;
 }
-unless (defined $selector)
+
+sub check_private_key {
+	my $var   = shift;
+	my $val   = shift;
+
+	return (-r $val) ? 1 : 0;
+}
+
+my $config = AppConfig::File->new($state);
+$config->parse($configfile);
+
+my $domains    = $state->get('domain_name');
+my $_methods   = $state->get('domain_method');
+my $_selectors = $state->get('domain_selector');
+my $_keys      = $state->get('domain_private_key');
+
+unless ( scalar @$domains == scalar @$_methods )
 {
-	pod2usage("Error: selector not specified");
+	pod2usage("Error: number of methods is different than number of domains");
 }
-unless (defined $domain_arg)
+unless ( scalar @$domains == scalar @$_selectors )
 {
-	pod2usage("Error: domain not specified");
+	pod2usage("Error: number of selectors is different than number of domains");
 }
-my @domains = split(/,\s*/, $domain_arg);
-unless (@domains)
+unless ( scalar @$domains == scalar @$_keys )
 {
-	pod2usage("Error: domain not specified");
+	pod2usage("Error: number of private keys is different than number of domains");
 }
-unless ($method eq "simple" || $method eq "nofws")
+
+my %methods;
+my %selectors;
+my %keys;
+
+for ( my $i = 0; $i < scalar @$domains; $i++ )
 {
-	die "Error: invalid method; must be simple or nofws\n";
+	$methods{ @$domains[$i] } = @$_methods[$i];
+	$selectors{ @$domains[$i] } = @$_selectors[$i];
+	$keys{ @$domains[$i] } = @$_keys[$i];
 }
 
+# Compare number of key files and number of domains
+# If there is only one key file, each domain will use
+# the same key (dkfilter original version). Otherwise
+# there should be as many key file as domain definitions.
+# If all goes well, create a hash with a key for each domain.
+
 use base "MySmtpProxyServer";
 main->run(
 		host => $srcaddr,
@@ -114,9 +168,9 @@ sub setup_client_socket
 {
 	# create an object for sending the outgoing SMTP commands
 	#  (and the signed message)
-    my $client = MSDW::SMTP::Client->new(
-			interface => $dstaddr,
-			port => $dstport);
+	my $client = MSDW::SMTP::Client->new(
+		interface => $dstaddr,
+		port => $dstport);
 	return $client;
 }
 
@@ -166,7 +220,7 @@ sub handle_end_of_data
 		{
 			while ($domain)
 			{
-				if (grep { lc($_) eq $domain } @domains)
+				if (grep { lc($_) eq $domain } @$domains)
 				{
 					last;
 				}
@@ -177,14 +231,14 @@ sub handle_end_of_data
 		unless ($domain)
 		{
 			# message has no senderdomain
-			$domain = $domains[0];
+			$domain = @$domains[0];
 		}
 
 		$result = $mess->sign(
-			Method => $method,
-			Selector => $selector,
+			Method => $methods{ $domain },
+			Selector => $selectors{ $domain },
 			Domain => $domain,
-			KeyFile => $keyfile,
+			KeyFile => $keys{ $domain },
 			Headers => $headers
 			);
 		$result_detail = $mess->result_detail;
@@ -246,11 +300,8 @@ __END__
   dkfilter.out [options] listen.addr:port talk.addr:port
     options:
       --reject-error
-      --keyfile=filename
-      --selector=SELECTOR
-      --domain=DOMAIN
-      --method=simple|nofws
       --headers
+      --configfile=filename
       --user=USER
       --group=GROUP
       --daemonize
@@ -259,6 +310,24 @@ __END__
   dkfilter.out --help
     to see a full description of the various options
 
+   Format of the configuration file:
+
+   # ------------------------------------------
+   # domain 1
+   [domain]
+   name        = domain.tld
+   method      = METHOD
+   selector    = SELECTOR
+   private_key = FILENAME
+
+   # domain 2
+   [domain]
+   name        = otherdomain.tld
+   method      = METHOD
+   selector    = SELECTOR
+   private_key = FILENAME
+   # ------------------------------------------
+
 =head1 OPTIONS
 
 =over
@@ -274,17 +343,18 @@ will be passed through without modificat
 
 The most common error is a message parse error.
 
-=item B<--keyfile=FILENAME>
+=item B<--configfile=FILENAME>
 
 This is a required argument. Use it to specify the filename containing
-the private key used in signing outgoing messages.
+the configuration of domains and private keys used in signing outgoing
+messages.
 
-=item B<--selector=SELECTOR>
+=item B<SELECTOR>
 
 This is a required argument. Use it to specify the name of the key
 selector.
 
-=item B<--domain=DOMAIN>
+=item B<DOMAIN>
 
 This is a required argument. Use it to specify what domain(s) emails
 are signed for. If you want to sign for multiple domains, specify the
@@ -293,11 +363,10 @@ the filter will attempt to match the mes
 specified in this argument. If it sees a match, it will sign the message
 using the matching domain.
 
-=item B<--method=simple|nofws>
+=item B<METHOD>
 
 This option specifies the canonicalization algorithm to use for signing
-messages. Specify either C<simple> or C<nofws>. If not specified,
-the default is C<simple>.
+messages. Specify either C<simple> or C<nofws>.
 
 =item B<--headers>
 
@@ -335,7 +404,7 @@ content.
 
 =head1 EXAMPLE
 
-  dkfilter.out --keyfile=private.key --selector=sydney \
-          --domain=example.org 127.0.0.1:10027 127.0.0.1:10028
+  dkfilter.out --configfile=/etc/dkfilter.conf \
+          127.0.0.1:10027 127.0.0.1:10028
 
 =cut
