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}