View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  package org.apache.hadoop.hbase.rest;
20  
21  import java.util.ArrayList;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Map.Entry;
25  
26  import org.apache.commons.cli.CommandLine;
27  import org.apache.commons.cli.HelpFormatter;
28  import org.apache.commons.cli.Options;
29  import org.apache.commons.cli.ParseException;
30  import org.apache.commons.cli.PosixParser;
31  import org.apache.commons.lang.ArrayUtils;
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  import org.apache.hadoop.hbase.classification.InterfaceAudience;
35  import org.apache.hadoop.conf.Configuration;
36  import org.apache.hadoop.hbase.HBaseConfiguration;
37  import org.apache.hadoop.hbase.HBaseInterfaceAudience;
38  import org.apache.hadoop.hbase.http.HttpServer;
39  import org.apache.hadoop.hbase.http.InfoServer;
40  import org.apache.hadoop.hbase.jetty.SslSelectChannelConnectorSecure;
41  import org.apache.hadoop.hbase.rest.filter.AuthFilter;
42  import org.apache.hadoop.hbase.rest.filter.RestCsrfPreventionFilter;
43  import org.apache.hadoop.hbase.security.UserProvider;
44  import org.apache.hadoop.hbase.util.DNS;
45  import org.apache.hadoop.hbase.util.HttpServerUtil;
46  import org.apache.hadoop.hbase.util.Pair;
47  import org.apache.hadoop.hbase.util.Strings;
48  import org.apache.hadoop.hbase.util.VersionInfo;
49  import org.apache.hadoop.util.StringUtils;
50  import org.mortbay.jetty.Connector;
51  import org.mortbay.jetty.Server;
52  import org.mortbay.jetty.nio.SelectChannelConnector;
53  import org.mortbay.jetty.servlet.Context;
54  import org.mortbay.jetty.servlet.FilterHolder;
55  import org.mortbay.jetty.servlet.ServletHolder;
56  import org.mortbay.thread.QueuedThreadPool;
57  
58  import com.google.common.base.Preconditions;
59  import com.sun.jersey.api.json.JSONConfiguration;
60  import com.sun.jersey.spi.container.servlet.ServletContainer;
61  
62  /**
63   * Main class for launching REST gateway as a servlet hosted by Jetty.
64   * <p>
65   * The following options are supported:
66   * <ul>
67   * <li>-p --port : service port</li>
68   * <li>-ro --readonly : server mode</li>
69   * </ul>
70   */
71  @InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.TOOLS)
72  public class RESTServer implements Constants {
73    static Log LOG = LogFactory.getLog("RESTServer");
74  
75    static String REST_CSRF_ENABLED_KEY = "hbase.rest.csrf.enabled";
76    static boolean REST_CSRF_ENABLED_DEFAULT = false;
77    static boolean restCSRFEnabled = false;
78    static String REST_CSRF_CUSTOM_HEADER_KEY ="hbase.rest.csrf.custom.header";
79    static String REST_CSRF_CUSTOM_HEADER_DEFAULT = "X-XSRF-HEADER";
80    static String REST_CSRF_METHODS_TO_IGNORE_KEY = "hbase.rest.csrf.methods.to.ignore";
81    static String REST_CSRF_METHODS_TO_IGNORE_DEFAULT = "GET,OPTIONS,HEAD,TRACE";
82    static String REST_HTTP_ALLOW_OPTIONS_METHOD = "hbase.rest.http.allow.options.method";
83    // HTTP OPTIONS method is commonly used in REST APIs for negotiation. It is disabled by default to
84    // maintain backward incompatibility
85    private static boolean REST_HTTP_ALLOW_OPTIONS_METHOD_DEFAULT = false;
86  
87    private static void printUsageAndExit(Options options, int exitCode) {
88      HelpFormatter formatter = new HelpFormatter();
89      formatter.printHelp("bin/hbase rest start", "", options,
90        "\nTo run the REST server as a daemon, execute " +
91        "bin/hbase-daemon.sh start|stop rest [--infoport <port>] [-p <port>] [-ro]\n", true);
92      System.exit(exitCode);
93    }
94  
95    /**
96     * Returns a list of strings from a comma-delimited configuration value.
97     *
98     * @param conf configuration to check
99     * @param name configuration property name
100    * @param defaultValue default value if no value found for name
101    * @return list of strings from comma-delimited configuration value, or an
102    *     empty list if not found
103    */
104   private static List<String> getTrimmedStringList(Configuration conf,
105     String name, String defaultValue) {
106     String valueString = conf.get(name, defaultValue);
107     if (valueString == null) {
108       return new ArrayList<>();
109     }
110     return new ArrayList<>(StringUtils.getTrimmedStringCollection(valueString));
111   }
112 
113   static String REST_CSRF_BROWSER_USERAGENTS_REGEX_KEY = "hbase.rest-csrf.browser-useragents-regex";
114   static void addCSRFFilter(Context context, Configuration conf) {
115     restCSRFEnabled = conf.getBoolean(REST_CSRF_ENABLED_KEY, REST_CSRF_ENABLED_DEFAULT);
116     if (restCSRFEnabled) {
117       String[] urls = { "/*" };
118       Map<String, String> restCsrfParams = RestCsrfPreventionFilter
119           .getFilterParams(conf, "hbase.rest-csrf.");
120       HttpServer.defineFilter(context, "csrf", RestCsrfPreventionFilter.class.getName(),
121         restCsrfParams, urls);
122     }
123   }
124 
125   // login the server principal (if using secure Hadoop)
126   private static Pair<FilterHolder, Class<? extends ServletContainer>> loginServerPrincipal(
127     UserProvider userProvider, Configuration conf) throws Exception {
128     Class<? extends ServletContainer> containerClass = ServletContainer.class;
129     if (userProvider.isHadoopSecurityEnabled() && userProvider.isHBaseSecurityEnabled()) {
130       String machineName = Strings.domainNamePointerToHostName(
131         DNS.getDefaultHost(conf.get(REST_DNS_INTERFACE, "default"),
132           conf.get(REST_DNS_NAMESERVER, "default")));
133       String keytabFilename = conf.get(REST_KEYTAB_FILE);
134       Preconditions.checkArgument(keytabFilename != null && !keytabFilename.isEmpty(),
135         REST_KEYTAB_FILE + " should be set if security is enabled");
136       String principalConfig = conf.get(REST_KERBEROS_PRINCIPAL);
137       Preconditions.checkArgument(principalConfig != null && !principalConfig.isEmpty(),
138         REST_KERBEROS_PRINCIPAL + " should be set if security is enabled");
139       userProvider.login(REST_KEYTAB_FILE, REST_KERBEROS_PRINCIPAL, machineName);
140       if (conf.get(REST_AUTHENTICATION_TYPE) != null) {
141         containerClass = RESTServletContainer.class;
142         FilterHolder authFilter = new FilterHolder();
143         authFilter.setClassName(AuthFilter.class.getName());
144         authFilter.setName("AuthenticationFilter");
145         return new Pair<FilterHolder, Class<? extends ServletContainer>>(authFilter,containerClass);
146       }
147     }
148     return new Pair<FilterHolder, Class<? extends ServletContainer>>(null, containerClass);
149   }
150 
151   private static void parseCommandLine(String[] args, RESTServlet servlet) {
152     Options options = new Options();
153     options.addOption("p", "port", true, "Port to bind to [default: " + DEFAULT_LISTEN_PORT + "]");
154     options.addOption("ro", "readonly", false, "Respond only to GET HTTP " +
155       "method requests [default: false]");
156     options.addOption(null, "infoport", true, "Port for web UI");
157 
158     CommandLine commandLine = null;
159     try {
160       commandLine = new PosixParser().parse(options, args);
161     } catch (ParseException e) {
162       LOG.error("Could not parse: ", e);
163       printUsageAndExit(options, -1);
164     }
165 
166     // check for user-defined port setting, if so override the conf
167     if (commandLine != null && commandLine.hasOption("port")) {
168       String val = commandLine.getOptionValue("port");
169       servlet.getConfiguration().setInt("hbase.rest.port", Integer.parseInt(val));
170       if (LOG.isDebugEnabled()) {
171         LOG.debug("port set to " + val);
172       }
173     }
174 
175     // check if server should only process GET requests, if so override the conf
176     if (commandLine != null && commandLine.hasOption("readonly")) {
177       servlet.getConfiguration().setBoolean("hbase.rest.readonly", true);
178       if (LOG.isDebugEnabled()) {
179         LOG.debug("readonly set to true");
180       }
181     }
182 
183     // check for user-defined info server port setting, if so override the conf
184     if (commandLine != null && commandLine.hasOption("infoport")) {
185       String val = commandLine.getOptionValue("infoport");
186       servlet.getConfiguration().setInt("hbase.rest.info.port", Integer.parseInt(val));
187       if (LOG.isDebugEnabled()) {
188         LOG.debug("Web UI port set to " + val);
189       }
190     }
191 
192     @SuppressWarnings("unchecked")
193     List<String> remainingArgs = commandLine != null ?
194         commandLine.getArgList() : new ArrayList<String>();
195     if (remainingArgs.size() != 1) {
196       printUsageAndExit(options, 1);
197     }
198 
199     String command = remainingArgs.get(0);
200     if ("start".equals(command)) {
201       // continue and start container
202     } else if ("stop".equals(command)) {
203       System.exit(1);
204     } else {
205       printUsageAndExit(options, 1);
206     }
207   }
208 
209   /**
210    * The main method for the HBase rest server.
211    * @param args command-line arguments
212    * @throws Exception exception
213    */
214   public static void main(String[] args) throws Exception {
215     VersionInfo.logVersion();
216     Configuration conf = HBaseConfiguration.create();
217     UserProvider userProvider = UserProvider.instantiate(conf);
218     Pair<FilterHolder, Class<? extends ServletContainer>> pair = loginServerPrincipal(
219       userProvider, conf);
220     FilterHolder authFilter = pair.getFirst();
221     Class<? extends ServletContainer> containerClass = pair.getSecond();
222     RESTServlet servlet = RESTServlet.getInstance(conf, userProvider);
223 
224     parseCommandLine(args, servlet);
225 
226     // set up the Jersey servlet container for Jetty
227     ServletHolder sh = new ServletHolder(containerClass);
228     sh.setInitParameter(
229       "com.sun.jersey.config.property.resourceConfigClass",
230       ResourceConfig.class.getCanonicalName());
231     sh.setInitParameter("com.sun.jersey.config.property.packages",
232       "jetty");
233     // The servlet holder below is instantiated to only handle the case
234     // of the /status/cluster returning arrays of nodes (live/dead). Without
235     // this servlet holder, the problem is that the node arrays in the response
236     // are collapsed to single nodes. We want to be able to treat the
237     // node lists as POJO in the response to /status/cluster servlet call,
238     // but not change the behavior for any of the other servlets
239     // Hence we don't use the servlet holder for all servlets / paths
240     ServletHolder shPojoMap = new ServletHolder(containerClass);
241     @SuppressWarnings("unchecked")
242     Map<String, String> shInitMap = sh.getInitParameters();
243     for (Entry<String, String> e : shInitMap.entrySet()) {
244       shPojoMap.setInitParameter(e.getKey(), e.getValue());
245     }
246     shPojoMap.setInitParameter(JSONConfiguration.FEATURE_POJO_MAPPING, "true");
247 
248     // set up Jetty and run the embedded server
249 
250     Server server = new Server();
251 
252     Connector connector = new SelectChannelConnector();
253     if(conf.getBoolean(REST_SSL_ENABLED, false)) {
254       SslSelectChannelConnectorSecure sslConnector = new SslSelectChannelConnectorSecure();
255       String keystore = conf.get(REST_SSL_KEYSTORE_STORE);
256       String password = HBaseConfiguration.getPassword(conf,
257         REST_SSL_KEYSTORE_PASSWORD, null);
258       String keyPassword = HBaseConfiguration.getPassword(conf,
259         REST_SSL_KEYSTORE_KEYPASSWORD, password);
260       sslConnector.setKeystore(keystore);
261       sslConnector.setPassword(password);
262       sslConnector.setKeyPassword(keyPassword);
263       connector = sslConnector;
264     }
265     connector.setPort(servlet.getConfiguration().getInt("hbase.rest.port", DEFAULT_LISTEN_PORT));
266     connector.setHost(servlet.getConfiguration().get("hbase.rest.host", "0.0.0.0"));
267     connector.setHeaderBufferSize(65536);
268 
269     server.addConnector(connector);
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("hbase.rest.threads.max", 100);
277     int minThreads = servlet.getConfiguration().getInt("hbase.rest.threads.min", 2);
278     QueuedThreadPool threadPool = new QueuedThreadPool(maxThreads);
279     threadPool.setMinThreads(minThreads);
280     server.setThreadPool(threadPool);
281 
282     server.setSendServerVersion(false);
283     server.setSendDateHeader(false);
284     server.setStopAtShutdown(true);
285       // set up context
286     Context context = new Context(server, "/", Context.SESSIONS);
287     context.addServlet(shPojoMap, "/status/cluster");
288     context.addServlet(sh, "/*");
289     if (authFilter != null) {
290       context.addFilter(authFilter, "/*", 1);
291     }
292 
293     // Load filters from configuration.
294     String[] filterClasses = servlet.getConfiguration().getStrings(FILTER_CLASSES,
295       ArrayUtils.EMPTY_STRING_ARRAY);
296     for (String filter : filterClasses) {
297       filter = filter.trim();
298       context.addFilter(Class.forName(filter), "/*", 0);
299     }
300     addCSRFFilter(context, conf);
301     HttpServerUtil.constrainHttpMethods(context, servlet.getConfiguration()
302         .getBoolean(REST_HTTP_ALLOW_OPTIONS_METHOD, REST_HTTP_ALLOW_OPTIONS_METHOD_DEFAULT));
303 
304     // Put up info server.
305     int port = conf.getInt("hbase.rest.info.port", 8085);
306     if (port >= 0) {
307       conf.setLong("startcode", System.currentTimeMillis());
308       String a = conf.get("hbase.rest.info.bindAddress", "0.0.0.0");
309       InfoServer infoServer = new InfoServer("rest", a, port, false, conf);
310       infoServer.setAttribute("hbase.conf", conf);
311       infoServer.start();
312     }
313     // start server
314     server.start();
315     server.join();
316   }
317 }