1818package org .apache .hadoop .hbase .rest .client ;
1919
2020import java .io .BufferedInputStream ;
21- import java .io .ByteArrayInputStream ;
2221import java .io .File ;
22+ import java .io .FileOutputStream ;
2323import java .io .IOException ;
2424import java .io .InputStream ;
2525import java .net .URI ;
2626import java .net .URISyntaxException ;
2727import java .net .URL ;
2828import java .nio .file .Files ;
29+ import java .nio .file .Path ;
30+ import java .security .GeneralSecurityException ;
2931import java .security .KeyManagementException ;
3032import java .security .KeyStore ;
3133import java .security .KeyStoreException ;
4446import org .apache .hadoop .security .authentication .client .AuthenticatedURL ;
4547import org .apache .hadoop .security .authentication .client .AuthenticationException ;
4648import org .apache .hadoop .security .authentication .client .KerberosAuthenticator ;
49+ import org .apache .hadoop .security .ssl .SSLFactory ;
50+ import org .apache .hadoop .security .ssl .SSLFactory .Mode ;
4751import org .apache .http .Header ;
52+ import org .apache .http .HttpHeaders ;
4853import org .apache .http .HttpResponse ;
4954import org .apache .http .HttpStatus ;
55+ import org .apache .http .auth .AuthScope ;
56+ import org .apache .http .auth .UsernamePasswordCredentials ;
5057import org .apache .http .client .HttpClient ;
5158import org .apache .http .client .config .RequestConfig ;
5259import org .apache .http .client .methods .HttpDelete ;
5562import org .apache .http .client .methods .HttpPost ;
5663import org .apache .http .client .methods .HttpPut ;
5764import org .apache .http .client .methods .HttpUriRequest ;
58- import org .apache .http .entity .InputStreamEntity ;
65+ import org .apache .http .client .protocol .HttpClientContext ;
66+ import org .apache .http .entity .ByteArrayEntity ;
67+ import org .apache .http .impl .client .BasicCredentialsProvider ;
5968import org .apache .http .impl .client .HttpClientBuilder ;
6069import org .apache .http .impl .client .HttpClients ;
70+ import org .apache .http .impl .cookie .BasicClientCookie ;
6171import org .apache .http .message .BasicHeader ;
6272import org .apache .http .ssl .SSLContexts ;
6373import org .apache .http .util .EntityUtils ;
@@ -86,8 +96,11 @@ public class Client {
8696 private boolean sslEnabled ;
8797 private HttpResponse resp ;
8898 private HttpGet httpGet = null ;
89-
99+ private HttpClientContext stickyContext = null ;
100+ private BasicCredentialsProvider provider ;
101+ private Optional <KeyStore > trustStore ;
90102 private Map <String , String > extraHeaders ;
103+ private KerberosAuthenticator authenticator ;
91104
92105 private static final String AUTH_COOKIE = "hadoop.auth" ;
93106 private static final String AUTH_COOKIE_EQ = AUTH_COOKIE + "=" ;
@@ -100,11 +113,13 @@ public Client() {
100113 this (null );
101114 }
102115
103- private void initialize (Cluster cluster , Configuration conf , boolean sslEnabled ,
104- Optional <KeyStore > trustStore ) {
116+ private void initialize (Cluster cluster , Configuration conf , boolean sslEnabled , boolean sticky ,
117+ Optional <KeyStore > trustStore , Optional <String > userName , Optional <String > password ,
118+ Optional <String > bearerToken ) {
105119 this .cluster = cluster ;
106120 this .conf = conf ;
107121 this .sslEnabled = sslEnabled ;
122+ this .trustStore = trustStore ;
108123 extraHeaders = new ConcurrentHashMap <>();
109124 String clspath = System .getProperty ("java.class.path" );
110125 LOG .debug ("classpath " + clspath );
@@ -136,38 +151,77 @@ private void initialize(Cluster cluster, Configuration conf, boolean sslEnabled,
136151 }
137152 }
138153
154+ if (userName .isPresent () && password .isPresent ()) {
155+ // We want to stick to the old very limited authentication and session handling when sticky is
156+ // not set
157+ // to preserve backwards compatibility
158+ if (!sticky ) {
159+ throw new IllegalArgumentException ("BASIC auth is only implemented when sticky is set" );
160+ }
161+ provider = new BasicCredentialsProvider ();
162+ // AuthScope.ANY is required for pre-emptive auth. We only ever use a single auth method
163+ // anyway.
164+ AuthScope anyAuthScope = AuthScope .ANY ;
165+ this .provider .setCredentials (anyAuthScope ,
166+ new UsernamePasswordCredentials (userName .get (), password .get ()));
167+ }
168+
169+ if (bearerToken .isPresent ()) {
170+ // We want to stick to the old very limited authentication and session handling when sticky is
171+ // not set
172+ // to preserve backwards compatibility
173+ if (!sticky ) {
174+ throw new IllegalArgumentException ("BEARER auth is only implemented when sticky is set" );
175+ }
176+ // We could also put the header into the context or connection, but that would have the same
177+ // effect.
178+ extraHeaders .put (HttpHeaders .AUTHORIZATION , "Bearer " + bearerToken .get ());
179+ }
180+
139181 this .httpClient = httpClientBuilder .build ();
182+ setSticky (sticky );
140183 }
141184
142185 /**
143- * Constructor
186+ * Constructor This constructor will create an object using the old faulty load balancing logic.
187+ * When specifying multiple servers in the cluster object, it is highly recommended to call
188+ * setSticky() on the created client, or use one of the preferred constructors instead.
144189 * @param cluster the cluster definition
145190 */
146191 public Client (Cluster cluster ) {
147192 this (cluster , false );
148193 }
149194
150195 /**
151- * Constructor
196+ * Constructor This constructor will create an object using the old faulty load balancing logic.
197+ * When specifying multiple servers in the cluster object, it is highly recommended to call
198+ * setSticky() on the created client, or use one of the preferred constructors instead.
152199 * @param cluster the cluster definition
153200 * @param sslEnabled enable SSL or not
154201 */
155202 public Client (Cluster cluster , boolean sslEnabled ) {
156- initialize (cluster , HBaseConfiguration .create (), sslEnabled , Optional .empty ());
203+ initialize (cluster , HBaseConfiguration .create (), sslEnabled , false , Optional .empty (),
204+ Optional .empty (), Optional .empty (), Optional .empty ());
157205 }
158206
159207 /**
160- * Constructor
208+ * Constructor This constructor will create an object using the old faulty load balancing logic.
209+ * When specifying multiple servers in the cluster object, it is highly recommended to call
210+ * setSticky() on the created client, or use one of the preferred constructors instead.
161211 * @param cluster the cluster definition
162212 * @param conf Configuration
163213 * @param sslEnabled enable SSL or not
164214 */
165215 public Client (Cluster cluster , Configuration conf , boolean sslEnabled ) {
166- initialize (cluster , conf , sslEnabled , Optional .empty ());
216+ initialize (cluster , conf , sslEnabled , false , Optional .empty (), Optional .empty (),
217+ Optional .empty (), Optional .empty ());
167218 }
168219
169220 /**
170- * Constructor, allowing to define custom trust store (only for SSL connections)
221+ * Constructor, allowing to define custom trust store (only for SSL connections) This constructor
222+ * will create an object using the old faulty load balancing logic. When specifying multiple
223+ * servers in the cluster object, it is highly recommended to call setSticky() on the created
224+ * client, or use one of the preferred constructors instead.
171225 * @param cluster the cluster definition
172226 * @param trustStorePath custom trust store to use for SSL connections
173227 * @param trustStorePassword password to use for custom trust store
@@ -176,22 +230,56 @@ public Client(Cluster cluster, Configuration conf, boolean sslEnabled) {
176230 */
177231 public Client (Cluster cluster , String trustStorePath , Optional <String > trustStorePassword ,
178232 Optional <String > trustStoreType ) {
179- this (cluster , HBaseConfiguration .create (), trustStorePath , trustStorePassword , trustStoreType );
233+ this (cluster , HBaseConfiguration .create (), true , trustStorePath , trustStorePassword ,
234+ trustStoreType );
180235 }
181236
182237 /**
183- * Constructor, allowing to define custom trust store (only for SSL connections)
238+ * Constructor that accepts an optional trustStore and authentication information for either BASIC
239+ * or BEARER authentication in sticky mode, which does not use the old faulty load balancing
240+ * logic, and enables correct session handling. If neither userName/password, nor the bearer token
241+ * is specified, the client falls back to SPNEGO auth. The loadTrustsore static method can be used
242+ * to load a local trustStore file. This is the preferred constructor to use.
243+ * @param cluster the cluster definition
244+ * @param conf HBase/Hadoop configuration
245+ * @param sslEnabled use HTTPS
246+ * @param trustStore the optional trustStore object
247+ * @param userName for BASIC auth
248+ * @param password for BASIC auth
249+ * @param bearerToken for BEAERER auth
250+ */
251+ public Client (Cluster cluster , Configuration conf , boolean sslEnabled ,
252+ Optional <KeyStore > trustStore , Optional <String > userName , Optional <String > password ,
253+ Optional <String > bearerToken ) {
254+ initialize (cluster , conf , sslEnabled , true , trustStore , userName , password , bearerToken );
255+ }
256+
257+ /**
258+ * Constructor, allowing to define custom trust store (only for SSL connections) This constructor
259+ * also enables sticky mode. This is a preferred constructor when not using BASIC or JWT
260+ * authentication. Clients created by this will use the old faulty load balancing logic.
184261 * @param cluster the cluster definition
185- * @param conf Configuration
262+ * @param conf HBase/Hadoop Configuration
186263 * @param trustStorePath custom trust store to use for SSL connections
187264 * @param trustStorePassword password to use for custom trust store
188265 * @param trustStoreType type of custom trust store
189266 * @throws ClientTrustStoreInitializationException if the trust store file can not be loaded
190267 */
191- public Client (Cluster cluster , Configuration conf , String trustStorePath ,
268+ public Client (Cluster cluster , Configuration conf , boolean sslEnabled , String trustStorePath ,
192269 Optional <String > trustStorePassword , Optional <String > trustStoreType ) {
270+ KeyStore trustStore = loadTruststore (trustStorePath , trustStorePassword , trustStoreType );
271+ initialize (cluster , conf , sslEnabled , false , Optional .of (trustStore ), Optional .empty (),
272+ Optional .empty (), Optional .empty ());
273+ }
274+
275+ /**
276+ * Loads a trustStore from the local fileSystem. Can be used to load the trustStore for the
277+ * preferred constructor.
278+ */
279+ public static KeyStore loadTruststore (String trustStorePath , Optional <String > trustStorePassword ,
280+ Optional <String > trustStoreType ) {
193281
194- char [] password = trustStorePassword .map (String ::toCharArray ).orElse (null );
282+ char [] truststorePassword = trustStorePassword .map (String ::toCharArray ).orElse (null );
195283 String type = trustStoreType .orElse (KeyStore .getDefaultType ());
196284
197285 KeyStore trustStore ;
@@ -202,13 +290,12 @@ public Client(Cluster cluster, Configuration conf, String trustStorePath,
202290 }
203291 try (InputStream inputStream =
204292 new BufferedInputStream (Files .newInputStream (new File (trustStorePath ).toPath ()))) {
205- trustStore .load (inputStream , password );
293+ trustStore .load (inputStream , truststorePassword );
206294 } catch (CertificateException | NoSuchAlgorithmException | IOException e ) {
207295 throw new ClientTrustStoreInitializationException ("Trust store load error: " + trustStorePath ,
208296 e );
209297 }
210-
211- initialize (cluster , conf , true , Optional .of (trustStore ));
298+ return trustStore ;
212299 }
213300
214301 /**
@@ -337,12 +424,24 @@ public HttpResponse executeURI(HttpUriRequest method, Header[] headers, String u
337424 }
338425 long startTime = EnvironmentEdgeManager .currentTime ();
339426 if (resp != null ) EntityUtils .consumeQuietly (resp .getEntity ());
340- resp = httpClient .execute (method );
427+ if (stickyContext != null ) {
428+ resp = httpClient .execute (method , stickyContext );
429+ } else {
430+ resp = httpClient .execute (method );
431+ }
341432 if (resp .getStatusLine ().getStatusCode () == HttpStatus .SC_UNAUTHORIZED ) {
342433 // Authentication error
343434 LOG .debug ("Performing negotiation with the server." );
344- negotiate (method , uri );
345- resp = httpClient .execute (method );
435+ try {
436+ negotiate (method , uri );
437+ } catch (GeneralSecurityException e ) {
438+ throw new IOException (e );
439+ }
440+ if (stickyContext != null ) {
441+ resp = httpClient .execute (method , stickyContext );
442+ } else {
443+ resp = httpClient .execute (method );
444+ }
346445 }
347446
348447 long endTime = EnvironmentEdgeManager .currentTime ();
@@ -377,19 +476,58 @@ public HttpResponse execute(Cluster cluster, HttpUriRequest method, Header[] hea
377476 * @param uri the String to parse as a URL.
378477 * @throws IOException if unknown protocol is found.
379478 */
380- private void negotiate (HttpUriRequest method , String uri ) throws IOException {
479+ private void negotiate (HttpUriRequest method , String uri )
480+ throws IOException , GeneralSecurityException {
381481 try {
382482 AuthenticatedURL .Token token = new AuthenticatedURL .Token ();
383- KerberosAuthenticator authenticator = new KerberosAuthenticator ();
384- authenticator .authenticate (new URL (uri ), token );
385- // Inject the obtained negotiated token in the method cookie
386- injectToken (method , token );
483+ if (authenticator == null ) {
484+ authenticator = new KerberosAuthenticator ();
485+ if (trustStore .isPresent ()) {
486+ // The authenticator does not use Apache HttpClient, so we need to
487+ // configure it separately to use the specified trustStore
488+ Configuration sslConf = setupTrustStoreForHadoop (trustStore .get ());
489+ SSLFactory sslFactory = new SSLFactory (Mode .CLIENT , sslConf );
490+ sslFactory .init ();
491+ authenticator .setConnectionConfigurator (sslFactory );
492+ }
493+ }
494+ URL url = new URL (uri );
495+ authenticator .authenticate (url , token );
496+ if (sticky ) {
497+ BasicClientCookie authCookie = new BasicClientCookie ("hadoop.auth" , token .toString ());
498+ // Hadoop eats the domain even if set by server
499+ authCookie .setDomain (url .getHost ());
500+ stickyContext .getCookieStore ().addCookie (authCookie );
501+ } else {
502+ // session cookie is NOT set for backwards compatibility for non-sticky mode
503+ // Inject the obtained negotiated token in the method cookie
504+ // This is only done for this single request, the next one will trigger a new SPENGO
505+ // handshake
506+ injectToken (method , token );
507+ }
387508 } catch (AuthenticationException e ) {
388509 LOG .error ("Failed to negotiate with the server." , e );
389510 throw new IOException (e );
390511 }
391512 }
392513
514+ private Configuration setupTrustStoreForHadoop (KeyStore trustStore )
515+ throws IOException , KeyStoreException , NoSuchAlgorithmException , CertificateException {
516+ Path tmpDirPath = Files .createTempDirectory ("hbase_rest_client_truststore" );
517+ File trustStoreFile = tmpDirPath .resolve ("truststore.jks" ).toFile ();
518+ // Shouldn't be needed with the secure temp dir, but let's generate a password anyway
519+ String password = Double .toString (Math .random ());
520+ try (FileOutputStream fos = new FileOutputStream (trustStoreFile )) {
521+ trustStore .store (fos , password .toCharArray ());
522+ }
523+
524+ Configuration sslConf = new Configuration ();
525+ // Type is the Java default, we use the same JVM to read this back
526+ sslConf .set ("ssl.client.keystore.location" , trustStoreFile .getAbsolutePath ());
527+ sslConf .set ("ssl.client.keystore.password" , password );
528+ return sslConf ;
529+ }
530+
393531 /**
394532 * Helper method that injects an authentication token to send with the method.
395533 * @param method method to inject the authentication token into.
@@ -431,11 +569,21 @@ public boolean isSticky() {
431569 * The default behaviour is load balancing by sending each request to a random host. This DOES NOT
432570 * work with scans, which have state on the REST servers. Set sticky to true before attempting
433571 * Scan related operations if more than one host is defined in the cluster. Nodes must not be
434- * added or removed from the Cluster object while sticky is true.
572+ * added or removed from the Cluster object while sticky is true. Setting the sticky flag also
573+ * enables session handling, which eliminates the need to re-authenticate each request, and lets
574+ * the client handle any other cookies (like the sticky cookie set by load balancers) correctly.
435575 * @param sticky whether subsequent requests will use the same host
436576 */
437577 public void setSticky (boolean sticky ) {
438578 lastNodeId = null ;
579+ if (sticky ) {
580+ stickyContext = new HttpClientContext ();
581+ if (provider != null ) {
582+ stickyContext .setCredentialsProvider (provider );
583+ }
584+ } else {
585+ stickyContext = null ;
586+ }
439587 this .sticky = sticky ;
440588 }
441589
@@ -654,7 +802,7 @@ public Response put(Cluster cluster, String path, Header[] headers, byte[] conte
654802 throws IOException {
655803 HttpPut method = new HttpPut (path );
656804 try {
657- method .setEntity (new InputStreamEntity ( new ByteArrayInputStream ( content ), content . length ));
805+ method .setEntity (new ByteArrayEntity ( content ));
658806 HttpResponse resp = execute (cluster , method , headers , path );
659807 headers = resp .getAllHeaders ();
660808 content = getResponseBody (resp );
@@ -748,7 +896,7 @@ public Response post(Cluster cluster, String path, Header[] headers, byte[] cont
748896 throws IOException {
749897 HttpPost method = new HttpPost (path );
750898 try {
751- method .setEntity (new InputStreamEntity ( new ByteArrayInputStream ( content ), content . length ));
899+ method .setEntity (new ByteArrayEntity ( content ));
752900 HttpResponse resp = execute (cluster , method , headers , path );
753901 headers = resp .getAllHeaders ();
754902 content = getResponseBody (resp );
0 commit comments