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