mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
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.yadav@shapeblue.com>
This commit is contained in:
parent
72b2eb0087
commit
00f687db1b
@ -31,7 +31,6 @@ import com.cloud.utils.server.ServerProperties;
|
|||||||
import org.apache.commons.daemon.Daemon;
|
import org.apache.commons.daemon.Daemon;
|
||||||
import org.apache.commons.daemon.DaemonContext;
|
import org.apache.commons.daemon.DaemonContext;
|
||||||
import org.eclipse.jetty.jmx.MBeanContainer;
|
import org.eclipse.jetty.jmx.MBeanContainer;
|
||||||
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
|
|
||||||
import org.eclipse.jetty.server.HttpConfiguration;
|
import org.eclipse.jetty.server.HttpConfiguration;
|
||||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||||
import org.eclipse.jetty.server.NCSARequestLog;
|
import org.eclipse.jetty.server.NCSARequestLog;
|
||||||
@ -167,7 +166,7 @@ public class ServerDaemon implements Daemon {
|
|||||||
|
|
||||||
// HTTP config
|
// HTTP config
|
||||||
final HttpConfiguration httpConfig = new HttpConfiguration();
|
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.setSecureScheme("https");
|
||||||
httpConfig.setSecurePort(httpsPort);
|
httpConfig.setSecurePort(httpsPort);
|
||||||
httpConfig.setOutputBufferSize(32768);
|
httpConfig.setOutputBufferSize(32768);
|
||||||
|
|||||||
@ -34,6 +34,7 @@ public class ConfigKey<T> {
|
|||||||
|
|
||||||
public static final String CATEGORY_ADVANCED = "Advanced";
|
public static final String CATEGORY_ADVANCED = "Advanced";
|
||||||
public static final String CATEGORY_ALERT = "Alert";
|
public static final String CATEGORY_ALERT = "Alert";
|
||||||
|
public static final String CATEGORY_NETWORK = "Network";
|
||||||
|
|
||||||
public enum Scope {
|
public enum Scope {
|
||||||
Global, Zone, Cluster, StoragePool, Account, ManagementServer, ImageStore, Domain
|
Global, Zone, Cluster, StoragePool, Account, ManagementServer, ImageStore, Domain
|
||||||
|
|||||||
@ -234,42 +234,42 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
|
|||||||
@Inject
|
@Inject
|
||||||
private MessageBus messageBus;
|
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
|
, Integer.class
|
||||||
, "integration.api.port"
|
, "integration.api.port"
|
||||||
, "0"
|
, "0"
|
||||||
, "Integration (unauthenticated) API port. To disable set it to 0 or negative."
|
, "Integration (unauthenticated) API port. To disable set it to 0 or negative."
|
||||||
, false
|
, false
|
||||||
, ConfigKey.Scope.Global);
|
, 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
|
, Long.class
|
||||||
, "concurrent.snapshots.threshold.perhost"
|
, "concurrent.snapshots.threshold.perhost"
|
||||||
, null
|
, null
|
||||||
, "Limits number of snapshots that can be handled by the host concurrently; default is NULL - unlimited"
|
, "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
|
, true // not sure if this is to be dynamic
|
||||||
, ConfigKey.Scope.Global);
|
, 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
|
, Boolean.class
|
||||||
, "encode.api.response"
|
, "encode.api.response"
|
||||||
, "false"
|
, "false"
|
||||||
, "Do URL encoding for the api response, false by default"
|
, "Do URL encoding for the api response, false by default"
|
||||||
, false
|
, false
|
||||||
, ConfigKey.Scope.Global);
|
, ConfigKey.Scope.Global);
|
||||||
static final ConfigKey<String> JSONcontentType = new ConfigKey<String>( "Advanced"
|
static final ConfigKey<String> JSONcontentType = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED
|
||||||
, String.class
|
, String.class
|
||||||
, "json.content.type"
|
, "json.content.type"
|
||||||
, "application/json; charset=UTF-8"
|
, "application/json; charset=UTF-8"
|
||||||
, "Http response content type for .js files (default is text/javascript)"
|
, "Http response content type for .js files (default is text/javascript)"
|
||||||
, false
|
, false
|
||||||
, ConfigKey.Scope.Global);
|
, ConfigKey.Scope.Global);
|
||||||
static final ConfigKey<Boolean> EnableSecureSessionCookie = new ConfigKey<Boolean>("Advanced"
|
static final ConfigKey<Boolean> EnableSecureSessionCookie = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED
|
||||||
, Boolean.class
|
, Boolean.class
|
||||||
, "enable.secure.session.cookie"
|
, "enable.secure.session.cookie"
|
||||||
, "false"
|
, "false"
|
||||||
, "Session cookie is marked as secure if this is enabled. Secure cookies only work when HTTPS is used."
|
, "Session cookie is marked as secure if this is enabled. Secure cookies only work when HTTPS is used."
|
||||||
, false
|
, false
|
||||||
, ConfigKey.Scope.Global);
|
, 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
|
, String.class
|
||||||
, "json.content.type"
|
, "json.content.type"
|
||||||
, "application/json; charset=UTF-8"
|
, "application/json; charset=UTF-8"
|
||||||
@ -277,13 +277,34 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
|
|||||||
, false
|
, false
|
||||||
, ConfigKey.Scope.Global);
|
, 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
|
, Boolean.class
|
||||||
, "event.accountinfo"
|
, "event.accountinfo"
|
||||||
, "false"
|
, "false"
|
||||||
, "use account info in event logging"
|
, "use account info in event logging"
|
||||||
, true
|
, true
|
||||||
, ConfigKey.Scope.Global);
|
, 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
|
@Override
|
||||||
public boolean configure(final String name, final Map<String, Object> params) throws ConfigurationException {
|
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,
|
ConcurrentSnapshotsThresholdPerHost,
|
||||||
EncodeApiResponse,
|
EncodeApiResponse,
|
||||||
EnableSecureSessionCookie,
|
EnableSecureSessionCookie,
|
||||||
JSONDefaultContentType
|
JSONDefaultContentType,
|
||||||
|
proxyForwardList,
|
||||||
|
useForwardHeader,
|
||||||
|
listOfForwardHeaders
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import java.net.InetAddress;
|
|||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -69,13 +68,9 @@ import com.cloud.utils.db.EntityManager;
|
|||||||
import com.cloud.utils.net.NetUtils;
|
import com.cloud.utils.net.NetUtils;
|
||||||
|
|
||||||
@Component("apiServlet")
|
@Component("apiServlet")
|
||||||
@SuppressWarnings("serial")
|
|
||||||
public class ApiServlet extends HttpServlet {
|
public class ApiServlet extends HttpServlet {
|
||||||
public static final Logger s_logger = Logger.getLogger(ApiServlet.class.getName());
|
public static final Logger s_logger = Logger.getLogger(ApiServlet.class.getName());
|
||||||
private static final Logger s_accessLogger = Logger.getLogger("apiserver." + 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 REPLACEMENT = "_";
|
||||||
private static final String LOG_REPLACEMENTS = "[\n\r\t]";
|
private static final String LOG_REPLACEMENTS = "[\n\r\t]";
|
||||||
|
|
||||||
@ -570,17 +565,39 @@ public class ApiServlet extends HttpServlet {
|
|||||||
}
|
}
|
||||||
return false;
|
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
|
//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 {
|
public InetAddress getClientAddress(final HttpServletRequest request) throws UnknownHostException {
|
||||||
for(final String header : s_clientAddressHeaders) {
|
String ip = null;
|
||||||
final String ip = getCorrectIPAddress(request.getHeader(header));
|
InetAddress pretender = InetAddress.getByName(request.getRemoteAddr());
|
||||||
if (ip != null) {
|
if(doUseForwardHeaders()) {
|
||||||
return InetAddress.getByName(ip);
|
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(request.getRemoteAddr());
|
return InetAddress.getByName(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] getClientAddressHeaders() {
|
||||||
|
return ApiServer.listOfForwardHeaders.value().split(",");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getCorrectIPAddress(String ip) {
|
private static String getCorrectIPAddress(String ip) {
|
||||||
|
|||||||
@ -99,15 +99,17 @@ public class ApiServletTest {
|
|||||||
@Mock
|
@Mock
|
||||||
AccountService accountMgr;
|
AccountService accountMgr;
|
||||||
|
|
||||||
|
@Mock ConfigKey<Boolean> useForwardHeader;
|
||||||
StringWriter responseWriter;
|
StringWriter responseWriter;
|
||||||
|
|
||||||
ApiServlet servlet;
|
ApiServlet servlet;
|
||||||
|
ApiServlet spyServlet;
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Before
|
@Before
|
||||||
public void setup() throws SecurityException, NoSuchFieldException,
|
public void setup() throws SecurityException, NoSuchFieldException,
|
||||||
IllegalArgumentException, IllegalAccessException, IOException, UnknownHostException {
|
IllegalArgumentException, IllegalAccessException, IOException, UnknownHostException {
|
||||||
servlet = new ApiServlet();
|
servlet = new ApiServlet();
|
||||||
|
spyServlet = Mockito.spy(servlet);
|
||||||
responseWriter = new StringWriter();
|
responseWriter = new StringWriter();
|
||||||
Mockito.when(response.getWriter()).thenReturn(
|
Mockito.when(response.getWriter()).thenReturn(
|
||||||
new PrintWriter(responseWriter));
|
new PrintWriter(responseWriter));
|
||||||
@ -261,32 +263,43 @@ public class ApiServletTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getClientAddressWithXForwardedFor() throws UnknownHostException {
|
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");
|
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
|
@Test
|
||||||
public void getClientAddressWithHttpXForwardedFor() throws UnknownHostException {
|
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");
|
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
|
@Test
|
||||||
public void getClientAddressWithXRemoteAddr() throws UnknownHostException {
|
public void getClientAddressWithRemoteAddr() throws UnknownHostException {
|
||||||
Mockito.when(request.getHeader(Mockito.eq("Remote_Addr"))).thenReturn("192.168.1.1");
|
String[] proxynet = {"127.0.0.0/8"};
|
||||||
Assert.assertEquals(InetAddress.getByName("192.168.1.1"), ApiServlet.getClientAddress(request));
|
Mockito.when(spyServlet.proxyNets()).thenReturn(proxynet);
|
||||||
|
Mockito.when(spyServlet.doUseForwardHeaders()).thenReturn(true);
|
||||||
|
Assert.assertEquals(InetAddress.getByName("127.0.0.1"), spyServlet.getClientAddress(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getClientAddressWithHttpClientIp() throws UnknownHostException {
|
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");
|
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
|
@Test
|
||||||
public void getClientAddressDefault() throws UnknownHostException {
|
public void getClientAddressDefault() throws UnknownHostException {
|
||||||
Mockito.when(request.getRemoteAddr()).thenReturn("127.0.0.1");
|
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
|
@Test
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import java.util.Map;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
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 final char[] hexChar = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
|
||||||
|
|
||||||
private static Charset preferredACSCharset;
|
private static Charset preferredACSCharset;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user