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.Credentials;
033import okhttp3.OkHttpClient;
034import okhttp3.Request;
035import okhttp3.Response;
036
037/**
038 * Represents an authenticated connection to the Box API.
039 *
040 * <p>This class handles storing authentication information, automatic token refresh, and rate-limiting. It can also be
041 * used to configure the Box API endpoint URL in order to hit a different version of the API. Multiple instances of
042 * BoxAPIConnection may be created to support multi-user login.</p>
043 */
044public class BoxAPIConnection {
045
046    /**
047     * Used as a marker to setup connection to use default HostnameVerifier
048     * Example:<pre>{@code
049     * BoxApiConnection api = new BoxApiConnection(...);
050     * HostnameVerifier myHostnameVerifier = ...
051     * api.configureSslCertificatesValidation(DEFAULT_TRUST_MANAGER, myHostnameVerifier);
052     * }</pre>
053     */
054    public static final X509TrustManager DEFAULT_TRUST_MANAGER = null;
055    /**
056     * Used as a marker to setup connection to use default HostnameVerifier
057     * Example:<pre>{@code
058     * BoxApiConnection api = new BoxApiConnection(...);
059     * X509TrustManager myTrustManager = ...
060     * api.configureSslCertificatesValidation(myTrustManager, DEFAULT_HOSTNAME_VERIFIER);
061     * }</pre>
062     */
063    public static final HostnameVerifier DEFAULT_HOSTNAME_VERIFIER = null;
064
065    /**
066     * The default maximum number of times an API request will be retried after an error response
067     * is received.
068     */
069    public static final int DEFAULT_MAX_RETRIES = 5;
070    /**
071     * Default authorization URL
072     */
073    protected static final String DEFAULT_BASE_AUTHORIZATION_URL = "https://account.box.com/api/";
074    static final String AS_USER_HEADER = "As-User";
075
076    private static final String API_VERSION = "2.0";
077    private static final String OAUTH_SUFFIX = "oauth2/authorize";
078    private static final String TOKEN_URL_SUFFIX = "oauth2/token";
079    private static final String REVOKE_URL_SUFFIX = "oauth2/revoke";
080    private static final String DEFAULT_BASE_URL = "https://api.box.com/";
081    private static final String DEFAULT_BASE_UPLOAD_URL = "https://upload.box.com/api/";
082    private static final String DEFAULT_BASE_APP_URL = "https://app.box.com";
083
084    private static final String BOX_NOTIFICATIONS_HEADER = "Box-Notifications";
085
086    private static final String JAVA_VERSION = System.getProperty("java.version");
087    private static final String SDK_VERSION = "4.0.0";
088
089    /**
090     * The amount of buffer time, in milliseconds, to use when determining if an access token should be refreshed. For
091     * example, if REFRESH_EPSILON = 60000 and the access token expires in less than one minute, it will be refreshed.
092     */
093    private static final long REFRESH_EPSILON = 60000;
094
095    private final String clientID;
096    private final String clientSecret;
097    private final ReadWriteLock refreshLock;
098    private X509TrustManager trustManager;
099    private HostnameVerifier hostnameVerifier;
100
101    // These volatile fields are used when determining if the access token needs to be refreshed. Since they are used in
102    // the double-checked lock in getAccessToken(), they must be atomic.
103    private volatile long lastRefresh;
104    private volatile long expires;
105
106    private Proxy proxy;
107    private String proxyUsername;
108    private String proxyPassword;
109
110    private String userAgent;
111    private String accessToken;
112    private String refreshToken;
113    private String tokenURL;
114    private String revokeURL;
115    private String baseURL;
116    private String baseUploadURL;
117    private String baseAppURL;
118    private String baseAuthorizationURL;
119    private boolean autoRefresh;
120    private int maxRetryAttempts;
121    private int connectTimeout;
122    private int readTimeout;
123    private final List<BoxAPIConnectionListener> listeners;
124    private RequestInterceptor interceptor;
125    private final Map<String, String> customHeaders;
126
127    private OkHttpClient httpClient;
128    private OkHttpClient noRedirectsHttpClient;
129    private Authenticator authenticator;
130
131    /**
132     * Constructs a new BoxAPIConnection that authenticates with a developer or access token.
133     *
134     * @param accessToken a developer or access token to use for authenticating with the API.
135     */
136    public BoxAPIConnection(String accessToken) {
137        this(null, null, accessToken, null);
138    }
139
140    /**
141     * Constructs a new BoxAPIConnection with an access token that can be refreshed.
142     *
143     * @param clientID     the client ID to use when refreshing the access token.
144     * @param clientSecret the client secret to use when refreshing the access token.
145     * @param accessToken  an initial access token to use for authenticating with the API.
146     * @param refreshToken an initial refresh token to use when refreshing the access token.
147     */
148    public BoxAPIConnection(String clientID, String clientSecret, String accessToken, String refreshToken) {
149        this.clientID = clientID;
150        this.clientSecret = clientSecret;
151        this.accessToken = accessToken;
152        this.refreshToken = refreshToken;
153        this.baseURL = fixBaseUrl(DEFAULT_BASE_URL);
154        this.baseUploadURL = fixBaseUrl(DEFAULT_BASE_UPLOAD_URL);
155        this.baseAppURL = DEFAULT_BASE_APP_URL;
156        this.baseAuthorizationURL = DEFAULT_BASE_AUTHORIZATION_URL;
157        this.autoRefresh = true;
158        this.maxRetryAttempts = BoxGlobalSettings.getMaxRetryAttempts();
159        this.connectTimeout = BoxGlobalSettings.getConnectTimeout();
160        this.readTimeout = BoxGlobalSettings.getReadTimeout();
161        this.refreshLock = new ReentrantReadWriteLock();
162        this.userAgent = "Box Java SDK v" + SDK_VERSION + " (Java " + JAVA_VERSION + ")";
163        this.listeners = new ArrayList<>();
164        this.customHeaders = new HashMap<>();
165
166        buildHttpClients();
167    }
168
169    /**
170     * Constructs a new BoxAPIConnection with an auth code that was obtained from the first half of OAuth.
171     *
172     * @param clientID     the client ID to use when exchanging the auth code for an access token.
173     * @param clientSecret the client secret to use when exchanging the auth code for an access token.
174     * @param authCode     an auth code obtained from the first half of the OAuth process.
175     */
176    public BoxAPIConnection(String clientID, String clientSecret, String authCode) {
177        this(clientID, clientSecret, null, null);
178        this.authenticate(authCode);
179    }
180
181    /**
182     * Constructs a new BoxAPIConnection.
183     *
184     * @param clientID     the client ID to use when exchanging the auth code for an access token.
185     * @param clientSecret the client secret to use when exchanging the auth code for an access token.
186     */
187    public BoxAPIConnection(String clientID, String clientSecret) {
188        this(clientID, clientSecret, null, null);
189    }
190
191    /**
192     * Constructs a new BoxAPIConnection levaraging BoxConfig.
193     *
194     * @param boxConfig BoxConfig file, which should have clientId and clientSecret
195     */
196    public BoxAPIConnection(BoxConfig boxConfig) {
197        this(boxConfig.getClientId(), boxConfig.getClientSecret(), null, null);
198    }
199
200    private void buildHttpClients() {
201        OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();
202        if (trustManager != null) {
203            try {
204                SSLContext sslContext = SSLContext.getInstance("SSL");
205                sslContext.init(null, new TrustManager[]{trustManager}, new java.security.SecureRandom());
206                httpClientBuilder.sslSocketFactory(sslContext.getSocketFactory(), trustManager);
207            } catch (NoSuchAlgorithmException | KeyManagementException e) {
208                throw new RuntimeException(e);
209            }
210        }
211
212        OkHttpClient.Builder builder = httpClientBuilder
213            .followSslRedirects(true)
214            .followRedirects(true)
215            .connectTimeout(Duration.ofMillis(connectTimeout))
216            .readTimeout(Duration.ofMillis(readTimeout))
217            .connectionSpecs(singletonList(MODERN_TLS));
218
219        if (hostnameVerifier != null) {
220            httpClientBuilder.hostnameVerifier(hostnameVerifier);
221        }
222
223        if (proxy != null) {
224            builder.proxy(proxy);
225            if (proxyUsername != null && proxyPassword != null) {
226                builder.proxyAuthenticator((route, response) -> {
227                    String credential = Credentials.basic(proxyUsername, proxyPassword);
228                    return response.request().newBuilder()
229                        .header("Proxy-Authorization", credential)
230                        .build();
231                });
232            }
233            if (this.authenticator != null) {
234                builder.proxyAuthenticator(authenticator);
235            }
236        }
237        builder = modifyHttpClientBuilder(builder);
238
239        this.httpClient = builder.build();
240        this.noRedirectsHttpClient = new OkHttpClient.Builder(httpClient)
241            .followSslRedirects(false)
242            .followRedirects(false)
243            .build();
244    }
245
246    /**
247     * Can be used to modify OkHttp.Builder used to create connection. This method is called after all modifications
248     * were done, thus allowing others to create their own connections and further customize builder.
249     * @param httpClientBuilder Builder that will be used to create http connection.
250     * @return Modified builder.
251     */
252    protected OkHttpClient.Builder modifyHttpClientBuilder(OkHttpClient.Builder httpClientBuilder) {
253        return httpClientBuilder;
254    }
255
256    /**
257     * Sets a proxy authenticator that will be used when proxy requires authentication.
258     * If you use {@link BoxAPIConnection#setProxyBasicAuthentication(String, String)} it adds an authenticator
259     * that performs Basic authorization. By calling this method you can override this behaviour.
260     * You do not need to call {@link BoxAPIConnection#setProxyBasicAuthentication(String, String)}
261     * in order to set custom authenticator.
262     *
263     * @param authenticator Custom authenticator that will be called when proxy asks for authorization.
264     */
265    public void setProxyAuthenticator(Authenticator authenticator) {
266        this.authenticator = authenticator;
267        buildHttpClients();
268    }
269
270    /**
271     * Restores a BoxAPIConnection from a saved state.
272     *
273     * @param clientID     the client ID to use with the connection.
274     * @param clientSecret the client secret to use with the connection.
275     * @param state        the saved state that was created with {@link #save}.
276     * @return a restored API connection.
277     * @see #save
278     */
279    public static BoxAPIConnection restore(String clientID, String clientSecret, String state) {
280        BoxAPIConnection api = new BoxAPIConnection(clientID, clientSecret);
281        api.restore(state);
282        return api;
283    }
284
285    /**
286     * Returns the default authorization URL which is used to perform the authorization_code based OAuth2 flow.
287     * If custom Authorization URL is needed use instance method {@link BoxAPIConnection#getAuthorizationURL}
288     *
289     * @param clientID    the client ID to use with the connection.
290     * @param redirectUri the URL to which Box redirects the browser when authentication completes.
291     * @param state       the text string that you choose.
292     *                    Box sends the same string to your redirect URL when authentication is complete.
293     * @param scopes      this optional parameter identifies the Box scopes available
294     *                    to the application once it's authenticated.
295     * @return the authorization URL
296     */
297    public static URL getAuthorizationURL(String clientID, URI redirectUri, String state, List<String> scopes) {
298        return createFullAuthorizationUrl(DEFAULT_BASE_AUTHORIZATION_URL, clientID, redirectUri, state, scopes);
299    }
300
301    private static URL createFullAuthorizationUrl(
302        String authorizationUrl, String clientID, URI redirectUri, String state, List<String> scopes
303    ) {
304        URLTemplate template = new URLTemplate(authorizationUrl + OAUTH_SUFFIX);
305        QueryStringBuilder queryBuilder = new QueryStringBuilder().appendParam("client_id", clientID)
306            .appendParam("response_type", "code")
307            .appendParam("redirect_uri", redirectUri.toString())
308            .appendParam("state", state);
309
310        if (scopes != null && !scopes.isEmpty()) {
311            queryBuilder.appendParam("scope", join(" ", scopes));
312        }
313
314        return template.buildWithQuery("", queryBuilder.toString());
315    }
316
317    /**
318     * Authenticates the API connection by obtaining access and refresh tokens using the auth code that was obtained
319     * from the first half of OAuth.
320     *
321     * @param authCode the auth code obtained from the first half of the OAuth process.
322     */
323    public void authenticate(String authCode) {
324        URL url;
325        try {
326            url = new URL(this.getTokenURL());
327        } catch (MalformedURLException e) {
328            assert false : "An invalid token URL indicates a bug in the SDK.";
329            throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e);
330        }
331
332        String urlParameters = format("grant_type=authorization_code&code=%s&client_id=%s&client_secret=%s",
333            authCode, this.clientID, this.clientSecret);
334
335        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
336        request.shouldAuthenticate(false);
337        request.setBody(urlParameters);
338
339        // authentication uses form url encoded but response is JSON
340        try (BoxJSONResponse response = (BoxJSONResponse) request.send()) {
341            String json = response.getJSON();
342
343            JsonObject jsonObject = Json.parse(json).asObject();
344            this.accessToken = jsonObject.get("access_token").asString();
345            this.refreshToken = jsonObject.get("refresh_token").asString();
346            this.lastRefresh = System.currentTimeMillis();
347            this.expires = jsonObject.get("expires_in").asLong() * 1000;
348        }
349    }
350
351    /**
352     * Gets the client ID.
353     *
354     * @return the client ID.
355     */
356    public String getClientID() {
357        return this.clientID;
358    }
359
360    /**
361     * Gets the client secret.
362     *
363     * @return the client secret.
364     */
365    public String getClientSecret() {
366        return this.clientSecret;
367    }
368
369    /**
370     * Gets the amount of time for which this connection's access token is valid.
371     *
372     * @return the amount of time in milliseconds.
373     */
374    public long getExpires() {
375        return this.expires;
376    }
377
378    /**
379     * Sets the amount of time for which this connection's access token is valid before it must be refreshed.
380     *
381     * @param milliseconds the number of milliseconds for which the access token is valid.
382     */
383    public void setExpires(long milliseconds) {
384        this.expires = milliseconds;
385    }
386
387    /**
388     * Gets the token URL that's used to request access tokens.  The default value is
389     * "https://www.box.com/api/oauth2/token".
390     * The URL is created from {@link BoxAPIConnection#baseURL} and {@link BoxAPIConnection#TOKEN_URL_SUFFIX}.
391     *
392     * @return the token URL.
393     */
394    public String getTokenURL() {
395        if (this.tokenURL != null) {
396            return this.tokenURL;
397        } else {
398            return this.baseURL + TOKEN_URL_SUFFIX;
399        }
400    }
401
402    /**
403     * Returns the URL used for token revocation.
404     * The URL is created from {@link BoxAPIConnection#baseURL} and {@link BoxAPIConnection#REVOKE_URL_SUFFIX}.
405     *
406     * @return The url used for token revocation.
407     */
408    public String getRevokeURL() {
409        if (this.revokeURL != null) {
410            return this.revokeURL;
411        } else {
412            return this.baseURL + REVOKE_URL_SUFFIX;
413        }
414    }
415
416    /**
417     * Gets the base URL that's used when sending requests to the Box API.
418     * The URL is created from {@link BoxAPIConnection#baseURL} and {@link BoxAPIConnection#API_VERSION}.
419     * The default value is "https://api.box.com/2.0/".
420     *
421     * @return the base URL.
422     */
423    public String getBaseURL() {
424        return this.baseURL + API_VERSION + "/";
425    }
426
427    /**
428     * Sets the base URL to be used when sending requests to the Box API. For example, the default base URL is
429     * "https://api.box.com/". This method changes how {@link BoxAPIConnection#getRevokeURL()}
430     * and {@link BoxAPIConnection#getTokenURL()} are constructed.
431     *
432     * @param baseURL a base URL
433     */
434    public void setBaseURL(String baseURL) {
435        this.baseURL = fixBaseUrl(baseURL);
436    }
437
438    /**
439     * Gets the base upload URL that's used when performing file uploads to Box.
440     * The URL is created from {@link BoxAPIConnection#baseUploadURL} and {@link BoxAPIConnection#API_VERSION}.
441     *
442     * @return the base upload URL.
443     */
444    public String getBaseUploadURL() {
445        return this.baseUploadURL + API_VERSION + "/";
446    }
447
448    /**
449     * Sets the base upload URL to be used when performing file uploads to Box.
450     *
451     * @param baseUploadURL a base upload URL.
452     */
453    public void setBaseUploadURL(String baseUploadURL) {
454        this.baseUploadURL = fixBaseUrl(baseUploadURL);
455    }
456
457    /**
458     * Returns the authorization URL which is used to perform the authorization_code based OAuth2 flow.
459     * The URL is created from {@link BoxAPIConnection#baseAuthorizationURL} and {@link BoxAPIConnection#OAUTH_SUFFIX}.
460     *
461     * @param redirectUri the URL to which Box redirects the browser when authentication completes.
462     * @param state       the text string that you choose.
463     *                    Box sends the same string to your redirect URL when authentication is complete.
464     * @param scopes      this optional parameter identifies the Box scopes available
465     *                    to the application once it's authenticated.
466     * @return the authorization URL
467     */
468    public URL getAuthorizationURL(URI redirectUri, String state, List<String> scopes) {
469        return createFullAuthorizationUrl(
470            this.baseAuthorizationURL + OAUTH_SUFFIX, this.clientID, redirectUri, state, scopes
471        );
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        return this.accessToken;
538    }
539
540    /**
541     * Sets the access token to use when authenticating API requests.
542     *
543     * @param accessToken a valid access token to use when authenticating API requests.
544     */
545    public void setAccessToken(String accessToken) {
546        this.accessToken = accessToken;
547    }
548
549    /**
550     * Gets the refresh lock to be used when refreshing an access token.
551     *
552     * @return the refresh lock.
553     */
554    protected ReadWriteLock getRefreshLock() {
555        return this.refreshLock;
556    }
557
558    /**
559     * Gets a refresh token that can be used to refresh an access token.
560     *
561     * @return a valid refresh token.
562     */
563    public String getRefreshToken() {
564        return this.refreshToken;
565    }
566
567    /**
568     * Sets the refresh token to use when refreshing an access token.
569     *
570     * @param refreshToken a valid refresh token.
571     */
572    public void setRefreshToken(String refreshToken) {
573        this.refreshToken = refreshToken;
574    }
575
576    /**
577     * Gets the last time that the access token was refreshed.
578     *
579     * @return the last refresh time in milliseconds.
580     */
581    public long getLastRefresh() {
582        return this.lastRefresh;
583    }
584
585    /**
586     * Sets the last time that the access token was refreshed.
587     *
588     * <p>This value is used when determining if an access token needs to be auto-refreshed. If the amount of time since
589     * the last refresh exceeds the access token's expiration time, then the access token will be refreshed.</p>
590     *
591     * @param lastRefresh the new last refresh time in milliseconds.
592     */
593    public void setLastRefresh(long lastRefresh) {
594        this.lastRefresh = lastRefresh;
595    }
596
597    /**
598     * Gets whether or not automatic refreshing of this connection's access token is enabled. Defaults to true.
599     *
600     * @return true if auto token refresh is enabled; otherwise false.
601     */
602    public boolean getAutoRefresh() {
603        return this.autoRefresh;
604    }
605
606    /**
607     * Enables or disables automatic refreshing of this connection's access token. Defaults to true.
608     *
609     * @param autoRefresh true to enable auto token refresh; otherwise false.
610     */
611    public void setAutoRefresh(boolean autoRefresh) {
612        this.autoRefresh = autoRefresh;
613    }
614
615
616    /**
617     * Gets the maximum number of times an API request will be retried after an error response
618     * is received.
619     *
620     * @return the maximum number of request attempts.
621     */
622    public int getMaxRetryAttempts() {
623        return this.maxRetryAttempts;
624    }
625
626    /**
627     * Sets the maximum number of times an API request will be retried after an error response
628     * is received.
629     *
630     * @param attempts the maximum number of request attempts.
631     */
632    public void setMaxRetryAttempts(int attempts) {
633        this.maxRetryAttempts = attempts;
634    }
635
636    /**
637     * Gets the connect timeout for this connection in milliseconds.
638     *
639     * @return the number of milliseconds to connect before timing out.
640     */
641    public int getConnectTimeout() {
642        return this.connectTimeout;
643    }
644
645    /**
646     * Sets the connect timeout for this connection.
647     *
648     * @param connectTimeout The number of milliseconds to wait for the connection to be established.
649     */
650    public void setConnectTimeout(int connectTimeout) {
651        this.connectTimeout = connectTimeout;
652        buildHttpClients();
653    }
654
655    /**
656     * Gets the read timeout for this connection in milliseconds.
657     *
658     * @return the number of milliseconds to wait for bytes to be read before timing out.
659     */
660    public int getReadTimeout() {
661        return this.readTimeout;
662    }
663
664    /**
665     * Sets the read timeout for this connection.
666     *
667     * @param readTimeout The number of milliseconds to wait for bytes to be read.
668     */
669    public void setReadTimeout(int readTimeout) {
670        this.readTimeout = readTimeout;
671        buildHttpClients();
672    }
673
674    /**
675     * Gets the proxy value to use for API calls to Box.
676     *
677     * @return the current proxy.
678     */
679    public Proxy getProxy() {
680        return this.proxy;
681    }
682
683    /**
684     * Sets the proxy to use for API calls to Box.
685     *
686     * @param proxy the proxy to use for API calls to Box.
687     */
688    public void setProxy(Proxy proxy) {
689        this.proxy = proxy;
690        buildHttpClients();
691    }
692
693    /**
694     * Gets the username to use for a proxy that requires basic auth.
695     *
696     * @return the username to use for a proxy that requires basic auth.
697     */
698    public String getProxyUsername() {
699        return this.proxyUsername;
700    }
701
702    /**
703     * Sets the username to use for a proxy that requires basic auth.
704     *
705     * @param proxyUsername the username to use for a proxy that requires basic auth.
706     * @deprecated Use {@link BoxAPIConnection#setProxyBasicAuthentication(String, String)}
707     */
708    public void setProxyUsername(String proxyUsername) {
709        this.proxyUsername = proxyUsername;
710        buildHttpClients();
711    }
712
713    /**
714     * Gets the password to use for a proxy that requires basic auth.
715     *
716     * @return the password to use for a proxy that requires basic auth.
717     */
718    public String getProxyPassword() {
719        return this.proxyPassword;
720    }
721
722    /**
723     * Sets the proxy user and password used in basic authentication
724     *
725     * @param proxyUsername Username to use for a proxy that requires basic auth.
726     * @param proxyPassword Password to use for a proxy that requires basic auth.
727     */
728    public void setProxyBasicAuthentication(String proxyUsername, String proxyPassword) {
729        this.proxyUsername = proxyUsername;
730        this.proxyPassword = proxyPassword;
731        buildHttpClients();
732    }
733
734    /**
735     * Sets the password to use for a proxy that requires basic auth.
736     *
737     * @param proxyPassword the password to use for a proxy that requires basic auth.
738     * @deprecated Use {@link BoxAPIConnection#setProxyBasicAuthentication(String, String)}
739     */
740    public void setProxyPassword(String proxyPassword) {
741        this.proxyPassword = proxyPassword;
742        buildHttpClients();
743    }
744
745    /**
746     * Determines if this connection's access token can be refreshed. An access token cannot be refreshed if a refresh
747     * token was never set.
748     *
749     * @return true if the access token can be refreshed; otherwise false.
750     */
751    public boolean canRefresh() {
752        return this.refreshToken != null;
753    }
754
755    /**
756     * Determines if this connection's access token has expired and needs to be refreshed.
757     *
758     * @return true if the access token needs to be refreshed; otherwise false.
759     */
760    public boolean needsRefresh() {
761        boolean needsRefresh;
762
763        this.refreshLock.readLock().lock();
764        long now = System.currentTimeMillis();
765        long tokenDuration = (now - this.lastRefresh);
766        needsRefresh = (tokenDuration >= this.expires - REFRESH_EPSILON);
767        this.refreshLock.readLock().unlock();
768
769        return needsRefresh;
770    }
771
772    /**
773     * Refresh's this connection's access token using its refresh token.
774     *
775     * @throws IllegalStateException if this connection's access token cannot be refreshed.
776     */
777    public void refresh() {
778        this.refreshLock.writeLock().lock();
779
780        if (!this.canRefresh()) {
781            this.refreshLock.writeLock().unlock();
782            throw new IllegalStateException("The BoxAPIConnection cannot be refreshed because it doesn't have a "
783                + "refresh token.");
784        }
785
786        URL url;
787        try {
788            url = new URL(getTokenURL());
789        } catch (MalformedURLException e) {
790            this.refreshLock.writeLock().unlock();
791            assert false : "An invalid refresh URL indicates a bug in the SDK.";
792            throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e);
793        }
794
795        BoxAPIRequest request = createTokenRequest(url);
796
797        String json;
798        try (BoxAPIResponse boxAPIResponse = request.send()) {
799            BoxJSONResponse response = (BoxJSONResponse) boxAPIResponse;
800            json = response.getJSON();
801        } catch (BoxAPIException e) {
802            this.refreshLock.writeLock().unlock();
803            this.notifyError(e);
804            throw e;
805        }
806
807        try {
808            extractTokens(Json.parse(json).asObject());
809
810            this.notifyRefresh();
811        } finally {
812            this.refreshLock.writeLock().unlock();
813        }
814    }
815
816    /**
817     * Restores a saved connection state into this BoxAPIConnection.
818     *
819     * @param state the saved state that was created with {@link #save}.
820     * @see #save
821     */
822    public void restore(String state) {
823        JsonObject json = Json.parse(state).asObject();
824        String accessToken = json.get("accessToken").asString();
825        String refreshToken = getKeyValueOrDefault(json, "refreshToken", null);
826        long lastRefresh = json.get("lastRefresh").asLong();
827        long expires = json.get("expires").asLong();
828        String userAgent = json.get("userAgent").asString();
829        String tokenURL = getKeyValueOrDefault(json, "tokenURL", null);
830        String revokeURL = getKeyValueOrDefault(json, "revokeURL", null);
831        String baseURL = adoptBaseUrlWhenLoadingFromOldVersion(
832            getKeyValueOrDefault(json, "baseURL", DEFAULT_BASE_URL)
833        );
834        String baseUploadURL = adoptUploadBaseUrlWhenLoadingFromOldVersion(
835            getKeyValueOrDefault(json, "baseUploadURL", DEFAULT_BASE_UPLOAD_URL)
836        );
837        String authorizationURL =
838            getKeyValueOrDefault(json, "authorizationURL", DEFAULT_BASE_AUTHORIZATION_URL);
839        boolean autoRefresh = json.get("autoRefresh").asBoolean();
840
841        // Try to read deprecated value
842        int maxRequestAttempts = -1;
843        if (json.names().contains("maxRequestAttempts")) {
844            maxRequestAttempts = json.get("maxRequestAttempts").asInt();
845        }
846
847        int maxRetryAttempts = -1;
848        if (json.names().contains("maxRetryAttempts")) {
849            maxRetryAttempts = json.get("maxRetryAttempts").asInt();
850        }
851
852        this.accessToken = accessToken;
853        this.refreshToken = refreshToken;
854        this.lastRefresh = lastRefresh;
855        this.expires = expires;
856        this.userAgent = userAgent;
857        this.tokenURL = tokenURL;
858        this.revokeURL = revokeURL;
859        this.setBaseURL(baseURL);
860        this.setBaseUploadURL(baseUploadURL);
861        this.setBaseAuthorizationURL(authorizationURL);
862        this.autoRefresh = autoRefresh;
863
864        // Try to use deprecated value "maxRequestAttempts", else use newer value "maxRetryAttempts"
865        if (maxRequestAttempts > -1) {
866            this.maxRetryAttempts = maxRequestAttempts - 1;
867        } else {
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    private Response executeOnClient(OkHttpClient httpClient, Request request) {
1258        try {
1259            return httpClient.newCall(request).execute();
1260        } catch (IOException e) {
1261            throw new BoxAPIException("Couldn't connect to the Box API due to a network error. Request\n" + request, e);
1262        }
1263    }
1264
1265    /**
1266     * Used to categorize the types of resource links.
1267     */
1268    protected enum ResourceLinkType {
1269        /**
1270         * Catch-all default for resource links that are unknown.
1271         */
1272        Unknown,
1273
1274        /**
1275         * Resource URLs that point to an API endipoint such as https://api.box.com/2.0/files/:file_id.
1276         */
1277        APIEndpoint,
1278
1279        /**
1280         * Resource URLs that point to a resource that has been shared
1281         * such as https://example.box.com/s/qwertyuiop1234567890asdfghjk
1282         * or https://example.app.box.com/notes/0987654321?s=zxcvbnm1234567890asdfghjk.
1283         */
1284        SharedLink
1285    }
1286}