HADOOP-13352. Make X-FRAME-OPTIONS configurable in HttpServer2. Contributed by Anu Engineer.
This commit is contained in:
parent
2e2caeb50e
commit
324923e1bc
|
@ -137,6 +137,11 @@ public final class HttpServer2 implements FilterContainer {
|
||||||
static final String STATE_DESCRIPTION_ALIVE = " - alive";
|
static final String STATE_DESCRIPTION_ALIVE = " - alive";
|
||||||
static final String STATE_DESCRIPTION_NOT_LIVE = " - not live";
|
static final String STATE_DESCRIPTION_NOT_LIVE = " - not live";
|
||||||
private final SignerSecretProvider secretProvider;
|
private final SignerSecretProvider secretProvider;
|
||||||
|
private XFrameOption xFrameOption;
|
||||||
|
private boolean xFrameOptionIsEnabled;
|
||||||
|
private static final String X_FRAME_VALUE = "xFrameOption";
|
||||||
|
private static final String X_FRAME_ENABLED = "X_FRAME_ENABLED";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to construct instances of HTTP server with specific options.
|
* Class to construct instances of HTTP server with specific options.
|
||||||
|
@ -169,6 +174,9 @@ public final class HttpServer2 implements FilterContainer {
|
||||||
private String authFilterConfigurationPrefix = "hadoop.http.authentication.";
|
private String authFilterConfigurationPrefix = "hadoop.http.authentication.";
|
||||||
private String excludeCiphers;
|
private String excludeCiphers;
|
||||||
|
|
||||||
|
private boolean xFrameEnabled;
|
||||||
|
private XFrameOption xFrameOption = XFrameOption.SAMEORIGIN;
|
||||||
|
|
||||||
public Builder setName(String name){
|
public Builder setName(String name){
|
||||||
this.name = name;
|
this.name = name;
|
||||||
return this;
|
return this;
|
||||||
|
@ -277,6 +285,30 @@ public final class HttpServer2 implements FilterContainer {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the ability to control X_FRAME_OPTIONS on HttpServer2.
|
||||||
|
* @param xFrameEnabled - True enables X_FRAME_OPTIONS false disables it.
|
||||||
|
* @return Builder.
|
||||||
|
*/
|
||||||
|
public Builder configureXFrame(boolean xFrameEnabled) {
|
||||||
|
this.xFrameEnabled = xFrameEnabled;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a valid X-Frame-option that can be used by HttpServer2.
|
||||||
|
* @param option - String DENY, SAMEORIGIN or ALLOW-FROM are the only valid
|
||||||
|
* options. Any other value will throw IllegalArgument
|
||||||
|
* Exception.
|
||||||
|
* @return Builder.
|
||||||
|
*/
|
||||||
|
public Builder setXFrameOption(String option) {
|
||||||
|
this.xFrameOption = XFrameOption.getEnum(option);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public HttpServer2 build() throws IOException {
|
public HttpServer2 build() throws IOException {
|
||||||
Preconditions.checkNotNull(name, "name is not set");
|
Preconditions.checkNotNull(name, "name is not set");
|
||||||
Preconditions.checkState(!endpoints.isEmpty(), "No endpoints specified");
|
Preconditions.checkState(!endpoints.isEmpty(), "No endpoints specified");
|
||||||
|
@ -343,6 +375,9 @@ public final class HttpServer2 implements FilterContainer {
|
||||||
this.webServer = new Server();
|
this.webServer = new Server();
|
||||||
this.adminsAcl = b.adminsAcl;
|
this.adminsAcl = b.adminsAcl;
|
||||||
this.webAppContext = createWebAppContext(b.name, b.conf, adminsAcl, appDir);
|
this.webAppContext = createWebAppContext(b.name, b.conf, adminsAcl, appDir);
|
||||||
|
this.xFrameOptionIsEnabled = b.xFrameEnabled;
|
||||||
|
this.xFrameOption = b.xFrameOption;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.secretProvider =
|
this.secretProvider =
|
||||||
constructSecretProvider(b, webAppContext.getServletContext());
|
constructSecretProvider(b, webAppContext.getServletContext());
|
||||||
|
@ -399,7 +434,11 @@ public final class HttpServer2 implements FilterContainer {
|
||||||
|
|
||||||
addDefaultApps(contexts, appDir, conf);
|
addDefaultApps(contexts, appDir, conf);
|
||||||
|
|
||||||
addGlobalFilter("safety", QuotingInputFilter.class.getName(), null);
|
Map<String, String> xFrameParams = new HashMap<>();
|
||||||
|
xFrameParams.put(X_FRAME_ENABLED,
|
||||||
|
String.valueOf(this.xFrameOptionIsEnabled));
|
||||||
|
xFrameParams.put(X_FRAME_VALUE, this.xFrameOption.toString());
|
||||||
|
addGlobalFilter("safety", QuotingInputFilter.class.getName(), xFrameParams);
|
||||||
final FilterInitializer[] initializers = getFilterInitializers(conf);
|
final FilterInitializer[] initializers = getFilterInitializers(conf);
|
||||||
if (initializers != null) {
|
if (initializers != null) {
|
||||||
conf = new Configuration(conf);
|
conf = new Configuration(conf);
|
||||||
|
@ -1151,7 +1190,7 @@ public final class HttpServer2 implements FilterContainer {
|
||||||
* sets X-FRAME-OPTIONS in the header to mitigate clickjacking attacks.
|
* sets X-FRAME-OPTIONS in the header to mitigate clickjacking attacks.
|
||||||
*/
|
*/
|
||||||
public static class QuotingInputFilter implements Filter {
|
public static class QuotingInputFilter implements Filter {
|
||||||
private static final XFrameOption X_FRAME_OPTION = XFrameOption.SAMEORIGIN;
|
|
||||||
private FilterConfig config;
|
private FilterConfig config;
|
||||||
|
|
||||||
public static class RequestQuoter extends HttpServletRequestWrapper {
|
public static class RequestQuoter extends HttpServletRequestWrapper {
|
||||||
|
@ -1271,7 +1310,11 @@ public final class HttpServer2 implements FilterContainer {
|
||||||
} else if (mime.startsWith("application/xml")) {
|
} else if (mime.startsWith("application/xml")) {
|
||||||
httpResponse.setContentType("text/xml; charset=utf-8");
|
httpResponse.setContentType("text/xml; charset=utf-8");
|
||||||
}
|
}
|
||||||
httpResponse.addHeader("X-FRAME-OPTIONS", X_FRAME_OPTION.toString());
|
|
||||||
|
if(Boolean.valueOf(this.config.getInitParameter(X_FRAME_ENABLED))) {
|
||||||
|
httpResponse.addHeader("X-FRAME-OPTIONS",
|
||||||
|
this.config.getInitParameter(X_FRAME_VALUE));
|
||||||
|
}
|
||||||
chain.doFilter(quoted, httpResponse);
|
chain.doFilter(quoted, httpResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1306,5 +1349,23 @@ public final class HttpServer2 implements FilterContainer {
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We cannot use valueOf since the AllowFrom enum differs from its value
|
||||||
|
* Allow-From. This is a helper method that does exactly what valueof does,
|
||||||
|
* but allows us to handle the AllowFrom issue gracefully.
|
||||||
|
*
|
||||||
|
* @param value - String must be DENY, SAMEORIGIN or ALLOW-FROM.
|
||||||
|
* @return XFrameOption or throws IllegalException.
|
||||||
|
*/
|
||||||
|
private static XFrameOption getEnum(String value) {
|
||||||
|
Preconditions.checkState(value != null && !value.isEmpty());
|
||||||
|
for (XFrameOption xoption : values()) {
|
||||||
|
if (value.equals(xoption.toString())) {
|
||||||
|
return xoption;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unexpected value in xFrameOption.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,6 +169,25 @@ public class HttpServerFunctionalTest extends Assert {
|
||||||
return localServerBuilder(webapp).setFindPort(true).setConf(conf).build();
|
return localServerBuilder(webapp).setFindPort(true).setConf(conf).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a test server with xFrame options enabled.
|
||||||
|
* @param xFrameEnabled - true to enable xFrameSupport
|
||||||
|
* @param xFrameOptionValue - Option Value
|
||||||
|
* @param conf the configuration to use for the server
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public static HttpServer2 createServer(boolean xFrameEnabled,
|
||||||
|
String xFrameOptionValue,
|
||||||
|
Configuration conf)
|
||||||
|
throws IOException {
|
||||||
|
return localServerBuilder(TEST).setFindPort(true)
|
||||||
|
.configureXFrame(xFrameEnabled)
|
||||||
|
.setXFrameOption(xFrameOptionValue)
|
||||||
|
.setConf(conf)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
public static HttpServer2 createServer(String webapp, Configuration conf, AccessControlList adminsAcl)
|
public static HttpServer2 createServer(String webapp, Configuration conf, AccessControlList adminsAcl)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
return localServerBuilder(webapp).setFindPort(true).setConf(conf).setACL(adminsAcl).build();
|
return localServerBuilder(webapp).setFindPort(true).setConf(conf).setACL(adminsAcl).build();
|
||||||
|
|
|
@ -31,7 +31,9 @@ import org.apache.hadoop.security.authorize.AccessControlList;
|
||||||
import org.junit.AfterClass;
|
import org.junit.AfterClass;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.ExpectedException;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import org.mockito.internal.util.reflection.Whitebox;
|
import org.mockito.internal.util.reflection.Whitebox;
|
||||||
import org.mortbay.jetty.Connector;
|
import org.mortbay.jetty.Connector;
|
||||||
|
@ -68,6 +70,9 @@ public class TestHttpServer extends HttpServerFunctionalTest {
|
||||||
static final Log LOG = LogFactory.getLog(TestHttpServer.class);
|
static final Log LOG = LogFactory.getLog(TestHttpServer.class);
|
||||||
private static HttpServer2 server;
|
private static HttpServer2 server;
|
||||||
private static final int MAX_THREADS = 10;
|
private static final int MAX_THREADS = 10;
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExpectedException exception = ExpectedException.none();
|
||||||
|
|
||||||
@SuppressWarnings("serial")
|
@SuppressWarnings("serial")
|
||||||
public static class EchoMapServlet extends HttpServlet {
|
public static class EchoMapServlet extends HttpServlet {
|
||||||
|
@ -236,15 +241,70 @@ public class TestHttpServer extends HttpServerFunctionalTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHttpResonseContainsXFrameOptions() throws IOException {
|
public void testHttpResonseContainsXFrameOptions() throws Exception {
|
||||||
URL url = new URL(baseUrl, "");
|
validateXFrameOption(HttpServer2.XFrameOption.SAMEORIGIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHttpResonseContainsDeny() throws Exception {
|
||||||
|
validateXFrameOption(HttpServer2.XFrameOption.DENY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHttpResonseContainsAllowFrom() throws Exception {
|
||||||
|
validateXFrameOption(HttpServer2.XFrameOption.ALLOWFROM);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateXFrameOption(HttpServer2.XFrameOption option) throws
|
||||||
|
Exception {
|
||||||
|
Configuration conf = new Configuration();
|
||||||
|
boolean xFrameEnabled = true;
|
||||||
|
HttpServer2 httpServer = createServer(xFrameEnabled,
|
||||||
|
option.toString(), conf);
|
||||||
|
try {
|
||||||
|
HttpURLConnection conn = getHttpURLConnection(httpServer);
|
||||||
|
String xfoHeader = conn.getHeaderField("X-FRAME-OPTIONS");
|
||||||
|
assertTrue("X-FRAME-OPTIONS is absent in the header", xfoHeader != null);
|
||||||
|
assertTrue(xfoHeader.endsWith(option.toString()));
|
||||||
|
} finally {
|
||||||
|
httpServer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHttpResonseDoesNotContainXFrameOptions() throws Exception {
|
||||||
|
Configuration conf = new Configuration();
|
||||||
|
boolean xFrameEnabled = false;
|
||||||
|
HttpServer2 httpServer = createServer(xFrameEnabled,
|
||||||
|
HttpServer2.XFrameOption.SAMEORIGIN.toString(), conf);
|
||||||
|
try {
|
||||||
|
HttpURLConnection conn = getHttpURLConnection(httpServer);
|
||||||
|
String xfoHeader = conn.getHeaderField("X-FRAME-OPTIONS");
|
||||||
|
assertTrue("Unexpected X-FRAME-OPTIONS in header", xfoHeader == null);
|
||||||
|
} finally {
|
||||||
|
httpServer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpURLConnection getHttpURLConnection(HttpServer2 httpServer)
|
||||||
|
throws IOException {
|
||||||
|
httpServer.start();
|
||||||
|
URL newURL = getServerURL(httpServer);
|
||||||
|
URL url = new URL(newURL, "");
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
conn.connect();
|
conn.connect();
|
||||||
|
return conn;
|
||||||
String xfoHeader = conn.getHeaderField("X-FRAME-OPTIONS");
|
|
||||||
assertTrue("X-FRAME-OPTIONS is absent in the header", xfoHeader != null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHttpResonseInvalidValueType() throws Exception {
|
||||||
|
Configuration conf = new Configuration();
|
||||||
|
boolean xFrameEnabled = true;
|
||||||
|
exception.expect(IllegalArgumentException.class);
|
||||||
|
createServer(xFrameEnabled, "Hadoop", conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dummy filter that mimics as an authentication filter. Obtains user identity
|
* Dummy filter that mimics as an authentication filter. Obtains user identity
|
||||||
* from the request parameter user.name. Wraps around the request so that
|
* from the request parameter user.name. Wraps around the request so that
|
||||||
|
|
Loading…
Reference in New Issue