1 /* 2 * Copyright (C) 2023 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.federatedcompute.services.http; 18 19 import static com.android.federatedcompute.services.common.FederatedComputeExecutors.getBlockingExecutor; 20 import static com.android.federatedcompute.services.http.HttpClientUtil.HTTP_OK_STATUS; 21 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 25 import com.android.federatedcompute.internal.util.LogUtil; 26 import com.android.federatedcompute.services.common.Flags; 27 import com.android.federatedcompute.services.common.PhFlags; 28 29 import com.google.common.annotations.VisibleForTesting; 30 import com.google.common.util.concurrent.Futures; 31 import com.google.common.util.concurrent.ListenableFuture; 32 33 import java.io.BufferedOutputStream; 34 import java.io.ByteArrayOutputStream; 35 import java.io.IOException; 36 import java.io.InputStream; 37 import java.net.HttpURLConnection; 38 import java.net.MalformedURLException; 39 import java.net.URL; 40 import java.net.URLConnection; 41 import java.util.Map; 42 import java.util.Objects; 43 import java.util.concurrent.TimeUnit; 44 45 /** 46 * The HTTP client to be used by the FederatedCompute to communicate with remote federated servers. 47 */ 48 public class HttpClient { 49 private static final String TAG = HttpClient.class.getSimpleName(); 50 private static final int NETWORK_CONNECT_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(5); 51 private static final int NETWORK_READ_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(30); 52 private final Flags mFlags; 53 HttpClient()54 public HttpClient() { 55 mFlags = PhFlags.getInstance(); 56 } 57 58 @NonNull 59 @VisibleForTesting setup(@onNull URL url)60 URLConnection setup(@NonNull URL url) throws IOException { 61 Objects.requireNonNull(url); 62 URLConnection urlConnection = url.openConnection(); 63 urlConnection.setConnectTimeout(NETWORK_CONNECT_TIMEOUT_MS); 64 urlConnection.setReadTimeout(NETWORK_READ_TIMEOUT_MS); 65 return urlConnection; 66 } 67 68 /** 69 * Perform HTTP requests based on given information asynchronously with retries in case http 70 * will return not OK response code. 71 */ 72 @NonNull performRequestAsyncWithRetry( FederatedComputeHttpRequest request)73 public ListenableFuture<FederatedComputeHttpResponse> performRequestAsyncWithRetry( 74 FederatedComputeHttpRequest request) { 75 try { 76 return getBlockingExecutor().submit(() -> performRequestWithRetry(request)); 77 } catch (Exception e) { 78 return Futures.immediateFailedFuture(e); 79 } 80 } 81 82 /** Perform HTTP requests based on given information with retries. */ 83 @NonNull performRequestWithRetry(FederatedComputeHttpRequest request)84 public FederatedComputeHttpResponse performRequestWithRetry(FederatedComputeHttpRequest request) 85 throws IOException { 86 int count = 0; 87 FederatedComputeHttpResponse response = null; 88 int retryLimit = mFlags.getHttpRequestRetryLimit(); 89 while (count < retryLimit) { 90 try { 91 response = performRequest(request); 92 if (HTTP_OK_STATUS.contains(response.getStatusCode())) { 93 return response; 94 } 95 // we want to continue retry in case it is IO exception. 96 } catch (IOException e) { 97 // propagate IO exception after RETRY_LIMIT times attempt. 98 if (count >= retryLimit - 1) { 99 throw e; 100 } 101 } finally { 102 count++; 103 } 104 } 105 return response; 106 } 107 108 /** Perform HTTP requests based on given information. */ 109 @NonNull performRequest(FederatedComputeHttpRequest request)110 public FederatedComputeHttpResponse performRequest(FederatedComputeHttpRequest request) 111 throws IOException { 112 if (request.getUri() == null || request.getHttpMethod() == null) { 113 LogUtil.e(TAG, "Endpoint or http method is empty"); 114 throw new IllegalArgumentException("Endpoint or http method is empty"); 115 } 116 117 URL url; 118 try { 119 url = new URL(request.getUri()); 120 } catch (MalformedURLException e) { 121 LogUtil.e(TAG, e, "Malformed registration target URL"); 122 throw new IllegalArgumentException("Malformed registration target URL", e); 123 } 124 125 HttpURLConnection urlConnection; 126 try { 127 urlConnection = (HttpURLConnection) setup(url); 128 } catch (IOException e) { 129 LogUtil.e(TAG, e, "Failed to open target URL"); 130 throw new IOException("Failed to open target URL", e); 131 } 132 133 try { 134 urlConnection.setRequestMethod(request.getHttpMethod().name()); 135 urlConnection.setInstanceFollowRedirects(true); 136 137 if (request.getExtraHeaders() != null && !request.getExtraHeaders().isEmpty()) { 138 for (Map.Entry<String, String> entry : request.getExtraHeaders().entrySet()) { 139 urlConnection.setRequestProperty(entry.getKey(), entry.getValue()); 140 } 141 } 142 143 if (request.getBody() != null && request.getBody().length > 0) { 144 urlConnection.setDoOutput(true); 145 try (BufferedOutputStream out = 146 new BufferedOutputStream(urlConnection.getOutputStream())) { 147 out.write(request.getBody()); 148 } 149 } 150 151 int responseCode = urlConnection.getResponseCode(); 152 if (HTTP_OK_STATUS.contains(responseCode)) { 153 return new FederatedComputeHttpResponse.Builder() 154 .setPayload( 155 getByteArray( 156 urlConnection.getInputStream(), 157 urlConnection.getContentLengthLong())) 158 .setHeaders(urlConnection.getHeaderFields()) 159 .setStatusCode(responseCode) 160 .build(); 161 } else { 162 return new FederatedComputeHttpResponse.Builder() 163 .setPayload( 164 getByteArray( 165 urlConnection.getErrorStream(), 166 urlConnection.getContentLengthLong())) 167 .setHeaders(urlConnection.getHeaderFields()) 168 .setStatusCode(responseCode) 169 .build(); 170 } 171 } catch (IOException e) { 172 LogUtil.e(TAG, e, "Failed to get registration response"); 173 throw new IOException("Failed to get registration response", e); 174 } finally { 175 if (urlConnection != null) { 176 urlConnection.disconnect(); 177 } 178 } 179 } 180 getByteArray(@ullable InputStream in, long contentLength)181 private byte[] getByteArray(@Nullable InputStream in, long contentLength) throws IOException { 182 if (contentLength == 0) { 183 return HttpClientUtil.EMPTY_BODY; 184 } 185 try { 186 // TODO(b/297952090): evaluate the large file download. 187 byte[] buffer = new byte[HttpClientUtil.DEFAULT_BUFFER_SIZE]; 188 ByteArrayOutputStream out = new ByteArrayOutputStream(); 189 int bytesRead; 190 while ((bytesRead = in.read(buffer)) != -1) { 191 out.write(buffer, 0, bytesRead); 192 } 193 return out.toByteArray(); 194 } finally { 195 in.close(); 196 } 197 } 198 } 199