OpenSRF Validator Service
authorThomas Berezansky <tsbere@mvlc.org>
Fri, 9 Dec 2011 20:19:12 +0000 (15:19 -0500)
committerDan Scott <dscott@laurentian.ca>
Mon, 12 Dec 2011 17:51:48 +0000 (12:51 -0500)
Add a new Validator service, and EmailAddress validators.

The service runs a chain of one or more validators, each one being fed the
normalized output of the previous one.

The return from each validator should be a hash of valid (0 or 1), the new
normalized output (the untouched input if invalid or nothing needed to be
changed), and if invalid an error string. Optionally, a validator can also
include an "additionals" hash of extra information to be included in the
final response.

The complete list of validators included is:

OpenSRF::Application::Validator::Base
    The base validator. Always returns valid.
OpenSRF::Application::Validator::Invalid
    Always returns invalid for testing purposes.
OpenSRF::Application::Validator::EmailAddress::Regex
    Does a very basic regular expression check on email addresses.
OpenSRF::Application::Validator::EmailAddress::DNS
    Uses Net::DNS to look up the domain on an email address

Signed-off-by: Thomas Berezansky <tsbere@mvlc.org>
Signed-off-by: Dan Scott <dscott@laurentian.ca>

examples/opensrf.xml.example
src/extras/Makefile.install
src/perl/lib/OpenSRF/Application/Validator.pm [new file with mode: 0644]
src/perl/lib/OpenSRF/Application/Validator/Base.pm [new file with mode: 0644]
src/perl/lib/OpenSRF/Application/Validator/EmailAddress/DNS.pm [new file with mode: 0644]
src/perl/lib/OpenSRF/Application/Validator/EmailAddress/Regex.pm [new file with mode: 0644]
src/perl/lib/OpenSRF/Application/Validator/Invalid.pm [new file with mode: 0644]

index 0dc2704..9b0ed97 100644 (file)
@@ -182,6 +182,45 @@ vim:et:ts=2:sw=2:
           <max_spare_children>5</max_spare_children>
         </unix_config>
       </opensrf.settings>
+
+      <opensrf.validator>
+        <keepalive>1</keepalive>
+        <stateless>1</stateless>
+        <language>perl</language>
+        <implementation>OpenSRF::Application::Validator</implementation>
+        <max_requests>17</max_requests>
+        <unix_config>
+          <unix_sock>opensrf.validator_unix.sock</unix_sock>
+          <unix_pid>opensrf.validator_unix.pid</unix_pid>
+          <max_requests>1000</max_requests>
+          <unix_log>opensrf.validator_unix.log</unix_log>
+          <min_children>5</min_children>
+          <max_children>15</max_children>
+          <min_spare_children>3</min_spare_children>
+          <max_spare_children>5</max_spare_children>
+        </unix_config>
+        <app_settings>
+          <validators>
+            <emailaddress>
+              <modules>
+                <a_regex>
+                    <implementation>OpenSRF::Application::Validator::EmailAddress::Regex</implementation>
+                </a_regex>
+                <b_dns>
+                    <implementation>OpenSRF::Application::Validator::EmailAddress::DNS</implementation>
+                    <check_mx_a>1</check_mx_a>
+                    <!-- Change this to a 1 to check for IPV6 records as well as IPV4 -->
+                    <check_aaaa>0</check_aaaa>
+                    <!-- Uncomment this to specify a resolve.conf-like config file for DNS lookups -->
+                    <!--<config_file>/path/to/file</config_file>-->
+                    <!-- A set of IPs to ignore - Useful when your DNS provider intercepts NXDOMAIN (say, OpenDNS) -->
+                    <ignore_ips>127.0.0.1,67.215.65.132</ignore_ips>
+                </b_dns>
+              </modules>
+            </emailaddress>
+          </validators>
+        </app_settings>
+      </opensrf.validator>
     </apps>
   </default>
 
@@ -202,6 +241,7 @@ vim:et:ts=2:sw=2:
         <appname>opensrf.settings</appname>
         <appname>opensrf.math</appname>
         <appname>opensrf.dbmath</appname>
+        <appname>opensrf.validator</appname>
       </activeapps>
 
       <apps>
index 56725c9..51d8890 100644 (file)
@@ -101,6 +101,7 @@ DEBS =  \
        libgdbm-dev \
        liblog-log4perl-perl\
        libmodule-build-perl\
+       libnet-dns-perl\
        libnet-jabber-perl\
        libperl-dev\
        libreadline5-dev\
@@ -161,6 +162,7 @@ CENTOS = \
        perl-Log-Log4perl \
        perl-Memcached-libmemcached \
        perl-Module-Build \
+       perl-Net-DNS \
        perl-Net-Server \
        perl-Template-Toolkit \
        perl-Test-Pod \
@@ -219,6 +221,7 @@ FEDORAS = \
        perl-libwww-perl \
        perl-Log-Log4perl \
        perl-Module-Build \
+       perl-Net-DNS \
        perl-Net-Jabber \
        perl-Net-Server \
        perl-RPC-XML \
diff --git a/src/perl/lib/OpenSRF/Application/Validator.pm b/src/perl/lib/OpenSRF/Application/Validator.pm
new file mode 100644 (file)
index 0000000..dc9b072
--- /dev/null
@@ -0,0 +1,50 @@
+package OpenSRF::Application::Validator;
+use base qw/OpenSRF::Application/;
+use OpenSRF::Application;
+use OpenSRF::Utils::SettingsClient;
+use OpenSRF::Utils::Logger;
+use Module::Load;
+
+my $logger = OpenSRF::Utils::Logger;
+my %modules;
+
+sub initialize {
+    my $sc = OpenSRF::Utils::SettingsClient->new;
+    my $validators = $sc->config_value( apps => 'opensrf.validator' => app_settings => 'validators' );
+    while(my $module = each %$validators ) {
+        __PACKAGE__->register_method(
+            api_name => "opensrf.validator.$module.validate",
+            method => 'do_validate',
+            argc => 1,
+            validator_name => $module
+        );
+        $modules{$module} = $validators->{$module};
+    }
+}
+
+sub do_validate {
+    my $self = shift;
+    my $client = shift;
+    my $input = shift;
+    my $return = { 'valid' => 1, 'normalized' => $input }; # Default return
+    my $validators = $modules{$self->{validator_name}}->{modules};
+    my @validator_names = sort keys %$validators;
+    my $additionals = ();
+
+    my $submodulename, $submodule;
+    while($return->{valid} && ($submodulename = shift @validator_names)) {
+        $submodule = $validators->{$submodulename};
+        my $implementation = $submodule->{implementation};
+        $logger->debug("Running request through $submodulename ($implementation)");
+        load $implementation;
+        my $result = $implementation->validate($return->{normalized}, $submodule);
+        if($result) {
+            $return = $result;
+            $additionals = {%$additionals, %{$return->{additionals}}} if $return->{additionals};
+        }
+    }
+    $return->{additionals} = $additionals;
+    return $return;
+}
+
+1;
diff --git a/src/perl/lib/OpenSRF/Application/Validator/Base.pm b/src/perl/lib/OpenSRF/Application/Validator/Base.pm
new file mode 100644 (file)
index 0000000..bf7d34f
--- /dev/null
@@ -0,0 +1,12 @@
+package OpenSRF::Application::Validator::Base;
+use strict;
+use warnings;
+
+sub validate {
+    my $self = shift;
+    my $input = shift;
+    my $settings = shift;
+    return { 'valid' => 1, 'normalized' => $input };
+}
+
+1;
diff --git a/src/perl/lib/OpenSRF/Application/Validator/EmailAddress/DNS.pm b/src/perl/lib/OpenSRF/Application/Validator/EmailAddress/DNS.pm
new file mode 100644 (file)
index 0000000..8121ccd
--- /dev/null
@@ -0,0 +1,96 @@
+package OpenSRF::Application::Validator::EmailAddress::DNS;
+use base qw/OpenSRF::Application::Validator::Base/;
+
+use OpenSRF::Application::Validator::Base;
+use Net::DNS;
+
+use strict;
+use warnings;
+
+sub validate {
+    my $self = shift;
+    my $input = shift;
+    my $settings = shift;
+    
+    my $return = { 'valid' => 0, 'normalized' => $input };
+
+    if(!$input->{emailaddress}) {
+        return { 'valid' => 0, 'normalized' => $input, 'error' => 'No Address' };
+    } elsif ($input->{emailaddress} !~ /@([^@]+)$/) {
+        return { 'valid' => 0, 'normalized' => $input, 'error' => 'Bad Address - Regex Check Failed' };
+    }
+    my $domain = $1;
+    my $checkMXA = $settings->{check_mx_a};
+    my $checkAAAA = $settings->{check_aaaa};
+    my $config_file = $settings->{config_file};
+    my @badAddrs;
+    if($settings->{ignore_ips}) {
+        @badAddrs = split(',', $settings->{ignore_ips});
+    }
+    my $res;
+    $res = Net::DNS::Resolver->new(config_file => $config_file, defnames => 0) if $config_file;
+    $res = Net::DNS::Resolver->new(defnames => 0) if !$config_file;
+    my @arecords;
+    # Look for MX records first
+    my $answer = $res->send($domain, 'MX');
+    foreach($answer->answer) {
+        if($_->type eq 'MX') {
+            push(@arecords, $_->exchange);
+        }
+    }
+    if(@arecords) {
+        if($checkMXA) {
+            OUTER: foreach my $checkdomain (@arecords) {
+                $answer = $res->send($checkdomain, 'A');
+                foreach my $record ($answer->answer) {
+                    last if $record->type eq 'CNAME' || $record->type eq 'DNAME';
+                    if($record->type eq 'A') {
+                        next if grep { $_ eq $record->address } @badAddrs;
+                        $return->{valid} = 1;
+                        last OUTER;
+                    }
+                }
+                if($checkAAAA) {
+                    $answer = $res->send($checkdomain, 'AAAA');
+                    foreach my $record ($answer->answer) {
+                        last if $record->type eq 'CNAME' || $record->type eq 'DNAME';
+                        if($record->type eq 'AAAA') {
+                            next if grep { $_ eq $record->address } @badAddrs;
+                            $return->{valid} = 1;
+                            last OUTER;
+                        }
+                    }
+                }
+            }
+            $return->{error} = "MX Records Invalid" if(!$return->{valid});
+        } else {
+            $return->{valid} = 1;
+        }
+    } else {
+        $answer = $res->send($domain,'A');
+        foreach my $record ($answer->answer) {
+            last if $record->type eq 'CNAME' || $record->type eq 'DNAME';
+            if($record->type eq 'A') {
+                next if grep { $_ eq $record->address } @badAddrs;
+                $return->{valid} = 1;
+                last;
+            }
+        }
+        if(!$return->{valid} && $checkAAAA) {
+            $answer = $res->send($domain, 'AAAA');
+            foreach my $record ($answer->answer) {
+                last if $record->type eq 'CNAME' || $record->type eq 'DNAME';
+                if($record->type eq 'AAAA') {
+                    next if grep { $_ eq $record->address } @badAddrs; 
+                    $return->{valid} = 1;
+                    last;
+                }
+            }
+        }
+        $return->{error} = "No A Records Found" if(!$return->{valid});
+    }
+    $return->{normalized}->{emailaddress} = lc($return->{normalized}->{emailaddress}) if($return->{valid});
+    return $return;
+}
+
+1;
diff --git a/src/perl/lib/OpenSRF/Application/Validator/EmailAddress/Regex.pm b/src/perl/lib/OpenSRF/Application/Validator/EmailAddress/Regex.pm
new file mode 100644 (file)
index 0000000..25036fc
--- /dev/null
@@ -0,0 +1,23 @@
+package OpenSRF::Application::Validator::EmailAddress::Regex;
+use base qw/OpenSRF::Application::Validator::Base/;
+
+use OpenSRF::Application::Validator::Base;
+
+use strict;
+use warnings;
+
+sub validate {
+    my $self = shift;
+    my $input = shift;
+    my $settings = shift;
+
+    if(!$input->{emailaddress}) {
+        return { 'valid' => 0, 'normalized' => $input, 'error' => 'No Address' };
+    } elsif ($input->{emailaddress} !~ /^.+@[^@]+\.[^@]{2,}$/) {
+        return { 'valid' => 0, 'normalized' => $input, 'error' => 'Bad Address - Regex Check Failed' };
+    }
+    $input->{emailaddress} = lc($input->{emailaddress});
+    return { 'valid' => 1, 'normalized' => $input };
+}
+
+1;
diff --git a/src/perl/lib/OpenSRF/Application/Validator/Invalid.pm b/src/perl/lib/OpenSRF/Application/Validator/Invalid.pm
new file mode 100644 (file)
index 0000000..76f9676
--- /dev/null
@@ -0,0 +1,16 @@
+package OpenSRF::Application::Validator::Invalid;
+use base qw/OpenSRF::Application::Validator::Base/;
+
+use OpenSRF::Application::Validator::Base;
+
+use strict;
+use warnings;
+
+sub validate {
+    my $self = shift;
+    my $input = shift;
+    my $settings = shift;
+    return { 'valid' => 0, 'normalized' => $input, 'error' => 'Forced Invalid' };
+}
+
+1;