Also keep in mind this was written for older Android versions (when I was still learning things) so it could probably be cleaned up quite a bit. I would rip some of this out to simplify it, but it might be helpful if someone wants to create a full fledged service.
The server is a service which listens on (I believe the first) available network connection it can. It listens with a MultiServer Thread which spawns of ClientConnection threads when a new connection is initiated. The run() part of the ClientConnection thread is where it responds (i.e. a Network Event Receiver) to connections.
1. On connection initiated, listen for 'quintessence\r\n'
2. Create a 'cookie' and send that to the client
3. Wait for response from client - compare it to the token md5('cookie:serverpass') and see if what they send matches
4. If it's valid, send 'accept' and wait for the event and payload
Code: Select all
package com.timhoeck.android.eventghost;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Enumeration;
import java.util.Random;
import com.timhoeck.android.providers.eventghost.DBProvider;
import com.timhoeck.android.eventghost.EventGhostIntent.OutgoingEvent;
import com.timhoeck.android.framework.SecurityHelper;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
public class EventGhostService extends Service {
private static final String TAG = "EventGhost";
private static final int NOTIFICATION_SERVER_STATUS = 1;
// Broadcasts that we act upon
private static final String BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
public static final String WIFI_STATE_CHANGED = "android.net.wifi.WIFI_STATE_CHANGED";
// Wifi States
public static final int WIFI_STATE_DISABLED = 1;
public static final int WIFI_STATE_ENABLED = 3;
public static final int WIFI_STATE_UNKNOWN = 4;
private static final boolean USE_EMULATOR_DEV_ADDRESS = false;
private final IBinder mBinder = new LocalBinder();
private InetAddress serverInetAddress;
MultiServerThread server;
ServerSocket serverSocket;
String serverPassword = "EG4Android!";
int serverPort = 1024;
Context mContext;
public static boolean startServer(Context context) {
Log.i(TAG, "Starting Server");
Intent start = new Intent();
start.setClass(context, EventGhostService.class);
if (context.startService(start) == null) {
//service was started
return false;
} else {
//service was already running
return true;
}
}
public static void stopServer(Context context) {
Log.i(TAG, "Stopping Server");
Intent stop = new Intent();
stop.setClass(context, EventGhostService.class);
context.stopService(stop);
context.getSharedPreferences(PreferencesActivity.PREFS_SETTINGS, 0).edit().putString(PreferencesActivity.KEY_SERVER_DETAILS, PreferencesActivity.SERVER_DETAILS_DISABLED).commit();
dismissNotification(context);
}
@Override
public void onStart(Intent intent, int startId) {
mContext = this.getBaseContext();
super.onStart(intent, startId);
SharedPreferences settings = mContext.getSharedPreferences(PreferencesActivity.PREFS_SETTINGS, 0);
if (settings.getBoolean(PreferencesActivity.KEY_ENABLE_SERVER, PreferencesActivity.KEY_ENABLE_SERVER_DEFAULT)) {
try {
serverPort = Integer.valueOf(settings.getString(PreferencesActivity.KEY_SERVER_PORT, PreferencesActivity.KEY_SERVER_PORT_DEFAULT));
} catch (Exception e){
serverPort = 1024;
}
String pw = settings.getString(PreferencesActivity.KEY_SERVER_PASSWORD, null);
if (pw != null) serverPassword = SecurityHelper.decrypt(pw); else serverPassword = PreferencesActivity.KEY_SERVER_PASSWORD_DEFAULT;
//start our server
serverInetAddress = getServerAddress();
try {
if (serverInetAddress != null) {
//If we have an availabled address, create a server socket
serverSocket = new ServerSocket(serverPort);
//serverSocket = new ServerSocket(serverPort, 10, serverInetAddress);
server = new MultiServerThread();
server.start();
//Find all addresses (IPv4/IPv6) associated with this interface
NetworkInterface n = NetworkInterface.getByInetAddress(serverInetAddress);
Enumeration<InetAddress> ips = n.getInetAddresses();
String ipString = "";
while (ips.hasMoreElements()) {
InetAddress i = ips.nextElement();
ipString += i.getHostAddress();
if (ips.hasMoreElements()) ipString += " / ";
}
Log.i(TAG, "Attempting to Bind on " + serverPort + " at address: " + ipString);
String details = "Listening on " + ipString + ", port " + serverPort;
mContext.getSharedPreferences(PreferencesActivity.PREFS_SETTINGS, 0).edit().putString(PreferencesActivity.KEY_SERVER_DETAILS, details).commit();
serverNotification(mContext, details);
} else {
Log.e(TAG, "Could not start server, no available InetAddress");
mContext.getSharedPreferences(PreferencesActivity.PREFS_SETTINGS, 0).edit().putString(PreferencesActivity.KEY_SERVER_DETAILS, PreferencesActivity.SERVER_DETAILS_DISABLED).commit();
}
} catch (Exception e) {
e.printStackTrace();
Log.w(TAG, "Error starting server (already running?)");
mContext.getSharedPreferences(PreferencesActivity.PREFS_SETTINGS, 0).edit().putString(PreferencesActivity.KEY_SERVER_DETAILS, PreferencesActivity.SERVER_DETAILS_DISABLED).commit();
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i(TAG, "EventGhost Service destroyed");
if (serverSocket != null) {
try {
mContext.getSharedPreferences(PreferencesActivity.PREFS_SETTINGS, 0).edit().putString(PreferencesActivity.KEY_SERVER_DETAILS, PreferencesActivity.SERVER_DETAILS_DISABLED).commit();
serverSocket.close();
} catch (IOException e) {
Log.w(TAG, "Unable to close server socket");
}
}
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public class LocalBinder extends Binder {
EventGhostService getService() {
return EventGhostService.this;
}
}
/**
* Searches network interfaces for available inet address
* @return If using the Emulator, it returns the internal ip, otherwise it looks for
* the first interface that is not the loopback
*/
public static InetAddress getServerAddress() {
try {
Enumeration<NetworkInterface> ni = NetworkInterface
.getNetworkInterfaces();
if (USE_EMULATOR_DEV_ADDRESS) {
return InetAddress.getByName("10.0.2.15");
} else {
// Find the appropriate network interface to bind to
while (ni.hasMoreElements()) {
NetworkInterface n = ni.nextElement();
Log.i(TAG, "Found network: " + n.getDisplayName());
Log.d(TAG, " Details: " + n.toString());
// we aren't interested in binding to localhost
if (!n.getDisplayName().equals("lo")) {
Enumeration<InetAddress> in = n.getInetAddresses();
while (in.hasMoreElements()) {
InetAddress i = in.nextElement();
Log.i(TAG, " IP: " + i.getHostAddress());
return i;
}
} else {
//Log.d(TAG, "Ignoring network: " + n.getDisplayName());
}
}
}
} catch (Exception e) {}
return null;
}
public static class ServiceStarter extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
SharedPreferences settings = context.getSharedPreferences(PreferencesActivity.PREFS_SETTINGS, 0);
if (settings.getBoolean(PreferencesActivity.KEY_ENABLE_SERVER, PreferencesActivity.KEY_ENABLE_SERVER_DEFAULT)) {
if (intent.getAction().equals(BOOT_COMPLETED)) {
startServer(context);
} else if (intent.getAction().equals(WIFI_STATE_CHANGED)) {
stopServer(context);
Log.i(TAG, "Received Wifi State Change, waiting for new network");
serverNotification(context, "Disabled, waiting for network connection");
int state = intent.getIntExtra("wifi_state",
WIFI_STATE_UNKNOWN);
if (state == WIFI_STATE_ENABLED) {
new ConnectivityWait().execute(context, true);
} else if (state == WIFI_STATE_DISABLED) {
//Log.i(TAG, "Received Wifi State Disabled, waiting for new network");
new ConnectivityWait().execute(context, false);
}
}
}
}
}
private static void dismissNotification(Context context) {
NotificationManager mgr = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
mgr.cancel(NOTIFICATION_SERVER_STATUS);
}
private static void serverNotification(Context context, String text) {
NotificationManager mgr = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
Notification notification = new Notification();
notification.flags = Notification.FLAG_ONGOING_EVENT;
notification.icon = R.drawable.icon_24;
Intent eg = new Intent();
eg.setClass(context, PreferencesActivity.class);
PendingIntent pending = PendingIntent.getActivity(context, 0, eg, PendingIntent.FLAG_UPDATE_CURRENT);
notification.setLatestEventInfo(context, "EventGhost Server", text, pending);
mgr.notify(NOTIFICATION_SERVER_STATUS, notification);
}
// Since WIFI_STATE_CHANGED doesn't confirm that the new network is active,
// we wait for it.
private static class ConnectivityWait extends
AsyncTask<Object, Integer, Object> {
@Override
protected Object doInBackground(Object... info) {
Context context = (Context) info[0];
boolean state = (Boolean) info[1];
return waitForConnectivity(context, state);
}
protected void onPostExecute(Object... result) {
// WHY ARE YOU NOT WORKING?!
Log.i(TAG, "ConnectivityWait finished");
}
}
private static boolean waitForConnectivity(Context context, boolean wifiState) {
try {
ConnectivityManager connMgr = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = null;
String currentNetwork = null;
int sleepTime = 0;
do {
Thread.sleep(3000);
sleepTime += 3000;
info = connMgr.getActiveNetworkInfo();
currentNetwork = info.getTypeName();
if (wifiState && currentNetwork.equals("WIFI")) break;
if (!wifiState && currentNetwork.equals("MOBILE")) break;
//Log.i(TAG, "Current Network: " + currentNetwork + ", Time: " + sleepTime / 1000 + "secs");
} while (sleepTime < 120000);
if (sleepTime < 120000) {
//we found a network, attempt to restart
Log.i(TAG, "Restarting Service due to Wifi change");
startServer(context);
} else {
serverNotification(context, "Disabled, timed out waiting for network");
context.getSharedPreferences(PreferencesActivity.PREFS_SETTINGS, 0).edit().putBoolean(PreferencesActivity.KEY_ENABLE_SERVER, false).commit();
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public static void restartServer(Context context) {
// restart server
stopServer(context);
startServer(context);
}
/***
* The MultiServerThread waits for incoming connections.
* When one arrives, it spawns a ClientConnection thread to handle that connection.
* @author thoeck
*
*/
public class MultiServerThread extends Thread {
public void run() {
boolean listening = true;
try {
Log.i(TAG, "Listening on " + serverInetAddress.getHostAddress()
+ ":" + serverPort);
while (listening) {
new ClientConnection(serverSocket.accept(), serverPassword)
.start();
}
} catch (Exception e) {
e.printStackTrace();
Log.w(TAG, "MultiServerThread closed");
try {
serverSocket.close();
} catch (Exception e2) {
Log.w(TAG, "Unable to close server socket");
}
}
}
}
/***
* A new ClientConnection class is created each time a connection is initiated
* by a client to the server:port.
* @author thoeck
*
*/
public class ClientConnection extends Thread {
private Socket client;
private String serverPassword;
public ClientConnection(Socket client, String serverPassword) {
this.client = client;
this.serverPassword = serverPassword;
}
public void run() {
try {
client.setKeepAlive(true);
client.setSoTimeout(5000);
DataInputStream in = new DataInputStream(client
.getInputStream());
DataOutputStream out = null;
byte[] ping = new byte[14];
in.read(ping);
String str = new String(ping);
// Get Incoming Request from EventGhost
//Clients use the string 'quintessence' to initiate authentication with the server
if (str.equals("quintessence\n\r")) {
//EventGhost Servers send a generated authentication 'cookie' to the client
Random gen = new Random();
String cookie = String.valueOf(gen.nextInt(65536));
String token = cookie + ":" + serverPassword;
String expected = MD5Hex.MD5Hex(token);
out = new DataOutputStream(client.getOutputStream());
out.writeBytes(cookie + "\n");
//To authenticate, the EventGhost client will send a response to the cookie,
//which contains the cookie, plus the password to validate the password
String passResponse = new String(in.readLine());
String serverhost = client.getInetAddress().getHostAddress();
if (passResponse.equals(expected)) {
//password is valid
out.writeBytes("accept\n");
//wait for the event/payload
String line = new String(in.readLine());
//Log.d(TAG, "Got line: " + line);
String event;
String payload;
//if there is a payload, it's received first
if (line.startsWith("payload ")) {
payload = line.replace("payload ", "");
event = new String(in.readLine());
} else {
payload = null;
event = line;
}
//Log.i(TAG, "Received Event: " + event + ", sending GENERATE_EVENT broadcast");
//TODO FromApp shouldn't be server host
OutgoingEvent sendEvent = new OutgoingEvent()
.setEvent(event)
.setPayload(payload)
.setFromApp(serverhost);
EventGhostIntent.generateEvent(mContext, sendEvent);
} else {
//password is invalid
Log.w(TAG, "Cookie incorrect for connection");
LogsActivity.addLogEntry(mContext, DBProvider.LogValues.TYPE_EVENT_INCOMING, "N/A", "N/A", serverhost, "Invalid Password from Server", false);
//Log.w(TAG, "Expected:" + expected);
}
} else {
//If we didn't get 'quintessence', the connection was not initiated properly by the client
//Log.w(TAG, "Invalid authentication method");
//Log.w(TAG, "Expected: quintessence");
}
//notify the client we are closing the connection
if (out != null) {
out.writeBytes("close\n");
out.close();
}
//close the server connection
in.close();
} catch (Exception e) {
//something didn't go right
e.printStackTrace();
Log.w(TAG, "Error communicating with client");
} finally {
//close out the client connection
try {
client.close();
} catch (Exception e2) {
Log.w(TAG, "Unable to close server socket");
}
}
}
}
}