2525import java .io .InputStreamReader ;
2626import java .io .OutputStreamWriter ;
2727import java .net .MalformedURLException ;
28+ import java .net .URI ;
2829import java .net .URL ;
30+ import java .net .http .HttpClient ;
31+ import java .net .http .HttpClient .Redirect ;
32+ import java .net .http .HttpRequest ;
33+ import java .net .http .HttpResponse ;
2934import java .security .KeyManagementException ;
3035import java .security .KeyStore ;
3136import java .security .KeyStoreException ;
3540import java .security .cert .CertificateException ;
3641import java .security .cert .CertificateFactory ;
3742import java .security .cert .X509Certificate ;
43+ import java .time .Duration ;
3844import java .util .Optional ;
45+ import java .util .Properties ;
46+ import java .util .concurrent .CompletableFuture ;
3947import javax .net .ssl .HttpsURLConnection ;
4048import javax .net .ssl .KeyManagerFactory ;
4149import javax .net .ssl .SSLContext ;
4250import javax .net .ssl .SSLSession ;
43- import javax .net .ssl .SSLSocketFactory ;
4451import javax .net .ssl .TrustManagerFactory ;
4552
4653/**
4956 * standard certificates exported from Qlik Sense without needing to convert them to
5057 * Java KeyStore (*.jks) certificates.
5158 *
52- * @version 1.0
59+ * @version 1.1
5360 * @author Steven Jenkins De Haro
5461 */
5562public class TicketRequest {
5663
5764 private static final String XRFKEY = "1234567890123456" ; // Xrfkey to prevent CSRF attacks.
5865 private static final String PROTOCOL = "TLS" ;
59- private final String _hostname ;
60- private final String _vpPrefix ;
66+ private final String _apiUrl ;
6167 private final String _clientCertPath ; // Client certificate with private key.
6268 private final char [] _clientCertPassword ;
6369 private final String _rootCertPath ; // Required in this example because Qlik Sense certs are used.
@@ -74,8 +80,8 @@ public TicketRequest(String hostname, Optional<String> virtualProxyPrefix,
7480 String clientCertPath , char [] clientCertPassword ,
7581 String rootCertPath ) {
7682
77- _hostname = hostname ;
78- _vpPrefix = virtualProxyPrefix .isPresent () ? "/" + virtualProxyPrefix .get () : "" ;
83+ _apiUrl = String . format ( "https://%1$s:4243/qps%2$s/ticket?xrfkey=%3$s" ,
84+ hostname , virtualProxyPrefix .isPresent () ? "/" + virtualProxyPrefix .get () : "" , XRFKEY ) ;
7985 _clientCertPath = clientCertPath ;
8086 _clientCertPassword = clientCertPassword ;
8187 _rootCertPath = rootCertPath ;
@@ -85,15 +91,15 @@ public TicketRequest(String hostname, Optional<String> virtualProxyPrefix,
8591 * Configures the needed certificates to validate the identity of the HTTPS
8692 * server against a list of trusted certificates and to authenticate to the
8793 * HTTPS server using a private key.
88- * @return A layered socket factory for TLS/SSL connections.
94+ * @return An initialized secure socket context for TLS/SSL connections.
8995 * @throws KeyStoreException
9096 * @throws IOException
9197 * @throws CertificateException
9298 * @throws NoSuchAlgorithmException
9399 * @throws UnrecoverableKeyException
94100 * @throws KeyManagementException
95101 */
96- private SSLSocketFactory getSSLSocketFactory ()
102+ private SSLContext getSSLContext ()
97103 throws KeyStoreException , IOException , CertificateException ,
98104 NoSuchAlgorithmException , UnrecoverableKeyException ,
99105 KeyManagementException {
@@ -108,7 +114,7 @@ private SSLSocketFactory getSSLSocketFactory()
108114 tmf .init (trustStore );
109115 context .init (kmf .getKeyManagers (), tmf .getTrustManagers (), new SecureRandom ());
110116
111- return context . getSocketFactory () ;
117+ return context ;
112118 }
113119
114120 /**
@@ -162,21 +168,20 @@ public String getTicket(String userDirectory, String userId)
162168 CertificateException , NoSuchAlgorithmException ,
163169 UnrecoverableKeyException , KeyManagementException {
164170
165- var apiUrl = String .format ("https://%1$s:4243/qps%2$s/ticket?xrfkey=%3$s" , _hostname , _vpPrefix , XRFKEY );
166171 var jsonRequestBody = String .format ("{ 'UserId':'%1$s','UserDirectory':'%2$s','Attributes': [] }" ,
167172 userId , userDirectory );
168- var url = new URL (apiUrl );
173+ var url = new URL (_apiUrl );
169174 var connection = (HttpsURLConnection ) url .openConnection ();
170-
175+
171176 /*
172- * When target hostname is not listed in server's certificate SAN field,
173- * use this as a whitelist for exceptions to continue. For example,
174- * hostname.equals("xx.xx.xx.xx" or "localhost") ? true : false
175- * See https://support.qlik.com/articles/000078616 for more info.
177+ * When target hostname is not listed in server's certificate SAN field,
178+ * use this as a whitelist for exceptions to continue. For example,
179+ * hostname.equals("xx.xx.xx.xx" or "localhost") ? true : false
180+ * See https://support.qlik.com/articles/000078616 for more info.
176181 */
177182 HttpsURLConnection .setDefaultHostnameVerifier ((String hostname , SSLSession session ) -> true );
178-
179- connection .setSSLSocketFactory (getSSLSocketFactory ());
183+
184+ connection .setSSLSocketFactory (getSSLContext (). getSocketFactory ());
180185 connection .setDoOutput (true );
181186 connection .setDoInput (true );
182187 connection .setConnectTimeout (30000 );
@@ -192,15 +197,62 @@ public String getTicket(String userDirectory, String userId)
192197 }
193198
194199 var sb = new StringBuilder ();
195-
200+
196201 // Gets the response from the QPS BufferedReader.
197- try (BufferedReader in = new BufferedReader (new InputStreamReader (connection .getInputStream ()))) {
202+ try (BufferedReader in = new BufferedReader (new InputStreamReader (connection .getInputStream ()))) {
198203 String inputLine ;
199204 while ((inputLine = in .readLine ()) != null ) {
200205 sb .append (inputLine );
201206 }
202207 }
203-
208+
204209 return sb .toString ();
205- }
210+ }
211+
212+ /**
213+ * Requests a ticket asynchronously from the Qlik Sense Proxy Service that is valid for one minute.Note: This function uses a more modern API than the {@link #getTicket(String, String) getTicket} function.
214+ * @param userDirectory Directory associated with user.
215+ * @param userId Login name of user.
216+ * @return CompletableFuture with Ticket to claim within one minute.
217+ * @throws MalformedURLException
218+ * @throws IOException
219+ * @throws KeyStoreException
220+ * @throws CertificateException
221+ * @throws NoSuchAlgorithmException
222+ * @throws UnrecoverableKeyException
223+ * @throws KeyManagementException
224+ */
225+ public CompletableFuture <String > getTicketAsync (String userDirectory , String userId )
226+ throws MalformedURLException , IOException , KeyStoreException ,
227+ CertificateException , NoSuchAlgorithmException ,
228+ UnrecoverableKeyException , KeyManagementException {
229+
230+ var jsonRequestBody = String .format ("{ 'UserId':'%1$s','UserDirectory':'%2$s','Attributes': [] }" ,
231+ userId , userDirectory );
232+ final Properties props = System .getProperties ();
233+
234+ /*
235+ * Disables hostname validation when hostname is not listed in server's
236+ * certificate SAN field.
237+ */
238+ props .setProperty ("jdk.internal.httpclient.disableHostnameVerification" , Boolean .TRUE .toString ());
239+
240+ var client = HttpClient .newBuilder ()
241+ .connectTimeout (Duration .ofSeconds (30 ))
242+ .followRedirects (Redirect .NORMAL )
243+ .sslContext (getSSLContext ())
244+ .build ();
245+
246+ var request = HttpRequest .newBuilder ()
247+ .uri (URI .create (_apiUrl ))
248+ .timeout (Duration .ofSeconds (30 ))
249+ .header ("X-Qlik-xrfkey" , XRFKEY )
250+ .header ("Content-Type" , "application/json" )
251+ .header ("Accept" , "application/json" )
252+ .POST (HttpRequest .BodyPublishers .ofString (jsonRequestBody ))
253+ .build ();
254+
255+ return client .sendAsync (request , HttpResponse .BodyHandlers .ofString ())
256+ .thenApply (response -> response .body ());
257+ }
206258}
0 commit comments