-
Notifications
You must be signed in to change notification settings - Fork 214
/
HostValidator.java
238 lines (214 loc) · 10 KB
/
HostValidator.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
/*
* Copyright (c) 2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.ditto.services.connectivity.messaging.validation;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.eclipse.ditto.model.base.headers.DittoHeaders;
import org.eclipse.ditto.model.connectivity.ConnectionConfigurationInvalidException;
import org.eclipse.ditto.services.connectivity.config.ConnectivityConfig;
import akka.event.LoggingAdapter;
/**
* Validates a given hostname against a set of fixed blocked addresses (e.g. loopback, multicast, ...) and a set of
* blocked/allowed hostnames from configuration.
* <p/>
* The allowed hostnames override the blocked hostnames e.g. if a host would be blocked because it resolves to a blocked
* address (localhost, site-local, ...), the host can be allowed by adding it to the list allowed hostnames.
*/
final class HostValidator {
private final Collection<String> allowedHostnames;
private final Collection<InetAddress> blockedAddresses;
private final AddressResolver resolver;
/**
* Creates a new instance of {@link HostValidator}.
*
* @param connectivityConfig the connectivity config used to load the allow-/blocklist
* @param loggingAdapter logging adapter
*/
HostValidator(final ConnectivityConfig connectivityConfig, final LoggingAdapter loggingAdapter) {
this(connectivityConfig, loggingAdapter, InetAddress::getAllByName);
}
/**
* Creates a new instance of {@link HostValidator}.
*
* @param connectivityConfig the connectivity config used to load the allow-/blocklist
* @param loggingAdapter logging adapter
* @param resolver custom resolver (used for tests only)
*/
HostValidator(final ConnectivityConfig connectivityConfig, final LoggingAdapter loggingAdapter,
final AddressResolver resolver) {
this.resolver = resolver;
this.allowedHostnames = connectivityConfig.getConnectionConfig().getAllowedHostnames();
final Collection<String> blockedHostnames = connectivityConfig.getConnectionConfig().getBlockedHostnames();
this.blockedAddresses = calculateBlockedAddresses(blockedHostnames, loggingAdapter);
}
/**
* Validate if connections to a host are allowed by checking (in this order):
* <ul>
* <li>if the blocklist is empty, this completely disables validation, every host is allowed</li>
* <li>if the host is contained in the allowlist, the host is allowed</li>
* <li>host is resolved to a blocked ip (loopback, site-local, multicast, wildcard ip)? host is blocked</li>
* <li>host is contained in the blocklist host is blocked</li>
* </ul>
* Loopback, private, multicast and wildcard addresses are allowed only if the blocklist is empty or explicitly
* contained in allowlist.
*
* @param host the host to check.
* @return whether connections to the host are permitted.
*/
HostValidationResult validateHost(final String host) {
if (blockedAddresses.isEmpty()) {
// If not even localhost is blocked, then permit even private, loopback, multicast and wildcard IPs.
return HostValidationResult.valid();
} else if (allowedHostnames.contains(host)) {
// the host is contained in the allow-list, do not block
return HostValidationResult.valid();
} else {
// Forbid blocked, private, loopback, multicast and wildcard IPs.
try {
final InetAddress[] inetAddresses = resolver.resolve(host);
for (final InetAddress requestAddress : inetAddresses) {
if (requestAddress.isLoopbackAddress()) {
return HostValidationResult.blocked(host, "the hostname resolved to a loopback address.");
} else if (requestAddress.isSiteLocalAddress()) {
return HostValidationResult.blocked(host, "the hostname resolved to a site local address.");
} else if (requestAddress.isMulticastAddress()) {
return HostValidationResult.blocked(host, "the hostname resolved to a multicast address.");
} else if (requestAddress.isAnyLocalAddress()) {
return HostValidationResult.blocked(host, "the hostname resolved to a wildcard address.");
} else if (blockedAddresses.contains(requestAddress)) {
// host is contained in the blocklist --> block
return HostValidationResult.blocked(host);
}
}
return HostValidationResult.valid();
} catch (UnknownHostException e) {
final String reason = String.format("The configured host '%s' is invalid: %s", host, e.getMessage());
return HostValidationResult.invalid(host, reason);
}
}
}
/**
* Resolve blocked hostnames into IP addresses that should not be accessed.
*
* @param blockedHostnames blocked hostnames.
* @param log the logger.
* @return blocked IP addresses.
*/
private Collection<InetAddress> calculateBlockedAddresses(final Collection<String> blockedHostnames,
final LoggingAdapter log) {
return blockedHostnames.stream()
.filter(host -> !host.isEmpty())
.flatMap(host -> {
try {
return Stream.of(resolver.resolve(host));
} catch (final UnknownHostException e) {
log.warning("Could not resolve hostname during building blocked hostnames set: <{}>", host);
return Stream.empty();
}
})
.collect(Collectors.toSet());
}
/**
* @throws ConnectionConfigurationInvalidException if the connection is not valid
*/
void validateHostname(final String connectionHost, final DittoHeaders dittoHeaders) {
final HostValidationResult validationResult = validateHost(connectionHost);
if (!validationResult.valid) {
throw validationResult.toException(dittoHeaders);
}
}
/**
* Holds the result of hostname validation and provides a method to create an appropriate
* {@link ConnectionConfigurationInvalidException}.
*/
static class HostValidationResult {
private final boolean valid;
@Nullable private final String host;
@Nullable private final String message;
private HostValidationResult(final boolean valid, @Nullable final String host, @Nullable final String message) {
this.valid = valid;
this.host = host;
this.message = message;
}
/**
* @return a valid {@link HostValidationResult}
*/
static HostValidationResult valid() {
return new HostValidationResult(true, null, null);
}
/**
* @param host the invalid host
* @param reason why the host is invalid
* @return the {@link HostValidationResult} for the invalid host
*/
static HostValidationResult invalid(final String host, final String reason) {
final String errorMessage = String.format("The configured host '%s' is invalid: %s", host, reason);
return new HostValidationResult(false, host, errorMessage);
}
/**
* @param host the blocked host
* @param reason why the host is blocked
* @return the {@link HostValidationResult} for the blocked host
*/
static HostValidationResult blocked(final String host, final String reason) {
final String exceptionMessage = String.format("The configured host '%s' may not be used for the " +
"connection because %s", host, reason);
return new HostValidationResult(false, host, exceptionMessage);
}
/**
* @param host the blocked host
* @return the {@link HostValidationResult} for the blocked host
*/
static HostValidationResult blocked(final String host) {
return blocked(host, "the host is blocked.");
}
/**
* @return whether the host is valid
*/
boolean isValid() {
return valid;
}
/**
* Creates a {@link ConnectionConfigurationInvalidException} with meaningful message and description.
*
* @param dittoHeaders the headers of the request
* @return the appropriate {@link ConnectionConfigurationInvalidException}
*/
ConnectionConfigurationInvalidException toException(final DittoHeaders dittoHeaders) {
final String errorMessage = String.format("The configured host '%s' may not be used for the " +
"connection because %s", host, message);
return ConnectionConfigurationInvalidException.newBuilder(errorMessage)
.description("It is a blocked or otherwise forbidden hostname which may not be used.")
.dittoHeaders(dittoHeaders)
.build();
}
}
/**
* Resolves host to ip addresses.
*/
interface AddressResolver {
/**
* Resolves the given host to its addresses.
*
* @param host the host to resolve
* @return the resolved {@link InetAddress}es
* @throws UnknownHostException if the given host cannot be resolved successfully
* (see {@link java.net.InetAddress#getAllByName(String)})
*/
InetAddress[] resolve(String host) throws UnknownHostException;
}
}