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