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.11.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        return this.accessToken;
537    }
538
539    /**
540     * Sets the access token to use when authenticating API requests.
541     *
542     * @param accessToken a valid access token to use when authenticating API requests.
543     */
544    public void setAccessToken(String accessToken) {
545        this.accessToken = accessToken;
546    }
547
548    /**
549     * Gets the refresh lock to be used when refreshing an access token.
550     *
551     * @return the refresh lock.
552     */
553    protected ReadWriteLock getRefreshLock() {
554        return this.refreshLock;
555    }
556
557    /**
558     * Gets a refresh token that can be used to refresh an access token.
559     *
560     * @return a valid refresh token.
561     */
562    public String getRefreshToken() {
563        return this.refreshToken;
564    }
565
566    /**
567     * Sets the refresh token to use when refreshing an access token.
568     *
569     * @param refreshToken a valid refresh token.
570     */
571    public void setRefreshToken(String refreshToken) {
572        this.refreshToken = refreshToken;
573    }
574
575    /**
576     * Gets the last time that the access token was refreshed.
577     *
578     * @return the last refresh time in milliseconds.
579     */
580    public long getLastRefresh() {
581        return this.lastRefresh;
582    }
583
584    /**
585     * Sets the last time that the access token was refreshed.
586     *
587     * <p>This value is used when determining if an access token needs to be auto-refreshed. If the amount of time since
588     * the last refresh exceeds the access token's expiration time, then the access token will be refreshed.</p>
589     *
590     * @param lastRefresh the new last refresh time in milliseconds.
591     */
592    public void setLastRefresh(long lastRefresh) {
593        this.lastRefresh = lastRefresh;
594    }
595
596    /**
597     * Gets whether or not automatic refreshing of this connection's access token is enabled. Defaults to true.
598     *
599     * @return true if auto token refresh is enabled; otherwise false.
600     */
601    public boolean getAutoRefresh() {
602        return this.autoRefresh;
603    }
604
605    /**
606     * Enables or disables automatic refreshing of this connection's access token. Defaults to true.
607     *
608     * @param autoRefresh true to enable auto token refresh; otherwise false.
609     */
610    public void setAutoRefresh(boolean autoRefresh) {
611        this.autoRefresh = autoRefresh;
612    }
613
614
615    /**
616     * Gets the maximum number of times an API request will be retried after an error response
617     * is received.
618     *
619     * @return the maximum number of request attempts.
620     */
621    public int getMaxRetryAttempts() {
622        return this.maxRetryAttempts;
623    }
624
625    /**
626     * Sets the maximum number of times an API request will be retried after an error response
627     * is received.
628     *
629     * @param attempts the maximum number of request attempts.
630     */
631    public void setMaxRetryAttempts(int attempts) {
632        this.maxRetryAttempts = attempts;
633    }
634
635    /**
636     * Gets the connect timeout for this connection in milliseconds.
637     *
638     * @return the number of milliseconds to connect before timing out.
639     */
640    public int getConnectTimeout() {
641        return this.connectTimeout;
642    }
643
644    /**
645     * Sets the connect timeout for this connection.
646     *
647     * @param connectTimeout The number of milliseconds to wait for the connection to be established.
648     */
649    public void setConnectTimeout(int connectTimeout) {
650        this.connectTimeout = connectTimeout;
651        buildHttpClients();
652    }
653
654    /**
655     * Gets the read timeout for this connection in milliseconds.
656     *
657     * @return the number of milliseconds to wait for bytes to be read before timing out.
658     */
659    public int getReadTimeout() {
660        return this.readTimeout;
661    }
662
663    /**
664     * Sets the read timeout for this connection.
665     *
666     * @param readTimeout The number of milliseconds to wait for bytes to be read.
667     */
668    public void setReadTimeout(int readTimeout) {
669        this.readTimeout = readTimeout;
670        buildHttpClients();
671    }
672
673    /**
674     * Gets the proxy value to use for API calls to Box.
675     *
676     * @return the current proxy.
677     */
678    public Proxy getProxy() {
679        return this.proxy;
680    }
681
682    /**
683     * Sets the proxy to use for API calls to Box.
684     *
685     * @param proxy the proxy to use for API calls to Box.
686     */
687    public void setProxy(Proxy proxy) {
688        this.proxy = proxy;
689        buildHttpClients();
690    }
691
692    /**
693     * Gets the username to use for a proxy that requires basic auth.
694     *
695     * @return the username to use for a proxy that requires basic auth.
696     */
697    public String getProxyUsername() {
698        return this.proxyUsername;
699    }
700
701    /**
702     * Sets the username to use for a proxy that requires basic auth.
703     *
704     * @param proxyUsername the username to use for a proxy that requires basic auth.
705     * @deprecated Use {@link BoxAPIConnection#setProxyBasicAuthentication(String, String)}
706     */
707    public void setProxyUsername(String proxyUsername) {
708        this.proxyUsername = proxyUsername;
709        buildHttpClients();
710    }
711
712    /**
713     * Gets the password to use for a proxy that requires basic auth.
714     *
715     * @return the password to use for a proxy that requires basic auth.
716     */
717    public String getProxyPassword() {
718        return this.proxyPassword;
719    }
720
721    /**
722     * Sets the proxy user and password used in basic authentication
723     *
724     * @param proxyUsername Username to use for a proxy that requires basic auth.
725     * @param proxyPassword Password to use for a proxy that requires basic auth.
726     */
727    public void setProxyBasicAuthentication(String proxyUsername, String proxyPassword) {
728        this.proxyUsername = proxyUsername;
729        this.proxyPassword = proxyPassword;
730        buildHttpClients();
731    }
732
733    /**
734     * Sets the password to use for a proxy that requires basic auth.
735     *
736     * @param proxyPassword the password to use for a proxy that requires basic auth.
737     * @deprecated Use {@link BoxAPIConnection#setProxyBasicAuthentication(String, String)}
738     */
739    public void setProxyPassword(String proxyPassword) {
740        this.proxyPassword = proxyPassword;
741        buildHttpClients();
742    }
743
744    /**
745     * Determines if this connection's access token can be refreshed. An access token cannot be refreshed if a refresh
746     * token was never set.
747     *
748     * @return true if the access token can be refreshed; otherwise false.
749     */
750    public boolean canRefresh() {
751        return this.refreshToken != null;
752    }
753
754    /**
755     * Determines if this connection's access token has expired and needs to be refreshed.
756     *
757     * @return true if the access token needs to be refreshed; otherwise false.
758     */
759    public boolean needsRefresh() {
760        boolean needsRefresh;
761
762        this.refreshLock.readLock().lock();
763        long now = System.currentTimeMillis();
764        long tokenDuration = (now - this.lastRefresh);
765        needsRefresh = (tokenDuration >= this.expires - REFRESH_EPSILON);
766        this.refreshLock.readLock().unlock();
767
768        return needsRefresh;
769    }
770
771    /**
772     * Refresh's this connection's access token using its refresh token.
773     *
774     * @throws IllegalStateException if this connection's access token cannot be refreshed.
775     */
776    public void refresh() {
777        this.refreshLock.writeLock().lock();
778
779        if (!this.canRefresh()) {
780            this.refreshLock.writeLock().unlock();
781            throw new IllegalStateException("The BoxAPIConnection cannot be refreshed because it doesn't have a "
782                + "refresh token.");
783        }
784
785        URL url;
786        try {
787            url = new URL(getTokenURL());
788        } catch (MalformedURLException e) {
789            this.refreshLock.writeLock().unlock();
790            assert false : "An invalid refresh URL indicates a bug in the SDK.";
791            throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e);
792        }
793
794        BoxAPIRequest request = createTokenRequest(url);
795
796        String json;
797        try (BoxAPIResponse boxAPIResponse = request.send()) {
798            BoxJSONResponse response = (BoxJSONResponse) boxAPIResponse;
799            json = response.getJSON();
800        } catch (BoxAPIException e) {
801            this.refreshLock.writeLock().unlock();
802            this.notifyError(e);
803            throw e;
804        }
805
806        try {
807            extractTokens(Json.parse(json).asObject());
808
809            this.notifyRefresh();
810        } finally {
811            this.refreshLock.writeLock().unlock();
812        }
813    }
814
815    /**
816     * Restores a saved connection state into this BoxAPIConnection.
817     *
818     * @param state the saved state that was created with {@link #save}.
819     * @see #save
820     */
821    public void restore(String state) {
822        JsonObject json = Json.parse(state).asObject();
823        String accessToken = json.get("accessToken").asString();
824        String refreshToken = getKeyValueOrDefault(json, "refreshToken", null);
825        long lastRefresh = json.get("lastRefresh").asLong();
826        long expires = json.get("expires").asLong();
827        String userAgent = json.get("userAgent").asString();
828        String tokenURL = getKeyValueOrDefault(json, "tokenURL", null);
829        String revokeURL = getKeyValueOrDefault(json, "revokeURL", null);
830        String baseURL = adoptBaseUrlWhenLoadingFromOldVersion(
831            getKeyValueOrDefault(json, "baseURL", DEFAULT_BASE_URL)
832        );
833        String baseUploadURL = adoptUploadBaseUrlWhenLoadingFromOldVersion(
834            getKeyValueOrDefault(json, "baseUploadURL", DEFAULT_BASE_UPLOAD_URL)
835        );
836        String authorizationURL =
837            getKeyValueOrDefault(json, "authorizationURL", DEFAULT_BASE_AUTHORIZATION_URL);
838        boolean autoRefresh = json.get("autoRefresh").asBoolean();
839
840        // Try to read deprecated value
841        int maxRequestAttempts = -1;
842        if (json.names().contains("maxRequestAttempts")) {
843            maxRequestAttempts = json.get("maxRequestAttempts").asInt();
844        }
845
846        int maxRetryAttempts = -1;
847        if (json.names().contains("maxRetryAttempts")) {
848            maxRetryAttempts = json.get("maxRetryAttempts").asInt();
849        }
850
851        this.accessToken = accessToken;
852        this.refreshToken = refreshToken;
853        this.lastRefresh = lastRefresh;
854        this.expires = expires;
855        this.userAgent = userAgent;
856        this.tokenURL = tokenURL;
857        this.revokeURL = revokeURL;
858        this.setBaseURL(baseURL);
859        this.setBaseUploadURL(baseUploadURL);
860        this.setBaseAuthorizationURL(authorizationURL);
861        this.autoRefresh = autoRefresh;
862
863        // Try to use deprecated value "maxRequestAttempts", else use newer value "maxRetryAttempts"
864        if (maxRequestAttempts > -1) {
865            this.maxRetryAttempts = maxRequestAttempts - 1;
866        }
867        if (maxRetryAttempts > -1) {
868            this.maxRetryAttempts = maxRetryAttempts;
869        }
870
871    }
872
873    private String adoptBaseUrlWhenLoadingFromOldVersion(String url) {
874        if (url == null) {
875            return null;
876        }
877        String urlEndingWithSlash = fixBaseUrl(url);
878        return urlEndingWithSlash.equals("https://api.box.com/2.0/")
879            ? DEFAULT_BASE_URL
880            : urlEndingWithSlash;
881    }
882
883    private String adoptUploadBaseUrlWhenLoadingFromOldVersion(String url) {
884        if (url == null) {
885            return null;
886        }
887        String urlEndingWithSlash = fixBaseUrl(url);
888        return urlEndingWithSlash.equals("https://upload.box.com/api/2.0/")
889            ? DEFAULT_BASE_UPLOAD_URL
890            : urlEndingWithSlash;
891    }
892
893    protected String getKeyValueOrDefault(JsonObject json, String key, String defaultValue) {
894        return Optional.ofNullable(json.get(key))
895            .filter(js -> !js.isNull())
896            .map(JsonValue::asString)
897            .orElse(defaultValue);
898    }
899
900    /**
901     * Notifies a refresh event to all the listeners.
902     */
903    protected void notifyRefresh() {
904        for (BoxAPIConnectionListener listener : this.listeners) {
905            listener.onRefresh(this);
906        }
907    }
908
909    /**
910     * Notifies an error event to all the listeners.
911     *
912     * @param error A BoxAPIException instance.
913     */
914    protected void notifyError(BoxAPIException error) {
915        for (BoxAPIConnectionListener listener : this.listeners) {
916            listener.onError(this, error);
917        }
918    }
919
920    /**
921     * Add a listener to listen to Box API connection events.
922     *
923     * @param listener a listener to listen to Box API connection.
924     */
925    public void addListener(BoxAPIConnectionListener listener) {
926        this.listeners.add(listener);
927    }
928
929    /**
930     * Remove a listener listening to Box API connection events.
931     *
932     * @param listener the listener to remove.
933     */
934    public void removeListener(BoxAPIConnectionListener listener) {
935        this.listeners.remove(listener);
936    }
937
938    /**
939     * Gets the RequestInterceptor associated with this API connection.
940     *
941     * @return the RequestInterceptor associated with this API connection.
942     */
943    public RequestInterceptor getRequestInterceptor() {
944        return this.interceptor;
945    }
946
947    /**
948     * Sets a RequestInterceptor that can intercept requests and manipulate them before they're sent to the Box API.
949     *
950     * @param interceptor the RequestInterceptor.
951     */
952    public void setRequestInterceptor(RequestInterceptor interceptor) {
953        this.interceptor = interceptor;
954    }
955
956    /**
957     * Get a lower-scoped token restricted to a resource for the list of scopes that are passed.
958     *
959     * @param scopes   the list of scopes to which the new token should be restricted for
960     * @param resource the resource for which the new token has to be obtained
961     * @return scopedToken which has access token and other details
962     * @throws BoxAPIException if resource is not a valid Box API endpoint or shared link
963     */
964    public ScopedToken getLowerScopedToken(List<String> scopes, String resource) {
965        assert (scopes != null);
966        assert (scopes.size() > 0);
967        URL url;
968        try {
969            url = new URL(this.getTokenURL());
970        } catch (MalformedURLException e) {
971            assert false : "An invalid refresh URL indicates a bug in the SDK.";
972            throw new BoxAPIException("An invalid refresh URL indicates a bug in the SDK.", e);
973        }
974
975        StringBuilder spaceSeparatedScopes = this.buildScopesForTokenDownscoping(scopes);
976
977        String urlParameters = format("grant_type=urn:ietf:params:oauth:grant-type:token-exchange"
978                + "&subject_token_type=urn:ietf:params:oauth:token-type:access_token&subject_token=%s"
979                + "&scope=%s",
980            this.getAccessToken(), spaceSeparatedScopes);
981
982        if (resource != null) {
983
984            ResourceLinkType resourceType = this.determineResourceLinkType(resource);
985
986            if (resourceType == ResourceLinkType.APIEndpoint) {
987                urlParameters = format(urlParameters + "&resource=%s", resource);
988            } else if (resourceType == ResourceLinkType.SharedLink) {
989                urlParameters = format(urlParameters + "&box_shared_link=%s", resource);
990            } else if (resourceType == ResourceLinkType.Unknown) {
991                String argExceptionMessage = format("Unable to determine resource type: %s", resource);
992                BoxAPIException e = new BoxAPIException(argExceptionMessage);
993                this.notifyError(e);
994                throw e;
995            } else {
996                String argExceptionMessage = format("Unhandled resource type: %s", resource);
997                BoxAPIException e = new BoxAPIException(argExceptionMessage);
998                this.notifyError(e);
999                throw e;
1000            }
1001        }
1002
1003        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
1004        request.shouldAuthenticate(false);
1005        request.setBody(urlParameters);
1006
1007        String jsonResponse;
1008        try (BoxJSONResponse response = (BoxJSONResponse) request.send()) {
1009            jsonResponse = response.getJSON();
1010        } catch (BoxAPIException e) {
1011            this.notifyError(e);
1012            throw e;
1013        }
1014
1015        JsonObject jsonObject = Json.parse(jsonResponse).asObject();
1016        ScopedToken token = new ScopedToken(jsonObject);
1017        token.setObtainedAt(System.currentTimeMillis());
1018        token.setExpiresIn(jsonObject.get("expires_in").asLong() * 1000);
1019        return token;
1020    }
1021
1022    /**
1023     * Convert List<String> to space-delimited String.
1024     * Needed for versions prior to Java 8, which don't have String.join(delimiter, list)
1025     *
1026     * @param scopes the list of scopes to read from
1027     * @return space-delimited String of scopes
1028     */
1029    private StringBuilder buildScopesForTokenDownscoping(List<String> scopes) {
1030        StringBuilder spaceSeparatedScopes = new StringBuilder();
1031        for (int i = 0; i < scopes.size(); i++) {
1032            spaceSeparatedScopes.append(scopes.get(i));
1033            if (i < scopes.size() - 1) {
1034                spaceSeparatedScopes.append(" ");
1035            }
1036        }
1037
1038        return spaceSeparatedScopes;
1039    }
1040
1041    /**
1042     * Determines the type of resource, given a link to a Box resource.
1043     *
1044     * @param resourceLink the resource URL to check
1045     * @return ResourceLinkType that categorizes the provided resourceLink
1046     */
1047    protected ResourceLinkType determineResourceLinkType(String resourceLink) {
1048
1049        ResourceLinkType resourceType = ResourceLinkType.Unknown;
1050
1051        try {
1052            URL validUrl = new URL(resourceLink);
1053            String validURLStr = validUrl.toString();
1054            final String apiFilesEndpointPattern = ".*box.com/2.0/files/\\d+";
1055            final String apiFoldersEndpointPattern = ".*box.com/2.0/folders/\\d+";
1056            final String sharedLinkPattern = "(.*box.com/s/.*|.*box.com.*s=.*)";
1057
1058            if (Pattern.matches(apiFilesEndpointPattern, validURLStr)
1059                || Pattern.matches(apiFoldersEndpointPattern, validURLStr)) {
1060                resourceType = ResourceLinkType.APIEndpoint;
1061            } else if (Pattern.matches(sharedLinkPattern, validURLStr)) {
1062                resourceType = ResourceLinkType.SharedLink;
1063            }
1064        } catch (MalformedURLException e) {
1065            //Swallow exception and return default ResourceLinkType set at top of function
1066        }
1067
1068        return resourceType;
1069    }
1070
1071    /**
1072     * Revokes the tokens associated with this API connection.  This results in the connection no
1073     * longer being able to make API calls until a fresh authorization is made by calling authenticate()
1074     */
1075    public void revokeToken() {
1076
1077        URL url;
1078        try {
1079            url = new URL(getRevokeURL());
1080        } catch (MalformedURLException e) {
1081            assert false : "An invalid refresh URL indicates a bug in the SDK.";
1082            throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e);
1083        }
1084
1085        String urlParameters = format("token=%s&client_id=%s&client_secret=%s",
1086            this.accessToken, this.clientID, this.clientSecret);
1087
1088        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
1089        request.shouldAuthenticate(false);
1090        request.setBody(urlParameters);
1091
1092        request.send().close();
1093    }
1094
1095    /**
1096     * Saves the state of this connection to a string so that it can be persisted and restored at a later time.
1097     *
1098     * <p>Note that proxy settings aren't automatically saved or restored. This is mainly due to security concerns
1099     * around persisting proxy authentication details to the state string. If your connection uses a proxy, you will
1100     * have to manually configure it again after restoring the connection.</p>
1101     *
1102     * @return the state of this connection.
1103     * @see #restore
1104     */
1105    public String save() {
1106        JsonObject state = new JsonObject()
1107            .add("accessToken", this.accessToken)
1108            .add("refreshToken", this.refreshToken)
1109            .add("lastRefresh", this.lastRefresh)
1110            .add("expires", this.expires)
1111            .add("userAgent", this.userAgent)
1112            .add("tokenURL", this.tokenURL)
1113            .add("revokeURL", this.revokeURL)
1114            .add("baseURL", this.baseURL)
1115            .add("baseUploadURL", this.baseUploadURL)
1116            .add("authorizationURL", this.baseAuthorizationURL)
1117            .add("autoRefresh", this.autoRefresh)
1118            .add("maxRetryAttempts", this.maxRetryAttempts);
1119        return state.toString();
1120    }
1121
1122    String lockAccessToken() {
1123        if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) {
1124            this.refreshLock.writeLock().lock();
1125            try {
1126                if (this.needsRefresh()) {
1127                    this.refresh();
1128                }
1129                this.refreshLock.readLock().lock();
1130            } finally {
1131                this.refreshLock.writeLock().unlock();
1132            }
1133        } else {
1134            this.refreshLock.readLock().lock();
1135        }
1136
1137        return this.accessToken;
1138    }
1139
1140    void unlockAccessToken() {
1141        this.refreshLock.readLock().unlock();
1142    }
1143
1144    /**
1145     * Get the value for the X-Box-UA header.
1146     *
1147     * @return the header value.
1148     */
1149    String getBoxUAHeader() {
1150
1151        return "agent=box-java-sdk/" + SDK_VERSION + "; env=Java/" + JAVA_VERSION;
1152    }
1153
1154    /**
1155     * Sets a custom header to be sent on all requests through this API connection.
1156     *
1157     * @param header the header name.
1158     * @param value  the header value.
1159     */
1160    public void setCustomHeader(String header, String value) {
1161        this.customHeaders.put(header, value);
1162    }
1163
1164    /**
1165     * Removes a custom header, so it will no longer be sent on requests through this API connection.
1166     *
1167     * @param header the header name.
1168     */
1169    public void removeCustomHeader(String header) {
1170        this.customHeaders.remove(header);
1171    }
1172
1173    /**
1174     * Suppresses email notifications from API actions.  This is typically used by security or admin applications
1175     * to prevent spamming end users when doing automated processing on their content.
1176     */
1177    public void suppressNotifications() {
1178        this.setCustomHeader(BOX_NOTIFICATIONS_HEADER, "off");
1179    }
1180
1181    /**
1182     * Re-enable email notifications from API actions if they have been suppressed.
1183     *
1184     * @see #suppressNotifications
1185     */
1186    public void enableNotifications() {
1187        this.removeCustomHeader(BOX_NOTIFICATIONS_HEADER);
1188    }
1189
1190    /**
1191     * Set this API connection to make API calls on behalf of another users, impersonating them.  This
1192     * functionality can only be used by admins and service accounts.
1193     *
1194     * @param userID the ID of the user to act as.
1195     */
1196    public void asUser(String userID) {
1197        this.setCustomHeader(AS_USER_HEADER, userID);
1198    }
1199
1200    /**
1201     * Sets this API connection to make API calls on behalf of the user with whom the access token is associated.
1202     * This undoes any previous calls to asUser().
1203     *
1204     * @see #asUser
1205     */
1206    public void asSelf() {
1207        this.removeCustomHeader(AS_USER_HEADER);
1208    }
1209
1210    /**
1211     * Used to override default SSL certification handling. For example, you can provide your own
1212     * trust manager or hostname verifier to allow self-signed certificates.
1213     * You can check examples <a href="https://github.com/box/box-java-sdk/blob/main/doc/configuration.md#ssl-configuration">here</a>.
1214     *
1215     * @param trustManager     TrustManager that verifies certificates are valid.
1216     * @param hostnameVerifier HostnameVerifier that allows you to specify what hostnames are allowed.
1217     */
1218    public void configureSslCertificatesValidation(X509TrustManager trustManager, HostnameVerifier hostnameVerifier) {
1219        this.trustManager = trustManager;
1220        this.hostnameVerifier = hostnameVerifier;
1221        buildHttpClients();
1222    }
1223
1224    Map<String, String> getHeaders() {
1225        return this.customHeaders;
1226    }
1227
1228    protected void extractTokens(JsonObject jsonObject) {
1229        this.accessToken = jsonObject.get("access_token").asString();
1230        this.refreshToken = jsonObject.get("refresh_token").asString();
1231        this.lastRefresh = System.currentTimeMillis();
1232        this.expires = jsonObject.get("expires_in").asLong() * 1000;
1233    }
1234
1235    protected BoxAPIRequest createTokenRequest(URL url) {
1236        String urlParameters = format("grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s",
1237            this.refreshToken, this.clientID, this.clientSecret);
1238
1239        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
1240        request.shouldAuthenticate(false);
1241        request.setBody(urlParameters);
1242        return request;
1243    }
1244
1245    private String fixBaseUrl(String baseUrl) {
1246        return baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
1247    }
1248
1249    Response execute(Request request) {
1250        return executeOnClient(httpClient, request);
1251    }
1252
1253    Response executeWithoutRedirect(Request request) {
1254        return executeOnClient(noRedirectsHttpClient, request);
1255    }
1256
1257    protected Call createNewCall(OkHttpClient httpClient, Request request) {
1258        return httpClient.newCall(request);
1259    }
1260
1261    private Response executeOnClient(OkHttpClient httpClient, Request request) {
1262        try {
1263            return createNewCall(httpClient, request).execute();
1264        } catch (IOException e) {
1265            throw new BoxAPIException("Couldn't connect to the Box API due to a network error. Request\n" + request, e);
1266        }
1267    }
1268
1269    protected X509TrustManager getTrustManager() {
1270        return trustManager;
1271    }
1272
1273    protected HostnameVerifier getHostnameVerifier() {
1274        return hostnameVerifier;
1275    }
1276
1277    /**
1278     * Used to categorize the types of resource links.
1279     */
1280    protected enum ResourceLinkType {
1281        /**
1282         * Catch-all default for resource links that are unknown.
1283         */
1284        Unknown,
1285
1286        /**
1287         * Resource URLs that point to an API endipoint such as https://api.box.com/2.0/files/:file_id.
1288         */
1289        APIEndpoint,
1290
1291        /**
1292         * Resource URLs that point to a resource that has been shared
1293         * such as https://example.box.com/s/qwertyuiop1234567890asdfghjk
1294         * or https://example.app.box.com/notes/0987654321?s=zxcvbnm1234567890asdfghjk.
1295         */
1296        SharedLink
1297    }
1298}