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