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