1 /* 2 * Copyright (C) 2024 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 17 package com.android.tradefed.util.gcs; 18 19 import com.android.tradefed.log.LogUtil.CLog; 20 21 import com.google.api.client.auth.oauth2.Credential; 22 import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler; 23 import com.google.api.client.http.HttpRequest; 24 import com.google.api.client.http.HttpRequestInitializer; 25 import com.google.api.client.http.HttpResponse; 26 import com.google.api.client.http.HttpUnsuccessfulResponseHandler; 27 import com.google.api.client.util.ExponentialBackOff; 28 import com.google.auth.Credentials; 29 import com.google.auth.oauth2.ComputeEngineCredentials; 30 import com.google.auth.oauth2.GoogleCredentials; 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.Collection; 39 40 public class GoogleApiClientUtilBase { 41 42 public static final String APP_NAME = "tradefed"; 43 private static GoogleApiClientUtilBase sInstance = null; 44 getInstance()45 private static GoogleApiClientUtilBase getInstance() { 46 if (sInstance == null) { 47 sInstance = new GoogleApiClientUtilBase(); 48 } 49 return sInstance; 50 } 51 52 /** 53 * Try to create Google API credential with default credential. 54 * 55 * <p>Only default credential is used. 56 * 57 * @param scopes scopes for the credential. 58 * @return a {@link Credential} 59 * @throws IOException 60 * @throws GeneralSecurityException 61 */ createCredential(Collection<String> scopes)62 public static Credentials createCredential(Collection<String> scopes) 63 throws IOException, GeneralSecurityException { 64 return getInstance().doCreateDefaultCredential(scopes); 65 } 66 67 /** 68 * Create credential from json key file. 69 * 70 * @param file is the p12 key file 71 * @param scopes is the API's scope. 72 * @return a {@link Credential}. 73 * @throws FileNotFoundException 74 * @throws IOException 75 * @throws GeneralSecurityException 76 */ createCredentialFromJsonKeyFile(File file, Collection<String> scopes)77 public static Credentials createCredentialFromJsonKeyFile(File file, Collection<String> scopes) 78 throws IOException, GeneralSecurityException { 79 return getInstance().doCreateCredentialFromJsonKeyFile(file, scopes); 80 } 81 82 @VisibleForTesting doCreateCredentialFromJsonKeyFile(File file, Collection<String> scopes)83 protected Credentials doCreateCredentialFromJsonKeyFile(File file, Collection<String> scopes) 84 throws IOException, GeneralSecurityException { 85 Credentials credentail = 86 GoogleCredentials.fromStream(new FileInputStream(file)).createScoped(scopes); 87 return credentail; 88 } 89 90 @VisibleForTesting doCreateDefaultCredential(Collection<String> scopes)91 protected Credentials doCreateDefaultCredential(Collection<String> scopes) throws IOException { 92 try { 93 CLog.d("Using local authentication."); 94 return ComputeEngineCredentials.getApplicationDefault().createScoped(scopes); 95 } catch (IOException e) { 96 CLog.e( 97 "Try 'gcloud auth application-default login' to login for " 98 + "personal account; Or 'export " 99 + "GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json' " 100 + "for service account."); 101 throw e; 102 } 103 } 104 105 /** 106 * @param requestInitializer a {@link HttpRequestInitializer}, normally it's {@link Credential}. 107 * @param connectTimeout connect timeout in milliseconds. 108 * @param readTimeout read timeout in milliseconds. 109 * @return a {@link HttpRequestInitializer} with timeout. 110 */ setHttpTimeout( final HttpRequestInitializer requestInitializer, int connectTimeout, int readTimeout)111 public static HttpRequestInitializer setHttpTimeout( 112 final HttpRequestInitializer requestInitializer, int connectTimeout, int readTimeout) { 113 return new HttpRequestInitializer() { 114 @Override 115 public void initialize(HttpRequest request) throws IOException { 116 requestInitializer.initialize(request); 117 request.setConnectTimeout(connectTimeout); 118 request.setReadTimeout(readTimeout); 119 } 120 }; 121 } 122 123 /** 124 * Setup a retry strategy for the provided HttpRequestInitializer. In case of server errors 125 * requests will be automatically retried with an exponential backoff. 126 * 127 * @param initializer - an initializer which will setup a retry strategy. 128 * @return an initializer that will retry failed requests automatically. 129 */ 130 public static HttpRequestInitializer configureRetryStrategyAndTimeout( 131 HttpRequestInitializer initializer, int connectTimeout, int readTimeout) { 132 return new HttpRequestInitializer() { 133 @Override 134 public void initialize(HttpRequest request) throws IOException { 135 initializer.initialize(request); 136 request.setConnectTimeout(connectTimeout); 137 request.setReadTimeout(readTimeout); 138 request.setUnsuccessfulResponseHandler(new RetryResponseHandler()); 139 } 140 }; 141 } 142 143 /** 144 * Setup a retry strategy for the provided HttpRequestInitializer. In case of server errors 145 * requests will be automatically retried with an exponential backoff. 146 * 147 * @param initializer - an initializer which will setup a retry strategy. 148 * @return an initializer that will retry failed requests automatically. 149 */ 150 public static HttpRequestInitializer configureRetryStrategy( 151 HttpRequestInitializer initializer) { 152 return new HttpRequestInitializer() { 153 @Override 154 public void initialize(HttpRequest request) throws IOException { 155 initializer.initialize(request); 156 request.setUnsuccessfulResponseHandler(new RetryResponseHandler()); 157 } 158 }; 159 } 160 161 private static class RetryResponseHandler implements HttpUnsuccessfulResponseHandler { 162 // Initial interval to wait before retrying if a request fails. 163 private static final int INITIAL_RETRY_INTERVAL = 1000; 164 private static final int MAX_RETRY_INTERVAL = 3 * 60000; // Set max interval to 3 minutes. 165 166 private final HttpUnsuccessfulResponseHandler backOffHandler; 167 168 public RetryResponseHandler() { 169 backOffHandler = 170 new HttpBackOffUnsuccessfulResponseHandler( 171 new ExponentialBackOff.Builder() 172 .setInitialIntervalMillis(INITIAL_RETRY_INTERVAL) 173 .setMaxIntervalMillis(MAX_RETRY_INTERVAL) 174 .build()); 175 } 176 177 @Override 178 public boolean handleResponse( 179 HttpRequest request, HttpResponse response, boolean supportsRetry) 180 throws IOException { 181 CLog.w( 182 "Request to %s failed: %d %s", 183 request.getUrl(), response.getStatusCode(), response.getStatusMessage()); 184 if (response.getStatusCode() == 400) { 185 return false; 186 } 187 return backOffHandler.handleResponse(request, response, supportsRetry); 188 } 189 } 190 } 191