3232import java .io .InputStream ;
3333import java .io .InputStreamReader ;
3434import java .io .UnsupportedEncodingException ;
35+ import java .net .URI ;
3536import java .net .URL ;
3637import java .net .URLEncoder ;
38+ import java .net .http .HttpClient ;
39+ import java .net .http .HttpClient .Redirect ;
40+ import java .net .http .HttpRequest ;
41+ import java .net .http .HttpResponse ;
42+ import java .net .http .HttpResponse .BodyHandlers ;
3743import java .nio .charset .StandardCharsets ;
3844import java .security .SecureRandom ;
3945import java .util .Map ;
46+ import java .util .Optional ;
4047
4148import javax .net .ssl .HttpsURLConnection ;
4249
50+ import lombok .extern .log4j .Log4j2 ;
51+ import net .klesatschke .threema .api .exceptions .ClientError ;
52+ import net .klesatschke .threema .api .exceptions .ServerError ;
53+ import net .klesatschke .threema .api .exceptions .ThreemaError ;
4354import net .klesatschke .threema .api .results .CapabilityResult ;
4455import net .klesatschke .threema .api .results .EncryptResult ;
4556import net .klesatschke .threema .api .results .UploadResult ;
4657
4758/** Facilitates HTTPS communication with the Threema Message API. */
59+ @ Log4j2
4860public class APIConnector {
4961 private static final int BUFFER_SIZE = 16384 ;
5062
@@ -72,6 +84,7 @@ public InputStreamLength(InputStream inputStream, int length) {
7284 private final PublicKeyStore publicKeyStore ;
7385 private final String apiIdentity ;
7486 private final String secret ;
87+ private HttpClient httpClient ;
7588
7689 public APIConnector (String apiIdentity , String secret , PublicKeyStore publicKeyStore ) {
7790 this (apiIdentity , secret , "https://msgapi.threema.ch/" , publicKeyStore );
@@ -83,6 +96,7 @@ public APIConnector(
8396 this .secret = secret ;
8497 this .apiUrl = apiUrl ;
8598 this .publicKeyStore = publicKeyStore ;
99+ httpClient = HttpClient .newBuilder ().followRedirects (Redirect .NEVER ).build ();
86100 }
87101
88102 /**
@@ -129,17 +143,27 @@ public String sendE2EMessage(String to, byte[] nonce, byte[] box) throws IOExcep
129143 * @throws IOException if a communication or server error occurs
130144 */
131145 public String lookupPhone (String phoneNumber ) throws IOException {
146+ var getParams = makeRequestParams ();
147+ var phoneHash = CryptTool .hashPhoneNo (phoneNumber );
132148
133149 try {
134- var getParams = makeRequestParams ();
135-
136- var phoneHash = CryptTool .hashPhoneNo (phoneNumber );
137-
138150 return doGet (
139- new URL (this .apiUrl + "lookup/phone_hash/" + DataUtils .byteArrayToHexString (phoneHash )),
151+ URI .create (
152+ this .apiUrl + "lookup/phone_hash/" + DataUtils .byteArrayToHexString (phoneHash )),
140153 getParams );
141- } catch (FileNotFoundException e ) {
142- return null ;
154+ } catch (ThreemaError e ) {
155+ switch (e .getResponse ().statusCode ()) {
156+ case 400 :
157+ throw new ClientError (e .getResponse (), "the hash length is wrong" );
158+ case 401 :
159+ throw new ClientError (e .getResponse (), "API identity or secret are incorrect" );
160+ case 404 :
161+ throw new ClientError (e .getResponse (), "no matching ID could be found" );
162+ case 500 :
163+ throw new ServerError (e .getResponse (), "a temporary internal server error occurs" );
164+ default :
165+ throw e ;
166+ }
143167 }
144168 }
145169
@@ -159,7 +183,8 @@ public String lookupEmail(String email) throws IOException {
159183 var emailHash = CryptTool .hashEmail (email );
160184
161185 return doGet (
162- new URL (this .apiUrl + "lookup/email_hash/" + DataUtils .byteArrayToHexString (emailHash )),
186+ URI .create (
187+ this .apiUrl + "lookup/email_hash/" + DataUtils .byteArrayToHexString (emailHash )),
163188 getParams );
164189 } catch (FileNotFoundException e ) {
165190 return null ;
@@ -178,7 +203,7 @@ public byte[] lookupKey(String id) throws IOException {
178203 if (key == null ) {
179204 try {
180205 var getParams = makeRequestParams ();
181- var pubkeyHex = doGet (new URL (this .apiUrl + "pubkeys/" + id ), getParams );
206+ var pubkeyHex = doGet (URI . create (this .apiUrl + "pubkeys/" + id ), getParams );
182207 key = DataUtils .hexStringToByteArray (pubkeyHex );
183208
184209 if (key != null ) {
@@ -199,15 +224,15 @@ public byte[] lookupKey(String id) throws IOException {
199224 * @throws IOException
200225 */
201226 public CapabilityResult lookupKeyCapability (String threemaId ) throws IOException {
202- var res = doGet (new URL (this .apiUrl + "capabilities/" + threemaId ), makeRequestParams ());
227+ var res = doGet (URI . create (this .apiUrl + "capabilities/" + threemaId ), makeRequestParams ());
203228 if (res != null ) {
204229 return new CapabilityResult (threemaId , res .split ("," ));
205230 }
206231 return null ;
207232 }
208233
209234 public Integer lookupCredits () throws IOException {
210- var res = doGet (new URL (this .apiUrl + "credits" ), makeRequestParams ());
235+ var res = doGet (URI . create (this .apiUrl + "credits" ), makeRequestParams ());
211236 if (res != null ) {
212237 return Integer .valueOf (res );
213238 }
@@ -358,29 +383,28 @@ private Map<String, String> makeRequestParams() {
358383 return Map .of ("from" , apiIdentity , "secret" , secret );
359384 }
360385
361- private String doGet (URL url , Map <String , String > getParams ) throws IOException {
386+ private String doGet (URI uri , Map <String , String > getParams ) throws IOException {
387+ var uriWithParameters =
388+ Optional .ofNullable (getParams )
389+ .map (params -> URI .create (uri .toString () + "?" + makeUrlEncoded (params )))
390+ .orElse (uri );
362391
363- if (getParams != null ) {
364- var queryString = makeUrlEncoded (getParams );
365-
366- url = new URL (url .toString () + "?" + queryString );
367- }
368-
369- var connection = (HttpsURLConnection ) url .openConnection ();
370- connection .setDoOutput (false );
371- connection .setDoInput (true );
372- connection .setInstanceFollowRedirects (false );
373- connection .setRequestMethod ("GET" );
374- connection .setUseCaches (false );
392+ var httpRequest = HttpRequest .newBuilder ().uri (uriWithParameters ).GET ().build ();
393+ try {
394+ HttpResponse <String > response = httpClient .send (httpRequest , BodyHandlers .ofString ());
375395
376- var is = connection . getInputStream ();
377- var br = new BufferedReader ( new InputStreamReader ( is ));
378- var response = br . readLine ( );
379- br . close ();
396+ var statusCode = response . statusCode ();
397+ if ( 400 <= statusCode ) {
398+ throw new ThreemaError ( response );
399+ }
380400
381- connection . disconnect ();
401+ return response . body ();
382402
383- return response ;
403+ } catch (InterruptedException e ) {
404+ log .warn ("Interrupted" );
405+ Thread .currentThread ().interrupt ();
406+ }
407+ return null ;
384408 }
385409
386410 private String doPost (URL url , Map <String , String > postParams ) throws IOException {
0 commit comments