View Javadoc

1   /*
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  
20  package org.apache.hadoop.hbase.rest.client;
21  
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.net.URL;
25  import java.util.Collections;
26  import java.util.Map;
27  import java.util.concurrent.ConcurrentHashMap;
28  
29  import org.apache.commons.httpclient.Header;
30  import org.apache.commons.httpclient.HttpClient;
31  import org.apache.commons.httpclient.HttpMethod;
32  import org.apache.commons.httpclient.HttpStatus;
33  import org.apache.commons.httpclient.HttpVersion;
34  import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
35  import org.apache.commons.httpclient.URI;
36  import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
37  import org.apache.commons.httpclient.methods.DeleteMethod;
38  import org.apache.commons.httpclient.methods.GetMethod;
39  import org.apache.commons.httpclient.methods.HeadMethod;
40  import org.apache.commons.httpclient.methods.PostMethod;
41  import org.apache.commons.httpclient.methods.PutMethod;
42  import org.apache.commons.httpclient.params.HttpClientParams;
43  import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
44  import org.apache.commons.logging.Log;
45  import org.apache.commons.logging.LogFactory;
46  import org.apache.hadoop.hbase.classification.InterfaceAudience;
47  import org.apache.hadoop.hbase.classification.InterfaceStability;
48  import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
49  import org.apache.hadoop.security.authentication.client.AuthenticationException;
50  import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
51  
52  /**
53   * A wrapper around HttpClient which provides some useful function and
54   * semantics for interacting with the REST gateway.
55   */
56  @InterfaceAudience.Public
57  @InterfaceStability.Stable
58  public class Client {
59    public static final Header[] EMPTY_HEADER_ARRAY = new Header[0];
60  
61    private static final Log LOG = LogFactory.getLog(Client.class);
62  
63    private HttpClient httpClient;
64    private Cluster cluster;
65    private boolean sslEnabled;
66  
67    private Map<String, String> extraHeaders;
68  
69    private static final String AUTH_COOKIE = "hadoop.auth";
70    private static final String AUTH_COOKIE_EQ = AUTH_COOKIE + "=";
71    private static final String COOKIE = "Cookie";
72  
73    /**
74     * Default Constructor
75     */
76    public Client() {
77      this(null);
78    }
79  
80    private void initialize(Cluster cluster, boolean sslEnabled) {
81      this.cluster = cluster;
82      this.sslEnabled = sslEnabled;
83      MultiThreadedHttpConnectionManager manager =
84        new MultiThreadedHttpConnectionManager();
85      HttpConnectionManagerParams managerParams = manager.getParams();
86      managerParams.setConnectionTimeout(2000); // 2 s
87      managerParams.setDefaultMaxConnectionsPerHost(10);
88      managerParams.setMaxTotalConnections(100);
89      extraHeaders = new ConcurrentHashMap<String, String>();
90      this.httpClient = new HttpClient(manager);
91      HttpClientParams clientParams = httpClient.getParams();
92      clientParams.setVersion(HttpVersion.HTTP_1_1);
93  
94    }
95    /**
96     * Constructor
97     * @param cluster the cluster definition
98     */
99    public Client(Cluster cluster) {
100     initialize(cluster, false);
101   }
102 
103   /**
104    * Constructor
105    * @param cluster the cluster definition
106    * @param sslEnabled enable SSL or not
107    */
108   public Client(Cluster cluster, boolean sslEnabled) {
109     initialize(cluster, sslEnabled);
110   }
111 
112   /**
113    * Shut down the client. Close any open persistent connections.
114    */
115   public void shutdown() {
116     MultiThreadedHttpConnectionManager manager =
117       (MultiThreadedHttpConnectionManager) httpClient.getHttpConnectionManager();
118     manager.shutdown();
119   }
120 
121   /**
122    * @return the wrapped HttpClient
123    */
124   public HttpClient getHttpClient() {
125     return httpClient;
126   }
127 
128   /**
129    * Add extra headers.  These extra headers will be applied to all http
130    * methods before they are removed. If any header is not used any more,
131    * client needs to remove it explicitly.
132    */
133   public void addExtraHeader(final String name, final String value) {
134     extraHeaders.put(name, value);
135   }
136 
137   /**
138    * Get an extra header value.
139    */
140   public String getExtraHeader(final String name) {
141     return extraHeaders.get(name);
142   }
143 
144   /**
145    * Get all extra headers (read-only).
146    */
147   public Map<String, String> getExtraHeaders() {
148     return Collections.unmodifiableMap(extraHeaders);
149   }
150 
151   /**
152    * Remove an extra header.
153    */
154   public void removeExtraHeader(final String name) {
155     extraHeaders.remove(name);
156   }
157 
158   /**
159    * Execute a transaction method given only the path. Will select at random
160    * one of the members of the supplied cluster definition and iterate through
161    * the list until a transaction can be successfully completed. The
162    * definition of success here is a complete HTTP transaction, irrespective
163    * of result code.
164    * @param cluster the cluster definition
165    * @param method the transaction method
166    * @param headers HTTP header values to send
167    * @param path the properly urlencoded path
168    * @return the HTTP response code
169    * @throws IOException
170    */
171   public int executePathOnly(Cluster cluster, HttpMethod method,
172       Header[] headers, String path) throws IOException {
173     IOException lastException;
174     if (cluster.nodes.size() < 1) {
175       throw new IOException("Cluster is empty");
176     }
177     int start = (int)Math.round((cluster.nodes.size() - 1) * Math.random());
178     int i = start;
179     do {
180       cluster.lastHost = cluster.nodes.get(i);
181       try {
182         StringBuilder sb = new StringBuilder();
183         if (sslEnabled) {
184           sb.append("https://");
185         } else {
186           sb.append("http://");
187         }
188         sb.append(cluster.lastHost);
189         sb.append(path);
190         URI uri = new URI(sb.toString(), true);
191         return executeURI(method, headers, uri.toString());
192       } catch (IOException e) {
193         lastException = e;
194       }
195     } while (++i != start && i < cluster.nodes.size());
196     throw lastException;
197   }
198 
199   /**
200    * Execute a transaction method given a complete URI.
201    * @param method the transaction method
202    * @param headers HTTP header values to send
203    * @param uri a properly urlencoded URI
204    * @return the HTTP response code
205    * @throws IOException
206    */
207   public int executeURI(HttpMethod method, Header[] headers, String uri)
208       throws IOException {
209     method.setURI(new URI(uri, true));
210     for (Map.Entry<String, String> e: extraHeaders.entrySet()) {
211       method.addRequestHeader(e.getKey(), e.getValue());
212     }
213     if (headers != null) {
214       for (Header header: headers) {
215         method.addRequestHeader(header);
216       }
217     }
218     long startTime = System.currentTimeMillis();
219     int code = httpClient.executeMethod(method);
220     if (code == HttpStatus.SC_UNAUTHORIZED) { // Authentication error
221       LOG.debug("Performing negotiation with the server.");
222       negotiate(method, uri);
223       code = httpClient.executeMethod(method);
224     }
225     long endTime = System.currentTimeMillis();
226     if (LOG.isTraceEnabled()) {
227       LOG.trace(method.getName() + " " + uri + " " + code + " " +
228         method.getStatusText() + " in " + (endTime - startTime) + " ms");
229     }
230     return code;
231   }
232 
233   /**
234    * Execute a transaction method. Will call either <tt>executePathOnly</tt>
235    * or <tt>executeURI</tt> depending on whether a path only is supplied in
236    * 'path', or if a complete URI is passed instead, respectively.
237    * @param cluster the cluster definition
238    * @param method the HTTP method
239    * @param headers HTTP header values to send
240    * @param path the properly urlencoded path or URI
241    * @return the HTTP response code
242    * @throws IOException
243    */
244   public int execute(Cluster cluster, HttpMethod method, Header[] headers,
245       String path) throws IOException {
246     if (path.startsWith("/")) {
247       return executePathOnly(cluster, method, headers, path);
248     }
249     return executeURI(method, headers, path);
250   }
251 
252   /**
253    * Initiate client side Kerberos negotiation with the server.
254    * @param method method to inject the authentication token into.
255    * @param uri the String to parse as a URL.
256    * @throws IOException if unknown protocol is found.
257    */
258   private void negotiate(HttpMethod method, String uri) throws IOException {
259     try {
260       AuthenticatedURL.Token token = new AuthenticatedURL.Token();
261       KerberosAuthenticator authenticator = new KerberosAuthenticator();
262       authenticator.authenticate(new URL(uri), token);
263       // Inject the obtained negotiated token in the method cookie
264       injectToken(method, token);
265     } catch (AuthenticationException e) {
266       LOG.error("Failed to negotiate with the server.", e);
267       throw new IOException(e);
268     }
269   }
270 
271   /**
272    * Helper method that injects an authentication token to send with the method.
273    * @param method method to inject the authentication token into.
274    * @param token authentication token to inject.
275    */
276   private void injectToken(HttpMethod method, AuthenticatedURL.Token token) {
277     String t = token.toString();
278     if (t != null) {
279       if (!t.startsWith("\"")) {
280         t = "\"" + t + "\"";
281       }
282       method.addRequestHeader(COOKIE, AUTH_COOKIE_EQ + t);
283     }
284   }
285 
286   /**
287    * @return the cluster definition
288    */
289   public Cluster getCluster() {
290     return cluster;
291   }
292 
293   /**
294    * @param cluster the cluster definition
295    */
296   public void setCluster(Cluster cluster) {
297     this.cluster = cluster;
298   }
299 
300   /**
301    * Send a HEAD request
302    * @param path the path or URI
303    * @return a Response object with response detail
304    * @throws IOException
305    */
306   public Response head(String path) throws IOException {
307     return head(cluster, path, null);
308   }
309 
310   /**
311    * Send a HEAD request
312    * @param cluster the cluster definition
313    * @param path the path or URI
314    * @param headers the HTTP headers to include in the request
315    * @return a Response object with response detail
316    * @throws IOException
317    */
318   public Response head(Cluster cluster, String path, Header[] headers)
319       throws IOException {
320     HeadMethod method = new HeadMethod();
321     try {
322       int code = execute(cluster, method, null, path);
323       headers = method.getResponseHeaders();
324       return new Response(code, headers, null);
325     } finally {
326       method.releaseConnection();
327     }
328   }
329 
330   /**
331    * Send a GET request
332    * @param path the path or URI
333    * @return a Response object with response detail
334    * @throws IOException
335    */
336   public Response get(String path) throws IOException {
337     return get(cluster, path);
338   }
339 
340   /**
341    * Send a GET request
342    * @param cluster the cluster definition
343    * @param path the path or URI
344    * @return a Response object with response detail
345    * @throws IOException
346    */
347   public Response get(Cluster cluster, String path) throws IOException {
348     return get(cluster, path, EMPTY_HEADER_ARRAY);
349   }
350 
351   /**
352    * Send a GET request
353    * @param path the path or URI
354    * @param accept Accept header value
355    * @return a Response object with response detail
356    * @throws IOException
357    */
358   public Response get(String path, String accept) throws IOException {
359     return get(cluster, path, accept);
360   }
361 
362   /**
363    * Send a GET request
364    * @param cluster the cluster definition
365    * @param path the path or URI
366    * @param accept Accept header value
367    * @return a Response object with response detail
368    * @throws IOException
369    */
370   public Response get(Cluster cluster, String path, String accept)
371       throws IOException {
372     Header[] headers = new Header[1];
373     headers[0] = new Header("Accept", accept);
374     return get(cluster, path, headers);
375   }
376 
377   /**
378    * Send a GET request
379    * @param path the path or URI
380    * @param headers the HTTP headers to include in the request,
381    * <tt>Accept</tt> must be supplied
382    * @return a Response object with response detail
383    * @throws IOException
384    */
385   public Response get(String path, Header[] headers) throws IOException {
386     return get(cluster, path, headers);
387   }
388 
389   /**
390    * Send a GET request
391    * @param c the cluster definition
392    * @param path the path or URI
393    * @param headers the HTTP headers to include in the request
394    * @return a Response object with response detail
395    * @throws IOException
396    */
397   public Response get(Cluster c, String path, Header[] headers)
398       throws IOException {
399     GetMethod method = new GetMethod();
400     try {
401       int code = execute(c, method, headers, path);
402       headers = method.getResponseHeaders();
403       byte[] body = method.getResponseBody();
404       InputStream in = method.getResponseBodyAsStream();
405       return new Response(code, headers, body, in);
406     } finally {
407       method.releaseConnection();
408     }
409   }
410 
411   /**
412    * Send a PUT request
413    * @param path the path or URI
414    * @param contentType the content MIME type
415    * @param content the content bytes
416    * @return a Response object with response detail
417    * @throws IOException
418    */
419   public Response put(String path, String contentType, byte[] content)
420       throws IOException {
421     return put(cluster, path, contentType, content);
422   }
423 
424   /**
425    * Send a PUT request
426    * @param cluster the cluster definition
427    * @param path the path or URI
428    * @param contentType the content MIME type
429    * @param content the content bytes
430    * @return a Response object with response detail
431    * @throws IOException
432    */
433   public Response put(Cluster cluster, String path, String contentType,
434       byte[] content) throws IOException {
435     Header[] headers = new Header[1];
436     headers[0] = new Header("Content-Type", contentType);
437     return put(cluster, path, headers, content);
438   }
439 
440   /**
441    * Send a PUT request
442    * @param path the path or URI
443    * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be
444    * supplied
445    * @param content the content bytes
446    * @return a Response object with response detail
447    * @throws IOException
448    */
449   public Response put(String path, Header[] headers, byte[] content)
450       throws IOException {
451     return put(cluster, path, headers, content);
452   }
453 
454   /**
455    * Send a PUT request
456    * @param cluster the cluster definition
457    * @param path the path or URI
458    * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be
459    * supplied
460    * @param content the content bytes
461    * @return a Response object with response detail
462    * @throws IOException
463    */
464   public Response put(Cluster cluster, String path, Header[] headers,
465       byte[] content) throws IOException {
466     PutMethod method = new PutMethod();
467     try {
468       method.setRequestEntity(new ByteArrayRequestEntity(content));
469       int code = execute(cluster, method, headers, path);
470       headers = method.getResponseHeaders();
471       content = method.getResponseBody();
472       return new Response(code, headers, content);
473     } finally {
474       method.releaseConnection();
475     }
476   }
477 
478   /**
479    * Send a POST request
480    * @param path the path or URI
481    * @param contentType the content MIME type
482    * @param content the content bytes
483    * @return a Response object with response detail
484    * @throws IOException
485    */
486   public Response post(String path, String contentType, byte[] content)
487       throws IOException {
488     return post(cluster, path, contentType, content);
489   }
490 
491   /**
492    * Send a POST request
493    * @param cluster the cluster definition
494    * @param path the path or URI
495    * @param contentType the content MIME type
496    * @param content the content bytes
497    * @return a Response object with response detail
498    * @throws IOException
499    */
500   public Response post(Cluster cluster, String path, String contentType,
501       byte[] content) throws IOException {
502     Header[] headers = new Header[1];
503     headers[0] = new Header("Content-Type", contentType);
504     return post(cluster, path, headers, content);
505   }
506 
507   /**
508    * Send a POST request
509    * @param path the path or URI
510    * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be
511    * supplied
512    * @param content the content bytes
513    * @return a Response object with response detail
514    * @throws IOException
515    */
516   public Response post(String path, Header[] headers, byte[] content)
517       throws IOException {
518     return post(cluster, path, headers, content);
519   }
520 
521   /**
522    * Send a POST request
523    * @param cluster the cluster definition
524    * @param path the path or URI
525    * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be
526    * supplied
527    * @param content the content bytes
528    * @return a Response object with response detail
529    * @throws IOException
530    */
531   public Response post(Cluster cluster, String path, Header[] headers,
532       byte[] content) throws IOException {
533     PostMethod method = new PostMethod();
534     try {
535       method.setRequestEntity(new ByteArrayRequestEntity(content));
536       int code = execute(cluster, method, headers, path);
537       headers = method.getResponseHeaders();
538       content = method.getResponseBody();
539       return new Response(code, headers, content);
540     } finally {
541       method.releaseConnection();
542     }
543   }
544 
545   /**
546    * Send a DELETE request
547    * @param path the path or URI
548    * @return a Response object with response detail
549    * @throws IOException
550    */
551   public Response delete(String path) throws IOException {
552     return delete(cluster, path);
553   }
554 
555   /**
556    * Send a DELETE request
557    * @param cluster the cluster definition
558    * @param path the path or URI
559    * @return a Response object with response detail
560    * @throws IOException
561    */
562   public Response delete(Cluster cluster, String path) throws IOException {
563     DeleteMethod method = new DeleteMethod();
564     try {
565       int code = execute(cluster, method, null, path);
566       Header[] headers = method.getResponseHeaders();
567       byte[] content = method.getResponseBody();
568       return new Response(code, headers, content);
569     } finally {
570       method.releaseConnection();
571     }
572   }
573 }