001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hdfs.server.datanode.web; 019 020import static org.apache.hadoop.hdfs.client.HdfsClientConfigKeys.DFS_WEBHDFS_REST_CSRF_ENABLED_DEFAULT; 021import static org.apache.hadoop.hdfs.client.HdfsClientConfigKeys.DFS_WEBHDFS_REST_CSRF_ENABLED_KEY; 022 023import java.util.Enumeration; 024import java.util.Map; 025import javax.servlet.FilterConfig; 026import javax.servlet.ServletContext; 027import javax.servlet.ServletException; 028 029import io.netty.bootstrap.ChannelFactory; 030import io.netty.bootstrap.ServerBootstrap; 031import io.netty.channel.ChannelFuture; 032import io.netty.channel.ChannelInitializer; 033import io.netty.channel.ChannelOption; 034import io.netty.channel.ChannelPipeline; 035import io.netty.channel.EventLoopGroup; 036import io.netty.channel.nio.NioEventLoopGroup; 037import io.netty.channel.socket.SocketChannel; 038import io.netty.channel.socket.nio.NioServerSocketChannel; 039import io.netty.handler.codec.http.HttpRequestDecoder; 040import io.netty.handler.codec.http.HttpResponseEncoder; 041import io.netty.handler.ssl.SslHandler; 042import io.netty.handler.stream.ChunkedWriteHandler; 043 044import org.apache.commons.logging.Log; 045import org.apache.commons.logging.LogFactory; 046import org.apache.hadoop.conf.Configuration; 047import org.apache.hadoop.fs.permission.FsPermission; 048import org.apache.hadoop.hdfs.DFSConfigKeys; 049import org.apache.hadoop.hdfs.DFSUtil; 050import org.apache.hadoop.hdfs.server.common.JspHelper; 051import org.apache.hadoop.hdfs.server.datanode.BlockScanner; 052import org.apache.hadoop.hdfs.server.datanode.DataNode; 053import org.apache.hadoop.hdfs.server.namenode.FileChecksumServlets; 054import org.apache.hadoop.hdfs.server.namenode.StreamFile; 055import org.apache.hadoop.hdfs.server.datanode.web.webhdfs.DataNodeUGIProvider; 056import org.apache.hadoop.http.HttpConfig; 057import org.apache.hadoop.http.HttpServer2; 058import org.apache.hadoop.net.NetUtils; 059import org.apache.hadoop.security.authorize.AccessControlList; 060import org.apache.hadoop.security.http.RestCsrfPreventionFilter; 061import org.apache.hadoop.security.ssl.SSLFactory; 062 063import java.io.Closeable; 064import java.io.IOException; 065import java.net.BindException; 066import java.net.InetSocketAddress; 067import java.net.SocketAddress; 068import java.net.SocketException; 069import java.net.URI; 070import java.nio.channels.ServerSocketChannel; 071import java.security.GeneralSecurityException; 072 073import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_ADMIN; 074import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_DATANODE_HTTPS_ADDRESS_DEFAULT; 075import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_DATANODE_HTTPS_ADDRESS_KEY; 076import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_DATANODE_HTTP_ADDRESS_KEY; 077 078public class DatanodeHttpServer implements Closeable { 079 private final HttpServer2 infoServer; 080 private final EventLoopGroup bossGroup; 081 private final EventLoopGroup workerGroup; 082 private final ServerSocketChannel externalHttpChannel; 083 private final ServerBootstrap httpServer; 084 private final SSLFactory sslFactory; 085 private final ServerBootstrap httpsServer; 086 private final Configuration conf; 087 private final Configuration confForCreate; 088 private final RestCsrfPreventionFilter restCsrfPreventionFilter; 089 private InetSocketAddress httpAddress; 090 private InetSocketAddress httpsAddress; 091 static final Log LOG = LogFactory.getLog(DatanodeHttpServer.class); 092 093 public DatanodeHttpServer(final Configuration conf, 094 final DataNode datanode, 095 final ServerSocketChannel externalHttpChannel) 096 throws IOException { 097 this.restCsrfPreventionFilter = createRestCsrfPreventionFilter(conf); 098 this.conf = conf; 099 100 Configuration confForInfoServer = new Configuration(conf); 101 confForInfoServer.setInt(HttpServer2.HTTP_MAX_THREADS, 10); 102 HttpServer2.Builder builder = new HttpServer2.Builder() 103 .setName("datanode") 104 .setConf(confForInfoServer) 105 .setACL(new AccessControlList(conf.get(DFS_ADMIN, " "))) 106 .hostName(getHostnameForSpnegoPrincipal(confForInfoServer)) 107 .addEndpoint(URI.create("http://localhost:0")) 108 .setFindPort(true); 109 110 final boolean xFrameEnabled = conf.getBoolean( 111 DFSConfigKeys.DFS_XFRAME_OPTION_ENABLED, 112 DFSConfigKeys.DFS_XFRAME_OPTION_ENABLED_DEFAULT); 113 114 final String xFrameOptionValue = conf.getTrimmed( 115 DFSConfigKeys.DFS_XFRAME_OPTION_VALUE, 116 DFSConfigKeys.DFS_XFRAME_OPTION_VALUE_DEFAULT); 117 118 builder.configureXFrame(xFrameEnabled).setXFrameOption(xFrameOptionValue); 119 120 this.infoServer = builder.build(); 121 122 this.infoServer.addInternalServlet(null, "/streamFile/*", StreamFile.class); 123 this.infoServer.addInternalServlet(null, "/getFileChecksum/*", 124 FileChecksumServlets.GetServlet.class); 125 126 this.infoServer.setAttribute("datanode", datanode); 127 this.infoServer.setAttribute(JspHelper.CURRENT_CONF, conf); 128 this.infoServer.addServlet(null, "/blockScannerReport", 129 BlockScanner.Servlet.class); 130 DataNodeUGIProvider.init(conf); 131 this.infoServer.start(); 132 final InetSocketAddress jettyAddr = infoServer.getConnectorAddress(0); 133 134 this.confForCreate = new Configuration(conf); 135 confForCreate.set(FsPermission.UMASK_LABEL, "000"); 136 137 this.bossGroup = new NioEventLoopGroup(); 138 this.workerGroup = new NioEventLoopGroup(); 139 this.externalHttpChannel = externalHttpChannel; 140 HttpConfig.Policy policy = DFSUtil.getHttpPolicy(conf); 141 142 if (policy.isHttpEnabled()) { 143 this.httpServer = new ServerBootstrap().group(bossGroup, workerGroup) 144 .childHandler(new ChannelInitializer<SocketChannel>() { 145 @Override 146 protected void initChannel(SocketChannel ch) throws Exception { 147 ChannelPipeline p = ch.pipeline(); 148 p.addLast(new HttpRequestDecoder(), 149 new HttpResponseEncoder()); 150 if (restCsrfPreventionFilter != null) { 151 p.addLast(new RestCsrfPreventionFilterHandler( 152 restCsrfPreventionFilter)); 153 } 154 p.addLast( 155 new ChunkedWriteHandler(), 156 new URLDispatcher(jettyAddr, conf, confForCreate)); 157 } 158 }); 159 160 this.httpServer.childOption( 161 ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 162 conf.getInt( 163 DFSConfigKeys.DFS_WEBHDFS_NETTY_HIGH_WATERMARK, 164 DFSConfigKeys.DFS_WEBHDFS_NETTY_HIGH_WATERMARK_DEFAULT)); 165 this.httpServer.childOption( 166 ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 167 conf.getInt( 168 DFSConfigKeys.DFS_WEBHDFS_NETTY_LOW_WATERMARK, 169 DFSConfigKeys.DFS_WEBHDFS_NETTY_LOW_WATERMARK_DEFAULT)); 170 171 if (externalHttpChannel == null) { 172 httpServer.channel(NioServerSocketChannel.class); 173 } else { 174 httpServer.channelFactory(new ChannelFactory<NioServerSocketChannel>() { 175 @Override 176 public NioServerSocketChannel newChannel() { 177 return new NioServerSocketChannel(externalHttpChannel) { 178 // The channel has been bounded externally via JSVC, 179 // thus bind() becomes a no-op. 180 @Override 181 protected void doBind(SocketAddress localAddress) throws Exception {} 182 }; 183 } 184 }); 185 } 186 } else { 187 this.httpServer = null; 188 } 189 190 if (policy.isHttpsEnabled()) { 191 this.sslFactory = new SSLFactory(SSLFactory.Mode.SERVER, conf); 192 try { 193 sslFactory.init(); 194 } catch (GeneralSecurityException e) { 195 throw new IOException(e); 196 } 197 this.httpsServer = new ServerBootstrap().group(bossGroup, workerGroup) 198 .channel(NioServerSocketChannel.class) 199 .childHandler(new ChannelInitializer<SocketChannel>() { 200 @Override 201 protected void initChannel(SocketChannel ch) throws Exception { 202 ChannelPipeline p = ch.pipeline(); 203 p.addLast( 204 new SslHandler(sslFactory.createSSLEngine()), 205 new HttpRequestDecoder(), 206 new HttpResponseEncoder()); 207 if (restCsrfPreventionFilter != null) { 208 p.addLast(new RestCsrfPreventionFilterHandler( 209 restCsrfPreventionFilter)); 210 } 211 p.addLast( 212 new ChunkedWriteHandler(), 213 new URLDispatcher(jettyAddr, conf, confForCreate)); 214 } 215 }); 216 } else { 217 this.httpsServer = null; 218 this.sslFactory = null; 219 } 220 } 221 222 public InetSocketAddress getHttpAddress() { 223 return httpAddress; 224 } 225 226 public InetSocketAddress getHttpsAddress() { 227 return httpsAddress; 228 } 229 230 public void start() throws IOException { 231 if (httpServer != null) { 232 InetSocketAddress infoAddr = DataNode.getInfoAddr(conf); 233 ChannelFuture f = httpServer.bind(infoAddr); 234 try { 235 f.syncUninterruptibly(); 236 } catch (Throwable e) { 237 if (e instanceof BindException) { 238 throw NetUtils.wrapException(null, 0, infoAddr.getHostName(), 239 infoAddr.getPort(), (SocketException) e); 240 } else { 241 throw e; 242 } 243 } 244 httpAddress = (InetSocketAddress) f.channel().localAddress(); 245 LOG.info("Listening HTTP traffic on " + httpAddress); 246 } 247 248 if (httpsServer != null) { 249 InetSocketAddress secInfoSocAddr = 250 NetUtils.createSocketAddr(conf.getTrimmed( 251 DFS_DATANODE_HTTPS_ADDRESS_KEY, 252 DFS_DATANODE_HTTPS_ADDRESS_DEFAULT)); 253 ChannelFuture f = httpsServer.bind(secInfoSocAddr); 254 255 try { 256 f.syncUninterruptibly(); 257 } catch (Throwable e) { 258 if (e instanceof BindException) { 259 throw NetUtils.wrapException(null, 0, secInfoSocAddr.getHostName(), 260 secInfoSocAddr.getPort(), (SocketException) e); 261 } else { 262 throw e; 263 } 264 } 265 httpsAddress = (InetSocketAddress) f.channel().localAddress(); 266 LOG.info("Listening HTTPS traffic on " + httpsAddress); 267 } 268 } 269 270 @Override 271 public void close() throws IOException { 272 bossGroup.shutdownGracefully(); 273 workerGroup.shutdownGracefully(); 274 if (sslFactory != null) { 275 sslFactory.destroy(); 276 } 277 if (externalHttpChannel != null) { 278 externalHttpChannel.close(); 279 } 280 try { 281 infoServer.stop(); 282 } catch (Exception e) { 283 throw new IOException(e); 284 } 285 } 286 287 private static String getHostnameForSpnegoPrincipal(Configuration conf) { 288 String addr = conf.getTrimmed(DFS_DATANODE_HTTP_ADDRESS_KEY, null); 289 if (addr == null) { 290 addr = conf.getTrimmed(DFS_DATANODE_HTTPS_ADDRESS_KEY, 291 DFS_DATANODE_HTTPS_ADDRESS_DEFAULT); 292 } 293 InetSocketAddress inetSocker = NetUtils.createSocketAddr(addr); 294 return inetSocker.getHostString(); 295 } 296 297 /** 298 * Creates the {@link RestCsrfPreventionFilter} for the DataNode. Since the 299 * DataNode HTTP server is not implemented in terms of the servlet API, it 300 * takes some extra effort to obtain an instance of the filter. This method 301 * takes care of configuration and implementing just enough of the servlet API 302 * and related interfaces so that the DataNode can get a fully initialized 303 * instance of the filter. 304 * 305 * @param conf configuration to read 306 * @return initialized filter, or null if CSRF protection not enabled 307 */ 308 private static RestCsrfPreventionFilter createRestCsrfPreventionFilter( 309 Configuration conf) { 310 if (!conf.getBoolean(DFS_WEBHDFS_REST_CSRF_ENABLED_KEY, 311 DFS_WEBHDFS_REST_CSRF_ENABLED_DEFAULT)) { 312 return null; 313 } 314 String restCsrfClassName = RestCsrfPreventionFilter.class.getName(); 315 Map<String, String> restCsrfParams = RestCsrfPreventionFilter 316 .getFilterParams(conf, "dfs.webhdfs.rest-csrf."); 317 RestCsrfPreventionFilter filter = new RestCsrfPreventionFilter(); 318 try { 319 filter.init(new MapBasedFilterConfig(restCsrfClassName, restCsrfParams)); 320 } catch (ServletException e) { 321 throw new IllegalStateException( 322 "Failed to initialize RestCsrfPreventionFilter.", e); 323 } 324 return filter; 325 } 326 327 /** 328 * A minimal {@link FilterConfig} implementation backed by a {@link Map}. 329 */ 330 private static final class MapBasedFilterConfig implements FilterConfig { 331 332 private final String filterName; 333 private final Map<String, String> parameters; 334 335 /** 336 * Creates a new MapBasedFilterConfig. 337 * 338 * @param filterName filter name 339 * @param parameters mapping of filter initialization parameters 340 */ 341 public MapBasedFilterConfig(String filterName, 342 Map<String, String> parameters) { 343 this.filterName = filterName; 344 this.parameters = parameters; 345 } 346 347 @Override 348 public String getFilterName() { 349 return this.filterName; 350 } 351 352 @Override 353 public String getInitParameter(String name) { 354 return this.parameters.get(name); 355 } 356 357 @Override 358 public Enumeration<String> getInitParameterNames() { 359 throw this.notImplemented(); 360 } 361 362 @Override 363 public ServletContext getServletContext() { 364 throw this.notImplemented(); 365 } 366 367 /** 368 * Creates an exception indicating that an interface method is not 369 * implemented. These should never be seen in practice, because it is only 370 * used for methods that are not called by {@link RestCsrfPreventionFilter}. 371 * 372 * @return exception indicating method not implemented 373 */ 374 private UnsupportedOperationException notImplemented() { 375 return new UnsupportedOperationException(this.getClass().getSimpleName() 376 + " does not implement this method."); 377 } 378 } 379}