001package com.box.sdk;
002
003import static java.lang.String.format;
004import static java.lang.String.join;
005import static java.util.Collections.singletonList;
006import static okhttp3.ConnectionSpec.MODERN_TLS;
007
008import com.eclipsesource.json.Json;
009import com.eclipsesource.json.JsonObject;
010import com.eclipsesource.json.JsonValue;
011import java.io.IOException;
012import java.net.MalformedURLException;
013import java.net.Proxy;
014import java.net.URI;
015import java.net.URL;
016import java.security.KeyManagementException;
017import java.security.NoSuchAlgorithmException;
018import java.time.Duration;
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024import java.util.concurrent.locks.ReadWriteLock;
025import java.util.concurrent.locks.ReentrantReadWriteLock;
026import java.util.regex.Pattern;
027import javax.net.ssl.HostnameVerifier;
028import javax.net.ssl.SSLContext;
029import javax.net.ssl.TrustManager;
030import javax.net.ssl.X509TrustManager;
031import okhttp3.Authenticator;
032import okhttp3.Call;
033import okhttp3.Credentials;
034import okhttp3.Headers;
035import okhttp3.OkHttpClient;
036import okhttp3.Request;
037import okhttp3.Response;
038
039/**
040 * @deprecated {@link BoxAPIConnection} class, and the entire the com.box.sdk package is deprecated,
041 *     it is recommended to use {@link com.box.sdkgen} package. Instead of this class use {@link
042 *     com.box.sdkgen.client.BoxClient}
043 *     <p>Represents an authenticated connection to the Box API.
044 *     <p>This class handles storing authentication information, automatic token refresh, and
045 *     rate-limiting. It can also be used to configure the Box API endpoint URL in order to hit a
046 *     different version of the API. Multiple instances of BoxAPIConnection may be created to
047 *     support multi-user login.
048 */
049@Deprecated
050public class BoxAPIConnection {
051
052  /**
053   * Used as a marker to setup connection to use default HostnameVerifier Example:
054   *
055   * <pre>{@code
056   * BoxApiConnection api = new BoxApiConnection(...);
057   * HostnameVerifier myHostnameVerifier = ...
058   * api.configureSslCertificatesValidation(DEFAULT_TRUST_MANAGER, myHostnameVerifier);
059   * }</pre>
060   */
061  public static final X509TrustManager DEFAULT_TRUST_MANAGER = null;
062  /**
063   * Used as a marker to setup connection to use default HostnameVerifier Example:
064   *
065   * <pre>{@code
066   * BoxApiConnection api = new BoxApiConnection(...);
067   * X509TrustManager myTrustManager = ...
068   * api.configureSslCertificatesValidation(myTrustManager, DEFAULT_HOSTNAME_VERIFIER);
069   * }</pre>
070   */
071  public static final HostnameVerifier DEFAULT_HOSTNAME_VERIFIER = null;
072
073  /**
074   * The default maximum number of times an API request will be retried after an error response is
075   * received.
076   */
077  public static final int DEFAULT_MAX_RETRIES = 5;
078  /** Default authorization URL */
079  protected static final String DEFAULT_BASE_AUTHORIZATION_URL = "https://account.box.com/api/";
080
081  static final String AS_USER_HEADER = "As-User";
082
083  private static final String API_VERSION = "2.0";
084  private static final String OAUTH_SUFFIX = "oauth2/authorize";
085  private static final String TOKEN_URL_SUFFIX = "oauth2/token";
086  private static final String REVOKE_URL_SUFFIX = "oauth2/revoke";
087  private static final String DEFAULT_BASE_URL = "https://api.box.com/";
088  private static final String DEFAULT_BASE_UPLOAD_URL = "https://upload.box.com/api/";
089  private static final String DEFAULT_BASE_APP_URL = "https://app.box.com";
090
091  private static final String BOX_NOTIFICATIONS_HEADER = "Box-Notifications";
092
093  private static final String JAVA_VERSION = System.getProperty("java.version");
094  private static final String SDK_VERSION = "5.9.0";
095
096  /**
097   * The amount of buffer time, in milliseconds, to use when determining if an access token should
098   * be refreshed. For example, if REFRESH_EPSILON = 60000 and the access token expires in less than
099   * one minute, it will be refreshed.
100   */
101  private static final long REFRESH_EPSILON = 60000;
102
103  private final String clientID;
104  private final String clientSecret;
105  private final ReadWriteLock refreshLock;
106  private X509TrustManager trustManager;
107  private HostnameVerifier hostnameVerifier;
108
109  // These volatile fields are used when determining if the access token needs to be refreshed.
110  // Since they are used in
111  // the double-checked lock in getAccessToken(), they must be atomic.
112  private volatile long lastRefresh;
113  private volatile long expires;
114
115  private Proxy proxy;
116  private String proxyUsername;
117  private String proxyPassword;
118
119  private String userAgent;
120  private String accessToken;
121  private String refreshToken;
122  private String tokenURL;
123  private String revokeURL;
124  private String baseURL;
125  private String baseUploadURL;
126  private String baseAppURL;
127  private String baseAuthorizationURL;
128  private boolean autoRefresh;
129  private int maxRetryAttempts;
130  private int connectTimeout;
131  private int readTimeout;
132  private boolean useZstdCompression;
133  private final List<BoxAPIConnectionListener> listeners;
134  private RequestInterceptor interceptor;
135  private final Map<String, String> customHeaders;
136
137  private OkHttpClient httpClient;
138  private OkHttpClient noRedirectsHttpClient;
139  private Authenticator authenticator;
140
141  /**
142   * @deprecated {@link BoxAPIConnection} class, and the entire com.box.sdk package is deprecated,
143   *     it is recommended to use {@link com.box.sdkgen} package. Instead of this class use {@link
144   *     com.box.sdkgen.client.BoxClient}
145   *     <p>Constructs a new BoxAPIConnection that authenticates with a developer or access token.
146   * @param accessToken a developer or access token to use for authenticating with the API.
147   */
148  @Deprecated
149  public BoxAPIConnection(String accessToken) {
150    this(null, null, accessToken, null);
151  }
152
153  /**
154   * @deprecated {@link BoxAPIConnection} class, and the entire com.box.sdk package is deprecated,
155   *     it is recommended to use {@link com.box.sdkgen} package. Instead of this class use {@link
156   *     com.box.sdkgen.client.BoxClient}
157   *     <p>Constructs a new BoxAPIConnection with an access token that can be refreshed.
158   * @param clientID the client ID to use when refreshing the access token.
159   * @param clientSecret the client secret to use when refreshing the access token.
160   * @param accessToken an initial access token to use for authenticating with the API.
161   * @param refreshToken an initial refresh token to use when refreshing the access token.
162   */
163  @Deprecated
164  public BoxAPIConnection(
165      String clientID, String clientSecret, String accessToken, String refreshToken) {
166    this.clientID = clientID;
167    this.clientSecret = clientSecret;
168    this.accessToken = accessToken;
169    this.refreshToken = refreshToken;
170    this.baseURL = fixBaseUrl(DEFAULT_BASE_URL);
171    this.baseUploadURL = fixBaseUrl(DEFAULT_BASE_UPLOAD_URL);
172    this.baseAppURL = DEFAULT_BASE_APP_URL;
173    this.baseAuthorizationURL = DEFAULT_BASE_AUTHORIZATION_URL;
174    this.autoRefresh = true;
175    this.maxRetryAttempts = BoxGlobalSettings.getMaxRetryAttempts();
176    this.connectTimeout = BoxGlobalSettings.getConnectTimeout();
177    this.readTimeout = BoxGlobalSettings.getReadTimeout();
178    this.useZstdCompression = BoxGlobalSettings.getUseZstdCompression();
179    this.refreshLock = new ReentrantReadWriteLock();
180    this.userAgent = "Box Java SDK v" + SDK_VERSION + " (Java " + JAVA_VERSION + ")";
181    this.listeners = new ArrayList<>();
182    this.customHeaders = new HashMap<>();
183    buildHttpClients();
184  }
185
186  /**
187   * @deprecated {@link BoxAPIConnection} class, and the entire com.box.sdk package is deprecated,
188   *     it is recommended to use {@link com.box.sdkgen} package. Instead of this class use {@link
189   *     com.box.sdkgen.client.BoxClient}
190   *     <p>Constructs a new BoxAPIConnection with an auth code that was obtained from the first
191   *     half of OAuth.
192   * @param clientID the client ID to use when exchanging the auth code for an access token.
193   * @param clientSecret the client secret to use when exchanging the auth code for an access token.
194   * @param authCode an auth code obtained from the first half of the OAuth process.
195   */
196  @Deprecated
197  public BoxAPIConnection(String clientID, String clientSecret, String authCode) {
198    this(clientID, clientSecret, null, null);
199    this.authenticate(authCode);
200  }
201
202  /**
203   * @deprecated {@link BoxAPIConnection} class, and the entire com.box.sdk package is deprecated,
204   *     it is recommended to use {@link com.box.sdkgen} package. Instead of this class use {@link
205   *     com.box.sdkgen.client.BoxClient}
206   *     <p>Constructs a new BoxAPIConnection.
207   * @param clientID the client ID to use when exchanging the auth code for an access token.
208   * @param clientSecret the client secret to use when exchanging the auth code for an access token.
209   */
210  @Deprecated
211  public BoxAPIConnection(String clientID, String clientSecret) {
212    this(clientID, clientSecret, null, null);
213  }
214
215  /**
216   * @deprecated {@link BoxAPIConnection} class, and the entire com.box.sdk package is deprecated,
217   *     it is recommended to use {@link com.box.sdkgen} package. Instead of this class use {@link
218   *     com.box.sdkgen.client.BoxClient}
219   *     <p>Constructs a new BoxAPIConnection leveraging BoxConfig.
220   * @param boxConfig BoxConfig file, which should have clientId and clientSecret
221   */
222  @Deprecated
223  public BoxAPIConnection(BoxConfig boxConfig) {
224    this(boxConfig.getClientId(), boxConfig.getClientSecret(), null, null);
225  }
226
227  private void buildHttpClients() {
228    OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();
229    if (trustManager != null) {
230      try {
231        SSLContext sslContext = SSLContext.getInstance("SSL");
232        sslContext.init(null, new TrustManager[] {trustManager}, new java.security.SecureRandom());
233        httpClientBuilder.sslSocketFactory(sslContext.getSocketFactory(), trustManager);
234      } catch (NoSuchAlgorithmException | KeyManagementException e) {
235        throw new RuntimeException(e);
236      }
237    }
238
239    OkHttpClient.Builder builder =
240        httpClientBuilder
241            .followSslRedirects(true)
242            .followRedirects(true)
243            .connectTimeout(Duration.ofMillis(connectTimeout))
244            .readTimeout(Duration.ofMillis(readTimeout))
245            .connectionSpecs(singletonList(MODERN_TLS));
246
247    if (hostnameVerifier != null) {
248      httpClientBuilder.hostnameVerifier(hostnameVerifier);
249    }
250
251    if (proxy != null) {
252      builder.proxy(proxy);
253      if (proxyUsername != null && proxyPassword != null) {
254        builder.proxyAuthenticator(
255            (route, response) -> {
256              String credential = Credentials.basic(proxyUsername, proxyPassword);
257              return response
258                  .request()
259                  .newBuilder()
260                  .header("Proxy-Authorization", credential)
261                  .build();
262            });
263      }
264      if (this.authenticator != null) {
265        builder.proxyAuthenticator(authenticator);
266      }
267    }
268    builder = modifyHttpClientBuilder(builder);
269    if (this.useZstdCompression) {
270      builder.addNetworkInterceptor(new ZstdInterceptor());
271    }
272
273    this.httpClient = builder.build();
274    this.noRedirectsHttpClient =
275        new OkHttpClient.Builder(httpClient)
276            .followSslRedirects(false)
277            .followRedirects(false)
278            .build();
279  }
280
281  /**
282   * Can be used to modify OkHttp.Builder used to create connection. This method is called after all
283   * modifications were done, thus allowing others to create their own connections and further
284   * customize builder.
285   *
286   * @param httpClientBuilder Builder that will be used to create http connection.
287   * @return Modified builder.
288   */
289  protected OkHttpClient.Builder modifyHttpClientBuilder(OkHttpClient.Builder httpClientBuilder) {
290    return httpClientBuilder;
291  }
292
293  /**
294   * Sets a proxy authenticator that will be used when proxy requires authentication. If you use
295   * {@link BoxAPIConnection#setProxyBasicAuthentication(String, String)} it adds an authenticator
296   * that performs Basic authorization. By calling this method you can override this behaviour. You
297   * do not need to call {@link BoxAPIConnection#setProxyBasicAuthentication(String, String)} in
298   * order to set custom authenticator.
299   *
300   * @param authenticator Custom authenticator that will be called when proxy asks for
301   *     authorization.
302   */
303  public void setProxyAuthenticator(Authenticator authenticator) {
304    this.authenticator = authenticator;
305    buildHttpClients();
306  }
307
308  /**
309   * Restores a BoxAPIConnection from a saved state.
310   *
311   * @param clientID the client ID to use with the connection.
312   * @param clientSecret the client secret to use with the connection.
313   * @param state the saved state that was created with {@link #save}.
314   * @return a restored API connection.
315   * @see #save
316   */
317  public static BoxAPIConnection restore(String clientID, String clientSecret, String state) {
318    BoxAPIConnection api = new BoxAPIConnection(clientID, clientSecret);
319    api.restore(state);
320    return api;
321  }
322
323  /**
324   * Returns the default authorization URL which is used to perform the authorization_code based
325   * OAuth2 flow. If custom Authorization URL is needed use instance method {@link
326   * BoxAPIConnection#getAuthorizationURL}
327   *
328   * @param clientID the client ID to use with the connection.
329   * @param redirectUri the URL to which Box redirects the browser when authentication completes.
330   * @param state the text string that you choose. Box sends the same string to your redirect URL
331   *     when authentication is complete.
332   * @param scopes this optional parameter identifies the Box scopes available to the application
333   *     once it's authenticated.
334   * @return the authorization URL
335   */
336  public static URL getAuthorizationURL(
337      String clientID, URI redirectUri, String state, List<String> scopes) {
338    return createFullAuthorizationUrl(
339        DEFAULT_BASE_AUTHORIZATION_URL, clientID, redirectUri, state, scopes);
340  }
341
342  private static URL createFullAuthorizationUrl(
343      String authorizationUrl,
344      String clientID,
345      URI redirectUri,
346      String state,
347      List<String> scopes) {
348    URLTemplate template = new URLTemplate(authorizationUrl + OAUTH_SUFFIX);
349    QueryStringBuilder queryBuilder =
350        new QueryStringBuilder()
351            .appendParam("client_id", clientID)
352            .appendParam("response_type", "code")
353            .appendParam("redirect_uri", redirectUri.toString())
354            .appendParam("state", state);
355
356    if (scopes != null && !scopes.isEmpty()) {
357      queryBuilder.appendParam("scope", join(" ", scopes));
358    }
359
360    return template.buildWithQuery("", queryBuilder.toString());
361  }
362
363  /**
364   * Authenticates the API connection by obtaining access and refresh tokens using the auth code
365   * that was obtained from the first half of OAuth.
366   *
367   * @param authCode the auth code obtained from the first half of the OAuth process.
368   */
369  public void authenticate(String authCode) {
370    URL url;
371    try {
372      url = new URL(this.getTokenURL());
373    } catch (MalformedURLException e) {
374      assert false : "An invalid token URL indicates a bug in the SDK.";
375      throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e);
376    }
377
378    String urlParameters =
379        format(
380            "grant_type=authorization_code&code=%s&client_id=%s&client_secret=%s",
381            authCode, this.clientID, this.clientSecret);
382
383    BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
384    request.shouldAuthenticate(false);
385    request.setBody(urlParameters);
386
387    // authentication uses form url encoded but response is JSON
388    try (BoxJSONResponse response = (BoxJSONResponse) request.send()) {
389      String json = response.getJSON();
390
391      JsonObject jsonObject = Json.parse(json).asObject();
392      this.accessToken = jsonObject.get("access_token").asString();
393      this.refreshToken = jsonObject.get("refresh_token").asString();
394      this.lastRefresh = System.currentTimeMillis();
395      this.expires = jsonObject.get("expires_in").asLong() * 1000;
396    }
397  }
398
399  /**
400   * Gets the client ID.
401   *
402   * @return the client ID.
403   */
404  public String getClientID() {
405    return this.clientID;
406  }
407
408  /**
409   * Gets the client secret.
410   *
411   * @return the client secret.
412   */
413  public String getClientSecret() {
414    return this.clientSecret;
415  }
416
417  /**
418   * Gets the amount of time for which this connection's access token is valid.
419   *
420   * @return the amount of time in milliseconds.
421   */
422  public long getExpires() {
423    return this.expires;
424  }
425
426  /**
427   * Sets the amount of time for which this connection's access token is valid before it must be
428   * refreshed.
429   *
430   * @param milliseconds the number of milliseconds for which the access token is valid.
431   */
432  public void setExpires(long milliseconds) {
433    this.expires = milliseconds;
434  }
435
436  /**
437   * Gets the token URL that's used to request access tokens. The default value is
438   * "https://api.box.com/oauth2/token". The URL is created from {@link BoxAPIConnection#baseURL}
439   * and {@link BoxAPIConnection#TOKEN_URL_SUFFIX}.
440   *
441   * @return the token URL.
442   */
443  public String getTokenURL() {
444    if (this.tokenURL != null) {
445      return this.tokenURL;
446    } else {
447      return this.baseURL + TOKEN_URL_SUFFIX;
448    }
449  }
450
451  /**
452   * Returns the URL used for token revocation. The URL is created from {@link
453   * BoxAPIConnection#baseURL} and {@link BoxAPIConnection#REVOKE_URL_SUFFIX}.
454   *
455   * @return The url used for token revocation.
456   */
457  public String getRevokeURL() {
458    if (this.revokeURL != null) {
459      return this.revokeURL;
460    } else {
461      return this.baseURL + REVOKE_URL_SUFFIX;
462    }
463  }
464
465  /**
466   * Gets the base URL that's used when sending requests to the Box API. The URL is created from
467   * {@link BoxAPIConnection#baseURL} and {@link BoxAPIConnection#API_VERSION}. The default value is
468   * "https://api.box.com/2.0/".
469   *
470   * @return the base URL.
471   */
472  public String getBaseURL() {
473    return this.baseURL + API_VERSION + "/";
474  }
475
476  /**
477   * Sets the base URL to be used when sending requests to the Box API. For example, the default
478   * base URL is "https://api.box.com/". This method changes how {@link
479   * BoxAPIConnection#getRevokeURL()} and {@link BoxAPIConnection#getTokenURL()} are constructed.
480   *
481   * @param baseURL a base URL
482   */
483  public void setBaseURL(String baseURL) {
484    this.baseURL = fixBaseUrl(baseURL);
485  }
486
487  /**
488   * Gets the base upload URL that's used when performing file uploads to Box. The URL is created
489   * from {@link BoxAPIConnection#baseUploadURL} and {@link BoxAPIConnection#API_VERSION}.
490   *
491   * @return the base upload URL.
492   */
493  public String getBaseUploadURL() {
494    return this.baseUploadURL + API_VERSION + "/";
495  }
496
497  /**
498   * Sets the base upload URL to be used when performing file uploads to Box.
499   *
500   * @param baseUploadURL a base upload URL.
501   */
502  public void setBaseUploadURL(String baseUploadURL) {
503    this.baseUploadURL = fixBaseUrl(baseUploadURL);
504  }
505
506  /**
507   * Returns the authorization URL which is used to perform the authorization_code based OAuth2
508   * flow. The URL is created from {@link BoxAPIConnection#baseAuthorizationURL} and {@link
509   * BoxAPIConnection#OAUTH_SUFFIX}.
510   *
511   * @param redirectUri the URL to which Box redirects the browser when authentication completes.
512   * @param state the text string that you choose. Box sends the same string to your redirect URL
513   *     when authentication is complete.
514   * @param scopes this optional parameter identifies the Box scopes available to the application
515   *     once it's authenticated.
516   * @return the authorization URL
517   */
518  public URL getAuthorizationURL(URI redirectUri, String state, List<String> scopes) {
519    return createFullAuthorizationUrl(
520        this.baseAuthorizationURL, this.clientID, redirectUri, state, scopes);
521  }
522
523  /**
524   * Sets authorization base URL which is used to perform the authorization_code based OAuth2 flow.
525   *
526   * @param baseAuthorizationURL Authorization URL. Default value is https://account.box.com/api/.
527   */
528  public void setBaseAuthorizationURL(String baseAuthorizationURL) {
529    this.baseAuthorizationURL = fixBaseUrl(baseAuthorizationURL);
530  }
531
532  /**
533   * Gets the user agent that's used when sending requests to the Box API.
534   *
535   * @return the user agent.
536   */
537  public String getUserAgent() {
538    return this.userAgent;
539  }
540
541  /**
542   * Sets the user agent to be used when sending requests to the Box API.
543   *
544   * @param userAgent the user agent.
545   */
546  public void setUserAgent(String userAgent) {
547    this.userAgent = userAgent;
548  }
549
550  /**
551   * Gets the base App url. Used for e.g. file requests.
552   *
553   * @return the base App Url.
554   */
555  public String getBaseAppUrl() {
556    return this.baseAppURL;
557  }
558
559  /**
560   * Sets the base App url. Used for e.g. file requests.
561   *
562   * @param baseAppURL a base App Url.
563   */
564  public void setBaseAppUrl(String baseAppURL) {
565    this.baseAppURL = baseAppURL;
566  }
567
568  /**
569   * Gets an access token that can be used to authenticate an API request. This method will
570   * automatically refresh the access token if it has expired since the last call to <code>
571   * getAccessToken()</code>.
572   *
573   * @return a valid access token that can be used to authenticate an API request.
574   */
575  public String getAccessToken() {
576    if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) {
577      this.refreshLock.writeLock().lock();
578      try {
579        if (this.needsRefresh()) {
580          this.refresh();
581        }
582      } finally {
583        this.refreshLock.writeLock().unlock();
584      }
585    }
586
587    this.refreshLock.readLock().lock();
588    try {
589      return this.accessToken;
590    } finally {
591      this.refreshLock.readLock().unlock();
592    }
593  }
594
595  /**
596   * Sets the access token to use when authenticating API requests.
597   *
598   * @param accessToken a valid access token to use when authenticating API requests.
599   */
600  public void setAccessToken(String accessToken) {
601    this.accessToken = accessToken;
602  }
603
604  /**
605   * Gets the refresh lock to be used when refreshing an access token.
606   *
607   * @return the refresh lock.
608   */
609  protected ReadWriteLock getRefreshLock() {
610    return this.refreshLock;
611  }
612
613  /**
614   * Gets a refresh token that can be used to refresh an access token.
615   *
616   * @return a valid refresh token.
617   */
618  public String getRefreshToken() {
619    return this.refreshToken;
620  }
621
622  /**
623   * Sets the refresh token to use when refreshing an access token.
624   *
625   * @param refreshToken a valid refresh token.
626   */
627  public void setRefreshToken(String refreshToken) {
628    this.refreshToken = refreshToken;
629  }
630
631  /**
632   * Gets the last time that the access token was refreshed.
633   *
634   * @return the last refresh time in milliseconds.
635   */
636  public long getLastRefresh() {
637    return this.lastRefresh;
638  }
639
640  /**
641   * Sets the last time that the access token was refreshed.
642   *
643   * <p>This value is used when determining if an access token needs to be auto-refreshed. If the
644   * amount of time since the last refresh exceeds the access token's expiration time, then the
645   * access token will be refreshed.
646   *
647   * @param lastRefresh the new last refresh time in milliseconds.
648   */
649  public void setLastRefresh(long lastRefresh) {
650    this.lastRefresh = lastRefresh;
651  }
652
653  /**
654   * Gets whether or not automatic refreshing of this connection's access token is enabled. Defaults
655   * to true.
656   *
657   * @return true if auto token refresh is enabled; otherwise false.
658   */
659  public boolean getAutoRefresh() {
660    return this.autoRefresh;
661  }
662
663  /**
664   * Enables or disables automatic refreshing of this connection's access token. Defaults to true.
665   *
666   * @param autoRefresh true to enable auto token refresh; otherwise false.
667   */
668  public void setAutoRefresh(boolean autoRefresh) {
669    this.autoRefresh = autoRefresh;
670  }
671
672  /**
673   * Gets the maximum number of times an API request will be retried after an error response is
674   * received.
675   *
676   * @return the maximum number of request attempts.
677   */
678  public int getMaxRetryAttempts() {
679    return this.maxRetryAttempts;
680  }
681
682  /**
683   * Sets the maximum number of times an API request will be retried after an error response is
684   * received.
685   *
686   * @param attempts the maximum number of request attempts.
687   */
688  public void setMaxRetryAttempts(int attempts) {
689    this.maxRetryAttempts = attempts;
690  }
691
692  /**
693   * Gets the connect timeout for this connection in milliseconds.
694   *
695   * @return the number of milliseconds to connect before timing out.
696   */
697  public int getConnectTimeout() {
698    return this.connectTimeout;
699  }
700
701  /**
702   * Sets the connect timeout for this connection.
703   *
704   * @param connectTimeout The number of milliseconds to wait for the connection to be established.
705   */
706  public void setConnectTimeout(int connectTimeout) {
707    this.connectTimeout = connectTimeout;
708    buildHttpClients();
709  }
710
711  /*
712   * Gets if request use zstd encoding when possible
713   * @return true if request use zstd encoding when possible
714   */
715  public boolean getUseZstdCompression() {
716    return this.useZstdCompression;
717  }
718
719  /*
720   * Sets if request use zstd encoding when possible
721   * @param useZstdCompression true if request use zstd encoding when possible
722   */
723  public void setUseZstdCompression(boolean useZstdCompression) {
724    this.useZstdCompression = useZstdCompression;
725    buildHttpClients();
726  }
727
728  /**
729   * Gets the read timeout for this connection in milliseconds.
730   *
731   * @return the number of milliseconds to wait for bytes to be read before timing out.
732   */
733  public int getReadTimeout() {
734    return this.readTimeout;
735  }
736
737  /**
738   * Sets the read timeout for this connection.
739   *
740   * @param readTimeout The number of milliseconds to wait for bytes to be read.
741   */
742  public void setReadTimeout(int readTimeout) {
743    this.readTimeout = readTimeout;
744    buildHttpClients();
745  }
746
747  /**
748   * Gets the proxy value to use for API calls to Box.
749   *
750   * @return the current proxy.
751   */
752  public Proxy getProxy() {
753    return this.proxy;
754  }
755
756  /**
757   * Sets the proxy to use for API calls to Box.
758   *
759   * @param proxy the proxy to use for API calls to Box.
760   */
761  public void setProxy(Proxy proxy) {
762    this.proxy = proxy;
763    buildHttpClients();
764  }
765
766  /**
767   * Gets the username to use for a proxy that requires basic auth.
768   *
769   * @return the username to use for a proxy that requires basic auth.
770   */
771  public String getProxyUsername() {
772    return this.proxyUsername;
773  }
774
775  /**
776   * Sets the username to use for a proxy that requires basic auth.
777   *
778   * @param proxyUsername the username to use for a proxy that requires basic auth.
779   * @deprecated Use {@link BoxAPIConnection#setProxyBasicAuthentication(String, String)}
780   */
781  public void setProxyUsername(String proxyUsername) {
782    this.proxyUsername = proxyUsername;
783    buildHttpClients();
784  }
785
786  /**
787   * Gets the password to use for a proxy that requires basic auth.
788   *
789   * @return the password to use for a proxy that requires basic auth.
790   */
791  public String getProxyPassword() {
792    return this.proxyPassword;
793  }
794
795  /**
796   * Sets the proxy user and password used in basic authentication
797   *
798   * @param proxyUsername Username to use for a proxy that requires basic auth.
799   * @param proxyPassword Password to use for a proxy that requires basic auth.
800   */
801  public void setProxyBasicAuthentication(String proxyUsername, String proxyPassword) {
802    this.proxyUsername = proxyUsername;
803    this.proxyPassword = proxyPassword;
804    buildHttpClients();
805  }
806
807  /**
808   * Sets the password to use for a proxy that requires basic auth.
809   *
810   * @param proxyPassword the password to use for a proxy that requires basic auth.
811   * @deprecated Use {@link BoxAPIConnection#setProxyBasicAuthentication(String, String)}
812   */
813  public void setProxyPassword(String proxyPassword) {
814    this.proxyPassword = proxyPassword;
815    buildHttpClients();
816  }
817
818  /**
819   * Determines if this connection's access token can be refreshed. An access token cannot be
820   * refreshed if a refresh token was never set.
821   *
822   * @return true if the access token can be refreshed; otherwise false.
823   */
824  public boolean canRefresh() {
825    return this.refreshToken != null;
826  }
827
828  /**
829   * Determines if this connection's access token has expired and needs to be refreshed.
830   *
831   * @return true if the access token needs to be refreshed; otherwise false.
832   */
833  public boolean needsRefresh() {
834    boolean needsRefresh;
835
836    this.refreshLock.readLock().lock();
837    try {
838      long now = System.currentTimeMillis();
839      long tokenDuration = (now - this.lastRefresh);
840      needsRefresh = (tokenDuration >= this.expires - REFRESH_EPSILON);
841    } finally {
842      this.refreshLock.readLock().unlock();
843    }
844
845    return needsRefresh;
846  }
847
848  /**
849   * Refresh's this connection's access token using its refresh token.
850   *
851   * @throws IllegalStateException if this connection's access token cannot be refreshed.
852   */
853  public void refresh() {
854    this.refreshLock.writeLock().lock();
855    try {
856      if (!this.canRefresh()) {
857        throw new IllegalStateException(
858            "The BoxAPIConnection cannot be refreshed because it doesn't have a "
859                + "refresh token.");
860      }
861
862      URL url;
863      try {
864        url = new URL(getTokenURL());
865      } catch (MalformedURLException e) {
866        assert false : "An invalid refresh URL indicates a bug in the SDK.";
867        throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e);
868      }
869
870      BoxAPIRequest request = createTokenRequest(url);
871
872      String json;
873      try (BoxAPIResponse boxAPIResponse = request.send()) {
874        BoxJSONResponse response = (BoxJSONResponse) boxAPIResponse;
875        json = response.getJSON();
876      } catch (BoxAPIException e) {
877        this.notifyError(e);
878        throw e;
879      }
880
881      extractTokens(Json.parse(json).asObject());
882      this.notifyRefresh();
883    } finally {
884      this.refreshLock.writeLock().unlock();
885    }
886  }
887
888  /**
889   * Restores a saved connection state into this BoxAPIConnection.
890   *
891   * @param state the saved state that was created with {@link #save}.
892   * @see #save
893   */
894  public void restore(String state) {
895    JsonObject json = Json.parse(state).asObject();
896    String accessToken = json.get("accessToken").asString();
897    String refreshToken = getKeyValueOrDefault(json, "refreshToken", null);
898    long lastRefresh = json.get("lastRefresh").asLong();
899    long expires = json.get("expires").asLong();
900    String userAgent = json.get("userAgent").asString();
901    String tokenURL = getKeyValueOrDefault(json, "tokenURL", null);
902    String revokeURL = getKeyValueOrDefault(json, "revokeURL", null);
903    String baseURL =
904        adoptBaseUrlWhenLoadingFromOldVersion(
905            getKeyValueOrDefault(json, "baseURL", DEFAULT_BASE_URL));
906    String baseUploadURL =
907        adoptUploadBaseUrlWhenLoadingFromOldVersion(
908            getKeyValueOrDefault(json, "baseUploadURL", DEFAULT_BASE_UPLOAD_URL));
909    String authorizationURL =
910        getKeyValueOrDefault(json, "authorizationURL", DEFAULT_BASE_AUTHORIZATION_URL);
911    boolean autoRefresh = json.get("autoRefresh").asBoolean();
912
913    // Try to read deprecated value
914    int maxRequestAttempts = -1;
915    if (json.names().contains("maxRequestAttempts")) {
916      maxRequestAttempts = json.get("maxRequestAttempts").asInt();
917    }
918
919    int maxRetryAttempts = -1;
920    if (json.names().contains("maxRetryAttempts")) {
921      maxRetryAttempts = json.get("maxRetryAttempts").asInt();
922    }
923
924    this.accessToken = accessToken;
925    this.refreshToken = refreshToken;
926    this.lastRefresh = lastRefresh;
927    this.expires = expires;
928    this.userAgent = userAgent;
929    this.tokenURL = tokenURL;
930    this.revokeURL = revokeURL;
931    this.setBaseURL(baseURL);
932    this.setBaseUploadURL(baseUploadURL);
933    this.setBaseAuthorizationURL(authorizationURL);
934    this.autoRefresh = autoRefresh;
935
936    // Try to use deprecated value "maxRequestAttempts", else use newer value "maxRetryAttempts"
937    if (maxRequestAttempts > -1) {
938      this.maxRetryAttempts = maxRequestAttempts - 1;
939    }
940    if (maxRetryAttempts > -1) {
941      this.maxRetryAttempts = maxRetryAttempts;
942    }
943  }
944
945  private String adoptBaseUrlWhenLoadingFromOldVersion(String url) {
946    if (url == null) {
947      return null;
948    }
949    String urlEndingWithSlash = fixBaseUrl(url);
950    return urlEndingWithSlash.equals("https://api.box.com/2.0/")
951        ? DEFAULT_BASE_URL
952        : urlEndingWithSlash;
953  }
954
955  private String adoptUploadBaseUrlWhenLoadingFromOldVersion(String url) {
956    if (url == null) {
957      return null;
958    }
959    String urlEndingWithSlash = fixBaseUrl(url);
960    return urlEndingWithSlash.equals("https://upload.box.com/api/2.0/")
961        ? DEFAULT_BASE_UPLOAD_URL
962        : urlEndingWithSlash;
963  }
964
965  protected String getKeyValueOrDefault(JsonObject json, String key, String defaultValue) {
966    return Optional.ofNullable(json.get(key))
967        .filter(js -> !js.isNull())
968        .map(JsonValue::asString)
969        .orElse(defaultValue);
970  }
971
972  /** Notifies a refresh event to all the listeners. */
973  protected void notifyRefresh() {
974    for (BoxAPIConnectionListener listener : this.listeners) {
975      listener.onRefresh(this);
976    }
977  }
978
979  /**
980   * Notifies an error event to all the listeners.
981   *
982   * @param error A BoxAPIException instance.
983   */
984  protected void notifyError(BoxAPIException error) {
985    for (BoxAPIConnectionListener listener : this.listeners) {
986      listener.onError(this, error);
987    }
988  }
989
990  /**
991   * Add a listener to listen to Box API connection events.
992   *
993   * @param listener a listener to listen to Box API connection.
994   */
995  public void addListener(BoxAPIConnectionListener listener) {
996    this.listeners.add(listener);
997  }
998
999  /**
1000   * Remove a listener listening to Box API connection events.
1001   *
1002   * @param listener the listener to remove.
1003   */
1004  public void removeListener(BoxAPIConnectionListener listener) {
1005    this.listeners.remove(listener);
1006  }
1007
1008  /**
1009   * Gets the RequestInterceptor associated with this API connection.
1010   *
1011   * @return the RequestInterceptor associated with this API connection.
1012   */
1013  public RequestInterceptor getRequestInterceptor() {
1014    return this.interceptor;
1015  }
1016
1017  /**
1018   * Sets a RequestInterceptor that can intercept requests and manipulate them before they're sent
1019   * to the Box API.
1020   *
1021   * @param interceptor the RequestInterceptor.
1022   */
1023  public void setRequestInterceptor(RequestInterceptor interceptor) {
1024    this.interceptor = interceptor;
1025  }
1026
1027  /**
1028   * Get a lower-scoped token restricted to a resource for the list of scopes that are passed.
1029   *
1030   * @param scopes the list of scopes to which the new token should be restricted for
1031   * @param resource the resource for which the new token has to be obtained
1032   * @return scopedToken which has access token and other details
1033   * @throws BoxAPIException if resource is not a valid Box API endpoint or shared link
1034   */
1035  public ScopedToken getLowerScopedToken(List<String> scopes, String resource) {
1036    assert (scopes != null);
1037    assert (scopes.size() > 0);
1038    URL url;
1039    try {
1040      url = new URL(this.getTokenURL());
1041    } catch (MalformedURLException e) {
1042      assert false : "An invalid refresh URL indicates a bug in the SDK.";
1043      throw new BoxAPIException("An invalid refresh URL indicates a bug in the SDK.", e);
1044    }
1045
1046    StringBuilder spaceSeparatedScopes = this.buildScopesForTokenDownscoping(scopes);
1047
1048    String urlParameters =
1049        format(
1050            "grant_type=urn:ietf:params:oauth:grant-type:token-exchange"
1051                + "&subject_token_type=urn:ietf:params:oauth:token-type:access_token&subject_token=%s"
1052                + "&scope=%s",
1053            this.getAccessToken(), spaceSeparatedScopes);
1054
1055    if (resource != null) {
1056
1057      ResourceLinkType resourceType = this.determineResourceLinkType(resource);
1058
1059      if (resourceType == ResourceLinkType.APIEndpoint) {
1060        urlParameters = format(urlParameters + "&resource=%s", resource);
1061      } else if (resourceType == ResourceLinkType.SharedLink) {
1062        urlParameters = format(urlParameters + "&box_shared_link=%s", resource);
1063      } else if (resourceType == ResourceLinkType.Unknown) {
1064        String argExceptionMessage = format("Unable to determine resource type: %s", resource);
1065        BoxAPIException e = new BoxAPIException(argExceptionMessage);
1066        this.notifyError(e);
1067        throw e;
1068      } else {
1069        String argExceptionMessage = format("Unhandled resource type: %s", resource);
1070        BoxAPIException e = new BoxAPIException(argExceptionMessage);
1071        this.notifyError(e);
1072        throw e;
1073      }
1074    }
1075
1076    BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
1077    request.shouldAuthenticate(false);
1078    request.setBody(urlParameters);
1079
1080    String jsonResponse;
1081    try (BoxJSONResponse response = (BoxJSONResponse) request.send()) {
1082      jsonResponse = response.getJSON();
1083    } catch (BoxAPIException e) {
1084      this.notifyError(e);
1085      throw e;
1086    }
1087
1088    JsonObject jsonObject = Json.parse(jsonResponse).asObject();
1089    ScopedToken token = new ScopedToken(jsonObject);
1090    token.setObtainedAt(System.currentTimeMillis());
1091    token.setExpiresIn(jsonObject.get("expires_in").asLong() * 1000);
1092    return token;
1093  }
1094
1095  /**
1096   * Convert List<String> to space-delimited String. Needed for versions prior to Java 8, which
1097   * don't have String.join(delimiter, list)
1098   *
1099   * @param scopes the list of scopes to read from
1100   * @return space-delimited String of scopes
1101   */
1102  private StringBuilder buildScopesForTokenDownscoping(List<String> scopes) {
1103    StringBuilder spaceSeparatedScopes = new StringBuilder();
1104    for (int i = 0; i < scopes.size(); i++) {
1105      spaceSeparatedScopes.append(scopes.get(i));
1106      if (i < scopes.size() - 1) {
1107        spaceSeparatedScopes.append(" ");
1108      }
1109    }
1110
1111    return spaceSeparatedScopes;
1112  }
1113
1114  /**
1115   * Determines the type of resource, given a link to a Box resource.
1116   *
1117   * @param resourceLink the resource URL to check
1118   * @return ResourceLinkType that categorizes the provided resourceLink
1119   */
1120  protected ResourceLinkType determineResourceLinkType(String resourceLink) {
1121
1122    ResourceLinkType resourceType = ResourceLinkType.Unknown;
1123
1124    try {
1125      URL validUrl = new URL(resourceLink);
1126      String validURLStr = validUrl.toString();
1127      final String apiFilesEndpointPattern = ".*box.com/2.0/files/\\d+";
1128      final String apiFoldersEndpointPattern = ".*box.com/2.0/folders/\\d+";
1129      final String sharedLinkPattern = "(.*box.com/s/.*|.*box.com.*s=.*)";
1130
1131      if (Pattern.matches(apiFilesEndpointPattern, validURLStr)
1132          || Pattern.matches(apiFoldersEndpointPattern, validURLStr)) {
1133        resourceType = ResourceLinkType.APIEndpoint;
1134      } else if (Pattern.matches(sharedLinkPattern, validURLStr)) {
1135        resourceType = ResourceLinkType.SharedLink;
1136      }
1137    } catch (MalformedURLException e) {
1138      // Swallow exception and return default ResourceLinkType set at top of function
1139    }
1140
1141    return resourceType;
1142  }
1143
1144  /**
1145   * Revokes the tokens associated with this API connection. This results in the connection no
1146   * longer being able to make API calls until a fresh authorization is made by calling
1147   * authenticate()
1148   */
1149  public void revokeToken() {
1150
1151    URL url;
1152    try {
1153      url = new URL(getRevokeURL());
1154    } catch (MalformedURLException e) {
1155      assert false : "An invalid refresh URL indicates a bug in the SDK.";
1156      throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e);
1157    }
1158
1159    String urlParameters =
1160        format(
1161            "token=%s&client_id=%s&client_secret=%s",
1162            this.accessToken, this.clientID, this.clientSecret);
1163
1164    BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
1165    request.shouldAuthenticate(false);
1166    request.setBody(urlParameters);
1167
1168    request.send().close();
1169  }
1170
1171  /**
1172   * Saves the state of this connection to a string so that it can be persisted and restored at a
1173   * later time.
1174   *
1175   * <p>Note that proxy settings aren't automatically saved or restored. This is mainly due to
1176   * security concerns around persisting proxy authentication details to the state string. If your
1177   * connection uses a proxy, you will have to manually configure it again after restoring the
1178   * connection.
1179   *
1180   * @return the state of this connection.
1181   * @see #restore
1182   */
1183  public String save() {
1184    JsonObject state =
1185        new JsonObject()
1186            .add("accessToken", this.accessToken)
1187            .add("refreshToken", this.refreshToken)
1188            .add("lastRefresh", this.lastRefresh)
1189            .add("expires", this.expires)
1190            .add("userAgent", this.userAgent)
1191            .add("tokenURL", this.tokenURL)
1192            .add("revokeURL", this.revokeURL)
1193            .add("baseURL", this.baseURL)
1194            .add("baseUploadURL", this.baseUploadURL)
1195            .add("authorizationURL", this.baseAuthorizationURL)
1196            .add("autoRefresh", this.autoRefresh)
1197            .add("maxRetryAttempts", this.maxRetryAttempts);
1198    return state.toString();
1199  }
1200
1201  String lockAccessToken() {
1202    if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) {
1203      this.refreshLock.writeLock().lock();
1204      try {
1205        if (this.needsRefresh()) {
1206          this.refresh();
1207        }
1208        this.refreshLock.readLock().lock();
1209      } finally {
1210        this.refreshLock.writeLock().unlock();
1211      }
1212    } else {
1213      this.refreshLock.readLock().lock();
1214    }
1215
1216    return this.accessToken;
1217  }
1218
1219  void unlockAccessToken() {
1220    this.refreshLock.readLock().unlock();
1221  }
1222
1223  /**
1224   * Get the value for the X-Box-UA header.
1225   *
1226   * @return the header value.
1227   */
1228  String getBoxUAHeader() {
1229
1230    return "agent=box-java-sdk/" + SDK_VERSION + "; env=Java/" + JAVA_VERSION;
1231  }
1232
1233  /**
1234   * Sets a custom header to be sent on all requests through this API connection.
1235   *
1236   * @param header the header name.
1237   * @param value the header value.
1238   */
1239  public void setCustomHeader(String header, String value) {
1240    this.customHeaders.put(header, value);
1241  }
1242
1243  /**
1244   * Removes a custom header, so it will no longer be sent on requests through this API connection.
1245   *
1246   * @param header the header name.
1247   */
1248  public void removeCustomHeader(String header) {
1249    this.customHeaders.remove(header);
1250  }
1251
1252  /**
1253   * Suppresses email notifications from API actions. This is typically used by security or admin
1254   * applications to prevent spamming end users when doing automated processing on their content.
1255   */
1256  public void suppressNotifications() {
1257    this.setCustomHeader(BOX_NOTIFICATIONS_HEADER, "off");
1258  }
1259
1260  /**
1261   * Re-enable email notifications from API actions if they have been suppressed.
1262   *
1263   * @see #suppressNotifications
1264   */
1265  public void enableNotifications() {
1266    this.removeCustomHeader(BOX_NOTIFICATIONS_HEADER);
1267  }
1268
1269  /**
1270   * Set this API connection to make API calls on behalf of another users, impersonating them. This
1271   * functionality can only be used by admins and service accounts.
1272   *
1273   * @param userID the ID of the user to act as.
1274   */
1275  public void asUser(String userID) {
1276    this.setCustomHeader(AS_USER_HEADER, userID);
1277  }
1278
1279  /**
1280   * Sets this API connection to make API calls on behalf of the user with whom the access token is
1281   * associated. This undoes any previous calls to asUser().
1282   *
1283   * @see #asUser
1284   */
1285  public void asSelf() {
1286    this.removeCustomHeader(AS_USER_HEADER);
1287  }
1288
1289  /**
1290   * Used to override default SSL certification handling. For example, you can provide your own
1291   * trust manager or hostname verifier to allow self-signed certificates. You can check examples <a
1292   * href="https://github.com/box/box-java-sdk/blob/combined-sdk/docs/sdk/configuration.md#ssl-configuration">here</a>.
1293   *
1294   * @param trustManager TrustManager that verifies certificates are valid.
1295   * @param hostnameVerifier HostnameVerifier that allows you to specify what hostnames are allowed.
1296   */
1297  public void configureSslCertificatesValidation(
1298      X509TrustManager trustManager, HostnameVerifier hostnameVerifier) {
1299    this.trustManager = trustManager;
1300    this.hostnameVerifier = hostnameVerifier;
1301    buildHttpClients();
1302  }
1303
1304  Map<String, String> getHeaders() {
1305    return this.customHeaders;
1306  }
1307
1308  protected void extractTokens(JsonObject jsonObject) {
1309    this.accessToken = jsonObject.get("access_token").asString();
1310    this.refreshToken = jsonObject.get("refresh_token").asString();
1311    this.lastRefresh = System.currentTimeMillis();
1312    this.expires = jsonObject.get("expires_in").asLong() * 1000;
1313  }
1314
1315  protected BoxAPIRequest createTokenRequest(URL url) {
1316    String urlParameters =
1317        format(
1318            "grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s",
1319            this.refreshToken, this.clientID, this.clientSecret);
1320
1321    BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
1322    request.shouldAuthenticate(false);
1323    request.setBody(urlParameters);
1324    return request;
1325  }
1326
1327  private String fixBaseUrl(String baseUrl) {
1328    return baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
1329  }
1330
1331  Response execute(Request request) {
1332    return executeOnClient(httpClient, request);
1333  }
1334
1335  Response executeWithoutRedirect(Request request) {
1336    return executeOnClient(noRedirectsHttpClient, request);
1337  }
1338
1339  protected Call createNewCall(OkHttpClient httpClient, Request request) {
1340    return httpClient.newCall(request);
1341  }
1342
1343  private Response executeOnClient(OkHttpClient httpClient, Request request) {
1344    try {
1345      return createNewCall(httpClient, request).execute();
1346    } catch (IOException e) {
1347      throw new BoxAPIException(
1348          "Couldn't connect to the Box API due to a network error. Request\n"
1349              + toSanitizedRequest(request),
1350          e);
1351    }
1352  }
1353
1354  protected X509TrustManager getTrustManager() {
1355    return trustManager;
1356  }
1357
1358  protected HostnameVerifier getHostnameVerifier() {
1359    return hostnameVerifier;
1360  }
1361
1362  /** Used to categorize the types of resource links. */
1363  protected enum ResourceLinkType {
1364    /** Catch-all default for resource links that are unknown. */
1365    Unknown,
1366
1367    /**
1368     * Resource URLs that point to an API endipoint such as https://api.box.com/2.0/files/:file_id.
1369     */
1370    APIEndpoint,
1371
1372    /**
1373     * Resource URLs that point to a resource that has been shared such as
1374     * https://example.box.com/s/qwertyuiop1234567890asdfghjk or
1375     * https://example.app.box.com/notes/0987654321?s=zxcvbnm1234567890asdfghjk.
1376     */
1377    SharedLink
1378  }
1379
1380  private Request toSanitizedRequest(Request originalRequest) {
1381    Headers sanitizedHeaders = BoxSensitiveDataSanitizer.sanitizeHeaders(originalRequest.headers());
1382
1383    return originalRequest.newBuilder().headers(sanitizedHeaders).build();
1384  }
1385}