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.hbase.rest; 019 020import static org.apache.hadoop.hbase.http.HttpServerUtil.PATH_SPEC_ANY; 021 022import java.lang.management.ManagementFactory; 023import java.net.UnknownHostException; 024import java.util.ArrayList; 025import java.util.EnumSet; 026import java.util.List; 027import java.util.Map; 028import java.util.concurrent.ArrayBlockingQueue; 029import javax.servlet.DispatcherType; 030import org.apache.commons.lang3.ArrayUtils; 031import org.apache.commons.lang3.StringUtils; 032import org.apache.hadoop.conf.Configuration; 033import org.apache.hadoop.hbase.HBaseConfiguration; 034import org.apache.hadoop.hbase.HBaseInterfaceAudience; 035import org.apache.hadoop.hbase.ServerName; 036import org.apache.hadoop.hbase.http.HttpServerUtil; 037import org.apache.hadoop.hbase.http.InfoServer; 038import org.apache.hadoop.hbase.log.HBaseMarkers; 039import org.apache.hadoop.hbase.rest.filter.AuthFilter; 040import org.apache.hadoop.hbase.rest.filter.GzipFilter; 041import org.apache.hadoop.hbase.rest.filter.RestCsrfPreventionFilter; 042import org.apache.hadoop.hbase.security.UserProvider; 043import org.apache.hadoop.hbase.util.DNS; 044import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 045import org.apache.hadoop.hbase.util.Pair; 046import org.apache.hadoop.hbase.util.ReflectionUtils; 047import org.apache.hadoop.hbase.util.Strings; 048import org.apache.hadoop.hbase.util.VersionInfo; 049import org.apache.yetus.audience.InterfaceAudience; 050import org.slf4j.Logger; 051import org.slf4j.LoggerFactory; 052 053import org.apache.hbase.thirdparty.com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; 054import org.apache.hbase.thirdparty.com.google.common.base.Preconditions; 055import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine; 056import org.apache.hbase.thirdparty.org.apache.commons.cli.HelpFormatter; 057import org.apache.hbase.thirdparty.org.apache.commons.cli.Options; 058import org.apache.hbase.thirdparty.org.apache.commons.cli.ParseException; 059import org.apache.hbase.thirdparty.org.apache.commons.cli.PosixParser; 060import org.apache.hbase.thirdparty.org.eclipse.jetty.http.HttpVersion; 061import org.apache.hbase.thirdparty.org.eclipse.jetty.jmx.MBeanContainer; 062import org.apache.hbase.thirdparty.org.eclipse.jetty.server.HttpConfiguration; 063import org.apache.hbase.thirdparty.org.eclipse.jetty.server.HttpConnectionFactory; 064import org.apache.hbase.thirdparty.org.eclipse.jetty.server.SecureRequestCustomizer; 065import org.apache.hbase.thirdparty.org.eclipse.jetty.server.Server; 066import org.apache.hbase.thirdparty.org.eclipse.jetty.server.ServerConnector; 067import org.apache.hbase.thirdparty.org.eclipse.jetty.server.SslConnectionFactory; 068import org.apache.hbase.thirdparty.org.eclipse.jetty.servlet.FilterHolder; 069import org.apache.hbase.thirdparty.org.eclipse.jetty.servlet.ServletContextHandler; 070import org.apache.hbase.thirdparty.org.eclipse.jetty.servlet.ServletHolder; 071import org.apache.hbase.thirdparty.org.eclipse.jetty.util.ssl.SslContextFactory; 072import org.apache.hbase.thirdparty.org.eclipse.jetty.util.thread.QueuedThreadPool; 073import org.apache.hbase.thirdparty.org.glassfish.jersey.server.ResourceConfig; 074import org.apache.hbase.thirdparty.org.glassfish.jersey.servlet.ServletContainer; 075 076/** 077 * Main class for launching REST gateway as a servlet hosted by Jetty. 078 * <p> 079 * The following options are supported: 080 * <ul> 081 * <li>-p --port : service port</li> 082 * <li>-ro --readonly : server mode</li> 083 * </ul> 084 */ 085@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.TOOLS) 086public class RESTServer implements Constants { 087 static Logger LOG = LoggerFactory.getLogger("RESTServer"); 088 public static final String REST_SERVER = "rest"; 089 090 static final String REST_CSRF_ENABLED_KEY = "hbase.rest.csrf.enabled"; 091 static final boolean REST_CSRF_ENABLED_DEFAULT = false; 092 boolean restCSRFEnabled = false; 093 static final String REST_CSRF_CUSTOM_HEADER_KEY = "hbase.rest.csrf.custom.header"; 094 static final String REST_CSRF_CUSTOM_HEADER_DEFAULT = "X-XSRF-HEADER"; 095 static final String REST_CSRF_METHODS_TO_IGNORE_KEY = "hbase.rest.csrf.methods.to.ignore"; 096 static final String REST_CSRF_METHODS_TO_IGNORE_DEFAULT = "GET,OPTIONS,HEAD,TRACE"; 097 public static final String SKIP_LOGIN_KEY = "hbase.rest.skip.login"; 098 static final int DEFAULT_HTTP_MAX_HEADER_SIZE = 64 * 1024; // 64k 099 static final String HTTP_HEADER_CACHE_SIZE = "hbase.rest.http.header.cache.size"; 100 static final int DEFAULT_HTTP_HEADER_CACHE_SIZE = Character.MAX_VALUE - 1; 101 102 static final String REST_HTTP_ALLOW_OPTIONS_METHOD = "hbase.rest.http.allow.options.method"; 103 // HTTP OPTIONS method is commonly used in REST APIs for negotiation. So it is enabled by default. 104 private static boolean REST_HTTP_ALLOW_OPTIONS_METHOD_DEFAULT = true; 105 static final String REST_CSRF_BROWSER_USERAGENTS_REGEX_KEY = 106 "hbase.rest-csrf.browser-useragents-regex"; 107 108 // HACK, making this static for AuthFilter to get at our configuration. Necessary for unit tests. 109 @edu.umd.cs.findbugs.annotations.SuppressWarnings( 110 value = { "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", "MS_CANNOT_BE_FINAL" }, 111 justification = "For testing") 112 public static Configuration conf = null; 113 private final UserProvider userProvider; 114 private Server server; 115 private InfoServer infoServer; 116 private ServerName serverName; 117 118 public RESTServer(Configuration conf) { 119 RESTServer.conf = conf; 120 this.userProvider = UserProvider.instantiate(conf); 121 } 122 123 private static void printUsageAndExit(Options options, int exitCode) { 124 HelpFormatter formatter = new HelpFormatter(); 125 formatter.printHelp("hbase rest start", "", options, 126 "\nTo run the REST server as a daemon, execute " 127 + "hbase-daemon.sh start|stop rest [-i <port>] [-p <port>] [-ro]\n", 128 true); 129 System.exit(exitCode); 130 } 131 132 void addCSRFFilter(ServletContextHandler ctxHandler, Configuration conf) { 133 restCSRFEnabled = conf.getBoolean(REST_CSRF_ENABLED_KEY, REST_CSRF_ENABLED_DEFAULT); 134 if (restCSRFEnabled) { 135 Map<String, String> restCsrfParams = 136 RestCsrfPreventionFilter.getFilterParams(conf, "hbase.rest-csrf."); 137 FilterHolder holder = new FilterHolder(); 138 holder.setName("csrf"); 139 holder.setClassName(RestCsrfPreventionFilter.class.getName()); 140 holder.setInitParameters(restCsrfParams); 141 ctxHandler.addFilter(holder, PATH_SPEC_ANY, EnumSet.allOf(DispatcherType.class)); 142 } 143 } 144 145 // login the server principal (if using secure Hadoop) 146 private static Pair<FilterHolder, Class<? extends ServletContainer>> 147 loginServerPrincipal(UserProvider userProvider, Configuration conf) throws Exception { 148 Class<? extends ServletContainer> containerClass = ServletContainer.class; 149 if (userProvider.isHadoopSecurityEnabled() && userProvider.isHBaseSecurityEnabled()) { 150 String machineName = getHostName(conf); 151 String keytabFilename = conf.get(REST_KEYTAB_FILE); 152 Preconditions.checkArgument(keytabFilename != null && !keytabFilename.isEmpty(), 153 REST_KEYTAB_FILE + " should be set if security is enabled"); 154 String principalConfig = conf.get(REST_KERBEROS_PRINCIPAL); 155 Preconditions.checkArgument(principalConfig != null && !principalConfig.isEmpty(), 156 REST_KERBEROS_PRINCIPAL + " should be set if security is enabled"); 157 // Hook for unit tests, this will log out any other user and mess up tests. 158 if (!conf.getBoolean(SKIP_LOGIN_KEY, false)) { 159 userProvider.login(REST_KEYTAB_FILE, REST_KERBEROS_PRINCIPAL, machineName); 160 } 161 if (conf.get(REST_AUTHENTICATION_TYPE) != null) { 162 containerClass = RESTServletContainer.class; 163 FilterHolder authFilter = new FilterHolder(); 164 authFilter.setClassName(AuthFilter.class.getName()); 165 authFilter.setName("AuthenticationFilter"); 166 return new Pair<>(authFilter, containerClass); 167 } 168 } 169 return new Pair<>(null, containerClass); 170 } 171 172 private static void parseCommandLine(String[] args, Configuration conf) { 173 Options options = new Options(); 174 options.addOption("p", "port", true, "Port to bind to [default: " + DEFAULT_LISTEN_PORT + "]"); 175 options.addOption("ro", "readonly", false, 176 "Respond only to GET HTTP " + "method requests [default: false]"); 177 options.addOption("i", "infoport", true, "Port for WEB UI"); 178 179 CommandLine commandLine = null; 180 try { 181 commandLine = new PosixParser().parse(options, args); 182 } catch (ParseException e) { 183 LOG.error("Could not parse: ", e); 184 printUsageAndExit(options, -1); 185 } 186 187 // check for user-defined port setting, if so override the conf 188 if (commandLine != null && commandLine.hasOption("port")) { 189 String val = commandLine.getOptionValue("port"); 190 conf.setInt("hbase.rest.port", Integer.parseInt(val)); 191 if (LOG.isDebugEnabled()) { 192 LOG.debug("port set to " + val); 193 } 194 } 195 196 // check if server should only process GET requests, if so override the conf 197 if (commandLine != null && commandLine.hasOption("readonly")) { 198 conf.setBoolean("hbase.rest.readonly", true); 199 if (LOG.isDebugEnabled()) { 200 LOG.debug("readonly set to true"); 201 } 202 } 203 204 // check for user-defined info server port setting, if so override the conf 205 if (commandLine != null && commandLine.hasOption("infoport")) { 206 String val = commandLine.getOptionValue("infoport"); 207 conf.setInt("hbase.rest.info.port", Integer.parseInt(val)); 208 if (LOG.isDebugEnabled()) { 209 LOG.debug("WEB UI port set to " + val); 210 } 211 } 212 213 if (commandLine != null && commandLine.hasOption("skipLogin")) { 214 conf.setBoolean(SKIP_LOGIN_KEY, true); 215 if (LOG.isDebugEnabled()) { 216 LOG.debug("Skipping Kerberos login for REST server"); 217 } 218 } 219 220 List<String> remainingArgs = commandLine != null ? commandLine.getArgList() : new ArrayList<>(); 221 if (remainingArgs.size() != 1) { 222 printUsageAndExit(options, 1); 223 } 224 225 String command = remainingArgs.get(0); 226 if ("start".equals(command)) { 227 // continue and start container 228 } else if ("stop".equals(command)) { 229 System.exit(1); 230 } else { 231 printUsageAndExit(options, 1); 232 } 233 } 234 235 /** 236 * Runs the REST server. 237 */ 238 public synchronized void run() throws Exception { 239 Pair<FilterHolder, Class<? extends ServletContainer>> pair = 240 loginServerPrincipal(userProvider, conf); 241 FilterHolder authFilter = pair.getFirst(); 242 Class<? extends ServletContainer> containerClass = pair.getSecond(); 243 RESTServlet servlet = RESTServlet.getInstance(conf, userProvider); 244 245 // set up the Jersey servlet container for Jetty 246 ResourceConfig application = new ResourceConfig().packages("org.apache.hadoop.hbase.rest") 247 .register(JacksonJaxbJsonProvider.class); 248 // Using our custom ServletContainer is tremendously important. This is what makes sure the 249 // UGI.doAs() is done for the remoteUser, and calls are not made as the REST server itself. 250 ServletContainer servletContainer = ReflectionUtils.newInstance(containerClass, application); 251 ServletHolder sh = new ServletHolder(servletContainer); 252 253 // Set the default max thread number to 100 to limit 254 // the number of concurrent requests so that REST server doesn't OOM easily. 255 // Jetty set the default max thread number to 250, if we don't set it. 256 // 257 // Our default min thread number 2 is the same as that used by Jetty. 258 int maxThreads = servlet.getConfiguration().getInt(REST_THREAD_POOL_THREADS_MAX, 100); 259 int minThreads = servlet.getConfiguration().getInt(REST_THREAD_POOL_THREADS_MIN, 2); 260 // Use the default queue (unbounded with Jetty 9.3) if the queue size is negative, otherwise use 261 // bounded {@link ArrayBlockingQueue} with the given size 262 int queueSize = servlet.getConfiguration().getInt(REST_THREAD_POOL_TASK_QUEUE_SIZE, -1); 263 int idleTimeout = 264 servlet.getConfiguration().getInt(REST_THREAD_POOL_THREAD_IDLE_TIMEOUT, 60000); 265 QueuedThreadPool threadPool = queueSize > 0 266 ? new QueuedThreadPool(maxThreads, minThreads, idleTimeout, 267 new ArrayBlockingQueue<>(queueSize)) 268 : new QueuedThreadPool(maxThreads, minThreads, idleTimeout); 269 270 this.server = new Server(threadPool); 271 272 // Setup JMX 273 MBeanContainer mbContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); 274 server.addEventListener(mbContainer); 275 server.addBean(mbContainer); 276 277 String host = servlet.getConfiguration().get("hbase.rest.host", "0.0.0.0"); 278 int servicePort = servlet.getConfiguration().getInt("hbase.rest.port", 8080); 279 int httpHeaderCacheSize = 280 servlet.getConfiguration().getInt(HTTP_HEADER_CACHE_SIZE, DEFAULT_HTTP_HEADER_CACHE_SIZE); 281 HttpConfiguration httpConfig = new HttpConfiguration(); 282 httpConfig.setSecureScheme("https"); 283 httpConfig.setSecurePort(servicePort); 284 httpConfig.setHeaderCacheSize(httpHeaderCacheSize); 285 httpConfig.setRequestHeaderSize(DEFAULT_HTTP_MAX_HEADER_SIZE); 286 httpConfig.setResponseHeaderSize(DEFAULT_HTTP_MAX_HEADER_SIZE); 287 httpConfig.setSendServerVersion(false); 288 httpConfig.setSendDateHeader(false); 289 290 ServerConnector serverConnector; 291 boolean isSecure = false; 292 if (conf.getBoolean(REST_SSL_ENABLED, false)) { 293 isSecure = true; 294 HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig); 295 httpsConfig.addCustomizer(new SecureRequestCustomizer()); 296 297 SslContextFactory.Server sslCtxFactory = new SslContextFactory.Server(); 298 String keystore = conf.get(REST_SSL_KEYSTORE_STORE); 299 String keystoreType = conf.get(REST_SSL_KEYSTORE_TYPE); 300 String password = HBaseConfiguration.getPassword(conf, REST_SSL_KEYSTORE_PASSWORD, null); 301 String keyPassword = 302 HBaseConfiguration.getPassword(conf, REST_SSL_KEYSTORE_KEYPASSWORD, password); 303 sslCtxFactory.setKeyStorePath(keystore); 304 if (StringUtils.isNotBlank(keystoreType)) { 305 sslCtxFactory.setKeyStoreType(keystoreType); 306 } 307 sslCtxFactory.setKeyStorePassword(password); 308 sslCtxFactory.setKeyManagerPassword(keyPassword); 309 310 String trustStore = conf.get(REST_SSL_TRUSTSTORE_STORE); 311 if (StringUtils.isNotBlank(trustStore)) { 312 sslCtxFactory.setTrustStorePath(trustStore); 313 } 314 String trustStorePassword = 315 HBaseConfiguration.getPassword(conf, REST_SSL_TRUSTSTORE_PASSWORD, null); 316 if (StringUtils.isNotBlank(trustStorePassword)) { 317 sslCtxFactory.setTrustStorePassword(trustStorePassword); 318 } 319 String trustStoreType = conf.get(REST_SSL_TRUSTSTORE_TYPE); 320 if (StringUtils.isNotBlank(trustStoreType)) { 321 sslCtxFactory.setTrustStoreType(trustStoreType); 322 } 323 324 String[] excludeCiphers = servlet.getConfiguration() 325 .getStrings(REST_SSL_EXCLUDE_CIPHER_SUITES, ArrayUtils.EMPTY_STRING_ARRAY); 326 if (excludeCiphers.length != 0) { 327 sslCtxFactory.setExcludeCipherSuites(excludeCiphers); 328 } 329 String[] includeCiphers = servlet.getConfiguration() 330 .getStrings(REST_SSL_INCLUDE_CIPHER_SUITES, ArrayUtils.EMPTY_STRING_ARRAY); 331 if (includeCiphers.length != 0) { 332 sslCtxFactory.setIncludeCipherSuites(includeCiphers); 333 } 334 335 String[] excludeProtocols = servlet.getConfiguration().getStrings(REST_SSL_EXCLUDE_PROTOCOLS, 336 ArrayUtils.EMPTY_STRING_ARRAY); 337 if (excludeProtocols.length != 0) { 338 sslCtxFactory.setExcludeProtocols(excludeProtocols); 339 } 340 String[] includeProtocols = servlet.getConfiguration().getStrings(REST_SSL_INCLUDE_PROTOCOLS, 341 ArrayUtils.EMPTY_STRING_ARRAY); 342 if (includeProtocols.length != 0) { 343 sslCtxFactory.setIncludeProtocols(includeProtocols); 344 } 345 346 serverConnector = new ServerConnector(server, 347 new SslConnectionFactory(sslCtxFactory, HttpVersion.HTTP_1_1.toString()), 348 new HttpConnectionFactory(httpsConfig)); 349 } else { 350 serverConnector = new ServerConnector(server, new HttpConnectionFactory(httpConfig)); 351 } 352 353 int acceptQueueSize = servlet.getConfiguration().getInt(REST_CONNECTOR_ACCEPT_QUEUE_SIZE, -1); 354 if (acceptQueueSize >= 0) { 355 serverConnector.setAcceptQueueSize(acceptQueueSize); 356 } 357 358 serverConnector.setPort(servicePort); 359 serverConnector.setHost(host); 360 361 server.addConnector(serverConnector); 362 server.setStopAtShutdown(true); 363 364 // set up context 365 ServletContextHandler ctxHandler = 366 new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS); 367 ctxHandler.addServlet(sh, PATH_SPEC_ANY); 368 if (authFilter != null) { 369 ctxHandler.addFilter(authFilter, PATH_SPEC_ANY, EnumSet.of(DispatcherType.REQUEST)); 370 } 371 372 // Load filters from configuration. 373 String[] filterClasses = 374 servlet.getConfiguration().getStrings(FILTER_CLASSES, GzipFilter.class.getName()); 375 for (String filter : filterClasses) { 376 filter = filter.trim(); 377 ctxHandler.addFilter(filter, PATH_SPEC_ANY, EnumSet.of(DispatcherType.REQUEST)); 378 } 379 addCSRFFilter(ctxHandler, conf); 380 HttpServerUtil.addClickjackingPreventionFilter(ctxHandler, conf, PATH_SPEC_ANY); 381 HttpServerUtil.addSecurityHeadersFilter(ctxHandler, conf, isSecure, PATH_SPEC_ANY); 382 HttpServerUtil.constrainHttpMethods(ctxHandler, servlet.getConfiguration() 383 .getBoolean(REST_HTTP_ALLOW_OPTIONS_METHOD, REST_HTTP_ALLOW_OPTIONS_METHOD_DEFAULT)); 384 385 // Put up info server. 386 int port = conf.getInt("hbase.rest.info.port", 8085); 387 if (port >= 0) { 388 final long startCode = EnvironmentEdgeManager.currentTime(); 389 conf.setLong("startcode", startCode); 390 this.serverName = ServerName.valueOf(getHostName(conf), servicePort, startCode); 391 392 String addr = conf.get("hbase.rest.info.bindAddress", "0.0.0.0"); 393 this.infoServer = new InfoServer(REST_SERVER, addr, port, false, conf); 394 this.infoServer.addPrivilegedServlet("dump", "/dump", RESTDumpServlet.class); 395 this.infoServer.setAttribute(REST_SERVER, this); 396 this.infoServer.setAttribute("hbase.conf", conf); 397 this.infoServer.start(); 398 } 399 // start server 400 server.start(); 401 } 402 403 private static String getHostName(Configuration conf) throws UnknownHostException { 404 return Strings.domainNamePointerToHostName(DNS.getDefaultHost( 405 conf.get(REST_DNS_INTERFACE, "default"), conf.get(REST_DNS_NAMESERVER, "default"))); 406 } 407 408 public synchronized void join() throws Exception { 409 if (server == null) { 410 throw new IllegalStateException("Server is not running"); 411 } 412 server.join(); 413 } 414 415 private void stopInfoServer() { 416 if (this.infoServer != null) { 417 LOG.info("Stop info server"); 418 try { 419 this.infoServer.stop(); 420 } catch (Exception e) { 421 LOG.error("Failed to stop infoServer", e); 422 } 423 } 424 } 425 426 public synchronized void stop() throws Exception { 427 stopInfoServer(); 428 if (server == null) { 429 throw new IllegalStateException("Server is not running"); 430 } 431 server.stop(); 432 server = null; 433 RESTServlet.stop(); 434 } 435 436 public synchronized int getPort() { 437 if (server == null) { 438 throw new IllegalStateException("Server is not running"); 439 } 440 return ((ServerConnector) server.getConnectors()[0]).getLocalPort(); 441 } 442 443 @SuppressWarnings("deprecation") 444 public synchronized int getInfoPort() { 445 if (infoServer == null) { 446 throw new IllegalStateException("InfoServer is not running"); 447 } 448 return infoServer.getPort(); 449 } 450 451 public ServerName getServerName() { 452 return serverName; 453 } 454 455 public Configuration getConf() { 456 return conf; 457 } 458 459 /** 460 * The main method for the HBase rest server. 461 * @param args command-line arguments 462 * @throws Exception exception 463 */ 464 public static void main(String[] args) throws Exception { 465 LOG.info("***** STARTING service '" + RESTServer.class.getSimpleName() + "' *****"); 466 VersionInfo.logVersion(); 467 final Configuration conf = HBaseConfiguration.create(); 468 parseCommandLine(args, conf); 469 RESTServer server = new RESTServer(conf); 470 471 try { 472 server.run(); 473 server.join(); 474 } catch (Exception e) { 475 LOG.error(HBaseMarkers.FATAL, "Failed to start server", e); 476 System.exit(1); 477 } 478 479 LOG.info("***** STOPPING service '" + RESTServer.class.getSimpleName() + "' *****"); 480 } 481}