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