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.client;
019
020import java.io.BufferedInputStream;
021import java.io.File;
022import java.io.FileOutputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.net.URI;
026import java.net.URISyntaxException;
027import java.net.URL;
028import java.nio.file.Files;
029import java.nio.file.Path;
030import java.security.GeneralSecurityException;
031import java.security.KeyManagementException;
032import java.security.KeyStore;
033import java.security.KeyStoreException;
034import java.security.NoSuchAlgorithmException;
035import java.security.cert.CertificateException;
036import java.util.Collections;
037import java.util.Map;
038import java.util.Optional;
039import java.util.concurrent.ConcurrentHashMap;
040import java.util.concurrent.ThreadLocalRandom;
041import javax.net.ssl.SSLContext;
042import org.apache.hadoop.conf.Configuration;
043import org.apache.hadoop.hbase.HBaseConfiguration;
044import org.apache.hadoop.hbase.rest.Constants;
045import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
046import org.apache.hadoop.security.authentication.client.AuthenticationException;
047import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
048import org.apache.hadoop.security.ssl.SSLFactory;
049import org.apache.hadoop.security.ssl.SSLFactory.Mode;
050import org.apache.http.Header;
051import org.apache.http.HttpHeaders;
052import org.apache.http.HttpResponse;
053import org.apache.http.HttpStatus;
054import org.apache.http.auth.AuthScope;
055import org.apache.http.auth.UsernamePasswordCredentials;
056import org.apache.http.client.HttpClient;
057import org.apache.http.client.config.RequestConfig;
058import org.apache.http.client.methods.HttpDelete;
059import org.apache.http.client.methods.HttpGet;
060import org.apache.http.client.methods.HttpHead;
061import org.apache.http.client.methods.HttpPost;
062import org.apache.http.client.methods.HttpPut;
063import org.apache.http.client.methods.HttpUriRequest;
064import org.apache.http.client.protocol.HttpClientContext;
065import org.apache.http.entity.ByteArrayEntity;
066import org.apache.http.impl.client.BasicCredentialsProvider;
067import org.apache.http.impl.client.HttpClientBuilder;
068import org.apache.http.impl.client.HttpClients;
069import org.apache.http.impl.cookie.BasicClientCookie;
070import org.apache.http.message.BasicHeader;
071import org.apache.http.ssl.SSLContexts;
072import org.apache.http.util.EntityUtils;
073import org.apache.yetus.audience.InterfaceAudience;
074import org.slf4j.Logger;
075import org.slf4j.LoggerFactory;
076
077import org.apache.hbase.thirdparty.com.google.common.io.ByteStreams;
078import org.apache.hbase.thirdparty.com.google.common.io.Closeables;
079
080/**
081 * A wrapper around HttpClient which provides some useful function and semantics for interacting
082 * with the REST gateway.
083 */
084@InterfaceAudience.Public
085public class Client {
086  public static final Header[] EMPTY_HEADER_ARRAY = new Header[0];
087
088  private static final Logger LOG = LoggerFactory.getLogger(Client.class);
089
090  private HttpClient httpClient;
091  private Cluster cluster;
092  private Integer lastNodeId;
093  private boolean sticky = false;
094  private Configuration conf;
095  private boolean sslEnabled;
096  private HttpResponse resp;
097  private HttpGet httpGet = null;
098  private HttpClientContext stickyContext = null;
099  private BasicCredentialsProvider provider;
100  private Optional<KeyStore> trustStore;
101  private Map<String, String> extraHeaders;
102  private KerberosAuthenticator authenticator;
103
104  private static final String AUTH_COOKIE = "hadoop.auth";
105  private static final String AUTH_COOKIE_EQ = AUTH_COOKIE + "=";
106  private static final String COOKIE = "Cookie";
107
108  /**
109   * Default Constructor
110   */
111  public Client() {
112    this(null);
113  }
114
115  private void initialize(Cluster cluster, Configuration conf, boolean sslEnabled, boolean sticky,
116    Optional<KeyStore> trustStore, Optional<String> userName, Optional<String> password,
117    Optional<String> bearerToken) {
118    this.cluster = cluster;
119    this.conf = conf;
120    this.sslEnabled = sslEnabled;
121    this.trustStore = trustStore;
122    extraHeaders = new ConcurrentHashMap<>();
123    String clspath = System.getProperty("java.class.path");
124    LOG.debug("classpath " + clspath);
125    HttpClientBuilder httpClientBuilder = HttpClients.custom();
126
127    int connTimeout = this.conf.getInt(Constants.REST_CLIENT_CONN_TIMEOUT,
128      Constants.DEFAULT_REST_CLIENT_CONN_TIMEOUT);
129    int socketTimeout = this.conf.getInt(Constants.REST_CLIENT_SOCKET_TIMEOUT,
130      Constants.DEFAULT_REST_CLIENT_SOCKET_TIMEOUT);
131    RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(connTimeout)
132      .setSocketTimeout(socketTimeout).setNormalizeUri(false) // URIs should not be normalized, see
133                                                              // HBASE-26903
134      .build();
135    httpClientBuilder.setDefaultRequestConfig(requestConfig);
136
137    // Since HBASE-25267 we don't use the deprecated DefaultHttpClient anymore.
138    // The new http client would decompress the gzip content automatically.
139    // In order to keep the original behaviour of this public class, we disable
140    // automatic content compression.
141    httpClientBuilder.disableContentCompression();
142
143    if (sslEnabled && trustStore.isPresent()) {
144      try {
145        SSLContext sslcontext =
146          SSLContexts.custom().loadTrustMaterial(trustStore.get(), null).build();
147        httpClientBuilder.setSSLContext(sslcontext);
148      } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
149        throw new ClientTrustStoreInitializationException("Error while processing truststore", e);
150      }
151    }
152
153    if (userName.isPresent() && password.isPresent()) {
154      // We want to stick to the old very limited authentication and session handling when sticky is
155      // not set
156      // to preserve backwards compatibility
157      if (!sticky) {
158        throw new IllegalArgumentException("BASIC auth is only implemented when sticky is set");
159      }
160      provider = new BasicCredentialsProvider();
161      // AuthScope.ANY is required for pre-emptive auth. We only ever use a single auth method
162      // anyway.
163      AuthScope anyAuthScope = AuthScope.ANY;
164      this.provider.setCredentials(anyAuthScope,
165        new UsernamePasswordCredentials(userName.get(), password.get()));
166    }
167
168    if (bearerToken.isPresent()) {
169      // We want to stick to the old very limited authentication and session handling when sticky is
170      // not set
171      // to preserve backwards compatibility
172      if (!sticky) {
173        throw new IllegalArgumentException("BEARER auth is only implemented when sticky is set");
174      }
175      // We could also put the header into the context or connection, but that would have the same
176      // effect.
177      extraHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + bearerToken.get());
178    }
179
180    this.httpClient = httpClientBuilder.build();
181    setSticky(sticky);
182  }
183
184  /**
185   * Constructor This constructor will create an object using the old faulty load balancing logic.
186   * When specifying multiple servers in the cluster object, it is highly recommended to call
187   * setSticky() on the created client, or use the preferred constructor instead.
188   * @param cluster the cluster definition
189   */
190  public Client(Cluster cluster) {
191    this(cluster, false);
192  }
193
194  /**
195   * Constructor This constructor will create an object using the old faulty load balancing logic.
196   * When specifying multiple servers in the cluster object, it is highly recommended to call
197   * setSticky() on the created client, or use the preferred constructor instead.
198   * @param cluster    the cluster definition
199   * @param sslEnabled enable SSL or not
200   */
201  public Client(Cluster cluster, boolean sslEnabled) {
202    initialize(cluster, HBaseConfiguration.create(), sslEnabled, false, Optional.empty(),
203      Optional.empty(), Optional.empty(), Optional.empty());
204  }
205
206  /**
207   * Constructor This constructor will create an object using the old faulty load balancing logic.
208   * When specifying multiple servers in the cluster object, it is highly recommended to call
209   * setSticky() on the created client, or use the preferred constructor instead.
210   * @param cluster    the cluster definition
211   * @param conf       Configuration
212   * @param sslEnabled enable SSL or not
213   */
214  public Client(Cluster cluster, Configuration conf, boolean sslEnabled) {
215    initialize(cluster, conf, sslEnabled, false, Optional.empty(), Optional.empty(),
216      Optional.empty(), Optional.empty());
217  }
218
219  /**
220   * Constructor, allowing to define custom trust store (only for SSL connections) This constructor
221   * will create an object using the old faulty load balancing logic. When specifying multiple
222   * servers in the cluster object, it is highly recommended to call setSticky() on the created
223   * client, or use the preferred constructor instead.
224   * @param cluster            the cluster definition
225   * @param trustStorePath     custom trust store to use for SSL connections
226   * @param trustStorePassword password to use for custom trust store
227   * @param trustStoreType     type of custom trust store
228   * @throws ClientTrustStoreInitializationException if the trust store file can not be loaded
229   */
230  public Client(Cluster cluster, String trustStorePath, Optional<String> trustStorePassword,
231    Optional<String> trustStoreType) {
232    this(cluster, HBaseConfiguration.create(), trustStorePath, trustStorePassword, trustStoreType);
233  }
234
235  /**
236   * Constructor that accepts an optional trustStore and authentication information for either BASIC
237   * or BEARER authentication in sticky mode, which does not use the old faulty load balancing
238   * logic, and enables correct session handling. If neither userName/password, nor the bearer token
239   * is specified, the client falls back to SPNEGO auth. The loadTrustsore static method can be used
240   * to load a local trustStore file. This is the preferred constructor to use.
241   * @param cluster     the cluster definition
242   * @param conf        HBase/Hadoop configuration
243   * @param sslEnabled  use HTTPS
244   * @param trustStore  the optional trustStore object
245   * @param userName    for BASIC auth
246   * @param password    for BASIC auth
247   * @param bearerToken for BEAERER auth
248   */
249  public Client(Cluster cluster, Configuration conf, boolean sslEnabled,
250    Optional<KeyStore> trustStore, Optional<String> userName, Optional<String> password,
251    Optional<String> bearerToken) {
252    initialize(cluster, conf, sslEnabled, true, trustStore, userName, password, bearerToken);
253  }
254
255  /**
256   * Constructor, allowing to define custom trust store (only for SSL connections). This constructor
257   * will create an object using the old faulty load balancing logic. When specifying multiple
258   * servers in the cluster object, it is highly recommended to call setSticky() on the created
259   * client, or use the preferred constructor instead.
260   * @param cluster            the cluster definition
261   * @param conf               HBase/Hadoop Configuration
262   * @param trustStorePath     custom trust store to use for SSL connections
263   * @param trustStorePassword password to use for custom trust store
264   * @param trustStoreType     type of custom trust store
265   * @throws ClientTrustStoreInitializationException if the trust store file can not be loaded
266   */
267  public Client(Cluster cluster, Configuration conf, String trustStorePath,
268    Optional<String> trustStorePassword, Optional<String> trustStoreType) {
269    KeyStore trustStore = loadTruststore(trustStorePath, trustStorePassword, trustStoreType);
270    initialize(cluster, conf, true, false, Optional.of(trustStore), Optional.empty(),
271      Optional.empty(), Optional.empty());
272  }
273
274  /**
275   * Loads a trustStore from the local fileSystem. Can be used to load the trustStore for the
276   * preferred constructor.
277   */
278  public static KeyStore loadTruststore(String trustStorePath, Optional<String> trustStorePassword,
279    Optional<String> trustStoreType) {
280
281    char[] truststorePassword = trustStorePassword.map(String::toCharArray).orElse(null);
282    String type = trustStoreType.orElse(KeyStore.getDefaultType());
283
284    KeyStore trustStore;
285    try {
286      trustStore = KeyStore.getInstance(type);
287    } catch (KeyStoreException e) {
288      throw new ClientTrustStoreInitializationException("Invalid trust store type: " + type, e);
289    }
290    try (InputStream inputStream =
291      new BufferedInputStream(Files.newInputStream(new File(trustStorePath).toPath()))) {
292      trustStore.load(inputStream, truststorePassword);
293    } catch (CertificateException | NoSuchAlgorithmException | IOException e) {
294      throw new ClientTrustStoreInitializationException("Trust store load error: " + trustStorePath,
295        e);
296    }
297    return trustStore;
298  }
299
300  /**
301   * Shut down the client. Close any open persistent connections.
302   */
303  public void shutdown() {
304  }
305
306  /** Returns the wrapped HttpClient */
307  public HttpClient getHttpClient() {
308    return httpClient;
309  }
310
311  /**
312   * Add extra headers. These extra headers will be applied to all http methods before they are
313   * removed. If any header is not used any more, client needs to remove it explicitly.
314   */
315  public void addExtraHeader(final String name, final String value) {
316    extraHeaders.put(name, value);
317  }
318
319  /**
320   * Get an extra header value.
321   */
322  public String getExtraHeader(final String name) {
323    return extraHeaders.get(name);
324  }
325
326  /**
327   * Get all extra headers (read-only).
328   */
329  public Map<String, String> getExtraHeaders() {
330    return Collections.unmodifiableMap(extraHeaders);
331  }
332
333  /**
334   * Remove an extra header.
335   */
336  public void removeExtraHeader(final String name) {
337    extraHeaders.remove(name);
338  }
339
340  /**
341   * Execute a transaction method given only the path. If sticky is false: Will select at random one
342   * of the members of the supplied cluster definition and iterate through the list until a
343   * transaction can be successfully completed. The definition of success here is a complete HTTP
344   * transaction, irrespective of result code. If sticky is true: For the first request it will
345   * select a random one of the members of the supplied cluster definition. For subsequent requests
346   * it will use the same member, and it will not automatically re-try if the call fails.
347   * @param cluster the cluster definition
348   * @param method  the transaction method
349   * @param headers HTTP header values to send
350   * @param path    the properly urlencoded path
351   * @return the HTTP response code
352   */
353  public HttpResponse executePathOnly(Cluster cluster, HttpUriRequest method, Header[] headers,
354    String path) throws IOException {
355    IOException lastException;
356    if (cluster.nodes.size() < 1) {
357      throw new IOException("Cluster is empty");
358    }
359    if (lastNodeId == null || !sticky) {
360      lastNodeId = ThreadLocalRandom.current().nextInt(cluster.nodes.size());
361    }
362    int start = lastNodeId;
363    do {
364      cluster.lastHost = cluster.nodes.get(lastNodeId);
365      try {
366        StringBuilder sb = new StringBuilder();
367        if (sslEnabled) {
368          sb.append("https://");
369        } else {
370          sb.append("http://");
371        }
372        sb.append(cluster.lastHost);
373        sb.append(path);
374        URI uri = new URI(sb.toString());
375        if (method instanceof HttpPut) {
376          HttpPut put = new HttpPut(uri);
377          put.setEntity(((HttpPut) method).getEntity());
378          put.setHeaders(method.getAllHeaders());
379          method = put;
380        } else if (method instanceof HttpGet) {
381          method = new HttpGet(uri);
382        } else if (method instanceof HttpHead) {
383          method = new HttpHead(uri);
384        } else if (method instanceof HttpDelete) {
385          method = new HttpDelete(uri);
386        } else if (method instanceof HttpPost) {
387          HttpPost post = new HttpPost(uri);
388          post.setEntity(((HttpPost) method).getEntity());
389          post.setHeaders(method.getAllHeaders());
390          method = post;
391        }
392        return executeURI(method, headers, uri.toString());
393      } catch (IOException e) {
394        lastException = e;
395      } catch (URISyntaxException use) {
396        lastException = new IOException(use);
397      }
398      if (!sticky) {
399        lastNodeId = (++lastNodeId) % cluster.nodes.size();
400      }
401      // Do not retry if sticky. Let the caller handle the error.
402    } while (!sticky && lastNodeId != start);
403    throw lastException;
404  }
405
406  /**
407   * Execute a transaction method given a complete URI.
408   * @param method  the transaction method
409   * @param headers HTTP header values to send
410   * @param uri     a properly urlencoded URI
411   * @return the HTTP response code
412   */
413  public HttpResponse executeURI(HttpUriRequest method, Header[] headers, String uri)
414    throws IOException {
415    // method.setURI(new URI(uri, true));
416    for (Map.Entry<String, String> e : extraHeaders.entrySet()) {
417      method.addHeader(e.getKey(), e.getValue());
418    }
419    if (headers != null) {
420      for (Header header : headers) {
421        method.addHeader(header);
422      }
423    }
424    long startTime = System.currentTimeMillis();
425    if (resp != null) EntityUtils.consumeQuietly(resp.getEntity());
426    if (stickyContext != null) {
427      resp = httpClient.execute(method, stickyContext);
428    } else {
429      resp = httpClient.execute(method);
430    }
431    if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
432      // Authentication error
433      LOG.debug("Performing negotiation with the server.");
434      try {
435        negotiate(method, uri);
436      } catch (GeneralSecurityException e) {
437        throw new IOException(e);
438      }
439      if (stickyContext != null) {
440        resp = httpClient.execute(method, stickyContext);
441      } else {
442        resp = httpClient.execute(method);
443      }
444    }
445
446    long endTime = System.currentTimeMillis();
447    if (LOG.isTraceEnabled()) {
448      LOG.trace(method.getMethod() + " " + uri + " " + resp.getStatusLine().getStatusCode() + " "
449        + resp.getStatusLine().getReasonPhrase() + " in " + (endTime - startTime) + " ms");
450    }
451    return resp;
452  }
453
454  /**
455   * Execute a transaction method. Will call either <tt>executePathOnly</tt> or <tt>executeURI</tt>
456   * depending on whether a path only is supplied in 'path', or if a complete URI is passed instead,
457   * respectively.
458   * @param cluster the cluster definition
459   * @param method  the HTTP method
460   * @param headers HTTP header values to send
461   * @param path    the properly urlencoded path or URI
462   * @return the HTTP response code
463   */
464  public HttpResponse execute(Cluster cluster, HttpUriRequest method, Header[] headers, String path)
465    throws IOException {
466    if (path.startsWith("/")) {
467      return executePathOnly(cluster, method, headers, path);
468    }
469    return executeURI(method, headers, path);
470  }
471
472  /**
473   * Initiate client side Kerberos negotiation with the server.
474   * @param method method to inject the authentication token into.
475   * @param uri    the String to parse as a URL.
476   * @throws IOException if unknown protocol is found.
477   */
478  private void negotiate(HttpUriRequest method, String uri)
479    throws IOException, GeneralSecurityException {
480    try {
481      AuthenticatedURL.Token token = new AuthenticatedURL.Token();
482      if (authenticator == null) {
483        authenticator = new KerberosAuthenticator();
484        if (trustStore.isPresent()) {
485          // The authenticator does not use Apache HttpClient, so we need to
486          // configure it separately to use the specified trustStore
487          Configuration sslConf = setupTrustStoreForHadoop(trustStore.get());
488          SSLFactory sslFactory = new SSLFactory(Mode.CLIENT, sslConf);
489          sslFactory.init();
490          authenticator.setConnectionConfigurator(sslFactory);
491        }
492      }
493      URL url = new URL(uri);
494      authenticator.authenticate(url, token);
495      if (sticky) {
496        BasicClientCookie authCookie = new BasicClientCookie("hadoop.auth", token.toString());
497        // Hadoop eats the domain even if set by server
498        authCookie.setDomain(url.getHost());
499        stickyContext.getCookieStore().addCookie(authCookie);
500      } else {
501        // session cookie is NOT set for backwards compatibility for non-sticky mode
502        // Inject the obtained negotiated token in the method cookie
503        // This is only done for this single request, the next one will trigger a new SPENGO
504        // handshake
505        injectToken(method, token);
506      }
507    } catch (AuthenticationException e) {
508      LOG.error("Failed to negotiate with the server.", e);
509      throw new IOException(e);
510    }
511  }
512
513  private Configuration setupTrustStoreForHadoop(KeyStore trustStore)
514    throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException {
515    Path tmpDirPath = Files.createTempDirectory("hbase_rest_client_truststore");
516    File trustStoreFile = tmpDirPath.resolve("truststore.jks").toFile();
517    // Shouldn't be needed with the secure temp dir, but let's generate a password anyway
518    String password = Double.toString(Math.random());
519    try (FileOutputStream fos = new FileOutputStream(trustStoreFile)) {
520      trustStore.store(fos, password.toCharArray());
521    }
522
523    Configuration sslConf = new Configuration();
524    // Type is the Java default, we use the same JVM to read this back
525    sslConf.set("ssl.client.keystore.location", trustStoreFile.getAbsolutePath());
526    sslConf.set("ssl.client.keystore.password", password);
527    return sslConf;
528  }
529
530  /**
531   * Helper method that injects an authentication token to send with the method.
532   * @param method method to inject the authentication token into.
533   * @param token  authentication token to inject.
534   */
535  private void injectToken(HttpUriRequest method, AuthenticatedURL.Token token) {
536    String t = token.toString();
537    if (t != null) {
538      if (!t.startsWith("\"")) {
539        t = "\"" + t + "\"";
540      }
541      method.addHeader(COOKIE, AUTH_COOKIE_EQ + t);
542    }
543  }
544
545  /** Returns the cluster definition */
546  public Cluster getCluster() {
547    return cluster;
548  }
549
550  /**
551   * @param cluster the cluster definition
552   */
553  public void setCluster(Cluster cluster) {
554    this.cluster = cluster;
555  }
556
557  /**
558   * The default behaviour is load balancing by sending each request to a random host. This DOES NOT
559   * work with scans, which have state on the REST servers. Make sure sticky is set to true before
560   * attempting Scan related operations if more than one host is defined in the cluster.
561   * @return whether subsequent requests will use the same host
562   */
563  public boolean isSticky() {
564    return sticky;
565  }
566
567  /**
568   * The default behaviour is load balancing by sending each request to a random host. This DOES NOT
569   * work with scans, which have state on the REST servers. Set sticky to true before attempting
570   * Scan related operations if more than one host is defined in the cluster. Nodes must not be
571   * added or removed from the Cluster object while sticky is true. Setting the sticky flag also
572   * enables session handling, which eliminates the need to re-authenticate each request, and lets
573   * the client handle any other cookies (like the sticky cookie set by load balancers) correctly.
574   * @param sticky whether subsequent requests will use the same host
575   */
576  public void setSticky(boolean sticky) {
577    lastNodeId = null;
578    if (sticky) {
579      stickyContext = new HttpClientContext();
580      if (provider != null) {
581        stickyContext.setCredentialsProvider(provider);
582      }
583    } else {
584      stickyContext = null;
585    }
586    this.sticky = sticky;
587  }
588
589  /**
590   * Send a HEAD request
591   * @param path the path or URI
592   * @return a Response object with response detail
593   */
594  public Response head(String path) throws IOException {
595    return head(cluster, path, null);
596  }
597
598  /**
599   * Send a HEAD request
600   * @param cluster the cluster definition
601   * @param path    the path or URI
602   * @param headers the HTTP headers to include in the request
603   * @return a Response object with response detail
604   */
605  public Response head(Cluster cluster, String path, Header[] headers) throws IOException {
606    HttpHead method = new HttpHead(path);
607    try {
608      HttpResponse resp = execute(cluster, method, null, path);
609      return new Response(resp.getStatusLine().getStatusCode(), resp.getAllHeaders(), null);
610    } finally {
611      method.releaseConnection();
612    }
613  }
614
615  /**
616   * Send a GET request
617   * @param path the path or URI
618   * @return a Response object with response detail
619   */
620  public Response get(String path) throws IOException {
621    return get(cluster, path);
622  }
623
624  /**
625   * Send a GET request
626   * @param cluster the cluster definition
627   * @param path    the path or URI
628   * @return a Response object with response detail
629   */
630  public Response get(Cluster cluster, String path) throws IOException {
631    return get(cluster, path, EMPTY_HEADER_ARRAY);
632  }
633
634  /**
635   * Send a GET request
636   * @param path   the path or URI
637   * @param accept Accept header value
638   * @return a Response object with response detail
639   */
640  public Response get(String path, String accept) throws IOException {
641    return get(cluster, path, accept);
642  }
643
644  /**
645   * Send a GET request
646   * @param cluster the cluster definition
647   * @param path    the path or URI
648   * @param accept  Accept header value
649   * @return a Response object with response detail
650   */
651  public Response get(Cluster cluster, String path, String accept) throws IOException {
652    Header[] headers = new Header[1];
653    headers[0] = new BasicHeader("Accept", accept);
654    return get(cluster, path, headers);
655  }
656
657  /**
658   * Send a GET request
659   * @param path    the path or URI
660   * @param headers the HTTP headers to include in the request, <tt>Accept</tt> must be supplied
661   * @return a Response object with response detail
662   */
663  public Response get(String path, Header[] headers) throws IOException {
664    return get(cluster, path, headers);
665  }
666
667  /**
668   * Returns the response body of the HTTPResponse, if any, as an array of bytes. If response body
669   * is not available or cannot be read, returns <tt>null</tt> Note: This will cause the entire
670   * response body to be buffered in memory. A malicious server may easily exhaust all the VM
671   * memory. It is strongly recommended, to use getResponseAsStream if the content length of the
672   * response is unknown or reasonably large.
673   * @param resp HttpResponse
674   * @return The response body, null if body is empty
675   * @throws IOException If an I/O (transport) problem occurs while obtaining the response body.
676   */
677  public static byte[] getResponseBody(HttpResponse resp) throws IOException {
678    if (resp.getEntity() == null) {
679      return null;
680    }
681    InputStream instream = resp.getEntity().getContent();
682    if (instream == null) {
683      return null;
684    }
685    try {
686      long contentLength = resp.getEntity().getContentLength();
687      if (contentLength > Integer.MAX_VALUE) {
688        // guard integer cast from overflow
689        throw new IOException("Content too large to be buffered: " + contentLength + " bytes");
690      }
691      if (contentLength > 0) {
692        byte[] content = new byte[(int) contentLength];
693        ByteStreams.readFully(instream, content);
694        return content;
695      } else {
696        return ByteStreams.toByteArray(instream);
697      }
698    } finally {
699      Closeables.closeQuietly(instream);
700    }
701  }
702
703  /**
704   * Send a GET request
705   * @param c       the cluster definition
706   * @param path    the path or URI
707   * @param headers the HTTP headers to include in the request
708   * @return a Response object with response detail
709   */
710  public Response get(Cluster c, String path, Header[] headers) throws IOException {
711    if (httpGet != null) {
712      httpGet.releaseConnection();
713    }
714    httpGet = new HttpGet(path);
715    HttpResponse resp = execute(c, httpGet, headers, path);
716    return new Response(resp.getStatusLine().getStatusCode(), resp.getAllHeaders(), resp,
717      resp.getEntity() == null ? null : resp.getEntity().getContent());
718  }
719
720  /**
721   * Send a PUT request
722   * @param path        the path or URI
723   * @param contentType the content MIME type
724   * @param content     the content bytes
725   * @return a Response object with response detail
726   */
727  public Response put(String path, String contentType, byte[] content) throws IOException {
728    return put(cluster, path, contentType, content);
729  }
730
731  /**
732   * Send a PUT request
733   * @param path        the path or URI
734   * @param contentType the content MIME type
735   * @param content     the content bytes
736   * @param extraHdr    extra Header to send
737   * @return a Response object with response detail
738   */
739  public Response put(String path, String contentType, byte[] content, Header extraHdr)
740    throws IOException {
741    return put(cluster, path, contentType, content, extraHdr);
742  }
743
744  /**
745   * Send a PUT request
746   * @param cluster     the cluster definition
747   * @param path        the path or URI
748   * @param contentType the content MIME type
749   * @param content     the content bytes
750   * @return a Response object with response detail
751   * @throws IOException for error
752   */
753  public Response put(Cluster cluster, String path, String contentType, byte[] content)
754    throws IOException {
755    Header[] headers = new Header[1];
756    headers[0] = new BasicHeader("Content-Type", contentType);
757    return put(cluster, path, headers, content);
758  }
759
760  /**
761   * Send a PUT request
762   * @param cluster     the cluster definition
763   * @param path        the path or URI
764   * @param contentType the content MIME type
765   * @param content     the content bytes
766   * @param extraHdr    additional Header to send
767   * @return a Response object with response detail
768   * @throws IOException for error
769   */
770  public Response put(Cluster cluster, String path, String contentType, byte[] content,
771    Header extraHdr) throws IOException {
772    int cnt = extraHdr == null ? 1 : 2;
773    Header[] headers = new Header[cnt];
774    headers[0] = new BasicHeader("Content-Type", contentType);
775    if (extraHdr != null) {
776      headers[1] = extraHdr;
777    }
778    return put(cluster, path, headers, content);
779  }
780
781  /**
782   * Send a PUT request
783   * @param path    the path or URI
784   * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be supplied
785   * @param content the content bytes
786   * @return a Response object with response detail
787   */
788  public Response put(String path, Header[] headers, byte[] content) throws IOException {
789    return put(cluster, path, headers, content);
790  }
791
792  /**
793   * Send a PUT request
794   * @param cluster the cluster definition
795   * @param path    the path or URI
796   * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be supplied
797   * @param content the content bytes
798   * @return a Response object with response detail
799   */
800  public Response put(Cluster cluster, String path, Header[] headers, byte[] content)
801    throws IOException {
802    HttpPut method = new HttpPut(path);
803    try {
804      method.setEntity(new ByteArrayEntity(content));
805      HttpResponse resp = execute(cluster, method, headers, path);
806      headers = resp.getAllHeaders();
807      content = getResponseBody(resp);
808      return new Response(resp.getStatusLine().getStatusCode(), headers, content);
809    } finally {
810      method.releaseConnection();
811    }
812  }
813
814  /**
815   * Send a POST request
816   * @param path        the path or URI
817   * @param contentType the content MIME type
818   * @param content     the content bytes
819   * @return a Response object with response detail
820   */
821  public Response post(String path, String contentType, byte[] content) throws IOException {
822    return post(cluster, path, contentType, content);
823  }
824
825  /**
826   * Send a POST request
827   * @param path        the path or URI
828   * @param contentType the content MIME type
829   * @param content     the content bytes
830   * @param extraHdr    additional Header to send
831   * @return a Response object with response detail
832   */
833  public Response post(String path, String contentType, byte[] content, Header extraHdr)
834    throws IOException {
835    return post(cluster, path, contentType, content, extraHdr);
836  }
837
838  /**
839   * Send a POST request
840   * @param cluster     the cluster definition
841   * @param path        the path or URI
842   * @param contentType the content MIME type
843   * @param content     the content bytes
844   * @return a Response object with response detail
845   * @throws IOException for error
846   */
847  public Response post(Cluster cluster, String path, String contentType, byte[] content)
848    throws IOException {
849    Header[] headers = new Header[1];
850    headers[0] = new BasicHeader("Content-Type", contentType);
851    return post(cluster, path, headers, content);
852  }
853
854  /**
855   * Send a POST request
856   * @param cluster     the cluster definition
857   * @param path        the path or URI
858   * @param contentType the content MIME type
859   * @param content     the content bytes
860   * @param extraHdr    additional Header to send
861   * @return a Response object with response detail
862   * @throws IOException for error
863   */
864  public Response post(Cluster cluster, String path, String contentType, byte[] content,
865    Header extraHdr) throws IOException {
866    int cnt = extraHdr == null ? 1 : 2;
867    Header[] headers = new Header[cnt];
868    headers[0] = new BasicHeader("Content-Type", contentType);
869    if (extraHdr != null) {
870      headers[1] = extraHdr;
871    }
872    return post(cluster, path, headers, content);
873  }
874
875  /**
876   * Send a POST request
877   * @param path    the path or URI
878   * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be supplied
879   * @param content the content bytes
880   * @return a Response object with response detail
881   */
882  public Response post(String path, Header[] headers, byte[] content) throws IOException {
883    return post(cluster, path, headers, content);
884  }
885
886  /**
887   * Send a POST request
888   * @param cluster the cluster definition
889   * @param path    the path or URI
890   * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be supplied
891   * @param content the content bytes
892   * @return a Response object with response detail
893   */
894  public Response post(Cluster cluster, String path, Header[] headers, byte[] content)
895    throws IOException {
896    HttpPost method = new HttpPost(path);
897    try {
898      method.setEntity(new ByteArrayEntity(content));
899      HttpResponse resp = execute(cluster, method, headers, path);
900      headers = resp.getAllHeaders();
901      content = getResponseBody(resp);
902      return new Response(resp.getStatusLine().getStatusCode(), headers, content);
903    } finally {
904      method.releaseConnection();
905    }
906  }
907
908  /**
909   * Send a DELETE request
910   * @param path the path or URI
911   * @return a Response object with response detail
912   */
913  public Response delete(String path) throws IOException {
914    return delete(cluster, path);
915  }
916
917  /**
918   * Send a DELETE request
919   * @param path     the path or URI
920   * @param extraHdr additional Header to send
921   * @return a Response object with response detail
922   */
923  public Response delete(String path, Header extraHdr) throws IOException {
924    return delete(cluster, path, extraHdr);
925  }
926
927  /**
928   * Send a DELETE request
929   * @param cluster the cluster definition
930   * @param path    the path or URI
931   * @return a Response object with response detail
932   * @throws IOException for error
933   */
934  public Response delete(Cluster cluster, String path) throws IOException {
935    HttpDelete method = new HttpDelete(path);
936    try {
937      HttpResponse resp = execute(cluster, method, null, path);
938      Header[] headers = resp.getAllHeaders();
939      byte[] content = getResponseBody(resp);
940      return new Response(resp.getStatusLine().getStatusCode(), headers, content);
941    } finally {
942      method.releaseConnection();
943    }
944  }
945
946  /**
947   * Send a DELETE request
948   * @param cluster the cluster definition
949   * @param path    the path or URI
950   * @return a Response object with response detail
951   * @throws IOException for error
952   */
953  public Response delete(Cluster cluster, String path, Header extraHdr) throws IOException {
954    HttpDelete method = new HttpDelete(path);
955    try {
956      Header[] headers = { extraHdr };
957      HttpResponse resp = execute(cluster, method, headers, path);
958      headers = resp.getAllHeaders();
959      byte[] content = getResponseBody(resp);
960      return new Response(resp.getStatusLine().getStatusCode(), headers, content);
961    } finally {
962      method.releaseConnection();
963    }
964  }
965
966  public static class ClientTrustStoreInitializationException extends RuntimeException {
967
968    public ClientTrustStoreInitializationException(String message, Throwable cause) {
969      super(message, cause);
970    }
971  }
972}