/*
* Copyright (C) 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.googlecode.android_scripting;
import com.google.common.collect.Lists;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.BindException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
//import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager;
/**
* A simple server.
* @author Damon Kohler (damonkohler@gmail.com)
*/
public abstract class SimpleServer {
private static int threadIndex = 0;
private final ConcurrentHashMap<Integer, ConnectionThread> mConnectionThreads =
new ConcurrentHashMap<Integer, ConnectionThread>();
private final List<SimpleServerObserver> mObservers = Lists.newArrayList();
private volatile boolean mStopServer = false;
private ServerSocket mServer;
private Thread mServerThread;
public interface SimpleServerObserver {
public void onConnect();
public void onDisconnect();
}
protected abstract void handleConnection(Socket socket) throws Exception;
protected abstract void handleRPCConnection(Socket socket,
Integer UID,
BufferedReader reader,
PrintWriter writer) throws Exception;
/** Adds an observer. */
public void addObserver(SimpleServerObserver observer) {
mObservers.add(observer);
}
/** Removes an observer. */
public void removeObserver(SimpleServerObserver observer) {
mObservers.remove(observer);
}
private void notifyOnConnect() {
for (SimpleServerObserver observer : mObservers) {
observer.onConnect();
}
}
private void notifyOnDisconnect() {
for (SimpleServerObserver observer : mObservers) {
observer.onDisconnect();
}
}
private final class ConnectionThread extends Thread {
private final Socket mmSocket;
private final BufferedReader reader;
private final PrintWriter writer;
private final Integer UID;
private final boolean isRpc;
private ConnectionThread(Socket socket, boolean rpc, Integer uid, BufferedReader reader, PrintWriter writer) {
setName("SimpleServer ConnectionThread " + getId());
mmSocket = socket;
this.UID = uid;
this.reader = reader;
this.writer = writer;
this.isRpc = rpc;
}
@Override
public void run() {
Log.v("Server thread " + getId() + " started.");
try {
if(isRpc) {
Log.d("Handling RPC connection in "+getId());
handleRPCConnection(mmSocket, UID, reader, writer);
}else{
Log.d("Handling Non-RPC connection in "+getId());
handleConnection(mmSocket);
}
} catch (Exception e) {
if (!mStopServer) {
Log.e("Server error.", e);
}
} finally {
close();
mConnectionThreads.remove(this.UID);
notifyOnDisconnect();
Log.v("Server thread " + getId() + " stopped.");
}
}
private void close() {
if (mmSocket != null) {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(e.getMessage(), e);
}
}
}
}
/** Returns the number of active connections to this server. */
public int getNumberOfConnections() {
return mConnectionThreads.size();
}
public static InetAddress getPrivateInetAddress() throws UnknownHostException, SocketException {
InetAddress candidate = null;
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
for (NetworkInterface netint : Collections.list(nets)) {
if (!netint.isLoopback() || !netint.isUp()) { // Ignore if localhost or not active
continue;
}
Enumeration<InetAddress> addresses = netint.getInetAddresses();
for (InetAddress address : Collections.list(addresses)) {
if (address instanceof Inet4Address) {
Log.d("local address " + address);
return address; // Prefer ipv4
}
candidate = address; // Probably an ipv6
}
}
if (candidate != null) {
return candidate; // return ipv6 address if no suitable ipv6
}
return InetAddress.getLocalHost(); // No damn matches. Give up, return local host.
}
public static InetAddress getPublicInetAddress() throws UnknownHostException, SocketException {
InetAddress candidate = null;
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
for (NetworkInterface netint : Collections.list(nets)) {
if (netint.isLoopback() || !netint.isUp()) { // Ignore if localhost or not active
continue;
}
Enumeration<InetAddress> addresses = netint.getInetAddresses();
for (InetAddress address : Collections.list(addresses)) {
if (address instanceof Inet4Address) {
return address; // Prefer ipv4
}
candidate = address; // Probably an ipv6
}
}
if (candidate != null) {
return candidate; // return ipv6 address if no suitable ipv6
}
return InetAddress.getLocalHost(); // No damn matches. Give up, return local host.
}
/**
* Starts the RPC server bound to the localhost address.
*
* @param port
* the port to bind to or 0 to pick any unused port
*
* @return the port that the server is bound to
* @throws IOException
*/
public InetSocketAddress startLocal(int port) {
InetAddress address;
try {
// address = InetAddress.getLocalHost();
address = getPrivateInetAddress();
mServer = new ServerSocket(port, 5, address);
} catch (BindException e) {
Log.e("Port " + port + " already in use.");
try {
address = getPrivateInetAddress();
mServer = new ServerSocket(0, 5, address);
} catch (IOException e1) {
e1.printStackTrace();
return null;
}
} catch (Exception e) {
Log.e("Failed to start server.", e);
return null;
}
int boundPort = start();
return InetSocketAddress.createUnresolved(mServer.getInetAddress().getHostAddress(), boundPort);
}
/**
* data Starts the RPC server bound to the public facing address.
*
* @param port
* the port to bind to or 0 to pick any unused port
*
* @return the port that the server is bound to
*/
public InetSocketAddress startPublic(int port) {
InetAddress address;
try {
// address = getPublicInetAddress();
address = null;
mServer = new ServerSocket(port, 5 /* backlog */, address);
} catch (Exception e) {
Log.e("Failed to start server.", e);
return null;
}
int boundPort = start();
return InetSocketAddress.createUnresolved(mServer.getInetAddress().getHostAddress(), boundPort);
}
/**
* data Starts the RPC server bound to all interfaces
*
* @param port
* the port to bind to or 0 to pick any unused port
*
* @return the port that the server is bound to
*/
public InetSocketAddress startAllInterfaces(int port) {
try {
mServer = new ServerSocket(port, 5 /* backlog */);
} catch (Exception e) {
Log.e("Failed to start server.", e);
return null;
}
int boundPort = start();
return InetSocketAddress.createUnresolved(mServer.getInetAddress().getHostAddress(), boundPort);
}
private int start() {
mServerThread = new Thread() {
@Override
public void run() {
while (!mStopServer) {
try {
Socket sock = mServer.accept();
if (!mStopServer) {
startConnectionThread(sock);
} else {
sock.close();
}
} catch (IOException e) {
if (!mStopServer) {
Log.e("Failed to accept connection.", e);
}
} catch (JSONException e) {
if (!mStopServer) {
Log.e("Failed to parse request.", e);
}
}
}
}
};
mServerThread.start();
Log.v("Bound to " + mServer.getInetAddress());
return mServer.getLocalPort();
}
private void startConnectionThread(final Socket sock) throws IOException, JSONException {
BufferedReader reader =
new BufferedReader(new InputStreamReader(sock.getInputStream()), 8192);
PrintWriter writer = new PrintWriter(sock.getOutputStream(), true);
String data;
if((data = reader.readLine()) != null) {
Log.v("Received: " + data);
JSONObject request = new JSONObject(data);
if(request.has("cmd") && request.has("uid")) {
String cmd = request.getString("cmd");
int uid = request.getInt("uid");
JSONObject result = new JSONObject();
if(cmd.equals("initiate")) {
Log.d("Initiate a new session");
threadIndex += 1;
int mUID = threadIndex;
ConnectionThread networkThread = new ConnectionThread(sock,true,mUID,reader,writer);
mConnectionThreads.put(mUID, networkThread);
networkThread.start();
notifyOnConnect();
result.put("uid", mUID);
result.put("status",true);
result.put("error", null);
}else if(cmd.equals("continue")) {
Log.d("Continue an existing session");
Log.d("keys: "+mConnectionThreads.keySet().toString());
if(!mConnectionThreads.containsKey(uid)) {
result.put("uid", uid);
result.put("status",false);
result.put("error", "Session does not exist.");
}else{
ConnectionThread networkThread = new ConnectionThread(sock,true,uid,reader,writer);
mConnectionThreads.put(uid, networkThread);
networkThread.start();
notifyOnConnect();
result.put("uid", uid);
result.put("status",true);
result.put("error", null);
}
}else {
result.put("uid", uid);
result.put("status",false);
result.put("error", "Unrecognized command.");
}
writer.write(result + "\n");
writer.flush();
Log.v("Sent: " + result);
}else{
ConnectionThread networkThread = new ConnectionThread(sock,false,0,reader,writer);
mConnectionThreads.put(0, networkThread);
networkThread.start();
notifyOnConnect();
}
}
}
public void shutdown() {
// Stop listening on the server socket to ensure that
// beyond this point there are no incoming requests.
mStopServer = true;
try {
mServer.close();
} catch (IOException e) {
Log.e("Failed to close server socket.", e);
}
// Since the server is not running, the mNetworkThreads set can only
// shrink from this point onward. We can just stop all of the running helper
// threads. In the worst case, one of the running threads will already have
// shut down. Since this is a CopyOnWriteList, we don't have to worry about
// concurrency issues while iterating over the set of threads.
for (ConnectionThread connectionThread : mConnectionThreads.values()) {
connectionThread.close();
}
for (SimpleServerObserver observer : mObservers) {
removeObserver(observer);
}
}
}