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