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