From 9f8ce62163b824e447e5569961b50e68ad983329 Mon Sep 17 00:00:00 2001 From: Kyle M Hall Date: Thu, 6 Jun 2019 15:35:10 -0400 Subject: [PATCH] Bug 23068: Add ability for Koha to handle X-Forwarded-For headers so REMOTE_ADDR features work behind a proxy 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: 172.22.0.1 1.1.1.1 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! --- C4/Context.pm | 21 +++++ Koha/Middleware/RealIP.pm | 118 +++++++++++++++++++++++++ debian/templates/koha-conf-site.xml.in | 5 ++ debian/templates/plack.psgi | 2 + etc/koha-conf.xml | 5 ++ offline_circ/process.pl | 2 + svc/cataloguing/metasearch | 2 + tools/background-job-progress.pl | 2 + tools/batchMod.pl | 2 + tools/batch_record_modification.pl | 2 + tools/manage-marc-import.pl | 2 + tools/stage-marc-import.pl | 2 + tools/upload-cover-image.pl | 2 + 13 files changed, 167 insertions(+) create mode 100644 Koha/Middleware/RealIP.pm diff --git a/C4/Context.pm b/C4/Context.pm index fa612472f7..664d644c19 100644 --- a/C4/Context.pm +++ b/C4/Context.pm @@ -95,6 +95,7 @@ use DateTime::TimeZone; use Encode; use File::Spec; use Module::Load::Conditional qw(can_load); +use Net::Netmask; use POSIX (); use ZOOM; @@ -1078,6 +1079,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; diff --git a/Koha/Middleware/RealIP.pm b/Koha/Middleware/RealIP.pm new file mode 100644 index 0000000000..256790ee54 --- /dev/null +++ b/Koha/Middleware/RealIP.pm @@ -0,0 +1,118 @@ +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 . + +use Modern::Perl; + +use parent qw(Plack::Middleware); + +use C4::Context; + +use Net::Netmask; +use Plack::Util::Accessor qw( trusted_proxy ); + +=head3 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() ); +} + +=head3 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 } if $self->trusted_proxy; + + 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); +} + +=head3 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; +} + +=head3 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 + +=cut + +1; diff --git a/debian/templates/koha-conf-site.xml.in b/debian/templates/koha-conf-site.xml.in index e9e562439d..6694cc76ee 100644 --- a/debian/templates/koha-conf-site.xml.in +++ b/debian/templates/koha-conf-site.xml.in @@ -345,6 +345,11 @@ __END_SRU_PUBLICSERVER__ 50 2 + + + localhost:9200 diff --git a/debian/templates/plack.psgi b/debian/templates/plack.psgi index 3538b9a5a1..cde680ee30 100644 --- a/debian/templates/plack.psgi +++ b/debian/templates/plack.psgi @@ -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; diff --git a/etc/koha-conf.xml b/etc/koha-conf.xml index fc7c7cfead..1f9342dd6e 100644 --- a/etc/koha-conf.xml +++ b/etc/koha-conf.xml @@ -161,6 +161,11 @@ __PAZPAR2_TOGGLE_XML_POST__ 50 2 + + + localhost:9200 diff --git a/offline_circ/process.pl b/offline_circ/process.pl index a8a59dd73f..fb247ee17a 100755 --- a/offline_circ/process.pl +++ b/offline_circ/process.pl @@ -24,6 +24,8 @@ use CGI qw ( -utf8 ); use C4::Auth; use C4::Circulation; +C4::Context->set_remote_address; + my $query = CGI->new; my ($template, $loggedinuser, $cookie) = get_template_and_user({ diff --git a/svc/cataloguing/metasearch b/svc/cataloguing/metasearch index 110732f7cf..55e425fab4 100755 --- a/svc/cataloguing/metasearch +++ b/svc/cataloguing/metasearch @@ -23,6 +23,8 @@ use C4::Service; use Encode qw( encode_utf8 ); use Koha::MetaSearcher; +C4::Context->set_remote_address; + my ( $query, $response ) = C4::Service->init( catalogue => 1 ); my ( $query_string, $servers ) = C4::Service->require_params( 'q', 'servers' ); diff --git a/tools/background-job-progress.pl b/tools/background-job-progress.pl index 625389f902..3c18b7a603 100755 --- a/tools/background-job-progress.pl +++ b/tools/background-job-progress.pl @@ -29,6 +29,8 @@ use C4::BackgroundJob; use CGI::Cookie; # need to check cookies before # having CGI parse the POST request +C4::Context->set_remote_address; + my $input = CGI->new; my %cookies = CGI::Cookie->fetch; my ($auth_status, $sessionID) = check_cookie_auth($cookies{'CGISESSID'}->value, { tools => '*' }); diff --git a/tools/batchMod.pl b/tools/batchMod.pl index 9b1f958950..d2f5c31e9f 100755 --- a/tools/batchMod.pl +++ b/tools/batchMod.pl @@ -41,6 +41,8 @@ use Koha::Items; use Koha::ItemTypes; use Koha::Patrons; +C4::Context->set_remote_address; + my $input = new CGI; my $dbh = C4::Context->dbh; my $error = $input->param('error'); diff --git a/tools/batch_record_modification.pl b/tools/batch_record_modification.pl index 1a82c476c3..405fc0e738 100755 --- a/tools/batch_record_modification.pl +++ b/tools/batch_record_modification.pl @@ -34,6 +34,8 @@ use Koha::Biblios; use Koha::MetadataRecord::Authority; use Koha::Virtualshelves; +C4::Context->set_remote_address; + my $input = new CGI; our $dbh = C4::Context->dbh; my $op = $input->param('op') // q|form|; diff --git a/tools/manage-marc-import.pl b/tools/manage-marc-import.pl index 4fea4005d7..bbfcbd1f98 100755 --- a/tools/manage-marc-import.pl +++ b/tools/manage-marc-import.pl @@ -37,6 +37,8 @@ use C4::BackgroundJob; use C4::Labels::Batch; use Koha::BiblioFrameworks; +C4::Context->set_remote_address; + my $script_name = "/cgi-bin/koha/tools/manage-marc-import.pl"; my $input = new CGI; diff --git a/tools/stage-marc-import.pl b/tools/stage-marc-import.pl index d76c35ac18..c3a2e342c2 100755 --- a/tools/stage-marc-import.pl +++ b/tools/stage-marc-import.pl @@ -43,6 +43,8 @@ use C4::BackgroundJob; use C4::MarcModificationTemplates; use Koha::Plugins; +C4::Context->set_remote_address; + my $input = new CGI; my $fileID = $input->param('uploadedfileid'); diff --git a/tools/upload-cover-image.pl b/tools/upload-cover-image.pl index ff89afb1d6..939290eeae 100755 --- a/tools/upload-cover-image.pl +++ b/tools/upload-cover-image.pl @@ -49,6 +49,8 @@ use C4::Images; use Koha::UploadedFiles; use C4::Log; +C4::Context->set_remote_address; + my $debug = 1; my $input = new CGI; -- 2.20.1 (Apple Git-117)