1/* 2 * Copyright (C) 2014 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 17package com.android.omadm.service; 18 19import android.telephony.TelephonyManager; 20import android.text.TextUtils; 21import android.util.Log; 22 23import java.io.FileOutputStream; 24import java.io.IOException; 25import java.io.InputStream; 26import java.io.OutputStream; 27import java.net.HttpURLConnection; 28import java.net.InetSocketAddress; 29import java.net.ProtocolException; 30import java.net.Proxy; 31import java.net.SocketAddress; 32import java.net.URL; 33import java.net.UnknownHostException; 34import java.util.List; 35import java.util.Map; 36 37public class DMHttpConnector { 38 private static final String TAG = "DMHttpConnector"; 39 private static final boolean DBG = DMClientService.DBG; 40 41 private HttpURLConnection mConnection; 42 43 private static final String USER_AGENT = "User-Agent"; 44 45 private static final String CACHE_CONTROL = "Cache-Control"; 46 47 private static final String ACCEPT = "Accept"; 48 49 private static final String ACCEPT_LANGUAGE = "Accept-Language"; 50 51 private static final String ACCEPT_CHARSET = "Accept-Charset"; 52 53 private static final String CONTENT_TYPE = "Content-Type"; 54 55 public static final String CONTENT_LENGTH = "Content-Length"; 56 57 private static final String X_SYNCML_HMAC = "x-syncml-hmac"; 58 59 // Sprint DM-Sess-29: User-Agent: <make>/<model> <DM-vendor>/<DM-version> 60 private static final String ANDROID_OMA_DM_CLIENT = "Google/Nexus Google/1"; 61 62 private static final String LANGUAGE_EN = "en"; 63 64 private static final String CHARSET_UTF8 = "utf-8"; 65 66 private static final String MIME_TYPE_SYNCML_DM = "application/vnd.syncml.dm"; 67 68 private static final String MIME_TYPE_SYNCML_DM_WBXML = MIME_TYPE_SYNCML_DM + "+wbxml"; 69 70 //private static final String MIME_TYPE_SYNCML_DM_XML = MIME_TYPE_SYNCML_DM + "+xml"; 71 72 private static final String CACHE_CONTROL_PRIVATE = "private"; 73 74 private String mContentType; 75 76 private final DMSession mSession; 77 78 private final DMClientService mContext; 79 80 private Proxy mProxy; 81 82 public DMHttpConnector(DMSession session) { 83 mSession = session; 84 mContext = session.getServiceContext(); 85 setHostProxy(); 86 } 87 88 /** 89 * Enable an APN by name. 90 * Called from JNI code. 91 * 92 * @param apnName 93 */ 94 public void enableApnByName(String apnName) { 95 if (DBG) logd("Enable Apn name=" + apnName); 96 } 97 98 /** 99 * Send an HTTP request. 100 * Called from JNI code. 101 * 102 * @param urlString the URL to request 103 * @param requestData the SyncML package to send 104 * @param hmacValue the HMAC value to send as a request header 105 * @return 106 */ 107 public int sendRequest(String urlString, byte[] requestData, String hmacValue) { 108 if (mContentType == null) { 109 mContentType = MIME_TYPE_SYNCML_DM_WBXML; 110 } 111 112 if (DBG) logd("Post url=" + urlString + " HMAC value=" + hmacValue); 113 114 if (urlString.isEmpty()) { 115 return DMResult.SYNCML_DM_INVALID_URI; 116 } 117 118 HttpURLConnection connection = mConnection; 119 URL url; 120 try { 121 if (connection != null) { 122 loge("overwriting old mConnection!"); 123 mConnection.disconnect(); 124 } 125 126 url = new URL(urlString); 127 128 // STOPSHIP: remove this hack for Sprint 129 if (url.getHost().contains("sprint")) { 130 String serverUrl = DMHelper.getServerUrl(mContext); 131 if (!TextUtils.isEmpty(serverUrl)) { 132 if (DBG) logd("replacing URL with Sprint URL: " + serverUrl); 133 url = new URL(serverUrl); 134 urlString = serverUrl; 135 if (DBG) logd("new URL is " + url); 136 } 137 } 138 139 if (mProxy != null) { 140 if (DBG) logd("opening connection with proxy: " + mProxy); 141 connection = (HttpURLConnection) url.openConnection(mProxy); 142 } else { 143 if (DBG) logd("opening direct connection"); 144 connection = (HttpURLConnection) url.openConnection(); 145 } 146 147 mConnection = connection; 148 } catch (Exception e) { 149 loge("bad URL", e); 150 return DMResult.SYNCML_DM_INVALID_URI; 151 } 152 153 try { 154 connection.setRequestMethod("POST"); 155 connection.addRequestProperty(ACCEPT, MIME_TYPE_SYNCML_DM_WBXML); 156 connection.addRequestProperty(ACCEPT_LANGUAGE, LANGUAGE_EN); 157 connection.addRequestProperty(ACCEPT_CHARSET, CHARSET_UTF8); 158 connection.addRequestProperty(USER_AGENT, ANDROID_OMA_DM_CLIENT); 159 connection.addRequestProperty(CACHE_CONTROL, CACHE_CONTROL_PRIVATE); 160 connection.addRequestProperty(CONTENT_TYPE, mContentType); 161 connection.addRequestProperty("Connection", "Close"); 162 if (!TextUtils.isEmpty(hmacValue)) { 163 connection.addRequestProperty(X_SYNCML_HMAC, hmacValue); 164 } 165 } catch (ProtocolException e) { 166 loge("error setting headers", e); 167 return DMResult.SYNCML_DM_IO_FAILURE; 168 } 169 170 // Log outgoing headers and content 171 HttpLog log = new HttpLog(mContext, mSession.getLogFileName()); 172 log.logHeaders(connection.getRequestProperties()); 173 log.logContent(mContentType, requestData); 174 log.closeLogFile(); 175 176 try { 177 // Send request data 178 OutputStream stream = connection.getOutputStream(); 179 stream.write(requestData); 180 stream.flush(); 181 182 int retcode = connection.getResponseCode(); 183 if (DBG) logd(urlString + " code: " + retcode + " status: " 184 + connection.getResponseMessage()); 185 return retcode; 186 } catch (UnknownHostException ignored) { 187 loge(url + " - Unknown host exception"); 188 return DMResult.SYNCML_DM_UNKNOWN_HOST; 189 } catch (IOException e) { 190 loge(url + " - IOException error: ", e); 191 return DMResult.SYNCML_DM_SOCKET_CONNECT_ERR; 192 } 193 } 194 195 /** 196 * Get the response length. 197 * Called from JNI code. 198 * 199 * @return 200 */ 201 long getResponseLength() { 202 if (mConnection != null) { 203 return mConnection.getContentLength(); 204 } 205 return -1; 206 } 207 208 /** 209 * Get the response data. 210 * Called from JNI code. 211 * 212 * @return 213 */ 214 public byte[] getResponseData() { 215 if (mConnection == null) { 216 return null; 217 } 218 219 int dataSize = (int) getResponseLength(); 220 if (dataSize <= 0) { 221 return null; 222 } 223 224 byte[] data = new byte[dataSize]; 225 if (DBG) logd("response dataSize=" + dataSize); 226 227 String contentType = mConnection.getContentType(); 228 if (contentType == null) { 229 loge("getResponseData: contentType is null"); 230 return null; 231 } 232 233 if (DBG) logd("content type = " + contentType); 234 235 try { 236 InputStream resInput = mConnection.getInputStream(); 237 if (resInput != null) { 238 if (DBG) logd("inputstream type = " + resInput.getClass().getName()); 239 int readTotal = 0; 240 synchronized (this) { 241 int read; 242 while ((read = resInput.read(data, readTotal, dataSize - readTotal)) != -1) { 243 readTotal += read; 244 } 245 } 246 if (DBG) logd("InputStream read len = " + readTotal); 247 } 248 } catch (IOException e) { 249 loge("IOException reading response", e); 250 } 251 252 // log incoming headers and content 253 HttpLog log = new HttpLog(mContext, mSession.getLogFileName()); 254 log.logHeaders(mConnection.getHeaderFields()); 255 log.logContent(contentType, data); 256 log.closeLogFile(); 257 258 return data; 259 } 260 261 /** 262 * Get the response header. 263 * Called from JNI code. 264 * 265 * @param fieldName 266 * @return 267 */ 268 public String getResponseHeader(String fieldName) { 269 if (mConnection != null) { 270 return mConnection.getHeaderField(fieldName); 271 } 272 return null; 273 } 274 275 /** 276 * Set the content type. 277 * Called from JNI code. 278 * 279 * @param type 280 */ 281 public void setContentType(String type) { 282 mContentType = type; 283 } 284 285 public int closeSession() { 286 if (mConnection != null) { 287 mConnection.disconnect(); 288 } 289 290 return 1; 291 } 292 293 private void setHostProxy() { 294 String hostname = DMHelper.getProxyHostname(mContext); 295 if (TextUtils.isEmpty(hostname)) { 296 TelephonyManager tm = (TelephonyManager) mContext 297 .getSystemService(mContext.TELEPHONY_SERVICE); 298 String simOperator = tm.getSimOperator(); 299 String imsi = tm.getSubscriberId(); 300 if ("310120".equals(simOperator) || (imsi != null && imsi.startsWith("310120"))) { 301 loge("Using default proxy hostname!!"); 302 hostname = "oma.ssprov.sprint.com"; 303 } else { 304 logd("no proxy"); 305 mProxy = null; 306 return; 307 } 308 } 309 310 SocketAddress sa = InetSocketAddress.createUnresolved(hostname, 80); 311 if (DBG) logd("unresolved socket address created"); 312 313 mProxy = new Proxy(Proxy.Type.HTTP, sa); 314 if (DBG) logd("Set Proxy: " + mProxy); 315 } 316 317 static final class HttpLog { 318 319 final int mLogLevel; 320 321 FileOutputStream mOut; 322 323 public HttpLog(DMClientService clientService, String logFileName) { 324 // TODO: make into a property or preference 325 mLogLevel = clientService.getConfigDB().getSyncMLLogLevel(); 326 try { 327 if (mLogLevel > 0) { 328 mOut = new FileOutputStream(logFileName, true); 329 logd("XXXXX creating log file " + logFileName + " XXXXX"); 330 } 331 } catch (Exception ex) { 332 loge("Exception opening syncml log file=" + logFileName, ex); 333 } 334 } 335 336 public void logHeaders(Map<String, List<String>> headers) { 337 FileOutputStream out = mOut; 338 if (out == null) { 339 return; 340 } 341 StringBuilder builder = new StringBuilder(256); 342 for (Map.Entry<String, List<String>> header : headers.entrySet()) { 343 for (String value : header.getValue()) { 344 builder.append(header.getKey()).append(':').append(value).append("\r\n"); 345 } 346 } 347 try { 348 out.write(builder.toString().getBytes()); 349 } catch (IOException ex) { 350 loge("Exception writing syncml headers log", ex); 351 } 352 } 353 354 public void logContent(String contentType, byte[] body) { 355 FileOutputStream out = mOut; 356 if (out == null) { 357 return; 358 } 359 try { 360 out.write("===================================".getBytes()); 361 if (body == null || body.length == 0) { 362 out.write("empty body".getBytes()); 363 return; 364 } 365 byte[] xml = null; 366 if ((contentType.toLowerCase()).startsWith(MIME_TYPE_SYNCML_DM_WBXML)) { 367 if (mLogLevel == 2) { 368 xml = NativeDM.nativeWbxmlToXml(body); 369 if (xml != null) { 370 out.write(xml); 371 } 372 } 373 } 374 if (xml == null) { 375 out.write(body); 376 } 377 out.write("===================================\n".getBytes()); 378 } catch (Exception ex) { // catch all in case JNI throws exception 379 loge("Exception writing syncml content log", ex); 380 } 381 382 } 383 384 public void closeLogFile() { 385 FileOutputStream out = mOut; 386 if (out != null) { 387 try { 388 out.close(); 389 } catch (IOException ex) { 390 loge("Exception writing syncml headers log", ex); 391 } 392 mOut = null; 393 } 394 } 395 } 396 397 private static void logd(String msg) { 398 Log.d(TAG, msg); 399 } 400 401 private static void loge(String msg) { 402 Log.e(TAG, msg); 403 } 404 405 private static void loge(String msg, Throwable tr) { 406 Log.e(TAG, msg, tr); 407 } 408} 409