@@ -, +, @@ for authenticated sessions. This is appropriate for configurations with no reverse proxy or load balancer, and is exactly the same as the previous behavior. IP for authenticated sessions. This is appropriate for configurations that are contained entirely within a LAN, and therefore non-routable IPs can be mapped to specific computers. proxy as the client IP for authenticated sessions. This is appropriate for configurations that include a reverse proxy or load balancer exposed via the public Internet. Anyone connecting through an additional proxy will have their session linked to that proxy's IP. my $clientip = get_clientip($remote_addr, $forwarded_for, $require_routable); # BEGIN SQUID CONFIGURATION # The next two lines must go at the top of the squid configuration file: # The next four lines must go AFTER the line "acl CONNECT method CONNECT # END SQUID CONFIGURATION --- C4/Auth.pm | 85 ++++++++++++++++---- installer/data/mysql/de-DE/mandatory/sysprefs.sql | 2 +- installer/data/mysql/en/mandatory/sysprefs.sql | 1 + installer/data/mysql/es-ES/mandatory/sysprefs.sql | 1 + .../1-Obligatoire/unimarc_standard_systemprefs.sql | 2 +- installer/data/mysql/it-IT/necessari/sysprefs.sql | 2 +- installer/data/mysql/pl-PL/mandatory/sysprefs.sql | 1 + ...m_preferences_full_optimal_for_install_only.sql | 1 + ...m_preferences_full_optimal_for_install_only.sql | 2 +- installer/data/mysql/updatedatabase.pl | 7 ++ .../prog/en/modules/admin/preferences/admin.pref | 8 ++ t/Auth.t | 17 ++++ 12 files changed, 110 insertions(+), 19 deletions(-) create mode 100644 t/Auth.t --- a/C4/Auth.pm +++ a/C4/Auth.pm @@ -633,6 +633,7 @@ sub checkauth { $timeout = $1 * 86400; }; $timeout = 600 unless $timeout; + my $clientip = get_clientip($ENV{'REMOTE_ADDR'}, $ENV{'HTTP_X_FORWARDED_FOR'}, C4::Context->preference('HandleXForwardedFor')); _version_check($type,$query); # state variables @@ -707,10 +708,10 @@ sub checkauth { $userid = undef; $sessionID = undef; } - elsif ( $ip ne $ENV{'REMOTE_ADDR'} ) { + elsif ($ip ne $clientip) { # Different ip than originally logged in from $info{'oldip'} = $ip; - $info{'newip'} = $ENV{'REMOTE_ADDR'}; + $info{'newip'} = $clientip; $info{'different_ip'} = 1; $session->delete(); C4::Context->_unset_userenv($sessionID); @@ -752,7 +753,7 @@ sub checkauth { $userid = $retuserid if ($retuserid ne ''); } if ($return) { - _session_log(sprintf "%20s from %16s logged in at %30s.\n", $userid,$ENV{'REMOTE_ADDR'},(strftime '%c', localtime)); + _session_log(sprintf "%20s from %16s logged in at %30s.\n", $userid,$clientip,(strftime '%c', localtime)); if ( $flags = haspermission( $userid, $flagsrequired ) ) { $loggedin = 1; } @@ -800,7 +801,6 @@ sub checkauth { # launch a sequence to check if we have a ip for the branch, i # if we have one we replace the branchcode of the userenv by the branch bound in the ip. - my $ip = $ENV{'REMOTE_ADDR'}; # if they specify at login, use that if ($query->param('branch')) { $branchcode = $query->param('branch'); @@ -810,7 +810,7 @@ sub checkauth { if (C4::Context->boolean_preference('IndependantBranches') && C4::Context->boolean_preference('Autolocation')){ # we have to check they are coming from the right ip range my $domain = $branches->{$branchcode}->{'branchip'}; - if ($ip !~ /^$domain/){ + if ($clientip !~ /^$domain/){ $loggedin=0; $info{'wrongip'} = 1; } @@ -820,7 +820,7 @@ sub checkauth { foreach my $br ( keys %$branches ) { # now we work with the treatment of ip my $domain = $branches->{$br}->{'branchip'}; - if ( $domain && $ip =~ /^$domain/ ) { + if ( $domain && $clientip =~ /^$domain/ ) { $branchcode = $branches->{$br}->{'branchcode'}; # new op dev : add the branchprinter and branchname in the cookie @@ -837,7 +837,7 @@ sub checkauth { $session->param('branchname',$branchname); $session->param('flags',$userflags); $session->param('emailaddress',$emailaddress); - $session->param('ip',$session->remote_addr()); + $session->param('ip',$clientip); $session->param('lasttime',time()); $debug and printf STDERR "AUTH_4: (%s)\t%s %s - %s\n", map {$session->param($_)} qw(cardnumber firstname surname branch) ; } @@ -853,7 +853,7 @@ sub checkauth { $session->param('branchname','NO_LIBRARY_SET'); $session->param('flags',1); $session->param('emailaddress', C4::Context->preference('KohaAdminEmailAddress')); - $session->param('ip',$session->remote_addr()); + $session->param('ip',$clientip); $session->param('lasttime',time()); } C4::Context::set_userenv( @@ -904,7 +904,7 @@ sub checkauth { C4::Context::set_shelves_userenv('tot',$total); # setting a couple of other session vars... - $session->param('ip',$session->remote_addr()); + $session->param('ip',$clientip); $session->param('lasttime',time()); $session->param('sessiontype','anon'); } @@ -1074,6 +1074,7 @@ sub check_api_auth { my $dbh = C4::Context->dbh; my $timeout = C4::Context->preference('timeout'); $timeout = 600 unless $timeout; + my $clientip = get_clientip($ENV{'REMOTE_ADDR'}, $ENV{'HTTP_X_FORWARDED_FOR'}, C4::Context->preference('HandleXForwardedFor')); unless (C4::Context->preference('Version')) { # database has not been installed yet @@ -1125,7 +1126,7 @@ sub check_api_auth { $userid = undef; $sessionID = undef; return ("expired", undef, undef); - } elsif ( $ip ne $ENV{'REMOTE_ADDR'} ) { + } elsif ( $ip ne $clientip ) { # IP address changed $session->delete(); C4::Context->_unset_userenv($sessionID); @@ -1215,7 +1216,6 @@ sub check_api_auth { } } - my $ip = $ENV{'REMOTE_ADDR'}; # if they specify at login, use that if ($query->param('branch')) { $branchcode = $query->param('branch'); @@ -1226,7 +1226,7 @@ sub check_api_auth { foreach my $br ( keys %$branches ) { # now we work with the treatment of ip my $domain = $branches->{$br}->{'branchip'}; - if ( $domain && $ip =~ /^$domain/ ) { + if ( $domain && $clientip =~ /^$domain/ ) { $branchcode = $branches->{$br}->{'branchcode'}; # new op dev : add the branchprinter and branchname in the cookie @@ -1243,7 +1243,7 @@ sub check_api_auth { $session->param('branchname',$branchname); $session->param('flags',$userflags); $session->param('emailaddress',$emailaddress); - $session->param('ip',$session->remote_addr()); + $session->param('ip',$clientip); $session->param('lasttime',time()); } elsif ( $return == 2 ) { #We suppose the user is the superlibrarian @@ -1256,7 +1256,7 @@ sub check_api_auth { $session->param('branchname','NO_LIBRARY_SET'); $session->param('flags',1); $session->param('emailaddress', C4::Context->preference('KohaAdminEmailAddress')); - $session->param('ip',$session->remote_addr()); + $session->param('ip',$clientip); $session->param('lasttime',time()); } C4::Context::set_userenv( @@ -1307,6 +1307,7 @@ sub check_cookie_auth { my $dbh = C4::Context->dbh; my $timeout = C4::Context->preference('timeout'); $timeout = 600 unless $timeout; + my $clientip = get_clientip($ENV{'REMOTE_ADDR'}, $ENV{'HTTP_X_FORWARDED_FOR'}, C4::Context->preference('HandleXForwardedFor')); unless (C4::Context->preference('Version')) { # database has not been installed yet @@ -1357,7 +1358,7 @@ sub check_cookie_auth { $userid = undef; $sessionID = undef; return ("expired", undef); - } elsif ( $ip ne $ENV{'REMOTE_ADDR'} ) { + } elsif ( $ip ne $clientip ) { # IP address changed $session->delete(); C4::Context->_unset_userenv($sessionID); @@ -1382,6 +1383,60 @@ sub check_cookie_auth { } } +=head2 get_clientip + + my $clientip = get_clientip($remote_addr, $forwarded_for, $require_routable); + +Parses the remote IP address (passed to the function in $remote_addr), the +X-Forwarded-For header (passed to the function in $forwarded_for), and retrieves +the IP address of the client, returning a string representation of the IP +address. If $require_routable is set to "first", this function will always +return the most-distant IP address. If $require_routable is set to "routable", +this function will choose the first routable IP address in the list of relays, +or the address immediately before the closest proxy. If $require_routable is set +to "ignore", this function will always return the most recent hop (i.e. the +remote address). "Ignore" is the default. + +=cut + +sub get_clientip { + my $remote_addr = shift; + my $forwarded_for = shift; + my $require_routable = shift; + my $clientip; + + $require_routable ||= 'ignore'; + + if ($require_routable eq 'ignore' || !length $forwarded_for) { + $clientip = $remote_addr; + } else { + my @ips = split(', ', $forwarded_for); + $ips[$#ips + 1] = $remote_addr; + if ($require_routable eq 'first') { + $clientip = $ips[0]; + } else { # $require_routable eq 'routable' + foreach (@ips) { + if ( $_ !~ /^(192\.|10\.|127\.|0\.|169\.254\.|2([345][0-9|2[4-9])\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/) { + $clientip = $_; + last; + # We've found the client if the IP is not in any of: + # * 192.0.0.0/8 + # * 10.0.0.0/8 + # * 127.0.0.0/8 + # * 169.254.0.0/16 + # * 224.0.0.0/4 or 240.0.0.0/4 + # * 172.16.0.0/12 + } + } + if (!length $clientip) { + $clientip = $ips[$#ips - 1]; + } + } + } + + return $clientip; +} + =head2 get_session use CGI::Session; --- a/installer/data/mysql/de-DE/mandatory/sysprefs.sql +++ a/installer/data/mysql/de-DE/mandatory/sysprefs.sql @@ -317,4 +317,4 @@ INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES ( INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('BasketConfirmations', '1', 'When closing or reopening a basket,', 'always ask for confirmation.|do not ask for confirmation.', 'Choice'); INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('MARCAuthorityControlField008', '|| aca||aabn | a|a d', NULL, NULL, 'Textarea'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OpenLibraryCovers',0, 'If ON Openlibrary book covers will be show',NULL,'YesNo'); - +INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('HandleXForwardedFor','ignore','Specify how to handle client IPs and the X-Forwarded-For header.','ignore|first|routable','Choice'); --- a/installer/data/mysql/en/mandatory/sysprefs.sql +++ a/installer/data/mysql/en/mandatory/sysprefs.sql @@ -317,3 +317,4 @@ INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES ( INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('BasketConfirmations', '1', 'When closing or reopening a basket,', 'always ask for confirmation.|do not ask for confirmation.', 'Choice'); INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('MARCAuthorityControlField008', '|| aca||aabn | a|a d', NULL, NULL, 'Textarea'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OpenLibraryCovers',0,'If ON Openlibrary book covers will be show',NULL,'YesNo'); +INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('HandleXForwardedFor','ignore','Specify how to handle client IPs and the X-Forwarded-For header.','ignore|first|routable','Choice'); --- a/installer/data/mysql/es-ES/mandatory/sysprefs.sql +++ a/installer/data/mysql/es-ES/mandatory/sysprefs.sql @@ -317,3 +317,4 @@ INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES ( INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('BasketConfirmations', '1', 'When closing or reopening a basket,', 'always ask for confirmation.|do not ask for confirmation.', 'Choice'); INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('MARCAuthorityControlField008', '|| aca||aabn | a|a d', NULL, NULL, 'Textarea'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OpenLibraryCovers',0,'If ON Openlibrary book covers will be show',NULL,'YesNo'); +INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('HandleXForwardedFor','ignore','Specify how to handle client IPs and the X-Forwarded-For header.','ignore|first|routable','Choice'); --- a/installer/data/mysql/fr-FR/1-Obligatoire/unimarc_standard_systemprefs.sql +++ a/installer/data/mysql/fr-FR/1-Obligatoire/unimarc_standard_systemprefs.sql @@ -318,4 +318,4 @@ INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES ( INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('BasketConfirmations', '1', 'When closing or reopening a basket,', 'always ask for confirmation.|do not ask for confirmation.', 'Choice'); INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('MARCAuthorityControlField008', '|| aca||aabn | a|a d', NULL, NULL, 'Textarea'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OpenLibraryCovers',0,'If ON Openlibrary book covers will be show',NULL,'YesNo'); - +INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('HandleXForwardedFor','ignore','Specify how to handle client IPs and the X-Forwarded-For header.','ignore|first|routable','Choice'); --- a/installer/data/mysql/it-IT/necessari/sysprefs.sql +++ a/installer/data/mysql/it-IT/necessari/sysprefs.sql @@ -304,4 +304,4 @@ INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES ( INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('BasketConfirmations', '1', 'When closing or reopening a basket,', 'always ask for confirmation.|do not ask for confirmation.', 'Choice'); INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('MARCAuthorityControlField008', '|| aca||aabn | a|a d', NULL, NULL, 'Textarea'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OpenLibraryCovers',0,'If ON Openlibrary book covers will be show',NULL,'YesNo'); - +INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('HandleXForwardedFor','ignore','Specify how to handle client IPs and the X-Forwarded-For header.','ignore|first|routable','Choice'); --- a/installer/data/mysql/pl-PL/mandatory/sysprefs.sql +++ a/installer/data/mysql/pl-PL/mandatory/sysprefs.sql @@ -316,3 +316,4 @@ INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES ( INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('BasketConfirmations', '1', 'When closing or reopening a basket,', 'always ask for confirmation.|do not ask for confirmation.', 'Choice'); INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('MARCAuthorityControlField008', '|| aca||aabn | a|a d', NULL, NULL, 'Textarea'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OpenLibraryCovers',0, 'If ON Openlibrary book covers will be show',NULL,'YesNo'); +INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('HandleXForwardedFor','ignore','Specify how to handle client IPs and the X-Forwarded-For header.','ignore|first|routable','Choice'); --- a/installer/data/mysql/ru-RU/mandatory/system_preferences_full_optimal_for_install_only.sql +++ a/installer/data/mysql/ru-RU/mandatory/system_preferences_full_optimal_for_install_only.sql @@ -371,3 +371,4 @@ INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES ( INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('BasketConfirmations', '1', 'When closing or reopening a basket,', 'always ask for confirmation.|do not ask for confirmation.', 'Choice'); INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('MARCAuthorityControlField008', '|| aca||aabn | a|a d', NULL, NULL, 'Textarea'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OpenLibraryCovers',0,'If ON Openlibrary book covers will be show',NULL,'YesNo'); +INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('HandleXForwardedFor','ignore','Specify how to handle client IPs and the X-Forwarded-For header.','ignore|first|routable','Choice'); --- a/installer/data/mysql/uk-UA/mandatory/system_preferences_full_optimal_for_install_only.sql +++ a/installer/data/mysql/uk-UA/mandatory/system_preferences_full_optimal_for_install_only.sql @@ -396,4 +396,4 @@ INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES ( INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('BasketConfirmations', '1', 'When closing or reopening a basket,', 'always ask for confirmation.|do not ask for confirmation.', 'Choice'); INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('MARCAuthorityControlField008', '|| aca||aabn | a|a d', NULL, NULL, 'Textarea'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OpenLibraryCovers',0, 'If ON Openlibrary book covers will be show',NULL,'YesNo'); - +INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('HandleXForwardedFor','ignore','Specify how to handle client IPs and the X-Forwarded-For header.','ignore|first|routable','Choice'); --- a/installer/data/mysql/updatedatabase.pl +++ a/installer/data/mysql/updatedatabase.pl @@ -4439,6 +4439,13 @@ if (C4::Context->preference("Version") < TransformToNum($DBversion)) { SetVersion($DBversion); } +$DBversion = "3.05.00.XXX"; +if (C4::Context->preference("Version") < TransformToNum($DBversion)) { + $dbh->do("INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('HandleXForwardedFor','ignore','Specify how to handle client IPs and the X-Forwarded-For header.','ignore|first|routable','Choice');"); + print "Upgrade to $DBversion done (add HandleXForwardedFor syspref- change if you are using a reverse proxy or load balancer)\n"; + SetVersion($DBversion); +} + =head1 FUNCTIONS --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/admin.pref +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/admin.pref @@ -71,6 +71,14 @@ Administration: tmp: as temporary files. memcached: in a memcached server. - + - pref: HandleXForwardedFor + default: ignore + choices: + ignore: Always use the address of the machine connecting to Koha + routable: Use the first routable address or address of the last hop before the proxy + first: Always use the address of the machine with the web browser + - as the client IP for authenticated sessions. + - - pref: IndependantBranches default: 0 choices: --- a/t/Auth.t +++ a/t/Auth.t @@ -0,0 +1,17 @@ +#!/usr/bin/perl +# +# This Koha test module is a stub! +# Add more tests here!!! + +use strict; +use warnings; + +use Test::More tests => 5; + +BEGIN { + use_ok('C4::Auth'); + ok(C4::Auth::get_clientip('192.168.101.2', '192.168.101.3, 192.168.101.4', 'ignore') eq '192.168.101.2', 'get_clientip: Ignore proxies for client IP'); + ok(C4::Auth::get_clientip('192.168.101.2', '192.168.101.3, 192.168.101.4', 'first') eq '192.168.101.3', 'get_clientip: Always use most remote client IP'); + ok(C4::Auth::get_clientip('192.168.101.2', '127.0.0.1, 192.168.102.3, 10.0.0.1, 53.42.191.136', 'routable') eq '53.42.191.136', 'get_clientip: Find routable client IP w/reverse proxy'); + ok(C4::Auth::get_clientip('192.168.101.2', '127.0.0.1, 192.168.102.3, 10.0.0.1', 'routable') eq '10.0.0.1', 'get_clientip: Handle no-routable IPs w/reverse proxy'); +} --