line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Mojolicious::Plugin::ClientIP::Pluggable; |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
# ABSTRACT: Client IP header handling for Mojolicious requests |
4
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
=head1 NAME |
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
Mojolicious::Plugin::ClientIP::Pluggable - Customizable client IP detection plugin for Mojolicious |
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
=head1 SYNOPSIS |
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
use Mojolicious::Lite; |
12
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
# CloudFlare-waware settings |
14
|
|
|
|
|
|
|
plugin 'ClientIP::Pluggable', |
15
|
|
|
|
|
|
|
analyze_headers => [qw/cf-pseudo-ipv4 cf-connecting-ip true-client-ip/], |
16
|
|
|
|
|
|
|
restrict_family => 'ipv4', |
17
|
|
|
|
|
|
|
fallbacks => [qw/rfc-7239 x-forwarded-for remote_address/]; |
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
get '/' => sub { |
21
|
|
|
|
|
|
|
my $c = shift; |
22
|
|
|
|
|
|
|
$c->render(text => $c->client_ip); |
23
|
|
|
|
|
|
|
}; |
24
|
|
|
|
|
|
|
|
25
|
|
|
|
|
|
|
app->start; |
26
|
|
|
|
|
|
|
|
27
|
|
|
|
|
|
|
=head1 DESCRIPTION |
28
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
Mojolicious::Plugin::ClientIP::Pluggable is a Mojolicious plugin to get an IP address, which |
30
|
|
|
|
|
|
|
allows to specify different HTTP-headers (and their priorities) for client IP address |
31
|
|
|
|
|
|
|
extraction. This is needed as different cloud providers set different headers to disclose |
32
|
|
|
|
|
|
|
real IP address. |
33
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
If the address cannot be extracted from headers different fallback options are available: |
35
|
|
|
|
|
|
|
detect IP address from C header, detect IP address from C header |
36
|
|
|
|
|
|
|
(rfc-7239), or use C environment. |
37
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
The plugin is inspired by L. |
39
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
=head1 METHODS |
41
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
=head2 client_ip |
43
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
Find a client IP address from the specified headers, with optional fallbacks. The address is |
45
|
|
|
|
|
|
|
validated that it is publicly available (aka routable) IP address. Empty string is returned |
46
|
|
|
|
|
|
|
if no valid address can be found. |
47
|
|
|
|
|
|
|
|
48
|
|
|
|
|
|
|
=head1 OPTIONS |
49
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
=head2 analyzed_headers |
51
|
|
|
|
|
|
|
|
52
|
|
|
|
|
|
|
Define order and names of cloud provider injected headers with client IP address. |
53
|
|
|
|
|
|
|
For C we found the following headers are suitable: |
54
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
plugin 'ClientIP::Pluggable', |
56
|
|
|
|
|
|
|
analyzed_headers => [qw/cf-pseudo-ipv4 cf-connecting-ip true-client-ip/]. |
57
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
This option is mandatory. |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
More details at L, |
61
|
|
|
|
|
|
|
L, |
62
|
|
|
|
|
|
|
L |
63
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
=head2 restrict_family |
65
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
plugin 'ClientIP::Pluggable', restrict_family => 'ipv4'; |
67
|
|
|
|
|
|
|
plugin 'ClientIP::Pluggable', restrict_family => 'ipv6'; |
68
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
If defined only IPv4 or IPv6 addresses are considered valid among the possible addresses. |
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
By default this option is not defined, allowing IPv4 and IPv6 addresses. |
72
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
=head2 fallbacks |
74
|
|
|
|
|
|
|
|
75
|
|
|
|
|
|
|
plugin 'ClientIP::Pluggable', |
76
|
|
|
|
|
|
|
fallbacks => [qw/rfc-7239 x-forwarded-for remote_address/]); |
77
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
Try to get valid client IP-address from fallback sources, if we fail to do that from |
79
|
|
|
|
|
|
|
cloud-provider headers. |
80
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
C uses C header, C use header |
82
|
|
|
|
|
|
|
(appeared before rfc-7239 and still widely used) or use remote_address environment |
83
|
|
|
|
|
|
|
(C<$c->tx->remote_address>). |
84
|
|
|
|
|
|
|
|
85
|
|
|
|
|
|
|
Default value is C<[remote_address]>. |
86
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
=head1 ENVIRONMENT |
88
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
=head2 CLIENTIP_PLUGGABLE_ALLOW_LOOPBACK |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
Allows non-routable loopback address (C<127.0.0.1>) to pass validation. Use it for |
92
|
|
|
|
|
|
|
test purposes. |
93
|
|
|
|
|
|
|
|
94
|
|
|
|
|
|
|
Default value is C<0>, i.e. loopback addresses do not pass IP-address validation. |
95
|
|
|
|
|
|
|
|
96
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
98
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
Copyright (C) 2017 binary.com |
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
=cut |
102
|
|
|
|
|
|
|
|
103
|
2
|
|
|
2
|
|
1149
|
use strict; |
|
2
|
|
|
|
|
5
|
|
|
2
|
|
|
|
|
50
|
|
104
|
2
|
|
|
2
|
|
9
|
use warnings; |
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
44
|
|
105
|
|
|
|
|
|
|
|
106
|
2
|
|
|
2
|
|
567
|
use Data::Validate::IP; |
|
2
|
|
|
|
|
43962
|
|
|
2
|
|
|
|
|
345
|
|
107
|
|
|
|
|
|
|
|
108
|
2
|
|
|
2
|
|
16
|
use Mojo::Base 'Mojolicious::Plugin'; |
|
2
|
|
|
|
|
6
|
|
|
2
|
|
|
|
|
17
|
|
109
|
|
|
|
|
|
|
|
110
|
|
|
|
|
|
|
our $VERSION = '0.01'; |
111
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
# for tests only |
113
|
2
|
|
100
|
2
|
|
421
|
use constant ALLOW_LOOPBACK => $ENV{CLIENTIP_PLUGGABLE_ALLOW_LOOPBACK} || 0; |
|
2
|
|
|
|
|
5
|
|
|
2
|
|
|
|
|
1546
|
|
114
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
sub _check_ipv4 { |
116
|
10
|
|
|
10
|
|
17
|
my ($ip) = @_; |
117
|
10
|
|
100
|
|
|
185
|
return Data::Validate::IP::is_public_ipv4($ip) |
118
|
|
|
|
|
|
|
|| (ALLOW_LOOPBACK && Data::Validate::IP::is_loopback_ipv4($ip)); |
119
|
|
|
|
|
|
|
} |
120
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
sub _check_ipv6 { |
122
|
3
|
|
|
3
|
|
5
|
my ($ip) = @_; |
123
|
3
|
|
33
|
|
|
62
|
return Data::Validate::IP::is_public_ipv6($ip) |
124
|
|
|
|
|
|
|
|| (ALLOW_LOOPBACK && Data::Validate::IP::is_loopback_ipv6($ip)); |
125
|
|
|
|
|
|
|
} |
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
sub _classify_ip { |
128
|
17
|
|
|
17
|
|
33
|
my ($ip) = @_; |
129
|
|
|
|
|
|
|
return |
130
|
17
|
50
|
|
|
|
31
|
Data::Validate::IP::is_ipv4($ip) ? 'ipv4' |
|
|
100
|
|
|
|
|
|
131
|
|
|
|
|
|
|
: Data::Validate::IP::is_ipv6($ip) ? 'ipv6' |
132
|
|
|
|
|
|
|
: undef; |
133
|
|
|
|
|
|
|
} |
134
|
|
|
|
|
|
|
|
135
|
|
|
|
|
|
|
sub _candidates_iterator { |
136
|
11
|
|
|
11
|
|
19
|
my ($c, $analyzed_headers, $fallback_options) = @_; |
137
|
11
|
|
|
|
|
31
|
my $headers = $c->tx->req->headers; |
138
|
11
|
|
66
|
|
|
138
|
my @candidates = map { $headers->header($_) // () } @$analyzed_headers; |
|
30
|
|
|
|
|
211
|
|
139
|
11
|
|
|
|
|
106
|
my $comma_re = qr/\s*,\s*/; |
140
|
11
|
|
|
|
|
27
|
for my $fallback (map { lc } @$fallback_options) { |
|
33
|
|
|
|
|
73
|
|
141
|
33
|
100
|
|
|
|
88
|
if ($fallback eq 'x-forwarded-for') { |
|
|
100
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
142
|
11
|
|
|
|
|
25
|
my $xff = $headers->header('x-forwarded-for'); |
143
|
11
|
100
|
|
|
|
103
|
next unless $xff; |
144
|
5
|
|
|
|
|
29
|
my @ips = split $comma_re, $xff; |
145
|
5
|
|
|
|
|
14
|
push @candidates, @ips; |
146
|
|
|
|
|
|
|
} elsif ($fallback eq 'remote_address') { |
147
|
11
|
|
|
|
|
29
|
push @candidates, $c->tx->remote_address; |
148
|
|
|
|
|
|
|
} elsif ($fallback eq 'rfc-7239') { |
149
|
11
|
|
|
|
|
23
|
my $f = $headers->header('forwarded'); |
150
|
11
|
100
|
|
|
|
86
|
next unless $f; |
151
|
4
|
|
|
|
|
13
|
my @pairs = map { split $comma_re, $_ } split ';', $f; |
|
8
|
|
|
|
|
38
|
|
152
|
|
|
|
|
|
|
my @ips = map { |
153
|
4
|
|
|
|
|
11
|
my $ipv4_mask = qr/\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}/; |
|
10
|
|
|
|
|
25
|
|
154
|
|
|
|
|
|
|
# it is not completely valid ipv6 mask, but enough |
155
|
|
|
|
|
|
|
# to extract address. It will be validated later |
156
|
10
|
|
|
|
|
21
|
my $ipv6_mask = qr/[\w:]+/; |
157
|
10
|
100
|
|
|
|
171
|
if (/for=($ipv4_mask)|(?:"?\[($ipv6_mask)\].*"?)/i) { |
158
|
6
|
|
66
|
|
|
40
|
($1 // $2); |
159
|
|
|
|
|
|
|
} else { |
160
|
4
|
|
|
|
|
9
|
(); |
161
|
|
|
|
|
|
|
} |
162
|
|
|
|
|
|
|
} @pairs; |
163
|
4
|
|
|
|
|
13
|
push @candidates, @ips; |
164
|
|
|
|
|
|
|
} else { |
165
|
0
|
|
|
|
|
0
|
warn "Unknown fallback option $fallback, ignoring"; |
166
|
|
|
|
|
|
|
} |
167
|
|
|
|
|
|
|
} |
168
|
11
|
|
|
|
|
181
|
my $idx = 0; |
169
|
|
|
|
|
|
|
return sub { |
170
|
18
|
50
|
|
18
|
|
222
|
if ($idx < @candidates) { |
171
|
18
|
|
|
|
|
55
|
return $candidates[$idx++]; |
172
|
|
|
|
|
|
|
} |
173
|
0
|
|
|
|
|
0
|
return (undef); |
174
|
11
|
|
|
|
|
57
|
}; |
175
|
|
|
|
|
|
|
} |
176
|
|
|
|
|
|
|
|
177
|
|
|
|
|
|
|
sub register { |
178
|
2
|
|
|
2
|
1
|
97
|
my ($self, $app, $conf) = @_; |
179
|
2
|
|
50
|
|
|
9
|
my $analyzed_headers = $conf->{analyze_headers} // die "Please, specify 'analyzed_headers' option"; |
180
|
2
|
|
|
|
|
8
|
my %validator_for = ( |
181
|
|
|
|
|
|
|
ipv4 => \&_check_ipv4, |
182
|
|
|
|
|
|
|
ipv6 => \&_check_ipv6, |
183
|
|
|
|
|
|
|
); |
184
|
2
|
|
|
|
|
5
|
my $restrict_family = $conf->{restrict_family}; |
185
|
2
|
|
50
|
|
|
6
|
my $fallback_options = $conf->{fallbacks} // [qw/remote_address/]; |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
$app->helper( |
188
|
|
|
|
|
|
|
client_ip => sub { |
189
|
11
|
|
|
11
|
|
96891
|
my ($c) = @_; |
190
|
|
|
|
|
|
|
|
191
|
11
|
|
|
|
|
30
|
my $next_candidate = _candidates_iterator($c, $analyzed_headers, $fallback_options); |
192
|
11
|
|
|
|
|
28
|
while (my $ip = $next_candidate->()) { |
193
|
|
|
|
|
|
|
# generic check |
194
|
18
|
100
|
|
|
|
51
|
next unless Data::Validate::IP::is_ip($ip); |
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
# classify & check |
197
|
17
|
|
|
|
|
403
|
my $address_family = _classify_ip($ip); |
198
|
17
|
50
|
|
|
|
290
|
next unless $address_family; |
199
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
# possibly limit to acceptable address family |
201
|
17
|
100
|
66
|
|
|
70
|
next if $restrict_family && $restrict_family ne $address_family; |
202
|
|
|
|
|
|
|
|
203
|
|
|
|
|
|
|
# validate by family |
204
|
13
|
|
|
|
|
29
|
my $validator = $validator_for{$address_family}; |
205
|
13
|
100
|
|
|
|
25
|
next unless $validator->($ip); |
206
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
# address seems valid, return its textual representation |
208
|
11
|
|
|
|
|
718
|
return $ip; |
209
|
|
|
|
|
|
|
} |
210
|
0
|
|
|
|
|
0
|
return ''; |
211
|
2
|
|
|
|
|
24
|
}); |
212
|
|
|
|
|
|
|
|
213
|
2
|
|
|
|
|
65
|
return; |
214
|
|
|
|
|
|
|
} |
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
1; |