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}