9a777fec Andy McCall 21 days ago ↶
1
2
3
4
5
6
7
8
9
use std:: net:: { IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr} ;
10
use std:: time:: Duration;
11
12
13
pub fn public ( ip : IpAddr) -> bool {
14
match ip {
15
IpAddr:: V4 ( v4) => public_v4 ( v4) ,
16
IpAddr:: V6 ( v6) => public_v6 ( v6) ,
17
}
18
}
19
20
fn public_v4 ( ip : Ipv4Addr) -> bool {
21
let [ a, b, c, _ ] = ip.octets ( ) ;
22
! ( ip.is_loopback ( )
23
| | ip.is_private ( )
24
| | ip.is_link_local ( )
25
| | ip.is_multicast ( )
26
| | ip.is_unspecified ( )
27
| | ip.is_broadcast ( )
28
| | ip.is_documentation ( )
29
| | a = = 0
30
| | ( a = = 100 & & ( 64 .. 128 ) .contains ( & b) )
31
| | ( a = = 192 & & b = = 0 & & c = = 0 )
32
| | ( a = = 198 & & ( b = = 18 | | b = = 19 ) )
33
| | a > = 240 )
34
}
35
36
fn public_v6 ( ip : Ipv6Addr) -> bool {
37
if let Some ( v4) = ip.to_ipv4_mapped ( ) {
38
return public_v4 ( v4) ;
39
}
40
let s = ip.segments ( ) ;
41
42
if s[ 0 ] = = 0x64 & & s[ 1 ] = = 0xff9b & & s[ 2 .. 6 ] = = [ 0 , 0 , 0 , 0 ] {
43
return public_v4 ( Ipv4Addr:: new( ( s[ 6 ] > > 8 ) as u8 , s[ 6 ] as u8 , ( s[ 7 ] > > 8 ) as u8 , s[ 7 ] as u8 ) ) ;
44
}
45
! ( ip.is_loopback ( )
46
| | ip.is_unspecified ( )
47
| | ip.is_multicast ( )
48
| | ( s[ 0 ] & 0xfe00 ) = = 0xfc00
49
| | ( s[ 0 ] & 0xffc0 ) = = 0xfe80
50
| | ( s[ 0 ] = = 0x2001 & & s[ 1 ] = = 0x0db8 ) )
51
}
52
53
54
55
pub fn allowed ( allow : & [String], url : & reqwest:: Url) -> bool {
56
allow.iter ( ) .filter_map ( | a | reqwest:: Url:: parse( a) .ok ( ) ) .any ( | a | {
57
a.scheme ( ) = = url.scheme ( )
58
& & a.host_str ( ) .map ( str :: to_ascii_lowercase) = = url.host_str ( ) .map ( str :: to_ascii_lowercase)
59
& & ( a.port ( ) .is_none ( ) | | a.port_or_known_default ( ) = = url.port_or_known_default ( ) )
60
} )
61
}
62
63
64
65
pub async fn client ( url : & str , allow : & [String], timeout : Duration) -> Result< reqwest:: Client, String> {
66
let parsed = reqwest:: Url:: parse( url) .map_err ( | _| format! ( " {url} is not an address" ) ) ? ;
67
if ! matches! ( parsed.scheme ( ) , " http" | " https" ) {
68
return Err ( " only http and https addresses are called" .into ( ) ) ;
69
}
70
let host = parsed.host_str ( ) .ok_or ( " the address has no host" ) ? .trim_start_matches ( ' [' ) .trim_end_matches ( ' ]' ) .to_string ( ) ;
71
let port = parsed.port_or_known_default ( ) .unwrap_or ( 443 ) ;
72
let builder = reqwest:: Client:: builder( )
73
.timeout ( timeout)
74
.redirect ( reqwest:: redirect:: Policy:: none( ) )
75
.user_agent ( format! ( " oxygenhub/{} " , env! ( " CARGO_PKG_VERSION" ) ) ) ;
76
if allowed ( allow, & parsed) {
77
return builder.build ( ) .map_err ( | e | e.to_string ( ) ) ;
78
}
79
let addrs: Vec< SocketAddr> = match host.parse:: < IpAddr> ( ) {
80
Ok ( ip) => vec! [ SocketAddr:: new( ip, port) ] ,
81
Err ( _ ) => tokio:: net:: lookup_host( ( host.as_str ( ) , port) ) .await.map_err ( | _| format! ( " {host} could not be found" ) ) ? .collect ( ) ,
82
} ;
83
if addrs.is_empty ( ) {
84
return Err ( format! ( " {host} has no address" ) ) ;
85
}
86
if let Some ( bad) = addrs.iter ( ) .find ( | a | ! public ( a.ip ( ) ) ) {
87
return Err ( format! (
88
" {host} is {} , a private or local address; the hub only calls public addresses (a site administrator can allow one with [outbound] allow in hub.toml)" ,
89
bad.ip ( )
90
) ) ;
91
}
92
builder.resolve_to_addrs ( & host, & addrs) .build ( ) .map_err ( | e | e.to_string ( ) )
93
}
94
95
# [ cfg ( test ) ]
96
mod tests {
97
use super :: * ;
98
99
# [ test ]
100
fn only_public_addresses_are_public ( ) {
101
for bad in [
102
" 127.0.0.1" ,
103
" 10.1.2.3" ,
104
" 172.16.0.1" ,
105
" 192.168.1.1" ,
106
" 169.254.169.254" ,
107
" 100.64.0.1" ,
108
" 0.0.0.0" ,
109
" 224.0.0.1" ,
110
" 255.255.255.255" ,
111
" ::1" ,
112
" ::" ,
113
" fc00::1" ,
114
" fd12:3456::1" ,
115
" fe80::1" ,
116
" ::ffff:127.0.0.1" ,
117
" ::ffff:10.0.0.1" ,
118
" 64:ff9b::a9fe:a9fe" ,
119
" 2001:db8::1" ,
120
] {
121
assert! ( ! public ( bad.parse ( ) .unwrap ( ) ) , " {bad} is not public" ) ;
122
}
123
for good in [ " 1.1.1.1" , " 8.8.8.8" , " 104.21.68.34" , " 2606:4700:4700::1111" , " ::ffff:8.8.8.8" ] {
124
assert! ( public ( good.parse ( ) .unwrap ( ) ) , " {good} is public" ) ;
125
}
126
}
127
128
# [ test ]
129
fn an_allow_entry_matches_its_host_and_port ( ) {
130
let allow = vec! [ " http://127.0.0.1:11434" .to_string ( ) , " http://ollama.lan" .to_string ( ) ] ;
131
let ok = | u : & str | allowed ( & allow, & reqwest:: Url:: parse( u) .unwrap ( ) ) ;
132
assert! ( ok ( " http://127.0.0.1:11434/v1/chat/completions" ) & & ok ( " http://ollama.lan:8080/x" ) ) ;
133
assert! ( ! ok ( " http://127.0.0.1:6379/" ) & & ! ok ( " https://127.0.0.1:11434/" ) & & ! ok ( " http://other.lan/" ) ) ;
134
}
135
136
# [ tokio ::test ]
137
async fn private_addresses_are_refused_with_a_reason ( ) {
138
for u in [
139
" http://127.0.0.1:9/" ,
140
" http://169.254.169.254/latest/meta-data/" ,
141
" http://[::1]/" ,
142
" http://10.0.0.5/hook" ,
143
" http://localhost:8080/" ,
144
] {
145
let e = client ( u, & [ ] , Duration:: from_secs( 1 ) ) .await.unwrap_err ( ) ;
146
assert! ( e.contains ( " private or local address" ) , " {u}: {e}" ) ;
147
}
148
assert! ( client ( " ftp://example.org/" , & [ ] , Duration:: from_secs( 1 ) ) .await.is_err ( ) ) ;
149
assert! ( client ( " http://127.0.0.1:11434/" , & [ " http://127.0.0.1:11434" .into ( ) ] , Duration:: from_secs( 1 ) ) .await.is_ok ( ) ) ;
150
}
151
}