/**
* Copyright (c) 2013, Sana
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the Sana nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL Sana BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.sana.android.service.impl;
import java.util.HashMap;
import java.util.UUID;
import org.apache.http.client.methods.HttpPost;
import org.sana.R;
import org.sana.android.Constants;
import org.sana.android.content.core.ObserverWrapper;
import org.sana.android.db.ModelWrapper;
import org.sana.android.net.HttpTask;
import org.sana.android.net.MDSInterface;
import org.sana.android.net.MDSInterface2;
import org.sana.net.MDSResult;
import org.sana.net.Response;
import org.sana.android.net.NetworkTaskListener;
import org.sana.android.provider.Observers;
import org.sana.android.service.ISessionCallback;
import org.sana.android.service.ISessionService;
import org.sana.api.IObserver;
import org.sana.util.UUIDUtil;
import android.app.Service;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
/**
* Service which provides session based authentication into the system.
*
* @author Sana Development
*
*/
public class SessionService extends Service{
public static final String TAG = SessionService.class.getSimpleName();
public static final String ACTION_START = "org.sana.service.SessionService.START";
public static final UUID INVALID = UUIDUtil.uuid3(UUIDUtil.EMPTY, "invalid");
public static final UUID INVALID_PASSWORD = UUIDUtil.uuid3(UUIDUtil.EMPTY, "password");
public static final UUID VALID = UUIDUtil.uuid3(UUIDUtil.EMPTY, "valid");
public static final int INDETERMINATE = -1;
public static final int STATE_REMOTE = 1;
public static final int STATE_LOCAL = 2;
public static final int FAILURE = 0;
public static final int SUCCESS = 1;
//TODO replace the two HashMaps with thread safe versions.
// map of user valid session key to credentials
private static final HashMap<String,String[]> openSessions = new HashMap<String,String[]>();
// map of temp key to credentials
private static final HashMap<String,String[]> tempSessions = new HashMap<String,String[]>();
private final RemoteCallbackList<ISessionCallback> mCallbacks
= new RemoteCallbackList<ISessionCallback>();
private final ISessionService.Stub mBinder = new ISessionService.Stub() {
@Override
public boolean read(String arg0) throws RemoteException {
boolean result = false;
if(TextUtils.isEmpty(arg0))
return false;
else {
result = isOpen(arg0);
}
return result;
}
@Override
public String create(String tempKey, String username, String password) throws RemoteException {
//String tempKey = SessionUtil.generateSessionKey(username);
addTempSession(tempKey, new String[]{ username, password });
openSession(STATE_REMOTE, tempKey);
return tempKey;
}
@Override
public boolean delete(String arg0) throws RemoteException {
return removeAuthenticatedSession(arg0);
}
@Override
public void registerCallback(ISessionCallback arg0, String arg1)
throws RemoteException {
Log.i(TAG + ".mBinder", "registerCallback(): " + arg1);
if (arg0 != null) mCallbacks.register(arg0, arg1);
}
@Override
public void unregisterCallback(ISessionCallback arg0)
throws RemoteException {
Log.i(TAG + ".mBinder", "unregisterCallback(): " + arg0);
if(arg0 != null) mCallbacks.unregister(arg0);
}
};
//TODO refactor this out
/**
* Simple callback interface for HttpSessions
*
* @author Sana Development
*
*
private class HttpSessionAuthListener implements NetworkTaskListener<MDSResult>{
String tempKey = null;
HttpSessionAuthListener(String key){
this.tempKey = key;
}
@Override
public void onTaskComplete(MDSResult t) {
// TODO Auto-generated method stub
if(t.succeeded()){
// MDSResult should return the actual session key
handleSessionAuthResult(SUCCESS, tempKey, t.getData());
} else if(TextUtils.isEmpty(t.getCode())){
handleSessionAuthResult(INDETERMINATE, tempKey, INVALID.toString());
} else {
// data should be some informative message
handleSessionAuthResult(FAILURE, tempKey, INVALID.toString());
}
}
}
*/
private class AuthListener implements NetworkTaskListener<Response<String>>{
String tempKey = null;
AuthListener(String key){
this.tempKey = key;
}
@Override
public void onTaskComplete(Response<String> t) {
// TODO Auto-generated method stub
if(t.succeeded()){
// MDSResult should return the actual session key
handleSessionAuthResult(SUCCESS, tempKey, t.getMessage());
} else if (t.getCode() == -1){
handleSessionAuthResult(INDETERMINATE, tempKey, INVALID.toString());
} else {
// data should be some informative message
handleSessionAuthResult(FAILURE, tempKey, INVALID.toString());
}
}
}
//HttpSessionAuthListener mNetListener = null;
HttpTask mNetTask = null;
/* (non-Javadoc)
* @see android.app.Service#onBind(android.content.Intent)
*/
@Override
public IBinder onBind(Intent arg0) {
Log.i(TAG, "onBind()");
// Try the call back binder defined in IRemoteService.onBind()
if(arg0.getAction().equals(SessionService.class.getName()) ||
arg0.getAction().equals(ACTION_START))
return mBinder;
else
return null;
}
@Override
public void onDestroy() {
Log.i(TAG, "onDestroy()");
if(mCallbacks != null)
mCallbacks.kill();
super.onDestroy();
}
@Override
public boolean onUnbind(Intent arg0) {
Log.i(TAG, "onUnbind()");
int connections = 0;
try{
connections = mCallbacks.beginBroadcast();
} finally {
mCallbacks.finishBroadcast();
}
if(!(connections > 0))
stopSelf();
return super.onUnbind(arg0);
}
/**
* Handles the logic for opening a session by first trying the remote
* dispatcher if connected and then falling back locally to either (1) the
* credential cache in the Observer ContentProvider or (2) the default
* admin credentials.
*
* @param state
* @param username
* @param password
*/
protected void openSession(int state, String tempKey){
switch(state){
case STATE_LOCAL:
openLocalSession(tempKey);
break;
case STATE_REMOTE:
openNetworkSession(tempKey);
break;
default:
throw new IllegalArgumentException("Use STATE_LOCAL or STATE_REMOTE only");
}
}
// Tries to open a session from the local ContentProvider and then the
// admin backdoor by default - admin backdoor only works once from the
// resource value
private void openLocalSession(String tempKey){
String[] credentials = tempSessions.get(tempKey);
String username = credentials[0];
String password = credentials[1];
String session = INVALID.toString();
if(isLocalUsername(username)){
try{
Log.d(TAG, "auth: " + username + " pass: " + password);
IObserver o = ObserverWrapper.getOneByAuth(
getContentResolver(), username, password);
// user exists and was validated locally
if(o != null){
session = o.getUuid();
handleSessionAuthResult(SUCCESS, tempKey, session);
// the user was good but password didn't match, return a fail
} else {
handleSessionAuthResult(FAILURE, tempKey, username);
}
} catch (Exception e){
e.printStackTrace();
Log.e(TAG, e.getMessage());
handleSessionAuthResult(FAILURE, tempKey, INVALID.toString());
}
// Try the admin backdoor for local connection only
} else if(username.equals(getString(R.string.admin_username))
&& password.equals(getString(R.string.admin_password))){
session = UUIDUtil.generateObserverUUID(username).toString();
registerNew(username,password,session);
handleSessionAuthResult(SUCCESS, tempKey, username);
} else {
handleSessionAuthResult(FAILURE, tempKey, INVALID.toString());
}
}
//TODO replace with an https connection.
// Starts an async http task to POST credentials to dispatch server
private void openNetworkSession(String tempKey){
Log.d(TAG, "Opening network session: " + tempKey);
if(!connected()){
Log.d(TAG, "openNetworkSession()..connected = false");
openLocalSession(tempKey);
} else {
try {
Log.d(TAG, "openNetworkSession()..connected = true");
String[] credentials = tempSessions.get(tempKey);
HttpPost post = MDSInterface2.createSessionRequest(this,
credentials[0], credentials[1]);
Log.i(TAG, "openNetworkSession(...) " + post.getURI());
new HttpTask<String>(new AuthListener(tempKey)).execute(post);
} catch(Exception e){
Log.e(TAG, e.getMessage());
e.printStackTrace();
}
}
}
/**
* Handles authentication attempts for temporary tempSessions. If successful,
* the temporary session will be moved to a list of authorized tempSessions.
*
* @param status {@link #SUCCESS}, {@link #FAILURE}, {@link #INDETERMINATE}
* @param tempKey the temporary key initially passed from the client.
* @param sessionKey the authorized key.
*/
protected void handleSessionAuthResult(int status, String tempKey,
String sessionKey)
{
Log.i(TAG, "Handling result: '" + status
+ "' for temp session: " + tempKey);
String[] credentials = tempSessions.get(tempKey);
final String username = credentials[0];
final String password = credentials[1];
switch(status){
case SUCCESS:
// Got successful authentication from network
// update cache with the validated password, may exist already but
// this should handle any situations where the network value changed
if(isLocalUsername(username)){
Log.i(TAG,"Updating credentials for user: " + username);
ContentValues values = new ContentValues();
values.put(Observers.Contract.PASSWORD, encrypt(password));
int updated = getContentResolver().update(
Observers.CONTENT_URI,
values,
Observers.Contract.USERNAME + " = ?",
new String[]{ username });
if(updated == 1)
Log.i(TAG, "Succesfully updated: " + username);
else
Log.w(TAG, "OOPS! Something went horribly wrong updating" +
"the password for user: " + username
+ " or the password was unchanged!");
// i name didn't exist already we need to insert instead of update.
} else {
ContentValues values = new ContentValues();
values.put(Observers.Contract.USERNAME, username);
values.put(Observers.Contract.UUID, sessionKey);
values.put(Observers.Contract.PASSWORD, encrypt(password));
getContentResolver().insert(Observers.CONTENT_URI,
values);
}
SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(this.getBaseContext());
preferences.edit().putString(
Constants.PREFERENCE_EMR_USERNAME, username);
preferences.edit().putString(
Constants.PREFERENCE_EMR_PASSWORD, password);
preferences.edit().commit();
// send result to the call back (INVALID, user uuid)
removeTempSession(tempKey);
addAuthenticatedSession(sessionKey, credentials);
sendResult(SUCCESS, tempKey, sessionKey);
break;
// connected and failed
case FAILURE:
// send result to the call back (INVALID, user )
removeTempSession(tempKey);
sendResult(FAILURE, tempKey, sessionKey);
break;
case INDETERMINATE:
openSession(STATE_LOCAL, tempKey);
break;
default:
throw new IllegalArgumentException();
}
}
// sends the callback message
protected void sendResult(int status, String cookie, String msg){
int i = mCallbacks.beginBroadcast();
Log.i(TAG, "sendResult( "+cookie+", "+ status+") --> " + i+" callbacks");
while(i > 0){
i--;
String instanceKey = mCallbacks.getBroadcastCookie(i).toString();
if(cookie.equals(instanceKey)){
Log.i(TAG, "....Sending result to " + cookie);
try {
mCallbacks.getBroadcastItem(i).onValueChanged(status, cookie, msg);
} catch (RemoteException e) {
Log.e(TAG, "....disconnected from " + cookie);
}
} else {
Log.i(TAG, "....Trying next "+ i);
}
}
mCallbacks.finishBroadcast();
}
// queries the content provider for the username
private boolean isLocalUsername(String username){
Cursor c = null;
boolean isValid = false;
try{
c = getContentResolver().query(Observers.CONTENT_URI,
new String[]{ Observers.Contract._ID },
Observers.Contract.USERNAME + " = ?",
new String[]{ username }, null);
//(Observers.CONTENT_URI, getContentResolver(),Observers.Contract.USERNAME,username);
if(c != null && c.moveToFirst() && c.getCount() == 1)
isValid = true;
} finally {
if(c != null)
c.close();
}
return isValid;
}
private synchronized void addAuthenticatedSession(String sessionKey, String[] credentials){
Log.d(TAG, "Adding authorized session: " + sessionKey+":"+credentials.length);
openSessions.put(sessionKey, credentials);
}
private synchronized boolean removeAuthenticatedSession(String sessionKey){
Log.d(TAG, "Removing authorized session: " + sessionKey);
boolean result = false;
result = (openSessions.remove(sessionKey) != null);
return result;
}
private synchronized void addTempSession(String sessionKey, String[] credentials){
Log.d(TAG, "Adding temp session: " + sessionKey+":"+credentials.length);
tempSessions.put(sessionKey, credentials);
}
private synchronized boolean removeTempSession(String sessionKey){
Log.d(TAG, "Removing temp session: " + sessionKey);
boolean result = false;
result = (tempSessions.remove(sessionKey) != null);
return result;
}
private synchronized boolean isOpen(String sessionKey){
boolean result = false;
synchronized(openSessions){
String[] session = openSessions.get(sessionKey);
result = (session != null);
}
openSessions.notify();
return result;
}
/**
* Retrieves the memory stored password for network authentication
* @param session
* @return
*/
protected String getPassword(String sessionKey){
return decrypt(openSessions.get(sessionKey)[1]);
}
//TODO do placeholder for encrypting the in memory String
private String encrypt(String str){
return str;
}
//TODO do placeholder for decrypting the in memory String
private String decrypt(String str){
return str;
}
private Uri registerNew(String username, String password, String uuid){
uuid = (uuid == null)? UUIDUtil.generateObserverUUID(username).toString(): uuid;
ContentValues values = new ContentValues();
values.put(Observers.Contract.USERNAME, username);
values.put(Observers.Contract.PASSWORD, encrypt(password));
values.put(Observers.Contract.UUID, uuid);
Uri uri = getContentResolver().insert(Observers.CONTENT_URI,
values);
return uri;
}
protected boolean connected(){
ConnectivityManager cm = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
return(activeNetwork != null &&
activeNetwork.isConnectedOrConnecting());
}
}