1
+ use ipnetwork:: IpNetwork ;
2
+
1
3
use crate :: publish_rate_limit:: PublishRateLimit ;
2
4
use crate :: { env, env_optional, uploaders:: Uploader , Env } ;
3
5
@@ -25,6 +27,7 @@ pub struct Server {
25
27
pub blocked_traffic : Vec < ( String , Vec < String > ) > ,
26
28
pub max_allowed_page_offset : u32 ,
27
29
pub page_offset_ua_blocklist : Vec < String > ,
30
+ pub page_offset_cidr_blocklist : Vec < IpNetwork > ,
28
31
pub excluded_crate_names : Vec < String > ,
29
32
pub domain_name : String ,
30
33
pub allowed_origins : Vec < String > ,
@@ -63,6 +66,9 @@ impl Default for Server {
63
66
/// be blocked if `WEB_MAX_ALLOWED_PAGE_OFFSET` is exceeded. Including an empty string in the
64
67
/// list will block *all* user-agents exceeding the offset. If not set or empty, no blocking
65
68
/// will occur.
69
+ /// - `WEB_PAGE_OFFSET_CIDR_BLOCKLIST`: A comma separated list of CIDR blocks that will be used
70
+ /// to block IP addresses given in the `X-Real-Ip` HTTP header, e.g. `192.168.1.0/24`.
71
+ /// If not set or empty, no blocking will occur.
66
72
/// - `INSTANCE_METRICS_LOG_EVERY_SECONDS`: How frequently should instance metrics be logged.
67
73
/// If the environment variable is not present instance metrics are not logged.
68
74
/// - `FORCE_UNCONDITIONAL_REDIRECTS`: Whether to force unconditional redirects in the download
@@ -84,6 +90,13 @@ impl Default for Server {
84
90
Some ( s) if s. is_empty ( ) => vec ! [ ] ,
85
91
Some ( s) => s. split ( ',' ) . map ( String :: from) . collect ( ) ,
86
92
} ;
93
+ let page_offset_cidr_blocklist =
94
+ match env_optional :: < String > ( "WEB_PAGE_OFFSET_CIDR_BLOCKLIST" ) {
95
+ None => vec ! [ ] ,
96
+ Some ( s) if s. is_empty ( ) => vec ! [ ] ,
97
+ Some ( s) => s. split ( ',' ) . map ( String :: from) . collect ( ) ,
98
+ } ;
99
+
87
100
let base = Base :: from_environment ( ) ;
88
101
let excluded_crate_names = match env_optional :: < String > ( "EXCLUDED_CRATE_NAMES" ) {
89
102
None => vec ! [ ] ,
@@ -103,6 +116,7 @@ impl Default for Server {
103
116
blocked_traffic : blocked_traffic ( ) ,
104
117
max_allowed_page_offset : env_optional ( "WEB_MAX_ALLOWED_PAGE_OFFSET" ) . unwrap_or ( 200 ) ,
105
118
page_offset_ua_blocklist,
119
+ page_offset_cidr_blocklist : parse_cidr_blocks ( & page_offset_cidr_blocklist) ,
106
120
excluded_crate_names,
107
121
domain_name : domain_name ( ) ,
108
122
allowed_origins,
@@ -144,6 +158,41 @@ pub(crate) fn domain_name() -> String {
144
158
dotenv:: var ( "DOMAIN_NAME" ) . unwrap_or_else ( |_| "crates.io" . into ( ) )
145
159
}
146
160
161
+ /// Parses list of CIDR block strings to valid `IpNetwork` structs.
162
+ ///
163
+ /// The purpose is to be able to block IP ranges that overload the API that uses pagination.
164
+ ///
165
+ /// The minimum number of bits for a host prefix must be
166
+ ///
167
+ /// * at least 16 for IPv4 based CIDRs.
168
+ /// * at least 64 for IPv6 based CIDRs
169
+ ///
170
+ fn parse_cidr_blocks ( blocks : & [ String ] ) -> Vec < IpNetwork > {
171
+ blocks
172
+ . iter ( )
173
+ . map ( |block| {
174
+ let network = block. parse :: < IpNetwork > ( ) ;
175
+ match network {
176
+ Ok ( cidr) => {
177
+ let host_prefix = match cidr {
178
+ IpNetwork :: V4 ( _) => 16 ,
179
+ IpNetwork :: V6 ( _) => 64 ,
180
+ } ;
181
+ if cidr. prefix ( ) < host_prefix {
182
+ panic ! (
183
+ "WEB_PAGE_OFFSET_CIDR_BLOCKLIST only allows CIDR blocks with a host prefix \
184
+ of at least 16 bits (IPv4) or 64 bits (IPv6)."
185
+ ) ;
186
+ } else {
187
+ cidr
188
+ }
189
+ } ,
190
+ Err ( _) => panic ! ( "WEB_PAGE_OFFSET_CIDR_BLOCKLIST must contain IPv4 or IPv6 CIDR blocks." ) ,
191
+ }
192
+ } )
193
+ . collect :: < Vec < _ > > ( )
194
+ }
195
+
147
196
fn blocked_traffic ( ) -> Vec < ( String , Vec < String > ) > {
148
197
let pattern_list = dotenv:: var ( "BLOCKED_TRAFFIC" ) . unwrap_or_default ( ) ;
149
198
parse_traffic_patterns ( & pattern_list)
@@ -183,3 +232,38 @@ fn parse_traffic_patterns_splits_on_comma_and_looks_for_equal_sign() {
183
232
184
233
assert_none ! ( parse_traffic_patterns( pattern_string_3) . next( ) ) ;
185
234
}
235
+
236
+ #[ test]
237
+ fn parse_cidr_block_list_successfully ( ) {
238
+ let cidr_blocks = vec ! [ "127.0.0.1/24" . to_string( ) , "192.168.0.1/31" . to_string( ) ] ;
239
+
240
+ let blocks = parse_cidr_blocks ( & cidr_blocks) ;
241
+ assert_eq ! (
242
+ vec![
243
+ "127.0.0.1/24" . parse:: <IpNetwork >( ) . unwrap( ) ,
244
+ "192.168.0.1/31" . parse:: <IpNetwork >( ) . unwrap( ) ,
245
+ ] ,
246
+ blocks,
247
+ ) ;
248
+ }
249
+
250
+ #[ test]
251
+ #[ should_panic]
252
+ fn parse_cidr_blocks_panics_when_host_ipv4_prefix_is_too_low ( ) {
253
+ parse_cidr_blocks ( & [ "127.0.0.1/8" . to_string ( ) ] ) ;
254
+ }
255
+
256
+ #[ test]
257
+ #[ should_panic]
258
+ fn parse_cidr_blocks_panics_when_host_ipv6_prefix_is_too_low ( ) {
259
+ parse_cidr_blocks ( & [ "2001:0db8:0123:4567:89ab:cdef:1234:5678/56" . to_string ( ) ] ) ;
260
+ }
261
+
262
+ #[ test]
263
+ fn parse_ipv6_based_cidr_blocks ( ) {
264
+ let input = vec ! [
265
+ "2002::1234:abcd:ffff:c0a8:101/64" . to_string( ) ,
266
+ "2001:0db8:0123:4567:89ab:cdef:1234:5678/92" . to_string( ) ,
267
+ ] ;
268
+ assert_eq ! ( 2 , parse_cidr_blocks( & input) . len( ) ) ;
269
+ }
0 commit comments