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