Bug 23068: Add ability for Koha to handle X-Forwarded-For headers so REMOTE_ADDR...
authorKyle M Hall <kyle@bywatersolutions.com>
Thu, 6 Jun 2019 19:35:10 +0000 (15:35 -0400)
committerMartin Renvoize <martin.renvoize@ptfs-europe.com>
Thu, 31 Oct 2019 16:10:17 +0000 (16:10 +0000)
Koha has a number of features that rely on knowing the IP address of the connecting client. If that server is behind a proxy these features do not work.
This patch adds a module to automatically convert the X-Forwarded-For header into the REMOTE_ADDR environment variable for both CGI and Plack processes.

TEST PLAN:
1) Apply this patch set
2) Install Plack::Middleware::RealIP via cpanm or your favorite utility
3) Update your plack.psgi with the changes you find in this patch set ( this process differs based on your testing environment )
4) Restart plack
5) Tail the plack error log for your instance
6) Use curl to access the OPAC, adding an X-Forwarded-For header: curl --header "X-Forwarded-For: 32.32.32.32" http://127.0.0.1:8080
7) Note the logs output this address if you are unproxied
8) If you are proxied, restart plack using a command like below, where the ip you see in the logs ("REAL IP) is what you put in the koha conf:
    <koha_trusted_proxies>172.22.0.1 1.1.1.1</koha_trusted_proxies>
9) Restart all the things!
10) Repeat step 6
11) You should now see "REAL IP: 32.32.32.32" in the plack logs as the remote address in your plack-error.log logs!
12) Disable plack so you are running in cgi mode, repeat step 6 again
13) You should see "REAL IP: 32.32.32.32" as the remove address in your opac-error.log logs!

Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Ed Veal <eveal@mckinneytexas.org>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>

C4/Auth.pm
C4/Context.pm
C4/Installer/PerlDependencies.pm
Koha/Middleware/RealIP.pm [new file with mode: 0644]
debian/templates/koha-conf-site.xml.in
debian/templates/plack.psgi
etc/koha-conf.xml

index cd53ecd..a882880 100644 (file)
@@ -57,6 +57,8 @@ BEGIN {
         else            { exit }
     }
 
+    C4::Context->set_remote_address;
+
     $debug     = $ENV{DEBUG};
     @ISA       = qw(Exporter);
     @EXPORT    = qw(&checkauth &get_template_and_user &haspermission &get_user_subpermissions);
index 54341da..0fe571d 100644 (file)
@@ -1099,6 +1099,26 @@ sub temporary_directory {
     return C4::Context->config('tmp_path') || File::Spec->tmpdir;
 }
 
+=head3 set_remote_address
+
+set_remote_address should be called at the beginning of every script
+that is *not* running under plack in order to the REMOTE_ADDR environment
+variable to be set correctly.
+
+=cut
+
+sub set_remote_address {
+    if ( C4::Context->config('koha_trusted_proxies') ) {
+        require CGI;
+        my $cgi    = CGI->new;
+        my $header = $cgi->http('HTTP_X_FORWARDED_FOR');
+
+        if ($header) {
+            require Koha::Middleware::RealIP;
+            $ENV{REMOTE_ADDR} = Koha::Middleware::RealIP::get_real_ip( $ENV{REMOTE_ADDR}, $header );
+        }
+    }
+}
 
 1;
 
index 63efca3..a62e633 100644 (file)
@@ -908,6 +908,11 @@ our $PERL_DEPS = {
         required => '1',
         min_ver  => '0.37',
     },
+    'Net::Netmask' => {
+        'usage'    => 'Koha X-Forwarded-For support',
+        'required' => '0',
+        'min_ver'  => '1.9022',
+    },
     'Net::Z3950::SimpleServer' => {
         'usage'    => 'Z39.50 responder',
         'required' => '0',
diff --git a/Koha/Middleware/RealIP.pm b/Koha/Middleware/RealIP.pm
new file mode 100644 (file)
index 0000000..22f40db
--- /dev/null
@@ -0,0 +1,120 @@
+package Koha::Middleware::RealIP;
+
+# Copyright 2019 ByWater Solutions and the Koha Dev Team
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+use parent qw(Plack::Middleware);
+
+use C4::Context;
+
+use Net::Netmask;
+use Plack::Util::Accessor qw( trusted_proxy );
+
+=head1 METHODS
+
+=head2 prepare_app
+
+This method generates and stores the list of trusted ip's as Netmask objects
+at the time Plack starts up, obviating the need to regerenate them on each request.
+
+=cut
+
+sub prepare_app {
+    my $self = shift;
+    $self->trusted_proxy( get_trusted_proxies() );
+}
+
+=head2 call
+
+This method is called for each request, and will ensure the correct remote address
+is set in the REMOTE_ADDR environment variable.
+
+=cut
+
+sub call {
+    my $self = shift;
+    my $env  = shift;
+
+    if ( $env->{HTTP_X_FORWARDED_FOR} ) {
+        my @trusted_proxy = $self->trusted_proxy ? @{ $self->trusted_proxy } : undef;
+
+        if (@trusted_proxy) {
+            my $addr = get_real_ip( $env->{REMOTE_ADDR}, $env->{HTTP_X_FORWARDED_FOR}, \@trusted_proxy );
+            $ENV{REMOTE_ADDR} = $addr;
+            $env->{REMOTE_ADDR} = $addr;
+        }
+    }
+
+    return $self->app->($env);
+}
+
+=head2 get_real_ip
+
+my $address = get_real_ip( $remote_addres, $x_forwarded_for_header );
+
+This method takes the current remote address and the x-forwarded-for header string,
+determines the correct external ip address, and returns it.
+
+=cut
+
+sub get_real_ip {
+    my ( $remote_addr, $header ) = @_;
+
+    my @forwarded_for = $header =~ /([^,\s]+)/g;
+    return $remote_addr unless @forwarded_for;
+
+    my $trusted_proxies = get_trusted_proxies();
+
+    my @unconfirmed = ( @forwarded_for, $remote_addr );
+
+    my $real_ip;
+    while (my $addr = pop @unconfirmed) {
+        my $has_matched = 0;
+        foreach my $netmask (@$trusted_proxies) {
+            $has_matched++, last if $netmask->match($addr);
+        }
+        $real_ip = $addr, last unless $has_matched;
+    }
+
+    return $real_ip;
+}
+
+=head2 get_trusted_proxies
+
+This method returns an arrayref of Net::Netmask objects for all
+the trusted proxies given to Koha.
+
+=cut
+
+sub get_trusted_proxies {
+    my $proxies_conf = C4::Context->config('koha_trusted_proxies');
+    return unless $proxies_conf;
+    my @trusted_proxies_ip = split( / /, $proxies_conf );
+    my @trusted_proxies = map { Net::Netmask->new($_) } @trusted_proxies_ip;
+    return \@trusted_proxies;
+}
+
+
+=head1 AUTHORS
+
+Kyle M Hall <kyle@bywatersolutions.com>
+
+=cut
+
+1;
index 5173511..f056138 100644 (file)
@@ -345,6 +345,11 @@ __END_SRU_PUBLICSERVER__
  <plack_max_requests>50</plack_max_requests>
  <plack_workers>2</plack_workers>
 
+ <!-- Configuration for X-Forwarded-For -->
+ <!--
+ <koha_trusted_proxies>1.2.3.4 2.3.4.5 3.4.5.6</koha_trusted_proxies>
+ -->
+
  <!-- Elasticsearch Configuration -->
  <elasticsearch>
      <server>localhost:9200</server> <!-- may be repeated to include all servers on your cluster -->
index 3538b9a..cde680e 100644 (file)
@@ -68,8 +68,10 @@ my $apiv1  = builder {
 builder {
     enable "ReverseProxy";
     enable "Plack::Middleware::Static";
+
     # + is required so Plack doesn't try to prefix Plack::Middleware::
     enable "+Koha::Middleware::SetEnv";
+    enable "+Koha::Middleware::RealIP";
 
     mount '/opac'          => $opac;
     mount '/intranet'      => $intranet;
index b10e951..e769943 100644 (file)
@@ -161,6 +161,11 @@ __PAZPAR2_TOGGLE_XML_POST__
  <plack_max_requests>50</plack_max_requests>
  <plack_workers>2</plack_workers>
 
+ <!-- Configuration for X-Forwarded-For -->
+ <!--
+ <koha_trusted_proxies>1.2.3.4 2.3.4.5 3.4.5.6</koha_trusted_proxies>
+ -->
+
  <!-- Elasticsearch Configuration -->
  <elasticsearch>
      <server>__ELASTICSEARCH_SERVERS__</server>