Skip to content

Commit 0d7efae

Browse files
authored
HDFS-17680. Fix redirect for https Datanode server (#7884)
Reviewed-by: Steve Loughran <[email protected]> Reviewed-by: Wei-Chiu Chuang <[email protected]> Signed-off-by: Wei-Chiu Chuang <[email protected]>
1 parent 840fc75 commit 0d7efae

File tree

4 files changed

+212
-6
lines changed

4 files changed

+212
-6
lines changed

hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/web/DatanodeHttpServer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ protected void initChannel(SocketChannel ch) throws Exception {
164164
}
165165
p.addLast(
166166
new ChunkedWriteHandler(),
167-
new URLDispatcher(jettyAddr, conf, confForCreate));
167+
new URLDispatcher(jettyAddr, conf, confForCreate, false));
168168
}
169169
});
170170

@@ -222,7 +222,7 @@ protected void initChannel(SocketChannel ch) throws Exception {
222222
}
223223
p.addLast(
224224
new ChunkedWriteHandler(),
225-
new URLDispatcher(jettyAddr, conf, confForCreate));
225+
new URLDispatcher(jettyAddr, conf, confForCreate, true));
226226
}
227227
});
228228
} else {

hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/web/SimpleHttpProxyHandler.java

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,12 @@
3333
import io.netty.handler.codec.http.DefaultHttpResponse;
3434
import io.netty.handler.codec.http.HttpRequest;
3535
import io.netty.handler.codec.http.HttpRequestEncoder;
36+
import io.netty.handler.codec.http.HttpResponse;
37+
import io.netty.handler.codec.http.HttpResponseDecoder;
3638
import io.netty.handler.codec.http.HttpResponseEncoder;
39+
import io.netty.handler.codec.http.HttpHeaderNames;
3740
import io.netty.handler.codec.http.HttpHeaderValues;
41+
3842
import org.slf4j.Logger;
3943

4044
import java.net.InetSocketAddress;
@@ -48,17 +52,26 @@
4852
* inside the context, assuming that the remote peer is reasonable fast and
4953
* the response is small. The upper layer should be filtering out malicious
5054
* inputs.
55+
*
56+
* Constructs an internal netty server to proxy the HttpRequest to 'host',
57+
* and forward the response back via the inbound channel.
5158
*/
5259
class SimpleHttpProxyHandler extends SimpleChannelInboundHandler<HttpRequest> {
5360
private String uri;
5461
private Channel proxiedChannel;
5562
private final InetSocketAddress host;
63+
private final boolean isSecure;
5664
static final Logger LOG = DatanodeHttpServer.LOG;
5765

58-
SimpleHttpProxyHandler(InetSocketAddress host) {
66+
SimpleHttpProxyHandler(InetSocketAddress host, boolean isSecure) {
5967
this.host = host;
68+
this.isSecure = isSecure;
6069
}
6170

71+
/**
72+
* Accepts the inbound response from the proxied server and forwards it
73+
* to the 'client' channel.
74+
*/
6275
private static class Forwarder extends ChannelInboundHandlerAdapter {
6376
private final String uri;
6477
private final Channel client;
@@ -95,6 +108,36 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
95108
}
96109
}
97110

111+
/**
112+
* SSL redirect rewriter to adapt HTTP redirects to HTTPS. In the context
113+
* SimpleHttpProxyHandler is used, 'host' is always an HTTP server; if it
114+
* performs a redirect, it will redirect to an HTTP URL (HDFS-17680), which
115+
* will fail if the external server is configured to use HTTPS.
116+
*
117+
* This handler rewrites the Location header of an HttpResponse to use HTTPS
118+
* instead of HTTP, so that the client can follow the redirect.
119+
*/
120+
private static final class SslRedirectRewriter extends ChannelInboundHandlerAdapter {
121+
private SslRedirectRewriter() { }
122+
123+
@Override
124+
public void channelRead(final ChannelHandlerContext ctx, Object message) {
125+
if (!(message instanceof HttpResponse)) {
126+
ctx.fireChannelRead(message);
127+
return;
128+
}
129+
130+
HttpResponse response = (HttpResponse) message;
131+
String location = response.headers().get(HttpHeaderNames.LOCATION);
132+
if (location != null && location.startsWith("http://")) {
133+
LOG.debug("Rewriting Location header from http to https: {}", location);
134+
location = location.replaceFirst("http://", "https://");
135+
response.headers().set(HttpHeaderNames.LOCATION, location);
136+
}
137+
ctx.fireChannelRead(response);
138+
}
139+
}
140+
98141
@Override
99142
public void channelRead0
100143
(final ChannelHandlerContext ctx, final HttpRequest req) {
@@ -107,7 +150,17 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
107150
@Override
108151
protected void initChannel(SocketChannel ch) throws Exception {
109152
ChannelPipeline p = ch.pipeline();
110-
p.addLast(new HttpRequestEncoder(), new Forwarder(uri, client));
153+
p.addLast(new HttpRequestEncoder());
154+
if (isSecure) {
155+
LOG.debug("Proxying secure request {} to {}", uri, host);
156+
// Decode the proxy response and - if it's a redirect - rewrite the
157+
// Location header to use https instead of http.
158+
p.addLast(new HttpResponseDecoder(), new SslRedirectRewriter());
159+
// The client (proxy) channel now needs to re-encode the response
160+
// from Forwarder before sending it.
161+
client.pipeline().addFirst(new HttpResponseEncoder());
162+
}
163+
p.addLast(new Forwarder(uri, client));
111164
}
112165
});
113166
ChannelFuture f = proxiedServer.connect(host);

hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/web/URLDispatcher.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@ class URLDispatcher extends SimpleChannelInboundHandler<HttpRequest> {
3232
private final InetSocketAddress proxyHost;
3333
private final Configuration conf;
3434
private final Configuration confForCreate;
35+
private final boolean isSecure;
3536

3637
URLDispatcher(InetSocketAddress proxyHost, Configuration conf,
37-
Configuration confForCreate) {
38+
Configuration confForCreate, boolean isSecure) {
3839
this.proxyHost = proxyHost;
3940
this.conf = conf;
4041
this.confForCreate = confForCreate;
42+
this.isSecure = isSecure;
4143
}
4244

4345
@Override
@@ -50,7 +52,7 @@ protected void channelRead0(ChannelHandlerContext ctx, HttpRequest req)
5052
p.replace(this, WebHdfsHandler.class.getSimpleName(), h);
5153
h.channelRead0(ctx, req);
5254
} else {
53-
SimpleHttpProxyHandler h = new SimpleHttpProxyHandler(proxyHost);
55+
SimpleHttpProxyHandler h = new SimpleHttpProxyHandler(proxyHost, isSecure);
5456
p.replace(this, SimpleHttpProxyHandler.class.getSimpleName(), h);
5557
h.channelRead0(ctx, req);
5658
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hdfs.server.datanode.web;
19+
20+
import java.io.BufferedReader;
21+
import java.io.File;
22+
import java.io.InputStreamReader;
23+
import java.net.InetSocketAddress;
24+
import java.net.URL;
25+
import java.net.URLConnection;
26+
import java.util.Arrays;
27+
import java.util.Collection;
28+
29+
import org.apache.hadoop.conf.Configuration;
30+
import org.apache.hadoop.fs.FileUtil;
31+
import org.apache.hadoop.hdfs.DFSConfigKeys;
32+
import org.apache.hadoop.hdfs.web.URLConnectionFactory;
33+
import org.apache.hadoop.http.HttpConfig;
34+
import org.apache.hadoop.http.HttpConfig.Policy;
35+
import org.apache.hadoop.net.NetUtils;
36+
import org.apache.hadoop.security.ssl.KeyStoreTestUtil;
37+
import org.apache.hadoop.test.GenericTestUtils;
38+
import org.junit.AfterClass;
39+
import org.junit.Assert;
40+
import org.junit.BeforeClass;
41+
import org.junit.Test;
42+
import org.junit.runner.RunWith;
43+
import org.junit.runners.Parameterized;
44+
import org.junit.runners.Parameterized.Parameters;
45+
46+
@RunWith(value = Parameterized.class)
47+
public class TestDatanodeHttpServer {
48+
private static final String BASEDIR = GenericTestUtils
49+
.getTempPath(TestDatanodeHttpServer.class.getSimpleName());
50+
private static String keystoresDir;
51+
private static String sslConfDir;
52+
private static Configuration conf;
53+
private static URLConnectionFactory connectionFactory;
54+
55+
@Parameters
56+
public static Collection<Object[]> policy() {
57+
Object[][] params = new Object[][] {{HttpConfig.Policy.HTTP_ONLY},
58+
{HttpConfig.Policy.HTTPS_ONLY}, {HttpConfig.Policy.HTTP_AND_HTTPS}};
59+
return Arrays.asList(params);
60+
}
61+
62+
private final HttpConfig.Policy policy;
63+
64+
public TestDatanodeHttpServer(Policy policy) {
65+
super();
66+
this.policy = policy;
67+
}
68+
69+
@BeforeClass
70+
public static void setUp() throws Exception {
71+
File base = new File(BASEDIR);
72+
FileUtil.fullyDelete(base);
73+
base.mkdirs();
74+
conf = new Configuration();
75+
keystoresDir = new File(BASEDIR).getAbsolutePath();
76+
sslConfDir = KeyStoreTestUtil.getClasspathDir(TestDatanodeHttpServer.class);
77+
KeyStoreTestUtil.setupSSLConfig(keystoresDir, sslConfDir, conf, false);
78+
connectionFactory = URLConnectionFactory
79+
.newDefaultURLConnectionFactory(conf);
80+
conf.set(DFSConfigKeys.DFS_CLIENT_HTTPS_KEYSTORE_RESOURCE_KEY,
81+
KeyStoreTestUtil.getClientSSLConfigFileName());
82+
conf.set(DFSConfigKeys.DFS_SERVER_HTTPS_KEYSTORE_RESOURCE_KEY,
83+
KeyStoreTestUtil.getServerSSLConfigFileName());
84+
}
85+
86+
@AfterClass
87+
public static void tearDown() throws Exception {
88+
FileUtil.fullyDelete(new File(BASEDIR));
89+
KeyStoreTestUtil.cleanupSSLConfig(keystoresDir, sslConfDir);
90+
}
91+
92+
@Test
93+
public void testHttpPolicy() throws Exception {
94+
conf.set(DFSConfigKeys.DFS_HTTP_POLICY_KEY, policy.name());
95+
conf.set(DFSConfigKeys.DFS_DATANODE_HTTP_ADDRESS_KEY, "localhost:0");
96+
conf.set(DFSConfigKeys.DFS_DATANODE_HTTPS_ADDRESS_KEY, "localhost:0");
97+
98+
DatanodeHttpServer server = null;
99+
try {
100+
server = new DatanodeHttpServer(conf, null, null);
101+
server.start();
102+
103+
Assert.assertTrue(implies(policy.isHttpEnabled(),
104+
canAccess("http", server.getHttpAddress())));
105+
Assert.assertTrue(implies(!policy.isHttpEnabled(),
106+
server.getHttpAddress() == null));
107+
108+
Assert.assertTrue(implies(policy.isHttpsEnabled(),
109+
canAccess("https", server.getHttpsAddress())));
110+
Assert.assertTrue(implies(!policy.isHttpsEnabled(),
111+
server.getHttpsAddress() == null));
112+
113+
} finally {
114+
if (server != null) {
115+
server.close();
116+
}
117+
}
118+
}
119+
120+
private static boolean canAccess(String scheme, InetSocketAddress addr) {
121+
if (addr == null) {
122+
return false;
123+
}
124+
try {
125+
URL url = new URL(scheme + "://" + NetUtils.getHostPortString(addr));
126+
URLConnection conn = connectionFactory.openConnection(url);
127+
conn.connect();
128+
Assert.assertTrue(conn instanceof java.net.HttpURLConnection);
129+
java.net.HttpURLConnection httpConn = (java.net.HttpURLConnection) conn;
130+
if (httpConn.getResponseCode() != 200) {
131+
return false;
132+
}
133+
134+
StringBuilder builder = new StringBuilder();
135+
InputStreamReader responseReader = new InputStreamReader((conn.getInputStream()));
136+
try (BufferedReader reader = new BufferedReader(responseReader)) {
137+
String output;
138+
while ((output = reader.readLine()) != null) {
139+
builder.append(output);
140+
}
141+
}
142+
return builder.toString().contains("Hadoop Administration");
143+
} catch (Exception e) {
144+
return false;
145+
}
146+
}
147+
148+
private static boolean implies(boolean a, boolean b) {
149+
return !a || b;
150+
}
151+
}

0 commit comments

Comments
 (0)