• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.libraries.rcs.simpleclient.filetransfer;
18 
19 import android.net.Uri;
20 import android.os.Build;
21 import android.util.Log;
22 
23 import com.android.internal.http.multipart.FilePart;
24 import com.android.internal.http.multipart.MultipartEntity;
25 import com.android.internal.http.multipart.Part;
26 import com.android.internal.http.multipart.StringPart;
27 import com.android.libraries.rcs.simpleclient.filetransfer.requestexecutor.HttpRequestExecutor;
28 
29 import com.google.common.io.ByteStreams;
30 import com.google.common.util.concurrent.FutureCallback;
31 import com.google.common.util.concurrent.Futures;
32 import com.google.common.util.concurrent.ListenableFuture;
33 import com.google.common.util.concurrent.ListeningExecutorService;
34 import com.google.common.util.concurrent.MoreExecutors;
35 
36 import org.apache.http.Header;
37 import org.apache.http.HttpEntity;
38 import org.apache.http.HttpResponse;
39 import org.apache.http.auth.AUTH;
40 import org.apache.http.auth.AuthScheme;
41 import org.apache.http.auth.MalformedChallengeException;
42 import org.apache.http.client.HttpClient;
43 import org.apache.http.client.methods.HttpPost;
44 import org.apache.http.client.params.AuthPolicy;
45 import org.apache.http.conn.ClientConnectionManager;
46 import org.apache.http.conn.scheme.Scheme;
47 import org.apache.http.conn.scheme.SchemeRegistry;
48 import org.apache.http.conn.ssl.SSLSocketFactory;
49 import org.apache.http.impl.auth.DigestScheme;
50 import org.apache.http.impl.auth.RFC2617Scheme;
51 import org.apache.http.impl.client.DefaultHttpClient;
52 import org.apache.http.protocol.BasicHttpContext;
53 import org.apache.http.protocol.HttpContext;
54 
55 import java.io.ByteArrayOutputStream;
56 import java.io.File;
57 import java.io.IOException;
58 import java.io.InputStream;
59 import java.net.HttpURLConnection;
60 import java.time.Instant;
61 import java.time.ZoneId;
62 import java.time.format.DateTimeFormatter;
63 import java.util.concurrent.Executors;
64 
65 /** File upload functionality. */
66 final class FileUploadController {
67 
68     private static final String TAG = "FileUploadController";
69     private static final String ATTRIBUTE_PREEMPTIVE_AUTH = "preemptive-auth";
70     private static final String PARAM_NONCE = "nonce";
71     private static final String PARAM_REALM = "realm";
72     private static final String FILE_PART_NAME = "File";
73     private static final String TRANSFER_ID_PART_NAME = "tid";
74     private static final String CONTENT_TYPE = "text/plain";
75     private static final String THREE_GPP_GBA = "3gpp-gba";
76     private static final int HTTPS_PORT = 443;
77 
78     private final HttpRequestExecutor requestExecutor;
79     private final String contentServerUri;
80     private final ListeningExecutorService executor =
81             MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(4));
82     private String mCarrierName;
83 
FileUploadController(HttpRequestExecutor requestExecutor, String contentServerUri, String carrierName)84     FileUploadController(HttpRequestExecutor requestExecutor, String contentServerUri,
85             String carrierName) {
86         this.requestExecutor = requestExecutor;
87         this.contentServerUri = contentServerUri;
88         this.mCarrierName = carrierName;
89     }
90 
uploadFile( String transactionId, File file)91     public ListenableFuture<String> uploadFile(
92             String transactionId, File file) {
93         DefaultHttpClient httpClient = getSecureHttpClient();
94 
95         Log.i(TAG, "sendEmptyPost");
96         // Send an empty post.
97         ListenableFuture<HttpResponse> initialResponseFuture = sendEmptyPost(httpClient);
98 
99         BasicHttpContext httpContext = new BasicHttpContext();
100         ListenableFuture<AuthScheme> prepareAuthFuture =
101                 Futures.transform(
102                         initialResponseFuture,
103                         initialResponse -> {
104                             Log.i(TAG, "Response for the empty post: "
105                                     + initialResponse.getStatusLine());
106                             if (initialResponse.getStatusLine().getStatusCode()
107                                     != HttpURLConnection.HTTP_UNAUTHORIZED) {
108                                 throw new IllegalArgumentException(
109                                         "Expected HTTP_UNAUTHORIZED, but got "
110                                                 + initialResponse.getStatusLine());
111                             }
112                             try {
113                                 initialResponse.getEntity().consumeContent();
114                             } catch (IOException e) {
115                                 throw new IllegalArgumentException(e);
116                             }
117 
118                             // Override nonce and realm in the HTTP context.
119                             RFC2617Scheme authScheme = createAuthScheme(initialResponse);
120                             httpContext.setAttribute(ATTRIBUTE_PREEMPTIVE_AUTH, authScheme);
121                             return authScheme;
122                         },
123                         executor);
124 
125         // Executing the post with credentials.
126         return Futures.transformAsync(
127                 prepareAuthFuture,
128                 authScheme ->
129                         executeAuthenticatedPost(
130                                 httpClient, httpContext, authScheme, transactionId, file),
131                 executor);
132     }
133 
createAuthScheme(HttpResponse initialResponse)134     private RFC2617Scheme createAuthScheme(HttpResponse initialResponse) {
135         if (!initialResponse.containsHeader(AUTH.WWW_AUTH)) {
136             throw new IllegalArgumentException(
137                     AUTH.WWW_AUTH + " header not found in the original response.");
138         }
139 
140         Header authHeader = initialResponse.getFirstHeader(AUTH.WWW_AUTH);
141         String scheme = authHeader.getValue();
142 
143         if (scheme.contains(AuthPolicy.DIGEST)) {
144             DigestScheme digestScheme = new DigestScheme();
145             try {
146                 digestScheme.processChallenge(authHeader);
147             } catch (MalformedChallengeException e) {
148                 throw new IllegalArgumentException(e);
149             }
150             return digestScheme;
151         } else {
152             throw new IllegalArgumentException("Unable to create authentication scheme " + scheme);
153         }
154     }
155 
getSecureHttpClient()156     private DefaultHttpClient getSecureHttpClient() {
157         SSLSocketFactory socketFactory = SSLSocketFactory.getSocketFactory();
158         Uri uri = Uri.parse(contentServerUri);
159         int port = uri.getPort();
160         if (port <= 0) {
161             port = HTTPS_PORT;
162         }
163 
164         Scheme scheme = new Scheme("https", socketFactory, port);
165         DefaultHttpClient httpClient = new DefaultHttpClient();
166         ClientConnectionManager manager = httpClient.getConnectionManager();
167         SchemeRegistry registry = manager.getSchemeRegistry();
168         registry.register(scheme);
169 
170         return httpClient;
171     }
172 
sendEmptyPost(HttpClient httpClient)173     private ListenableFuture<HttpResponse> sendEmptyPost(HttpClient httpClient) {
174         Log.i(TAG, "Sending an empty post: ");
175         HttpPost emptyPost = new HttpPost(contentServerUri);
176         emptyPost.setHeader("User-Agent", getUserAgent());
177         return executor.submit(() -> httpClient.execute(emptyPost));
178     }
179 
executeAuthenticatedPost( DefaultHttpClient httpClient, HttpContext context, AuthScheme authScheme, String transactionId, File file)180     private ListenableFuture<String> executeAuthenticatedPost(
181             DefaultHttpClient httpClient,
182             HttpContext context,
183             AuthScheme authScheme,
184             String transactionId,
185             File file)
186             throws IOException {
187 
188         Part[] parts = {
189                 new StringPart(TRANSFER_ID_PART_NAME, transactionId),
190                 new FilePart(FILE_PART_NAME, file)
191         };
192         MultipartEntity entity = new MultipartEntity(parts);
193 
194         HttpPost postRequest = new HttpPost(contentServerUri);
195         postRequest.setHeader("User-Agent", getUserAgent());
196         postRequest.setEntity(entity);
197         Log.i(TAG, "Created file upload POST:" + contentServerUri);
198 
199         ListenableFuture<HttpResponse> responseFuture =
200                 requestExecutor.executeAuthenticatedRequest(httpClient, context, postRequest,
201                         authScheme);
202 
203         Futures.addCallback(
204                 responseFuture,
205                 new FutureCallback<HttpResponse>() {
206                     @Override
207                     public void onSuccess(HttpResponse response) {
208                         Log.i(TAG, "onSuccess:" + response.toString());
209                         Log.i(TAG, "statusLine:" + response.getStatusLine());
210                         Log.i(TAG, "statusCode:" + response.getStatusLine().getStatusCode());
211                         Log.i(TAG, "contentLentgh:" + response.getEntity().getContentLength());
212                         Log.i(TAG, "contentType:" + response.getEntity().getContentType());
213                     }
214 
215                     @Override
216                     public void onFailure(Throwable t) {
217                         Log.e(TAG, "onFailure", t);
218                         throw new IllegalArgumentException(t);
219                     }
220                 },
221                 executor);
222 
223         return Futures.transform(
224                 responseFuture,
225                 response -> {
226                     try {
227                         return consumeResponse(response);
228                     } catch (IOException e) {
229                         throw new IllegalArgumentException(e);
230                     }
231                 },
232                 executor);
233     }
234 
235     public String consumeResponse(HttpResponse response) throws IOException {
236         int statusCode = response.getStatusLine().getStatusCode();
237         if (statusCode != HttpURLConnection.HTTP_OK) {
238             throw new IllegalArgumentException(
239                     "Server responded with error code " + statusCode + "!");
240         }
241         HttpEntity responseEntity = response.getEntity();
242 
243         if (responseEntity == null) {
244             throw new IOException("Did not receive a response body.");
245         }
246 
247         return readResponseData(responseEntity.getContent());
248     }
249 
250     public String readResponseData(InputStream inputStream) throws IOException {
251         Log.i(TAG, "readResponseData");
252         ByteArrayOutputStream data = new ByteArrayOutputStream();
253         ByteStreams.copy(inputStream, data);
254 
255         data.flush();
256         Log.i(TAG, "Parsed HTTP POST response: " + data.toString());
257 
258         return data.toString();
259     }
260 
261     private String getUserAgent() {
262         String buildId = Build.ID;
263         String buildDate = DateTimeFormatter.ofPattern("yyyy-MM-dd")
264                 .withZone(ZoneId.systemDefault())
265                 .format(Instant.ofEpochMilli(Build.TIME));
266         String buildVersion = Build.VERSION.RELEASE_OR_CODENAME;
267         String deviceName = Build.DEVICE;
268         String userAgent = String.format("%s %s %s %s %s %s %s",
269                 mCarrierName, buildId, buildDate, "Android", buildVersion,
270                 deviceName, THREE_GPP_GBA);
271         Log.i(TAG, "UserAgent:" + userAgent);
272         return userAgent;
273     }
274 }
275