Skip to content

Commit b1e0bf9

Browse files
DaanHooglandrohityadavcloud
authored andcommitted
api: client verification in servlet
This introduces new global settings to handle how client address checks are handled by the API layer: proxy.header.verify: enables/disables checking of ipaddresses from a proxy set header proxy.header.names: a list of names to check for allowed ipaddresses from a proxy set header. proxy.cidr: a list of cidrs for which \"proxy.header.names\" are honoured if the \"Remote_Addr\" is in this list. (cherry picked from commit b655466) Signed-off-by: Rohit Yadav <[email protected]>
1 parent 7aea9db commit b1e0bf9

File tree

6 files changed

+85
-31
lines changed

6 files changed

+85
-31
lines changed

client/src/main/java/org/apache/cloudstack/ServerDaemon.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import org.apache.commons.lang3.StringUtils;
3232
import org.apache.log4j.Logger;
3333
import org.eclipse.jetty.jmx.MBeanContainer;
34-
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
3534
import org.eclipse.jetty.server.HttpConfiguration;
3635
import org.eclipse.jetty.server.HttpConnectionFactory;
3736
import org.eclipse.jetty.server.NCSARequestLog;
@@ -173,7 +172,7 @@ public void start() throws Exception {
173172

174173
// HTTP config
175174
final HttpConfiguration httpConfig = new HttpConfiguration();
176-
httpConfig.addCustomizer( new ForwardedRequestCustomizer() );
175+
// it would be nice to make this dynamic but we take care of this ourselves for now: httpConfig.addCustomizer( new ForwardedRequestCustomizer() );
177176
httpConfig.setSecureScheme("https");
178177
httpConfig.setSecurePort(httpsPort);
179178
httpConfig.setOutputBufferSize(32768);

framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class ConfigKey<T> {
3434

3535
public static final String CATEGORY_ADVANCED = "Advanced";
3636
public static final String CATEGORY_ALERT = "Alert";
37+
public static final String CATEGORY_NETWORK = "Network";
3738

3839
public enum Scope {
3940
Global, Zone, Cluster, StoragePool, Account, ManagementServer, ImageStore, Domain

server/src/main/java/com/cloud/api/ApiServer.java

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -234,56 +234,77 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
234234
@Inject
235235
private MessageBus messageBus;
236236

237-
private static final ConfigKey<Integer> IntegrationAPIPort = new ConfigKey<Integer>("Advanced"
237+
private static final ConfigKey<Integer> IntegrationAPIPort = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED
238238
, Integer.class
239239
, "integration.api.port"
240240
, "0"
241241
, "Integration (unauthenticated) API port. To disable set it to 0 or negative."
242242
, false
243243
, ConfigKey.Scope.Global);
244-
private static final ConfigKey<Long> ConcurrentSnapshotsThresholdPerHost = new ConfigKey<Long>("Advanced"
244+
private static final ConfigKey<Long> ConcurrentSnapshotsThresholdPerHost = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED
245245
, Long.class
246246
, "concurrent.snapshots.threshold.perhost"
247247
, null
248248
, "Limits number of snapshots that can be handled by the host concurrently; default is NULL - unlimited"
249249
, true // not sure if this is to be dynamic
250250
, ConfigKey.Scope.Global);
251-
private static final ConfigKey<Boolean> EncodeApiResponse = new ConfigKey<Boolean>("Advanced"
251+
private static final ConfigKey<Boolean> EncodeApiResponse = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED
252252
, Boolean.class
253253
, "encode.api.response"
254254
, "false"
255255
, "Do URL encoding for the api response, false by default"
256256
, false
257257
, ConfigKey.Scope.Global);
258-
static final ConfigKey<String> JSONcontentType = new ConfigKey<String>( "Advanced"
258+
static final ConfigKey<String> JSONcontentType = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED
259259
, String.class
260260
, "json.content.type"
261261
, "application/json; charset=UTF-8"
262262
, "Http response content type for .js files (default is text/javascript)"
263263
, false
264264
, ConfigKey.Scope.Global);
265-
static final ConfigKey<Boolean> EnableSecureSessionCookie = new ConfigKey<Boolean>("Advanced"
265+
static final ConfigKey<Boolean> EnableSecureSessionCookie = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED
266266
, Boolean.class
267267
, "enable.secure.session.cookie"
268268
, "false"
269269
, "Session cookie is marked as secure if this is enabled. Secure cookies only work when HTTPS is used."
270270
, false
271271
, ConfigKey.Scope.Global);
272-
private static final ConfigKey<String> JSONDefaultContentType = new ConfigKey<String> ("Advanced"
272+
private static final ConfigKey<String> JSONDefaultContentType = new ConfigKey<> (ConfigKey.CATEGORY_ADVANCED
273273
, String.class
274274
, "json.content.type"
275275
, "application/json; charset=UTF-8"
276276
, "Http response content type for JSON"
277277
, false
278278
, ConfigKey.Scope.Global);
279279

280-
private static final ConfigKey<Boolean> UseEventAccountInfo = new ConfigKey<Boolean>( "advanced"
280+
private static final ConfigKey<Boolean> UseEventAccountInfo = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED
281281
, Boolean.class
282282
, "event.accountinfo"
283283
, "false"
284284
, "use account info in event logging"
285285
, true
286286
, ConfigKey.Scope.Global);
287+
static final ConfigKey<Boolean> useForwardHeader = new ConfigKey<>(ConfigKey.CATEGORY_NETWORK
288+
, Boolean.class
289+
, "proxy.header.verify"
290+
, "false"
291+
, "enables/disables checking of ipaddresses from a proxy set header. See \"proxy.header.names\" for the headers to allow."
292+
, true
293+
, ConfigKey.Scope.Global);
294+
static final ConfigKey<String> listOfForwardHeaders = new ConfigKey<>(ConfigKey.CATEGORY_NETWORK
295+
, String.class
296+
, "proxy.header.names"
297+
, "X-Forwarded-For,HTTP_CLIENT_IP,HTTP_X_FORWARDED_FOR"
298+
, "a list of names to check for allowed ipaddresses from a proxy set header. See \"proxy.cidr\" for the proxies allowed to set these headers."
299+
, true
300+
, ConfigKey.Scope.Global);
301+
static final ConfigKey<String> proxyForwardList = new ConfigKey<>(ConfigKey.CATEGORY_NETWORK
302+
, String.class
303+
, "proxy.cidr"
304+
, ""
305+
, "a list of cidrs for which \"proxy.header.names\" are honoured if the \"Remote_Addr\" is in this list."
306+
, true
307+
, ConfigKey.Scope.Global);
287308

288309
@Override
289310
public boolean configure(final String name, final Map<String, Object> params) throws ConfigurationException {
@@ -1499,7 +1520,10 @@ public ConfigKey<?>[] getConfigKeys() {
14991520
ConcurrentSnapshotsThresholdPerHost,
15001521
EncodeApiResponse,
15011522
EnableSecureSessionCookie,
1502-
JSONDefaultContentType
1523+
JSONDefaultContentType,
1524+
proxyForwardList,
1525+
useForwardHeader,
1526+
listOfForwardHeaders
15031527
};
15041528
}
15051529
}

server/src/main/java/com/cloud/api/ApiServlet.java

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.net.URLDecoder;
2222
import java.net.UnknownHostException;
2323
import java.util.Arrays;
24-
import java.util.Collections;
2524
import java.util.HashMap;
2625
import java.util.List;
2726
import java.util.Map;
@@ -69,13 +68,9 @@
6968
import com.cloud.utils.net.NetUtils;
7069

7170
@Component("apiServlet")
72-
@SuppressWarnings("serial")
7371
public class ApiServlet extends HttpServlet {
7472
public static final Logger s_logger = Logger.getLogger(ApiServlet.class.getName());
7573
private static final Logger s_accessLogger = Logger.getLogger("apiserver." + ApiServlet.class.getName());
76-
private final static List<String> s_clientAddressHeaders = Collections
77-
.unmodifiableList(Arrays.asList("X-Forwarded-For",
78-
"HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR", "Remote_Addr"));
7974
private static final String REPLACEMENT = "_";
8075
private static final String LOG_REPLACEMENTS = "[\n\r\t]";
8176

@@ -570,17 +565,39 @@ private boolean isSpecificAPI(String commandName) {
570565
}
571566
return false;
572567
}
568+
boolean doUseForwardHeaders() {
569+
return Boolean.TRUE.equals(ApiServer.useForwardHeader.value());
570+
}
573571

572+
String[] proxyNets() {
573+
return ApiServer.proxyForwardList.value().split(",");
574+
}
574575
//This method will try to get login IP of user even if servlet is behind reverseProxy or loadBalancer
575-
public static InetAddress getClientAddress(final HttpServletRequest request) throws UnknownHostException {
576-
for(final String header : s_clientAddressHeaders) {
577-
final String ip = getCorrectIPAddress(request.getHeader(header));
578-
if (ip != null) {
579-
return InetAddress.getByName(ip);
580-
}
576+
public InetAddress getClientAddress(final HttpServletRequest request) throws UnknownHostException {
577+
String ip = null;
578+
InetAddress pretender = InetAddress.getByName(request.getRemoteAddr());
579+
if(doUseForwardHeaders()) {
580+
if (NetUtils.isIpInCidrList(pretender, proxyNets())) {
581+
for (String header : getClientAddressHeaders()) {
582+
header = header.trim();
583+
ip = getCorrectIPAddress(request.getHeader(header));
584+
if (StringUtils.isNotBlank(ip)) {
585+
s_logger.debug(String.format("found ip %s in header %s ", ip, header));
586+
break;
587+
}
588+
} // no address found in header so ip is blank and use remote addr
589+
} // else not an allowed proxy address, ip is blank and use remote addr
581590
}
591+
if (StringUtils.isBlank(ip)) {
592+
s_logger.trace(String.format("no ip found in headers, returning remote address %s.", pretender.getHostAddress()));
593+
return pretender;
594+
}
595+
596+
return InetAddress.getByName(ip);
597+
}
582598

583-
return InetAddress.getByName(request.getRemoteAddr());
599+
private String[] getClientAddressHeaders() {
600+
return ApiServer.listOfForwardHeaders.value().split(",");
584601
}
585602

586603
private static String getCorrectIPAddress(String ip) {

server/src/test/java/com/cloud/api/ApiServletTest.java

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,17 @@ public class ApiServletTest {
9797
@Mock
9898
AccountService accountMgr;
9999

100+
@Mock ConfigKey<Boolean> useForwardHeader;
100101
StringWriter responseWriter;
101102

102103
ApiServlet servlet;
103-
104+
ApiServlet spyServlet;
104105
@SuppressWarnings("unchecked")
105106
@Before
106107
public void setup() throws SecurityException, NoSuchFieldException,
107108
IllegalArgumentException, IllegalAccessException, IOException, UnknownHostException {
108109
servlet = new ApiServlet();
110+
spyServlet = Mockito.spy(servlet);
109111
responseWriter = new StringWriter();
110112
Mockito.when(response.getWriter()).thenReturn(
111113
new PrintWriter(responseWriter));
@@ -259,32 +261,43 @@ public void processRequestInContextLogin() throws UnknownHostException {
259261

260262
@Test
261263
public void getClientAddressWithXForwardedFor() throws UnknownHostException {
264+
String[] proxynet = {"127.0.0.0/8"};
265+
Mockito.when(spyServlet.proxyNets()).thenReturn(proxynet);
266+
Mockito.when(spyServlet.doUseForwardHeaders()).thenReturn(true);
262267
Mockito.when(request.getHeader(Mockito.eq("X-Forwarded-For"))).thenReturn("192.168.1.1");
263-
Assert.assertEquals(InetAddress.getByName("192.168.1.1"), ApiServlet.getClientAddress(request));
268+
Assert.assertEquals(InetAddress.getByName("192.168.1.1"), spyServlet.getClientAddress(request));
264269
}
265270

266271
@Test
267272
public void getClientAddressWithHttpXForwardedFor() throws UnknownHostException {
273+
String[] proxynet = {"127.0.0.0/8"};
274+
Mockito.when(spyServlet.proxyNets()).thenReturn(proxynet);
275+
Mockito.when(spyServlet.doUseForwardHeaders()).thenReturn(true);
268276
Mockito.when(request.getHeader(Mockito.eq("HTTP_X_FORWARDED_FOR"))).thenReturn("192.168.1.1");
269-
Assert.assertEquals(InetAddress.getByName("192.168.1.1"), ApiServlet.getClientAddress(request));
277+
Assert.assertEquals(InetAddress.getByName("192.168.1.1"), spyServlet.getClientAddress(request));
270278
}
271279

272280
@Test
273-
public void getClientAddressWithXRemoteAddr() throws UnknownHostException {
274-
Mockito.when(request.getHeader(Mockito.eq("Remote_Addr"))).thenReturn("192.168.1.1");
275-
Assert.assertEquals(InetAddress.getByName("192.168.1.1"), ApiServlet.getClientAddress(request));
281+
public void getClientAddressWithRemoteAddr() throws UnknownHostException {
282+
String[] proxynet = {"127.0.0.0/8"};
283+
Mockito.when(spyServlet.proxyNets()).thenReturn(proxynet);
284+
Mockito.when(spyServlet.doUseForwardHeaders()).thenReturn(true);
285+
Assert.assertEquals(InetAddress.getByName("127.0.0.1"), spyServlet.getClientAddress(request));
276286
}
277287

278288
@Test
279289
public void getClientAddressWithHttpClientIp() throws UnknownHostException {
290+
String[] proxynet = {"127.0.0.0/8"};
291+
Mockito.when(spyServlet.proxyNets()).thenReturn(proxynet);
292+
Mockito.when(spyServlet.doUseForwardHeaders()).thenReturn(true);
280293
Mockito.when(request.getHeader(Mockito.eq("HTTP_CLIENT_IP"))).thenReturn("192.168.1.1");
281-
Assert.assertEquals(InetAddress.getByName("192.168.1.1"), ApiServlet.getClientAddress(request));
294+
Assert.assertEquals(InetAddress.getByName("192.168.1.1"), spyServlet.getClientAddress(request));
282295
}
283296

284297
@Test
285298
public void getClientAddressDefault() throws UnknownHostException {
286299
Mockito.when(request.getRemoteAddr()).thenReturn("127.0.0.1");
287-
Assert.assertEquals(InetAddress.getByName("127.0.0.1"), ApiServlet.getClientAddress(request));
300+
Assert.assertEquals(InetAddress.getByName("127.0.0.1"), spyServlet.getClientAddress(request));
288301
}
289302

290303
@Test

utils/src/main/java/com/cloud/utils/StringUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import java.util.regex.Matcher;
2828
import java.util.regex.Pattern;
2929

30-
public class StringUtils {
30+
public class StringUtils extends org.apache.commons.lang3.StringUtils {
3131
private static final char[] hexChar = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
3232

3333
private static Charset preferredACSCharset;

0 commit comments

Comments
 (0)