/*
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();
}
}