1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.tradefed.util;
17 
18 import com.android.tradefed.config.GlobalConfiguration;
19 import com.android.tradefed.host.HostOptions;
20 import com.android.tradefed.log.LogUtil.CLog;
21 
22 import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
23 import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
24 import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler;
25 import com.google.api.client.http.HttpRequest;
26 import com.google.api.client.http.HttpRequestInitializer;
27 import com.google.api.client.http.HttpResponse;
28 import com.google.api.client.http.HttpUnsuccessfulResponseHandler;
29 import com.google.api.client.json.jackson2.JacksonFactory;
30 import com.google.api.client.util.ExponentialBackOff;
31 import com.google.common.annotations.VisibleForTesting;
32 
33 import java.io.File;
34 import java.io.FileInputStream;
35 import java.io.FileNotFoundException;
36 import java.io.IOException;
37 import java.security.GeneralSecurityException;
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.Collection;
41 import java.util.List;
42 
43 /** Utils for create Google API client. */
44 public class GoogleApiClientUtil {
45 
46     public static final String APP_NAME = "tradefed";
47     private static GoogleApiClientUtil sInstance = null;
48 
getInstance()49     private static GoogleApiClientUtil getInstance() {
50         if (sInstance == null) {
51             sInstance = new GoogleApiClientUtil();
52         }
53         return sInstance;
54     }
55 
56     /**
57      * Create credential from json key file.
58      *
59      * @param file is the p12 key file
60      * @param scopes is the API's scope.
61      * @return a {@link GoogleCredential}.
62      * @throws FileNotFoundException
63      * @throws IOException
64      * @throws GeneralSecurityException
65      */
createCredentialFromJsonKeyFile( File file, Collection<String> scopes)66     public static GoogleCredential createCredentialFromJsonKeyFile(
67             File file, Collection<String> scopes) throws IOException, GeneralSecurityException {
68         return getInstance().doCreateCredentialFromJsonKeyFile(file, scopes);
69     }
70 
71     @VisibleForTesting
doCreateCredentialFromJsonKeyFile(File file, Collection<String> scopes)72     GoogleCredential doCreateCredentialFromJsonKeyFile(File file, Collection<String> scopes)
73             throws IOException, GeneralSecurityException {
74         return GoogleCredential.fromStream(
75                         new FileInputStream(file),
76                         GoogleNetHttpTransport.newTrustedTransport(),
77                         JacksonFactory.getDefaultInstance())
78                 .createScoped(scopes);
79     }
80 
81     /**
82      * Try to create credential with different key files or from local host.
83      *
84      * <p>1. If primaryKeyFile is set, try to use it to create credential. 2. Try to get
85      * corresponding key files from {@link HostOptions}. 3. Try to use backup key files. 4. Use
86      * local default credential.
87      *
88      * @param scopes scopes for the credential.
89      * @param primaryKeyFile the primary json key file; it can be null.
90      * @param hostOptionKeyFileName {@link HostOptions}'service-account-json-key-file option's key;
91      *     it can be null.
92      * @param backupKeyFiles backup key files.
93      * @return a {@link GoogleCredential}
94      * @throws IOException
95      * @throws GeneralSecurityException
96      */
createCredential( Collection<String> scopes, File primaryKeyFile, String hostOptionKeyFileName, File... backupKeyFiles)97     public static GoogleCredential createCredential(
98             Collection<String> scopes,
99             File primaryKeyFile,
100             String hostOptionKeyFileName,
101             File... backupKeyFiles)
102             throws IOException, GeneralSecurityException {
103         return getInstance()
104                 .doCreateCredential(scopes, primaryKeyFile, hostOptionKeyFileName, backupKeyFiles);
105     }
106 
107     @VisibleForTesting
doCreateCredential( Collection<String> scopes, File primaryKeyFile, String hostOptionKeyFileName, File... backupKeyFiles)108     GoogleCredential doCreateCredential(
109             Collection<String> scopes,
110             File primaryKeyFile,
111             String hostOptionKeyFileName,
112             File... backupKeyFiles)
113             throws IOException, GeneralSecurityException {
114         List<File> keyFiles = new ArrayList<File>();
115         if (primaryKeyFile != null) {
116             keyFiles.add(primaryKeyFile);
117         }
118         File hostOptionKeyFile = null;
119         if (hostOptionKeyFileName != null) {
120             try {
121                 hostOptionKeyFile =
122                         GlobalConfiguration.getInstance()
123                                 .getHostOptions()
124                                 .getServiceAccountJsonKeyFiles()
125                                 .get(hostOptionKeyFileName);
126                 if (hostOptionKeyFile != null) {
127                     keyFiles.add(hostOptionKeyFile);
128                 }
129             } catch (IllegalStateException e) {
130                 CLog.d("Global configuration haven't been initialized.");
131             }
132         }
133         keyFiles.addAll(Arrays.asList(backupKeyFiles));
134         for (File keyFile : keyFiles) {
135             if (keyFile != null) {
136                 if (keyFile.exists() && keyFile.canRead()) {
137                     CLog.d("Using %s.", keyFile.getAbsolutePath());
138                     return doCreateCredentialFromJsonKeyFile(keyFile, scopes);
139                 } else {
140                     CLog.i("No access to %s.", keyFile.getAbsolutePath());
141                 }
142             }
143         }
144         return doCreateDefaultCredential(scopes);
145     }
146 
147     @VisibleForTesting
doCreateDefaultCredential(Collection<String> scopes)148     GoogleCredential doCreateDefaultCredential(Collection<String> scopes) throws IOException {
149         try {
150             CLog.d("Using local authentication.");
151             return GoogleCredential.getApplicationDefault().createScoped(scopes);
152         } catch (IOException e) {
153             CLog.e(
154                     "Try 'gcloud auth application-default login' to login for "
155                             + "personal account; Or 'export "
156                             + "GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json' "
157                             + "for service account.");
158             throw e;
159         }
160     }
161 
162     /**
163      * Create credential from p12 file for service account.
164      *
165      * @deprecated It's better to use json key file, since p12 is deprecated by Google App Engine.
166      *     And json key file have more information.
167      * @param serviceAccount is the service account
168      * @param keyFile is the p12 key file
169      * @param scopes is the API's scope.
170      * @return a {@link GoogleCredential}.
171      * @throws GeneralSecurityException
172      * @throws IOException
173      */
174     @Deprecated
createCredentialFromP12File( String serviceAccount, File keyFile, Collection<String> scopes)175     public static GoogleCredential createCredentialFromP12File(
176             String serviceAccount, File keyFile, Collection<String> scopes)
177             throws GeneralSecurityException, IOException {
178         return new GoogleCredential.Builder()
179                 .setTransport(GoogleNetHttpTransport.newTrustedTransport())
180                 .setJsonFactory(JacksonFactory.getDefaultInstance())
181                 .setServiceAccountId(serviceAccount)
182                 .setServiceAccountScopes(scopes)
183                 .setServiceAccountPrivateKeyFromP12File(keyFile)
184                 .build();
185     }
186 
187     /**
188      * @param requestInitializer a {@link HttpRequestInitializer}, normally it's {@link
189      *     GoogleCredential}.
190      * @param connectTimeout connect timeout in milliseconds.
191      * @param readTimeout read timeout in milliseconds.
192      * @return a {@link HttpRequestInitializer} with timeout.
193      */
setHttpTimeout( final HttpRequestInitializer requestInitializer, int connectTimeout, int readTimeout)194     public static HttpRequestInitializer setHttpTimeout(
195             final HttpRequestInitializer requestInitializer, int connectTimeout, int readTimeout) {
196         return new HttpRequestInitializer() {
197             @Override
198             public void initialize(HttpRequest request) throws IOException {
199                 requestInitializer.initialize(request);
200                 request.setConnectTimeout(connectTimeout);
201                 request.setReadTimeout(readTimeout);
202             }
203         };
204     }
205 
206     /**
207      * Setup a retry strategy for the provided HttpRequestInitializer. In case of server errors
208      * requests will be automatically retried with an exponential backoff.
209      *
210      * @param initializer - an initializer which will setup a retry strategy.
211      * @return an initializer that will retry failed requests automatically.
212      */
213     public static HttpRequestInitializer configureRetryStrategy(
214             HttpRequestInitializer initializer) {
215         return new HttpRequestInitializer() {
216             @Override
217             public void initialize(HttpRequest request) throws IOException {
218                 initializer.initialize(request);
219                 request.setUnsuccessfulResponseHandler(new RetryResponseHandler());
220             }
221         };
222     }
223 
224     private static class RetryResponseHandler implements HttpUnsuccessfulResponseHandler {
225         // Initial interval to wait before retrying if a request fails.
226         private static final int INITIAL_RETRY_INTERVAL = 1000;
227         private static final int MAX_RETRY_INTERVAL = 3 * 60000; // Set max interval to 3 minutes.
228 
229         private final HttpUnsuccessfulResponseHandler backOffHandler;
230 
231         public RetryResponseHandler() {
232             backOffHandler =
233                     new HttpBackOffUnsuccessfulResponseHandler(
234                             new ExponentialBackOff.Builder()
235                                     .setInitialIntervalMillis(INITIAL_RETRY_INTERVAL)
236                                     .setMaxIntervalMillis(MAX_RETRY_INTERVAL)
237                                     .build());
238         }
239 
240         @Override
241         public boolean handleResponse(
242                 HttpRequest request, HttpResponse response, boolean supportsRetry)
243                 throws IOException {
244             CLog.w(
245                     "Request to %s failed: %d %s",
246                     request.getUrl(), response.getStatusCode(), response.getStatusMessage());
247             return backOffHandler.handleResponse(request, response, supportsRetry);
248         }
249     }
250 }
251