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 com.android.federatedcompute.internal.util.LogUtil;
20 
21 import com.google.common.collect.ImmutableSet;
22 import com.google.protobuf.ByteString;
23 
24 import org.json.JSONObject;
25 
26 import java.io.ByteArrayInputStream;
27 import java.io.ByteArrayOutputStream;
28 import java.io.IOException;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.zip.GZIPInputStream;
32 import java.util.zip.GZIPOutputStream;
33 
34 /** Utility class containing http related variable e.g. headers, method. */
35 public final class HttpClientUtil {
36     private static final String TAG = HttpClientUtil.class.getSimpleName();
37     public static final String CONTENT_ENCODING_HDR = "Content-Encoding";
38 
39     public static final String ACCEPT_ENCODING_HDR = "Accept-Encoding";
40     public static final String CONTENT_LENGTH_HDR = "Content-Length";
41     public static final String GZIP_ENCODING_HDR = "gzip";
42     public static final String CONTENT_TYPE_HDR = "Content-Type";
43     public static final String PROTOBUF_CONTENT_TYPE = "application/x-protobuf";
44     public static final String OCTET_STREAM = "application/octet-stream";
45     public static final ImmutableSet<Integer> HTTP_OK_STATUS = ImmutableSet.of(200, 201);
46 
47     public static final Integer HTTP_UNAUTHENTICATED_STATUS = 401;
48 
49     public static final Integer HTTP_UNAUTHORIZED_STATUS = 403;
50 
51     public static final ImmutableSet<Integer> HTTP_OK_OR_UNAUTHENTICATED_STATUS =
52             ImmutableSet.of(200, 201, 401);
53 
54     // This key indicates the key attestation record used for authentication.
55     public static final String ODP_AUTHENTICATION_KEY = "odp-authentication-key";
56 
57     // This key indicates a UUID as a verified token for the device.
58     public static final String ODP_AUTHORIZATION_KEY = "odp-authorization-key";
59 
60     public static final String ODP_IDEMPOTENCY_KEY = "odp-idempotency-key";
61 
62     public static final String FCP_OWNER_ID_DIGEST = "fcp-owner-id-digest";
63 
64     public static final int DEFAULT_BUFFER_SIZE = 1024;
65     public static final byte[] EMPTY_BODY = new byte[0];
66 
67     /** The supported http methods. */
68     public enum HttpMethod {
69         GET,
70         POST,
71         PUT,
72     }
73 
74     public static final class FederatedComputePayloadDataContract {
75         public static final String KEY_ID = "keyId";
76 
77         public static final String ENCRYPTED_PAYLOAD = "encryptedPayload";
78 
79         public static final String ASSOCIATED_DATA_KEY = "associatedData";
80 
81         public static final byte[] ASSOCIATED_DATA = new JSONObject().toString().getBytes();
82     }
83 
84     /** Compresses the input data using Gzip. */
compressWithGzip(byte[] uncompressedData)85     public static byte[] compressWithGzip(byte[] uncompressedData) {
86         try (ByteString.Output outputStream = ByteString.newOutput(uncompressedData.length);
87                 GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream)) {
88             gzipOutputStream.write(uncompressedData);
89             gzipOutputStream.finish();
90             return outputStream.toByteString().toByteArray();
91         } catch (IOException e) {
92             LogUtil.e(TAG, "Failed to compress using Gzip");
93             throw new IllegalStateException("Failed to compress using Gzip", e);
94         }
95     }
96 
97     /** Uncompresses the input data using Gzip. */
uncompressWithGzip(byte[] data)98     public static byte[] uncompressWithGzip(byte[] data) {
99         try (ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
100                 GZIPInputStream gzip = new GZIPInputStream(inputStream);
101                 ByteArrayOutputStream result = new ByteArrayOutputStream()) {
102             int length;
103             byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
104             while ((length = gzip.read(buffer, 0, DEFAULT_BUFFER_SIZE)) > 0) {
105                 result.write(buffer, 0, length);
106             }
107             return result.toByteArray();
108         } catch (Exception e) {
109             LogUtil.e(TAG, e, "Failed to decompress the data.");
110             throw new IllegalStateException("Failed to unscompress using Gzip", e);
111         }
112     }
113 
114     /** Calculates total bytes are sent via network based on provided http request. */
getTotalSentBytes(FederatedComputeHttpRequest request)115     public static long getTotalSentBytes(FederatedComputeHttpRequest request) {
116         long totalBytes = 0;
117         totalBytes +=
118                 request.getHttpMethod().name().length()
119                         + " ".length()
120                         + request.getUri().length()
121                         + " HTTP/1.1\r\n".length();
122         for (String key : request.getExtraHeaders().keySet()) {
123             totalBytes +=
124                     key.length()
125                             + ": ".length()
126                             + request.getExtraHeaders().get(key).length()
127                             + "\r\n".length();
128         }
129         if (request.getExtraHeaders().containsKey(CONTENT_LENGTH_HDR)) {
130             totalBytes += Long.parseLong(request.getExtraHeaders().get(CONTENT_LENGTH_HDR));
131         }
132         return totalBytes;
133     }
134 
135     /** Calculates total bytes are received via network based on provided http response. */
getTotalReceivedBytes(FederatedComputeHttpResponse response)136     public static long getTotalReceivedBytes(FederatedComputeHttpResponse response) {
137         long totalBytes = 0;
138         boolean foundContentLengthHdr = false;
139         for (Map.Entry<String, List<String>> header : response.getHeaders().entrySet()) {
140             if (header.getKey() == null) {
141                 continue;
142             }
143             for (String headerValue : header.getValue()) {
144                 totalBytes += header.getKey().length() + ": ".length();
145                 totalBytes += headerValue == null ? 0 : headerValue.length();
146             }
147             // Uses Content-Length header to estimate total received bytes which is the most
148             // accurate.
149             if (header.getKey().equals(CONTENT_LENGTH_HDR)) {
150                 totalBytes += Long.parseLong(header.getValue().get(0));
151                 foundContentLengthHdr = true;
152             }
153         }
154         if (!foundContentLengthHdr && response.getPayload() != null) {
155             totalBytes += response.getPayload().length;
156         }
157         return totalBytes;
158     }
159 
HttpClientUtil()160     private HttpClientUtil() {}
161 }
162