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