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