| 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; |