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