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