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