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.ByteArrayInputStream;
022import java.io.File;
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.security.KeyManagementException;
030import java.security.KeyStore;
031import java.security.KeyStoreException;
032import java.security.NoSuchAlgorithmException;
033import java.security.cert.CertificateException;
034import java.util.Collections;
035import java.util.Map;
036import java.util.Optional;
037import java.util.concurrent.ConcurrentHashMap;
038import javax.net.ssl.SSLContext;
039import org.apache.hadoop.conf.Configuration;
040import org.apache.hadoop.hbase.HBaseConfiguration;
041import org.apache.hadoop.hbase.rest.Constants;
042import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
043import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
044import org.apache.hadoop.security.authentication.client.AuthenticationException;
045import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
046import org.apache.http.Header;
047import org.apache.http.HttpResponse;
048import org.apache.http.HttpStatus;
049import org.apache.http.client.HttpClient;
050import org.apache.http.client.config.RequestConfig;
051import org.apache.http.client.methods.HttpDelete;
052import org.apache.http.client.methods.HttpGet;
053import org.apache.http.client.methods.HttpHead;
054import org.apache.http.client.methods.HttpPost;
055import org.apache.http.client.methods.HttpPut;
056import org.apache.http.client.methods.HttpUriRequest;
057import org.apache.http.entity.InputStreamEntity;
058import org.apache.http.impl.client.HttpClientBuilder;
059import org.apache.http.impl.client.HttpClients;
060import org.apache.http.message.BasicHeader;
061import org.apache.http.ssl.SSLContexts;
062import org.apache.http.util.EntityUtils;
063import org.apache.yetus.audience.InterfaceAudience;
064import org.slf4j.Logger;
065import org.slf4j.LoggerFactory;
066
067import org.apache.hbase.thirdparty.com.google.common.io.ByteStreams;
068import org.apache.hbase.thirdparty.com.google.common.io.Closeables;
069
070/**
071 * A wrapper around HttpClient which provides some useful function and semantics for interacting
072 * with the REST gateway.
073 */
074@InterfaceAudience.Public
075public class Client {
076  public static final Header[] EMPTY_HEADER_ARRAY = new Header[0];
077
078  private static final Logger LOG = LoggerFactory.getLogger(Client.class);
079
080  private HttpClient httpClient;
081  private Cluster cluster;
082  private Configuration conf;
083  private boolean sslEnabled;
084  private HttpResponse resp;
085  private HttpGet httpGet = null;
086
087  private Map<String, String> extraHeaders;
088
089  private static final String AUTH_COOKIE = "hadoop.auth";
090  private static final String AUTH_COOKIE_EQ = AUTH_COOKIE + "=";
091  private static final String COOKIE = "Cookie";
092
093  /**
094   * Default Constructor
095   */
096  public Client() {
097    this(null);
098  }
099
100  private void initialize(Cluster cluster, Configuration conf, boolean sslEnabled,
101    Optional<KeyStore> trustStore) {
102    this.cluster = cluster;
103    this.conf = conf;
104    this.sslEnabled = sslEnabled;
105    extraHeaders = new ConcurrentHashMap<>();
106    String clspath = System.getProperty("java.class.path");
107    LOG.debug("classpath " + clspath);
108    HttpClientBuilder httpClientBuilder = HttpClients.custom();
109
110    int connTimeout = this.conf.getInt(Constants.REST_CLIENT_CONN_TIMEOUT,
111      Constants.DEFAULT_REST_CLIENT_CONN_TIMEOUT);
112    int socketTimeout = this.conf.getInt(Constants.REST_CLIENT_SOCKET_TIMEOUT,
113      Constants.DEFAULT_REST_CLIENT_SOCKET_TIMEOUT);
114    RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(connTimeout)
115      .setSocketTimeout(socketTimeout).setNormalizeUri(false) // URIs should not be normalized, see
116                                                              // HBASE-26903
117      .build();
118    httpClientBuilder.setDefaultRequestConfig(requestConfig);
119
120    // Since HBASE-25267 we don't use the deprecated DefaultHttpClient anymore.
121    // The new http client would decompress the gzip content automatically.
122    // In order to keep the original behaviour of this public class, we disable
123    // automatic content compression.
124    httpClientBuilder.disableContentCompression();
125
126    if (sslEnabled && trustStore.isPresent()) {
127      try {
128        SSLContext sslcontext =
129          SSLContexts.custom().loadTrustMaterial(trustStore.get(), null).build();
130        httpClientBuilder.setSSLContext(sslcontext);
131      } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
132        throw new ClientTrustStoreInitializationException("Error while processing truststore", e);
133      }
134    }
135
136    this.httpClient = httpClientBuilder.build();
137  }
138
139  /**
140   * Constructor
141   * @param cluster the cluster definition
142   */
143  public Client(Cluster cluster) {
144    this(cluster, false);
145  }
146
147  /**
148   * Constructor
149   * @param cluster    the cluster definition
150   * @param sslEnabled enable SSL or not
151   */
152  public Client(Cluster cluster, boolean sslEnabled) {
153    initialize(cluster, HBaseConfiguration.create(), sslEnabled, Optional.empty());
154  }
155
156  /**
157   * Constructor
158   * @param cluster    the cluster definition
159   * @param conf       Configuration
160   * @param sslEnabled enable SSL or not
161   */
162  public Client(Cluster cluster, Configuration conf, boolean sslEnabled) {
163    initialize(cluster, conf, sslEnabled, Optional.empty());
164  }
165
166  /**
167   * Constructor, allowing to define custom trust store (only for SSL connections)
168   * @param cluster            the cluster definition
169   * @param trustStorePath     custom trust store to use for SSL connections
170   * @param trustStorePassword password to use for custom trust store
171   * @param trustStoreType     type of custom trust store
172   * @throws ClientTrustStoreInitializationException if the trust store file can not be loaded
173   */
174  public Client(Cluster cluster, String trustStorePath, Optional<String> trustStorePassword,
175    Optional<String> trustStoreType) {
176    this(cluster, HBaseConfiguration.create(), trustStorePath, trustStorePassword, trustStoreType);
177  }
178
179  /**
180   * Constructor, allowing to define custom trust store (only for SSL connections)
181   * @param cluster            the cluster definition
182   * @param conf               Configuration
183   * @param trustStorePath     custom trust store to use for SSL connections
184   * @param trustStorePassword password to use for custom trust store
185   * @param trustStoreType     type of custom trust store
186   * @throws ClientTrustStoreInitializationException if the trust store file can not be loaded
187   */
188  public Client(Cluster cluster, Configuration conf, String trustStorePath,
189    Optional<String> trustStorePassword, Optional<String> trustStoreType) {
190
191    char[] password = trustStorePassword.map(String::toCharArray).orElse(null);
192    String type = trustStoreType.orElse(KeyStore.getDefaultType());
193
194    KeyStore trustStore;
195    try {
196      trustStore = KeyStore.getInstance(type);
197    } catch (KeyStoreException e) {
198      throw new ClientTrustStoreInitializationException("Invalid trust store type: " + type, e);
199    }
200    try (InputStream inputStream =
201      new BufferedInputStream(Files.newInputStream(new File(trustStorePath).toPath()))) {
202      trustStore.load(inputStream, password);
203    } catch (CertificateException | NoSuchAlgorithmException | IOException e) {
204      throw new ClientTrustStoreInitializationException("Trust store load error: " + trustStorePath,
205        e);
206    }
207
208    initialize(cluster, conf, true, Optional.of(trustStore));
209  }
210
211  /**
212   * Shut down the client. Close any open persistent connections.
213   */
214  public void shutdown() {
215  }
216
217  /** Returns the wrapped HttpClient */
218  public HttpClient getHttpClient() {
219    return httpClient;
220  }
221
222  /**
223   * Add extra headers. These extra headers will be applied to all http methods before they are
224   * removed. If any header is not used any more, client needs to remove it explicitly.
225   */
226  public void addExtraHeader(final String name, final String value) {
227    extraHeaders.put(name, value);
228  }
229
230  /**
231   * Get an extra header value.
232   */
233  public String getExtraHeader(final String name) {
234    return extraHeaders.get(name);
235  }
236
237  /**
238   * Get all extra headers (read-only).
239   */
240  public Map<String, String> getExtraHeaders() {
241    return Collections.unmodifiableMap(extraHeaders);
242  }
243
244  /**
245   * Remove an extra header.
246   */
247  public void removeExtraHeader(final String name) {
248    extraHeaders.remove(name);
249  }
250
251  /**
252   * Execute a transaction method given only the path. Will select at random one of the members of
253   * the supplied cluster definition and iterate through the list until a transaction can be
254   * successfully completed. The definition of success here is a complete HTTP transaction,
255   * irrespective of result code.
256   * @param cluster the cluster definition
257   * @param method  the transaction method
258   * @param headers HTTP header values to send
259   * @param path    the properly urlencoded path
260   * @return the HTTP response code
261   */
262  public HttpResponse executePathOnly(Cluster cluster, HttpUriRequest method, Header[] headers,
263    String path) throws IOException {
264    IOException lastException;
265    if (cluster.nodes.size() < 1) {
266      throw new IOException("Cluster is empty");
267    }
268    int start = (int) Math.round((cluster.nodes.size() - 1) * Math.random());
269    int i = start;
270    do {
271      cluster.lastHost = cluster.nodes.get(i);
272      try {
273        StringBuilder sb = new StringBuilder();
274        if (sslEnabled) {
275          sb.append("https://");
276        } else {
277          sb.append("http://");
278        }
279        sb.append(cluster.lastHost);
280        sb.append(path);
281        URI uri = new URI(sb.toString());
282        if (method instanceof HttpPut) {
283          HttpPut put = new HttpPut(uri);
284          put.setEntity(((HttpPut) method).getEntity());
285          put.setHeaders(method.getAllHeaders());
286          method = put;
287        } else if (method instanceof HttpGet) {
288          method = new HttpGet(uri);
289        } else if (method instanceof HttpHead) {
290          method = new HttpHead(uri);
291        } else if (method instanceof HttpDelete) {
292          method = new HttpDelete(uri);
293        } else if (method instanceof HttpPost) {
294          HttpPost post = new HttpPost(uri);
295          post.setEntity(((HttpPost) method).getEntity());
296          post.setHeaders(method.getAllHeaders());
297          method = post;
298        }
299        return executeURI(method, headers, uri.toString());
300      } catch (IOException e) {
301        lastException = e;
302      } catch (URISyntaxException use) {
303        lastException = new IOException(use);
304      }
305    } while (++i != start && i < cluster.nodes.size());
306    throw lastException;
307  }
308
309  /**
310   * Execute a transaction method given a complete URI.
311   * @param method  the transaction method
312   * @param headers HTTP header values to send
313   * @param uri     a properly urlencoded URI
314   * @return the HTTP response code
315   */
316  public HttpResponse executeURI(HttpUriRequest method, Header[] headers, String uri)
317    throws IOException {
318    // method.setURI(new URI(uri, true));
319    for (Map.Entry<String, String> e : extraHeaders.entrySet()) {
320      method.addHeader(e.getKey(), e.getValue());
321    }
322    if (headers != null) {
323      for (Header header : headers) {
324        method.addHeader(header);
325      }
326    }
327    long startTime = EnvironmentEdgeManager.currentTime();
328    if (resp != null) EntityUtils.consumeQuietly(resp.getEntity());
329    resp = httpClient.execute(method);
330    if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
331      // Authentication error
332      LOG.debug("Performing negotiation with the server.");
333      negotiate(method, uri);
334      resp = httpClient.execute(method);
335    }
336
337    long endTime = EnvironmentEdgeManager.currentTime();
338    if (LOG.isTraceEnabled()) {
339      LOG.trace(method.getMethod() + " " + uri + " " + resp.getStatusLine().getStatusCode() + " "
340        + resp.getStatusLine().getReasonPhrase() + " in " + (endTime - startTime) + " ms");
341    }
342    return resp;
343  }
344
345  /**
346   * Execute a transaction method. Will call either <tt>executePathOnly</tt> or <tt>executeURI</tt>
347   * depending on whether a path only is supplied in 'path', or if a complete URI is passed instead,
348   * respectively.
349   * @param cluster the cluster definition
350   * @param method  the HTTP method
351   * @param headers HTTP header values to send
352   * @param path    the properly urlencoded path or URI
353   * @return the HTTP response code
354   */
355  public HttpResponse execute(Cluster cluster, HttpUriRequest method, Header[] headers, String path)
356    throws IOException {
357    if (path.startsWith("/")) {
358      return executePathOnly(cluster, method, headers, path);
359    }
360    return executeURI(method, headers, path);
361  }
362
363  /**
364   * Initiate client side Kerberos negotiation with the server.
365   * @param method method to inject the authentication token into.
366   * @param uri    the String to parse as a URL.
367   * @throws IOException if unknown protocol is found.
368   */
369  private void negotiate(HttpUriRequest method, String uri) throws IOException {
370    try {
371      AuthenticatedURL.Token token = new AuthenticatedURL.Token();
372      KerberosAuthenticator authenticator = new KerberosAuthenticator();
373      authenticator.authenticate(new URL(uri), token);
374      // Inject the obtained negotiated token in the method cookie
375      injectToken(method, token);
376    } catch (AuthenticationException e) {
377      LOG.error("Failed to negotiate with the server.", e);
378      throw new IOException(e);
379    }
380  }
381
382  /**
383   * Helper method that injects an authentication token to send with the method.
384   * @param method method to inject the authentication token into.
385   * @param token  authentication token to inject.
386   */
387  private void injectToken(HttpUriRequest method, AuthenticatedURL.Token token) {
388    String t = token.toString();
389    if (t != null) {
390      if (!t.startsWith("\"")) {
391        t = "\"" + t + "\"";
392      }
393      method.addHeader(COOKIE, AUTH_COOKIE_EQ + t);
394    }
395  }
396
397  /** Returns the cluster definition */
398  public Cluster getCluster() {
399    return cluster;
400  }
401
402  /**
403   * @param cluster the cluster definition
404   */
405  public void setCluster(Cluster cluster) {
406    this.cluster = cluster;
407  }
408
409  /**
410   * Send a HEAD request
411   * @param path the path or URI
412   * @return a Response object with response detail
413   */
414  public Response head(String path) throws IOException {
415    return head(cluster, path, null);
416  }
417
418  /**
419   * Send a HEAD request
420   * @param cluster the cluster definition
421   * @param path    the path or URI
422   * @param headers the HTTP headers to include in the request
423   * @return a Response object with response detail
424   */
425  public Response head(Cluster cluster, String path, Header[] headers) throws IOException {
426    HttpHead method = new HttpHead(path);
427    try {
428      HttpResponse resp = execute(cluster, method, null, path);
429      return new Response(resp.getStatusLine().getStatusCode(), resp.getAllHeaders(), null);
430    } finally {
431      method.releaseConnection();
432    }
433  }
434
435  /**
436   * Send a GET request
437   * @param path the path or URI
438   * @return a Response object with response detail
439   */
440  public Response get(String path) throws IOException {
441    return get(cluster, path);
442  }
443
444  /**
445   * Send a GET request
446   * @param cluster the cluster definition
447   * @param path    the path or URI
448   * @return a Response object with response detail
449   */
450  public Response get(Cluster cluster, String path) throws IOException {
451    return get(cluster, path, EMPTY_HEADER_ARRAY);
452  }
453
454  /**
455   * Send a GET request
456   * @param path   the path or URI
457   * @param accept Accept header value
458   * @return a Response object with response detail
459   */
460  public Response get(String path, String accept) throws IOException {
461    return get(cluster, path, accept);
462  }
463
464  /**
465   * Send a GET request
466   * @param cluster the cluster definition
467   * @param path    the path or URI
468   * @param accept  Accept header value
469   * @return a Response object with response detail
470   */
471  public Response get(Cluster cluster, String path, String accept) throws IOException {
472    Header[] headers = new Header[1];
473    headers[0] = new BasicHeader("Accept", accept);
474    return get(cluster, path, headers);
475  }
476
477  /**
478   * Send a GET request
479   * @param path    the path or URI
480   * @param headers the HTTP headers to include in the request, <tt>Accept</tt> must be supplied
481   * @return a Response object with response detail
482   */
483  public Response get(String path, Header[] headers) throws IOException {
484    return get(cluster, path, headers);
485  }
486
487  /**
488   * Returns the response body of the HTTPResponse, if any, as an array of bytes. If response body
489   * is not available or cannot be read, returns <tt>null</tt> Note: This will cause the entire
490   * response body to be buffered in memory. A malicious server may easily exhaust all the VM
491   * memory. It is strongly recommended, to use getResponseAsStream if the content length of the
492   * response is unknown or reasonably large.
493   * @param resp HttpResponse
494   * @return The response body, null if body is empty
495   * @throws IOException If an I/O (transport) problem occurs while obtaining the response body.
496   */
497  public static byte[] getResponseBody(HttpResponse resp) throws IOException {
498    if (resp.getEntity() == null) {
499      return null;
500    }
501    InputStream instream = resp.getEntity().getContent();
502    if (instream == null) {
503      return null;
504    }
505    try {
506      long contentLength = resp.getEntity().getContentLength();
507      if (contentLength > Integer.MAX_VALUE) {
508        // guard integer cast from overflow
509        throw new IOException("Content too large to be buffered: " + contentLength + " bytes");
510      }
511      if (contentLength > 0) {
512        byte[] content = new byte[(int) contentLength];
513        ByteStreams.readFully(instream, content);
514        return content;
515      } else {
516        return ByteStreams.toByteArray(instream);
517      }
518    } finally {
519      Closeables.closeQuietly(instream);
520    }
521  }
522
523  /**
524   * Send a GET request
525   * @param c       the cluster definition
526   * @param path    the path or URI
527   * @param headers the HTTP headers to include in the request
528   * @return a Response object with response detail
529   */
530  public Response get(Cluster c, String path, Header[] headers) throws IOException {
531    if (httpGet != null) {
532      httpGet.releaseConnection();
533    }
534    httpGet = new HttpGet(path);
535    HttpResponse resp = execute(c, httpGet, headers, path);
536    return new Response(resp.getStatusLine().getStatusCode(), resp.getAllHeaders(), resp,
537      resp.getEntity() == null ? null : resp.getEntity().getContent());
538  }
539
540  /**
541   * Send a PUT request
542   * @param path        the path or URI
543   * @param contentType the content MIME type
544   * @param content     the content bytes
545   * @return a Response object with response detail
546   */
547  public Response put(String path, String contentType, byte[] content) throws IOException {
548    return put(cluster, path, contentType, content);
549  }
550
551  /**
552   * Send a PUT request
553   * @param path        the path or URI
554   * @param contentType the content MIME type
555   * @param content     the content bytes
556   * @param extraHdr    extra Header to send
557   * @return a Response object with response detail
558   */
559  public Response put(String path, String contentType, byte[] content, Header extraHdr)
560    throws IOException {
561    return put(cluster, path, contentType, content, extraHdr);
562  }
563
564  /**
565   * Send a PUT request
566   * @param cluster     the cluster definition
567   * @param path        the path or URI
568   * @param contentType the content MIME type
569   * @param content     the content bytes
570   * @return a Response object with response detail
571   * @throws IOException for error
572   */
573  public Response put(Cluster cluster, String path, String contentType, byte[] content)
574    throws IOException {
575    Header[] headers = new Header[1];
576    headers[0] = new BasicHeader("Content-Type", contentType);
577    return put(cluster, path, headers, content);
578  }
579
580  /**
581   * Send a PUT request
582   * @param cluster     the cluster definition
583   * @param path        the path or URI
584   * @param contentType the content MIME type
585   * @param content     the content bytes
586   * @param extraHdr    additional Header to send
587   * @return a Response object with response detail
588   * @throws IOException for error
589   */
590  public Response put(Cluster cluster, String path, String contentType, byte[] content,
591    Header extraHdr) throws IOException {
592    int cnt = extraHdr == null ? 1 : 2;
593    Header[] headers = new Header[cnt];
594    headers[0] = new BasicHeader("Content-Type", contentType);
595    if (extraHdr != null) {
596      headers[1] = extraHdr;
597    }
598    return put(cluster, path, headers, content);
599  }
600
601  /**
602   * Send a PUT request
603   * @param path    the path or URI
604   * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be supplied
605   * @param content the content bytes
606   * @return a Response object with response detail
607   */
608  public Response put(String path, Header[] headers, byte[] content) throws IOException {
609    return put(cluster, path, headers, content);
610  }
611
612  /**
613   * Send a PUT request
614   * @param cluster the cluster definition
615   * @param path    the path or URI
616   * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be supplied
617   * @param content the content bytes
618   * @return a Response object with response detail
619   */
620  public Response put(Cluster cluster, String path, Header[] headers, byte[] content)
621    throws IOException {
622    HttpPut method = new HttpPut(path);
623    try {
624      method.setEntity(new InputStreamEntity(new ByteArrayInputStream(content), content.length));
625      HttpResponse resp = execute(cluster, method, headers, path);
626      headers = resp.getAllHeaders();
627      content = getResponseBody(resp);
628      return new Response(resp.getStatusLine().getStatusCode(), headers, content);
629    } finally {
630      method.releaseConnection();
631    }
632  }
633
634  /**
635   * Send a POST request
636   * @param path        the path or URI
637   * @param contentType the content MIME type
638   * @param content     the content bytes
639   * @return a Response object with response detail
640   */
641  public Response post(String path, String contentType, byte[] content) throws IOException {
642    return post(cluster, path, contentType, content);
643  }
644
645  /**
646   * Send a POST request
647   * @param path        the path or URI
648   * @param contentType the content MIME type
649   * @param content     the content bytes
650   * @param extraHdr    additional Header to send
651   * @return a Response object with response detail
652   */
653  public Response post(String path, String contentType, byte[] content, Header extraHdr)
654    throws IOException {
655    return post(cluster, path, contentType, content, extraHdr);
656  }
657
658  /**
659   * Send a POST request
660   * @param cluster     the cluster definition
661   * @param path        the path or URI
662   * @param contentType the content MIME type
663   * @param content     the content bytes
664   * @return a Response object with response detail
665   * @throws IOException for error
666   */
667  public Response post(Cluster cluster, String path, String contentType, byte[] content)
668    throws IOException {
669    Header[] headers = new Header[1];
670    headers[0] = new BasicHeader("Content-Type", contentType);
671    return post(cluster, path, headers, content);
672  }
673
674  /**
675   * Send a POST request
676   * @param cluster     the cluster definition
677   * @param path        the path or URI
678   * @param contentType the content MIME type
679   * @param content     the content bytes
680   * @param extraHdr    additional Header to send
681   * @return a Response object with response detail
682   * @throws IOException for error
683   */
684  public Response post(Cluster cluster, String path, String contentType, byte[] content,
685    Header extraHdr) throws IOException {
686    int cnt = extraHdr == null ? 1 : 2;
687    Header[] headers = new Header[cnt];
688    headers[0] = new BasicHeader("Content-Type", contentType);
689    if (extraHdr != null) {
690      headers[1] = extraHdr;
691    }
692    return post(cluster, path, headers, content);
693  }
694
695  /**
696   * Send a POST request
697   * @param path    the path or URI
698   * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be supplied
699   * @param content the content bytes
700   * @return a Response object with response detail
701   */
702  public Response post(String path, Header[] headers, byte[] content) throws IOException {
703    return post(cluster, path, headers, content);
704  }
705
706  /**
707   * Send a POST request
708   * @param cluster the cluster definition
709   * @param path    the path or URI
710   * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be supplied
711   * @param content the content bytes
712   * @return a Response object with response detail
713   */
714  public Response post(Cluster cluster, String path, Header[] headers, byte[] content)
715    throws IOException {
716    HttpPost method = new HttpPost(path);
717    try {
718      method.setEntity(new InputStreamEntity(new ByteArrayInputStream(content), content.length));
719      HttpResponse resp = execute(cluster, method, headers, path);
720      headers = resp.getAllHeaders();
721      content = getResponseBody(resp);
722      return new Response(resp.getStatusLine().getStatusCode(), headers, content);
723    } finally {
724      method.releaseConnection();
725    }
726  }
727
728  /**
729   * Send a DELETE request
730   * @param path the path or URI
731   * @return a Response object with response detail
732   */
733  public Response delete(String path) throws IOException {
734    return delete(cluster, path);
735  }
736
737  /**
738   * Send a DELETE request
739   * @param path     the path or URI
740   * @param extraHdr additional Header to send
741   * @return a Response object with response detail
742   */
743  public Response delete(String path, Header extraHdr) throws IOException {
744    return delete(cluster, path, extraHdr);
745  }
746
747  /**
748   * Send a DELETE request
749   * @param cluster the cluster definition
750   * @param path    the path or URI
751   * @return a Response object with response detail
752   * @throws IOException for error
753   */
754  public Response delete(Cluster cluster, String path) throws IOException {
755    HttpDelete method = new HttpDelete(path);
756    try {
757      HttpResponse resp = execute(cluster, method, null, path);
758      Header[] headers = resp.getAllHeaders();
759      byte[] content = getResponseBody(resp);
760      return new Response(resp.getStatusLine().getStatusCode(), headers, content);
761    } finally {
762      method.releaseConnection();
763    }
764  }
765
766  /**
767   * Send a DELETE request
768   * @param cluster the cluster definition
769   * @param path    the path or URI
770   * @return a Response object with response detail
771   * @throws IOException for error
772   */
773  public Response delete(Cluster cluster, String path, Header extraHdr) throws IOException {
774    HttpDelete method = new HttpDelete(path);
775    try {
776      Header[] headers = { extraHdr };
777      HttpResponse resp = execute(cluster, method, headers, path);
778      headers = resp.getAllHeaders();
779      byte[] content = getResponseBody(resp);
780      return new Response(resp.getStatusLine().getStatusCode(), headers, content);
781    } finally {
782      method.releaseConnection();
783    }
784  }
785
786  public static class ClientTrustStoreInitializationException extends RuntimeException {
787
788    public ClientTrustStoreInitializationException(String message, Throwable cause) {
789      super(message, cause);
790    }
791  }
792}