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}