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