This is an automated email from the ASF dual-hosted git repository.

rohit pushed a commit to branch 4.19
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit 67e2061f4b87934c863cbafc5031821ad7ef6d02
Author: dahn <d...@onecht.net>
AuthorDate: Tue Mar 26 07:13:23 2024 +0100

    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 b65546636d84a5790e0297b1b0ca8e5a67a48dbc)
    Signed-off-by: Rohit Yadav <rohit.ya...@shapeblue.com>
    (cherry picked from commit b1e0bf9dbd464f8fb7c22f36505dee0148e2d6f4)
    Signed-off-by: Rohit Yadav <rohit.ya...@shapeblue.com>
---
 .../java/org/apache/cloudstack/ServerDaemon.java   |  3 +-
 .../cloudstack/framework/config/ConfigKey.java     |  1 +
 server/src/main/java/com/cloud/api/ApiServer.java  | 40 ++++++++++++++++-----
 server/src/main/java/com/cloud/api/ApiServlet.java | 41 +++++++++++++++-------
 .../test/java/com/cloud/api/ApiServletTest.java    | 29 ++++++++++-----
 .../src/main/java/com/cloud/utils/StringUtils.java |  2 +-
 6 files changed, 85 insertions(+), 31 deletions(-)

diff --git a/client/src/main/java/org/apache/cloudstack/ServerDaemon.java 
b/client/src/main/java/org/apache/cloudstack/ServerDaemon.java
index 763c274c7f5..fb84e1297e6 100644
--- a/client/src/main/java/org/apache/cloudstack/ServerDaemon.java
+++ b/client/src/main/java/org/apache/cloudstack/ServerDaemon.java
@@ -31,7 +31,6 @@ import org.apache.commons.daemon.DaemonContext;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.log4j.Logger;
 import org.eclipse.jetty.jmx.MBeanContainer;
-import org.eclipse.jetty.server.ForwardedRequestCustomizer;
 import org.eclipse.jetty.server.HttpConfiguration;
 import org.eclipse.jetty.server.HttpConnectionFactory;
 import org.eclipse.jetty.server.NCSARequestLog;
@@ -173,7 +172,7 @@ public class ServerDaemon implements Daemon {
 
         // HTTP config
         final HttpConfiguration httpConfig = new HttpConfiguration();
-        httpConfig.addCustomizer( new ForwardedRequestCustomizer() );
+// it would be nice to make this dynamic but we take care of this ourselves 
for now: httpConfig.addCustomizer( new ForwardedRequestCustomizer() );
         httpConfig.setSecureScheme("https");
         httpConfig.setSecurePort(httpsPort);
         httpConfig.setOutputBufferSize(32768);
diff --git 
a/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java
 
b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java
index 192c556bd2a..df93f78fa83 100644
--- 
a/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java
+++ 
b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java
@@ -34,6 +34,7 @@ public class ConfigKey<T> {
 
     public static final String CATEGORY_ADVANCED = "Advanced";
     public static final String CATEGORY_ALERT = "Alert";
+    public static final String CATEGORY_NETWORK = "Network";
 
     public enum Scope {
         Global, Zone, Cluster, StoragePool, Account, ManagementServer, 
ImageStore, Domain
diff --git a/server/src/main/java/com/cloud/api/ApiServer.java 
b/server/src/main/java/com/cloud/api/ApiServer.java
index b602ed2edbc..76a436f74d4 100644
--- a/server/src/main/java/com/cloud/api/ApiServer.java
+++ b/server/src/main/java/com/cloud/api/ApiServer.java
@@ -234,42 +234,42 @@ public class ApiServer extends ManagerBase implements 
HttpRequestHandler, ApiSer
     @Inject
     private MessageBus messageBus;
 
-    private static final ConfigKey<Integer> IntegrationAPIPort = new 
ConfigKey<Integer>("Advanced"
+    private static final ConfigKey<Integer> IntegrationAPIPort = new 
ConfigKey<>(ConfigKey.CATEGORY_ADVANCED
             , Integer.class
             , "integration.api.port"
             , "0"
             , "Integration (unauthenticated) API port. To disable set it to 0 
or negative."
             , false
             , ConfigKey.Scope.Global);
-    private static final ConfigKey<Long> ConcurrentSnapshotsThresholdPerHost = 
new ConfigKey<Long>("Advanced"
+    private static final ConfigKey<Long> ConcurrentSnapshotsThresholdPerHost = 
new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED
             , Long.class
             , "concurrent.snapshots.threshold.perhost"
             , null
             , "Limits number of snapshots that can be handled by the host 
concurrently; default is NULL - unlimited"
             , true // not sure if this is to be dynamic
             , ConfigKey.Scope.Global);
-    private static final ConfigKey<Boolean> EncodeApiResponse = new 
ConfigKey<Boolean>("Advanced"
+    private static final ConfigKey<Boolean> EncodeApiResponse = new 
ConfigKey<>(ConfigKey.CATEGORY_ADVANCED
             , Boolean.class
             , "encode.api.response"
             , "false"
             , "Do URL encoding for the api response, false by default"
             , false
             , ConfigKey.Scope.Global);
-    static final ConfigKey<String> JSONcontentType = new ConfigKey<String>( 
"Advanced"
+    static final ConfigKey<String> JSONcontentType = new 
ConfigKey<>(ConfigKey.CATEGORY_ADVANCED
             , String.class
             , "json.content.type"
             , "application/json; charset=UTF-8"
             , "Http response content type for .js files (default is 
text/javascript)"
             , false
             , ConfigKey.Scope.Global);
-    static final ConfigKey<Boolean> EnableSecureSessionCookie = new 
ConfigKey<Boolean>("Advanced"
+    static final ConfigKey<Boolean> EnableSecureSessionCookie = new 
ConfigKey<>(ConfigKey.CATEGORY_ADVANCED
             , Boolean.class
             , "enable.secure.session.cookie"
             , "false"
             , "Session cookie is marked as secure if this is enabled. Secure 
cookies only work when HTTPS is used."
             , false
             , ConfigKey.Scope.Global);
-    private static final ConfigKey<String> JSONDefaultContentType = new 
ConfigKey<String> ("Advanced"
+    private static final ConfigKey<String> JSONDefaultContentType = new 
ConfigKey<> (ConfigKey.CATEGORY_ADVANCED
             , String.class
             , "json.content.type"
             , "application/json; charset=UTF-8"
@@ -277,13 +277,34 @@ public class ApiServer extends ManagerBase implements 
HttpRequestHandler, ApiSer
             , false
             , ConfigKey.Scope.Global);
 
-    private static final ConfigKey<Boolean> UseEventAccountInfo = new 
ConfigKey<Boolean>( "advanced"
+    private static final ConfigKey<Boolean> UseEventAccountInfo = new 
ConfigKey<>(ConfigKey.CATEGORY_ADVANCED
             , Boolean.class
             , "event.accountinfo"
             , "false"
             , "use account info in event logging"
             , true
             , ConfigKey.Scope.Global);
+    static final ConfigKey<Boolean> useForwardHeader = new 
ConfigKey<>(ConfigKey.CATEGORY_NETWORK
+            , Boolean.class
+            , "proxy.header.verify"
+            , "false"
+            , "enables/disables checking of ipaddresses from a proxy set 
header. See \"proxy.header.names\" for the headers to allow."
+            , true
+            , ConfigKey.Scope.Global);
+    static final ConfigKey<String> listOfForwardHeaders = new 
ConfigKey<>(ConfigKey.CATEGORY_NETWORK
+            , String.class
+            , "proxy.header.names"
+            , "X-Forwarded-For,HTTP_CLIENT_IP,HTTP_X_FORWARDED_FOR"
+            , "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."
+            , true
+            , ConfigKey.Scope.Global);
+    static final ConfigKey<String> proxyForwardList = new 
ConfigKey<>(ConfigKey.CATEGORY_NETWORK
+            , String.class
+            , "proxy.cidr"
+            , ""
+            , "a list of cidrs for which \"proxy.header.names\" are honoured 
if the \"Remote_Addr\" is in this list."
+            , true
+            , ConfigKey.Scope.Global);
 
     @Override
     public boolean configure(final String name, final Map<String, Object> 
params) throws ConfigurationException {
@@ -1499,7 +1520,10 @@ public class ApiServer extends ManagerBase implements 
HttpRequestHandler, ApiSer
                 ConcurrentSnapshotsThresholdPerHost,
                 EncodeApiResponse,
                 EnableSecureSessionCookie,
-                JSONDefaultContentType
+                JSONDefaultContentType,
+                proxyForwardList,
+                useForwardHeader,
+                listOfForwardHeaders
         };
     }
 }
diff --git a/server/src/main/java/com/cloud/api/ApiServlet.java 
b/server/src/main/java/com/cloud/api/ApiServlet.java
index 626678649d7..f6f46419c04 100644
--- a/server/src/main/java/com/cloud/api/ApiServlet.java
+++ b/server/src/main/java/com/cloud/api/ApiServlet.java
@@ -21,7 +21,6 @@ import java.net.InetAddress;
 import java.net.URLDecoder;
 import java.net.UnknownHostException;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -69,13 +68,9 @@ import com.cloud.utils.db.EntityManager;
 import com.cloud.utils.net.NetUtils;
 
 @Component("apiServlet")
-@SuppressWarnings("serial")
 public class ApiServlet extends HttpServlet {
     public static final Logger s_logger = 
Logger.getLogger(ApiServlet.class.getName());
     private static final Logger s_accessLogger = Logger.getLogger("apiserver." 
+ ApiServlet.class.getName());
-    private final static List<String> s_clientAddressHeaders = Collections
-            .unmodifiableList(Arrays.asList("X-Forwarded-For",
-                    "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR", "Remote_Addr"));
     private static final String REPLACEMENT = "_";
     private static final String LOG_REPLACEMENTS = "[\n\r\t]";
 
@@ -570,17 +565,39 @@ public class ApiServlet extends HttpServlet {
         }
         return false;
     }
+    boolean doUseForwardHeaders() {
+        return Boolean.TRUE.equals(ApiServer.useForwardHeader.value());
+    }
 
+    String[] proxyNets() {
+        return ApiServer.proxyForwardList.value().split(",");
+    }
     //This method will try to get login IP of user even if servlet is behind 
reverseProxy or loadBalancer
-    public static InetAddress getClientAddress(final HttpServletRequest 
request) throws UnknownHostException {
-        for(final String header : s_clientAddressHeaders) {
-            final String ip = getCorrectIPAddress(request.getHeader(header));
-            if (ip != null) {
-                return InetAddress.getByName(ip);
-            }
+    public InetAddress getClientAddress(final HttpServletRequest request) 
throws UnknownHostException {
+        String ip = null;
+        InetAddress pretender = InetAddress.getByName(request.getRemoteAddr());
+        if(doUseForwardHeaders()) {
+            if (NetUtils.isIpInCidrList(pretender, proxyNets())) {
+                for (String header : getClientAddressHeaders()) {
+                    header = header.trim();
+                    ip = getCorrectIPAddress(request.getHeader(header));
+                    if (StringUtils.isNotBlank(ip)) {
+                        s_logger.debug(String.format("found ip %s in header %s 
", ip, header));
+                        break;
+                    }
+                } // no address found in header so ip is blank and use remote 
addr
+            } // else not an allowed proxy address, ip is blank and use remote 
addr
         }
+        if (StringUtils.isBlank(ip)) {
+            s_logger.trace(String.format("no ip found in headers, returning 
remote address %s.", pretender.getHostAddress()));
+            return pretender;
+        }
+
+        return InetAddress.getByName(ip);
+    }
 
-        return InetAddress.getByName(request.getRemoteAddr());
+    private String[] getClientAddressHeaders() {
+        return ApiServer.listOfForwardHeaders.value().split(",");
     }
 
     private static String getCorrectIPAddress(String ip) {
diff --git a/server/src/test/java/com/cloud/api/ApiServletTest.java 
b/server/src/test/java/com/cloud/api/ApiServletTest.java
index cefbbe2d630..4d4f0a12098 100644
--- a/server/src/test/java/com/cloud/api/ApiServletTest.java
+++ b/server/src/test/java/com/cloud/api/ApiServletTest.java
@@ -97,15 +97,17 @@ public class ApiServletTest {
     @Mock
     AccountService accountMgr;
 
+    @Mock ConfigKey<Boolean> useForwardHeader;
     StringWriter responseWriter;
 
     ApiServlet servlet;
-
+    ApiServlet spyServlet;
     @SuppressWarnings("unchecked")
     @Before
     public void setup() throws SecurityException, NoSuchFieldException,
     IllegalArgumentException, IllegalAccessException, IOException, 
UnknownHostException {
         servlet = new ApiServlet();
+        spyServlet = Mockito.spy(servlet);
         responseWriter = new StringWriter();
         Mockito.when(response.getWriter()).thenReturn(
                 new PrintWriter(responseWriter));
@@ -259,32 +261,43 @@ public class ApiServletTest {
 
     @Test
     public void getClientAddressWithXForwardedFor() throws 
UnknownHostException {
+        String[] proxynet = {"127.0.0.0/8"};
+        Mockito.when(spyServlet.proxyNets()).thenReturn(proxynet);
+        Mockito.when(spyServlet.doUseForwardHeaders()).thenReturn(true);
         
Mockito.when(request.getHeader(Mockito.eq("X-Forwarded-For"))).thenReturn("192.168.1.1");
-        Assert.assertEquals(InetAddress.getByName("192.168.1.1"), 
ApiServlet.getClientAddress(request));
+        Assert.assertEquals(InetAddress.getByName("192.168.1.1"), 
spyServlet.getClientAddress(request));
     }
 
     @Test
     public void getClientAddressWithHttpXForwardedFor() throws 
UnknownHostException {
+        String[] proxynet = {"127.0.0.0/8"};
+        Mockito.when(spyServlet.proxyNets()).thenReturn(proxynet);
+        Mockito.when(spyServlet.doUseForwardHeaders()).thenReturn(true);
         
Mockito.when(request.getHeader(Mockito.eq("HTTP_X_FORWARDED_FOR"))).thenReturn("192.168.1.1");
-        Assert.assertEquals(InetAddress.getByName("192.168.1.1"), 
ApiServlet.getClientAddress(request));
+        Assert.assertEquals(InetAddress.getByName("192.168.1.1"), 
spyServlet.getClientAddress(request));
     }
 
     @Test
-    public void getClientAddressWithXRemoteAddr() throws UnknownHostException {
-        
Mockito.when(request.getHeader(Mockito.eq("Remote_Addr"))).thenReturn("192.168.1.1");
-        Assert.assertEquals(InetAddress.getByName("192.168.1.1"), 
ApiServlet.getClientAddress(request));
+    public void getClientAddressWithRemoteAddr() throws UnknownHostException {
+        String[] proxynet = {"127.0.0.0/8"};
+        Mockito.when(spyServlet.proxyNets()).thenReturn(proxynet);
+        Mockito.when(spyServlet.doUseForwardHeaders()).thenReturn(true);
+        Assert.assertEquals(InetAddress.getByName("127.0.0.1"), 
spyServlet.getClientAddress(request));
     }
 
     @Test
     public void getClientAddressWithHttpClientIp() throws UnknownHostException 
{
+        String[] proxynet = {"127.0.0.0/8"};
+        Mockito.when(spyServlet.proxyNets()).thenReturn(proxynet);
+        Mockito.when(spyServlet.doUseForwardHeaders()).thenReturn(true);
         
Mockito.when(request.getHeader(Mockito.eq("HTTP_CLIENT_IP"))).thenReturn("192.168.1.1");
-        Assert.assertEquals(InetAddress.getByName("192.168.1.1"), 
ApiServlet.getClientAddress(request));
+        Assert.assertEquals(InetAddress.getByName("192.168.1.1"), 
spyServlet.getClientAddress(request));
     }
 
     @Test
     public void getClientAddressDefault() throws UnknownHostException {
         Mockito.when(request.getRemoteAddr()).thenReturn("127.0.0.1");
-        Assert.assertEquals(InetAddress.getByName("127.0.0.1"), 
ApiServlet.getClientAddress(request));
+        Assert.assertEquals(InetAddress.getByName("127.0.0.1"), 
spyServlet.getClientAddress(request));
     }
 
     @Test
diff --git a/utils/src/main/java/com/cloud/utils/StringUtils.java 
b/utils/src/main/java/com/cloud/utils/StringUtils.java
index fd4f7aba698..01b6f833271 100644
--- a/utils/src/main/java/com/cloud/utils/StringUtils.java
+++ b/utils/src/main/java/com/cloud/utils/StringUtils.java
@@ -31,7 +31,7 @@ import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-public class StringUtils {
+public class StringUtils extends org.apache.commons.lang3.StringUtils {
     private static final char[] hexChar = {'0', '1', '2', '3', '4', '5', '6', 
'7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
 
     private static Charset preferredACSCharset;

Reply via email to