/* Swisscom Safe Connect Copyright (C) 2014 Swisscom This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.swisscom.safeconnect.backend; import android.content.Context; import android.os.Build; import android.util.Base64; import android.util.Log; import com.swisscom.safeconnect.BuildConfig; import com.swisscom.safeconnect.model.IncidentType; import com.swisscom.safeconnect.model.PlumberAuthResponse; import com.swisscom.safeconnect.model.PlumberDevicesResponseList; import com.swisscom.safeconnect.model.PlumberIncidentCountResponse; import com.swisscom.safeconnect.model.PlumberIncidentsResponseList; import com.swisscom.safeconnect.model.PlumberLastConnectionLogResponseList; import com.swisscom.safeconnect.model.PlumberPurchaseResponse; import com.swisscom.safeconnect.model.PlumberResponse; import com.swisscom.safeconnect.model.PlumberStatsResponse; import com.swisscom.safeconnect.model.PlumberSubscriptionResponse; import com.swisscom.safeconnect.model.PlumberSubscriptionResponseList; import com.swisscom.safeconnect.model.RawResponse; import com.swisscom.safeconnect.security.Token; import com.swisscom.safeconnect.utils.Config; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.StringEntity; import org.json.JSONException; import org.json.JSONObject; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; /** * Created by cianci on 17.04.14. */ public class BackendConnector { public static interface ResponseCallback<T extends PlumberResponse> { public void onRequestComplete(int statusCode, T response); } /** * provides a task which is used to execute HTTP request to Plumber */ public static interface TaskProvider { public PlumberTask getTask(); } private TaskProvider taskProvider; public BackendConnector(TaskProvider taskProvider) { this.taskProvider = taskProvider; } public BackendConnector(final Context context) { this(new TaskProvider() { @Override public PlumberTask getTask() { return new PlumberTask(context); } }); } /** * This method is used the first time for registering a new user * Returns the http response code * * @param phoneNumber */ public void registerUser(final String phoneNumber, final String deviceId, final String language, final ResponseCallback<PlumberAuthResponse> callback) { PlumberTask task = taskProvider.getTask(); InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("auth/register", phoneNumber, "l", language, "d", deviceId); return new HttpGet(url); } @Override public void onRequestComplete() { if (response.status == 200) { callback.onRequestComplete(response.status, new PlumberAuthResponse(response.body)); } else { callback.onRequestComplete(response.status, null); } } }; task.execute(taskCallback); } /** * This method should be used to authenticate a device which is going to be used * to remove other devices if user reached the device registration limit */ public void registerForDeviceRemoval(final String phoneNumber, final String deviceId, final String language, final ResponseCallback<PlumberAuthResponse> callback) { PlumberTask task = taskProvider.getTask(); InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("auth/authrm", phoneNumber, "l", language, "d", deviceId); return new HttpGet(url); } @Override public void onRequestComplete() { if (response.status == 200) { callback.onRequestComplete(response.status, new PlumberAuthResponse(response.body)); } else { callback.onRequestComplete(response.status, null); } } }; task.execute(taskCallback); } /** * This method is used to validate the SMS Token the user received * Returns status, username, password and token in json format * * @param phoneNumber * @param smsToken * @return */ public void validateUser(final String phoneNumber, final String deviceId, final String smsToken, final int tablet, final String language, final ResponseCallback<PlumberAuthResponse> callback) { PlumberTask task = taskProvider.getTask(); InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("auth/validate", phoneNumber, "v", smsToken, "tbl", String.valueOf(tablet), "l", language, "d", deviceId); return new HttpGet(url); } @Override public void onRequestComplete() { if (response.status == 200) { callback.onRequestComplete(response.status, new PlumberAuthResponse(response.body)); } else { callback.onRequestComplete(response.status, null); } } }; task.execute(taskCallback); } /** * This method is used for authenticate a user with his previous authentication token * Returns status, username, password and token in json format * * @param phoneNumber * @param authToken * @return */ public void authenticateUser(final String phoneNumber, final String deviceId, final String authToken, final ResponseCallback<PlumberAuthResponse> callback) { PlumberTask task = taskProvider.getTask(); InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("auth/auth", phoneNumber, "a", authToken, "d", deviceId); return new HttpGet(url); } @Override public void onRequestComplete() { if (response.status == 200) { callback.onRequestComplete(response.status, new PlumberAuthResponse(response.body)); } else { callback.onRequestComplete(response.status, null); } } }; task.execute(taskCallback); } /** * will auth user on the caller's thread * @param phoneNumber * @param authToken * @return */ public PlumberAuthResponse authenticateUserSync(final String phoneNumber, final String deviceId, final String authToken) { String url = buildUrl("auth/auth", phoneNumber, "a", authToken, "d", deviceId); RawResponse response = taskProvider.getTask().doSyncRequest(new HttpGet(url)); PlumberAuthResponse auth = new PlumberAuthResponse(response.body); return auth; } /** * will do an HTTP request for user stats on the caller's thread * @param phoneNumber phone number * @return statistics */ public PlumberStatsResponse getUserStatsSync(String phoneNumber, String deviceId) { String url = buildUrl("stats/stat", phoneNumber, "offset", String.valueOf(UtcOffset.get()), "d", deviceId); RawResponse response = taskProvider.getTask().doSyncRequest(new HttpGet(url)); if (response.status != 200) { return new PlumberStatsResponse(); } PlumberStatsResponse stats = new PlumberStatsResponse(response.body); return stats; } /** * Same as getUserStatsSync, but async * * @param phoneNumber phone number * @return statistics */ public void getUserStatsAsync(final String phoneNumber, final String deviceId, final ResponseCallback<PlumberStatsResponse> callback) { PlumberTask task = taskProvider.getTask(); task.setHttpTimeout(2000); InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("stats/stat", phoneNumber, "offset", String.valueOf(UtcOffset.get()), "d", deviceId); return new HttpGet(url); } @Override public void onRequestComplete() { if (response.status == 200) { callback.onRequestComplete(response.status, new PlumberStatsResponse(response.body)); } else { callback.onRequestComplete(response.status, null); } } }; task.execute(taskCallback); } /** * gets incidents * @param phoneNumber * @param deviceId * @param type incident type * @param limit returns only "limit" number of incidents * @param callback */ public void getIncidents(final String phoneNumber, final String deviceId, final IncidentType type, final int limit, final Config.StatisticsPeriod statsPeriod, final ResponseCallback<PlumberIncidentsResponseList> callback) { PlumberTask task = taskProvider.getTask(); InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("alert/incident", phoneNumber, "d", deviceId, "l", String.valueOf(limit), "y", type.toUrlParam(), "offset", String.valueOf(UtcOffset.get()), "p", String.valueOf(statsPeriod.ordinal())); return new HttpGet(url); } @Override public void onRequestComplete() { callback.onRequestComplete(response.status, new PlumberIncidentsResponseList(response.body)); } }; task.execute(taskCallback); } /** * gets number of incidents by type * @param phoneNumber * @param deviceId * @return */ public PlumberIncidentCountResponse getIncidentsCntSync(final String phoneNumber, final String deviceId, Config.StatisticsPeriod statsPeriod) { String url = buildUrl("alert/incidentcount", phoneNumber, "d", deviceId, "offset", String.valueOf(UtcOffset.get()), "p", String.valueOf(statsPeriod.ordinal())); RawResponse response = taskProvider.getTask().doSyncRequest(new HttpGet(url)); if (response.status != 200) { return new PlumberIncidentCountResponse(""); } PlumberIncidentCountResponse result = new PlumberIncidentCountResponse(response.body); return result; } /** * gets number of incidents by type - Async * @param phoneNumber * @param deviceId * @return */ public void getIncidentsCntASync(final String phoneNumber, final String deviceId, final Config.StatisticsPeriod statsPeriod, final ResponseCallback<PlumberIncidentCountResponse> callback) { PlumberTask task = taskProvider.getTask(); InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("alert/incidentcount", phoneNumber, "d", deviceId, "offset", String.valueOf(UtcOffset.get()), "p", String.valueOf(statsPeriod.ordinal())); return new HttpGet(url); } @Override public void onRequestComplete() { callback.onRequestComplete(response.status, new PlumberIncidentCountResponse(response.body)); } }; task.execute(taskCallback); } /** * Returns the current subscription of a user * * @param phoneNumber * @return */ public void getOwnSubscription(final String phoneNumber, final String deviceId, final ResponseCallback<PlumberSubscriptionResponse> callback) { PlumberTask task = taskProvider.getTask(); InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("subscription/my", phoneNumber, "d", deviceId); return new HttpGet(url); } @Override public void onRequestComplete() { callback.onRequestComplete(response.status, new PlumberSubscriptionResponse(response.body)); } }; task.execute(taskCallback); } /** * Get all available subscriptions * * @param phoneNumber * @return */ public void getAvailableSubscriptions(final String phoneNumber, final ResponseCallback<PlumberSubscriptionResponseList> callback) { PlumberTask task = taskProvider.getTask(); InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("subscription/list", phoneNumber); return new HttpGet(url); } @Override public void onRequestComplete() { callback.onRequestComplete(response.status, new PlumberSubscriptionResponseList(response.body)); } }; task.execute(taskCallback); } /** * Subscribe to a new subscription * * @param phoneNumber * @param subscriptionId * @return */ public void subscribe(final String phoneNumber, final String deviceId, final int subscriptionId, final ResponseCallback<PlumberSubscriptionResponse> callback) { PlumberTask task = taskProvider.getTask(); //workaround because the client gets disconnected and does not receive any http response task.setHttpTimeout(2000); InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("subscription/subscribe", phoneNumber, "s", String.valueOf(subscriptionId), "d", deviceId); return new HttpGet(url); } @Override public void onRequestComplete() { callback.onRequestComplete(response.status, new PlumberSubscriptionResponse(response.body)); } }; task.execute(taskCallback); } /** * Get all active registered devices for a phone number * * @param phoneNumber * @param deviceId * @return */ public void listRegisteredDevices(final String phoneNumber, final String deviceId, final ResponseCallback<PlumberDevicesResponseList> callback) { PlumberTask task = taskProvider.getTask(); InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("auth/listdev", phoneNumber, "d", deviceId); return new HttpGet(url); } @Override public void onRequestComplete() { callback.onRequestComplete(response.status, new PlumberDevicesResponseList(response.body)); } }; task.execute(taskCallback); } /** * removes registered device for a concrete phone number * * @param phoneNumber phone number * @param authToken device token * @param deviceId device id from which the request is issued * @param targetDeviceId device id to remove * @return */ public void removeDevice(final String phoneNumber, final String deviceId, final String authToken, final String targetDeviceId, final ResponseCallback<PlumberAuthResponse> callback) { PlumberTask task = taskProvider.getTask(); //workaround because it might happen the client gets disconnected and does not receive any http response task.setHttpTimeout(2000); InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("auth/rmdev", phoneNumber, "d", deviceId, "a", authToken, "r", targetDeviceId); return new HttpGet(url); } @Override public void onRequestComplete() { if (response.status == 200) { callback.onRequestComplete(response.status, new PlumberAuthResponse(response.body)); } else { callback.onRequestComplete(response.status, null); } } }; task.execute(taskCallback); } /** * removes all registered devices for a concrete phone number * * @param phoneNumber phone number * @param authToken device token * @param deviceId device id from which the request is issued * @return */ public void removeAllDevices(final String phoneNumber, final String deviceId, final String authToken, final ResponseCallback<PlumberResponse> callback) { PlumberTask task = taskProvider.getTask(); //workaround because the client gets disconnected and does not receive any http response task.setHttpTimeout(2000); InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("auth/rmalldevs", phoneNumber, "d", deviceId, "a", authToken); return new HttpGet(url); } @Override public void onRequestComplete() { callback.onRequestComplete(response.status, null); } }; task.execute(taskCallback); } /** * This method is used to save the GCM id of a user to the backend * so the backend can send push notifications to the users * * @param phoneNumber * @param gcmId * @return */ public void saveGcmId(final String phoneNumber, final String deviceId, final String gcmId, final ResponseCallback<PlumberResponse> callback) { PlumberTask task = taskProvider.getTask(); InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("auth/gcm", phoneNumber, "d", deviceId, "g", gcmId); return new HttpGet(url); } @Override public void onRequestComplete() { callback.onRequestComplete(response.status, null); } }; task.execute(taskCallback); } /** * Get the last connections the vpn user made * * @param phoneNumber * @return */ public void getLastConnections(final String phoneNumber, final String deviceId, final ResponseCallback<PlumberLastConnectionLogResponseList> callback) { PlumberTask task = taskProvider.getTask(); InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("stats/conn", phoneNumber, "d", deviceId); return new HttpGet(url); } @Override public void onRequestComplete() { callback.onRequestComplete(response.status, new PlumberLastConnectionLogResponseList(response.body)); } }; task.execute(taskCallback); } /** * get the last connections synchronously * @param phoneNumber * @param deviceId * @return list of last connections */ public PlumberLastConnectionLogResponseList getLastConnectionsSync(final String phoneNumber, final String deviceId) { PlumberTask task = taskProvider.getTask(); task.setHttpTimeout(2000); String url = buildUrl("stats/conn", phoneNumber, "d", deviceId); RawResponse response = task.doSyncRequest(new HttpGet(url)); if (response.status != 200) { return new PlumberLastConnectionLogResponseList(""); } PlumberLastConnectionLogResponseList list = new PlumberLastConnectionLogResponseList(response.body); return list; } public static String getBase64EncodedString(String string) { try { String b64EncodedPhoneNumber = Base64.encodeToString(string.getBytes("UTF-8"), Base64.URL_SAFE); return URLEncoder.encode(b64EncodedPhoneNumber, "UTF-8"); } catch (UnsupportedEncodingException e) { if (BuildConfig.DEBUG) Log.e(Config.TAG, "Exception when encoding url string", e); return null; } } /** * verifies developer payload for purchases * * @return PlumberAuthResponse - check only status */ public PlumberAuthResponse verifyPurchaseIdSync(final String phoneNumber, final String uid, final int subscriptionId) { String url = buildUrl("subscription/check", phoneNumber, "u", uid, "s", String.valueOf(subscriptionId)); RawResponse response = taskProvider.getTask().doSyncRequest(new HttpGet(url)); return new PlumberAuthResponse(response.body); } /** * starts subscription purchase process * @param phoneNumber * @param subscriptionId * @param callback */ public void startPurchase(final String phoneNumber, final int subscriptionId, final ResponseCallback<PlumberPurchaseResponse> callback) { PlumberTask task = taskProvider.getTask(); InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("subscription/start", phoneNumber, "s", String.valueOf(subscriptionId)); return new HttpGet(url); } @Override public void onRequestComplete() { callback.onRequestComplete(response.status, new PlumberPurchaseResponse(response.body)); } }; task.execute(taskCallback); } /** * activates a voicher * @param phoneNumber phone number * @param voucherCode voucher code * @param callback callback */ public void activateVoucher(final String phoneNumber, final String voucherCode, final ResponseCallback<PlumberSubscriptionResponse> callback) { PlumberTask task = taskProvider.getTask(); String code = voucherCode; try { code = URLEncoder.encode(voucherCode.replaceAll("\\s+", ""), "UTF-8"); } catch (UnsupportedEncodingException e) { if (BuildConfig.DEBUG) Log.e(Config.TAG, "failed to urlencode", e); } final String urlcode = code; InternalCallback taskCallback = new InternalCallback() { @Override public HttpGet getHttpRequest() { String url = buildUrl("voucher/activate", phoneNumber, "c", urlcode); return new HttpGet(url); } @Override public void onRequestComplete() { callback.onRequestComplete(response.status, new PlumberSubscriptionResponse(response.body)); } }; task.execute(taskCallback); } /** * call to end the purchase process and activate the subscription * @param phoneNumber * @param uid * @param signature subscription signature * @param callback */ public void finishPurchase(final String phoneNumber, final String uid, final String signature, final String purchaseData, final ResponseCallback<PlumberSubscriptionResponse> callback) { PlumberTask task = taskProvider.getTask(); InternalCallback taskCallback = new InternalCallback() { @Override public HttpRequestBase getHttpRequest() { String url = buildUrl("subscription/subscribe_a", phoneNumber, "u", uid); HttpPost r = new HttpPost(url); r.addHeader("content-type", "application/json"); JSONObject json = new JSONObject(); String jsonStr = ""; try { json.put("data", getBase64EncodedString(purchaseData)); json.put("signature", signature); jsonStr = json.toString(); } catch (JSONException e) { if (BuildConfig.DEBUG) Log.e(Config.TAG, "error constructing json", e); } StringEntity params = null; try { params = new StringEntity(jsonStr); } catch (UnsupportedEncodingException e) { if (BuildConfig.DEBUG) Log.e(Config.TAG, "error constructing body", e); } r.setEntity(params); return r; } @Override public void onRequestComplete() { callback.onRequestComplete(response.status, new PlumberSubscriptionResponse(response.body)); } }; task.execute(taskCallback); } private static String buildUrl(String path, String phoneNumber, String... params) { if (params.length % 2 != 0) return ""; StringBuilder sb = new StringBuilder("https://"); sb.append(Config.PLUMBER_ADDR).append("/"); sb.append(path).append("/"); Map<String, String> paramsMap = new HashMap<>(); for (int i = 0; i < params.length / 2; i++) { paramsMap.put(params[i*2], params[i*2+1]); } SortedMap<String, String> sortedParams = new TreeMap<>(paramsMap); StringBuilder tokenData = new StringBuilder(); for (String k: sortedParams.keySet()) { tokenData.append(sortedParams.get(k)); } String token = Token.generateToken(phoneNumber, tokenData.toString()); sb.append("?"); for (String k: paramsMap.keySet()) { sb.append(k).append("=").append(paramsMap.get(k)).append("&"); } sb.append("t=").append(token); sb.append("&id=").append(BackendConnector.getBase64EncodedString(phoneNumber)); return sb.toString(); } }