/* WebServer.java Copyright (c) 2016 NTT DOCOMO,INC. Released under the MIT license http://opensource.org/licenses/mit-license.php */ package org.deviceconnect.android.deviceplugin.awsiot.cores.p2p; import android.content.Context; import android.util.Log; import org.deviceconnect.android.deviceplugin.awsiot.remote.BuildConfig; import org.deviceconnect.android.deviceplugin.awsiot.udt.P2PConnection; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class WebServer extends AWSIotP2PManager { private static final boolean DEBUG = BuildConfig.DEBUG; private static final String TAG = "AWS"; private static final int MAX_CLIENT_SIZE = 8; private String mPath = "/" + UUID.randomUUID().toString(); private boolean mStopFlag; private ServerSocket mServerSocket; private Map<Integer, ServerRunnable> mServerRunnableMap = new ConcurrentHashMap<>(); private final ExecutorService mExecutor = Executors.newFixedThreadPool(MAX_CLIENT_SIZE); private String mDestAddress; private Context mContext; public WebServer(final Context context, final String address) { mContext = context; mDestAddress = address; } public void setPath(String path) { mPath = path; } public String getUrl() { if (mServerSocket == null || mPath == null) { return null; } return "http://localhost:" + mServerSocket.getLocalPort() + mPath; } public synchronized String start() { if (mServerSocket != null) { throw new RuntimeException("WebServer is already running."); } try { mServerSocket = openServerSocket(); } catch (IOException e) { // Failed to open server socket mStopFlag = true; return null; } Executors.newSingleThreadExecutor().submit(new Runnable() { @Override public void run() { mStopFlag = false; try { while (!mStopFlag) { mExecutor.execute(new ServerRunnable(mServerSocket.accept())); } } catch (IOException e) { if (DEBUG) { Log.w(TAG, "WebServer#start: " + e.getMessage()); } } finally { stop(); } } }); return getUrl(); } public void stop() { if (mStopFlag) { return; } mStopFlag = true; mExecutor.shutdown(); if (mServerSocket != null) { try { mServerSocket.close(); mServerSocket = null; } catch (IOException e) { if (DEBUG) { Log.w(TAG, "", e); } } } mPath = null; } @Override public void onReceivedSignaling(final String signaling) { if (DEBUG) { Log.i(TAG, "WebServer#onReceivedSignaling:" + signaling); } ServerRunnable run = mServerRunnableMap.get(getConnectionId(signaling)); if (run != null) { run.onReceivedSignaling(signaling); } } public boolean hasConnectionId(final String signaling) { return mServerRunnableMap.get(getConnectionId(signaling)) != null; } protected void onConnected() { } protected void onDisconnected() { } private ServerSocket openServerSocket() throws IOException { for (int i = 9000; i < 10000; i++) { try { return new ServerSocket(i); } catch (IOException e) { if (DEBUG) { Log.w(TAG, "already use port=" + i); } } } throw new IOException("Cannot open server socket."); } private byte[] decodeHeader(final byte[] buf, final int len) throws IOException { BufferedReader in = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, len))); ByteArrayOutputStream out = new ByteArrayOutputStream(); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out)); String line; while ((line = in.readLine()) != null) { if (line.toLowerCase().startsWith("host")) { writer.write("Host: " + mDestAddress); } else { writer.write(line); } writer.write("\r\n"); writer.flush(); } return out.toByteArray(); } private class ServerRunnable implements Runnable { private static final int BUF_SIZE = 1024 * 8; private Socket mSocket; private boolean mStopFlag; private P2PConnection mP2PConnection; private int mConnectionId; public ServerRunnable(final Socket socket) { mConnectionId = P2PConnection.generateConnectionId(); mSocket = socket; } private void connectP2P() { try { mP2PConnection = new P2PConnection(mConnectionId); mP2PConnection.setOnP2PConnectionListener(mOnP2PConnectionListener); mP2PConnection.open(); } catch (IOException e) { if (DEBUG) { Log.w(TAG, "ServerRunnable#connectP2P"); } } } private void onReceivedSignaling(final String signaling) { if (DEBUG) { Log.i(TAG, "ServerRunnable#onReceivedSignaling"); } if (mP2PConnection != null) { try { mP2PConnection.close(); } catch (IOException e) { if (DEBUG) { Log.w(TAG, "ServerRunnable#onReceivedSignaling", e); } } } mP2PConnection = createP2PConnection(signaling, mOnP2PConnectionListener); if (mP2PConnection == null) { sendFailedToConnect(); mStopFlag = true; } } private void relayHttpRequest() { final byte[] buf = new byte[BUF_SIZE]; int headerSize = 0; int readLength = 0; int read; if (DEBUG) { Log.e(TAG, "WebServer#relayHttpRequest"); } try { InputStream in = mSocket.getInputStream(); read = in.read(buf, 0, BUF_SIZE); if (read == -1) { // error return; } while (read > 0) { readLength += read; headerSize = findHeaderEnd(buf, readLength); if (headerSize > 0) { break; } read = in.read(buf, readLength, BUF_SIZE - readLength); } mP2PConnection.sendData(decodeHeader(buf, headerSize)); mP2PConnection.sendData(buf, headerSize, readLength - headerSize); while (!mStopFlag && (read = in.read(buf)) != -1) { mP2PConnection.sendData(buf, 0, read); } } catch (Exception e) { if (DEBUG) { Log.w(TAG, "ServerRunnable#relayHttpRequest", e); } } } private void sendFailedToConnect() { try { mSocket.getOutputStream().write(generateInternalServerError().getBytes()); mSocket.getOutputStream().flush(); } catch (IOException e) { if (DEBUG) { Log.w(TAG, "", e); } } } @Override public void run() { if (DEBUG) { Log.i(TAG, "ServerRunnable Start. Socket=" + mSocket); } mServerRunnableMap.put(mConnectionId, this); onConnected(); try { connectP2P(); while (!mStopFlag && !mSocket.isClosed()) { Thread.sleep(1000); } } catch (Exception e) { if (DEBUG) { Log.w(TAG, "ServerRunnable#run", e); } sendFailedToConnect(); } finally { if (DEBUG) { Log.i(TAG, "ServerRunnable End. Socket=" + mSocket); } mServerRunnableMap.remove(mConnectionId); if (mP2PConnection != null) { try { mP2PConnection.close(); } catch (IOException e) { e.printStackTrace(); } } if (mSocket != null) { try { mSocket.close(); } catch (IOException e) { e.printStackTrace(); } } onDisconnected(); } } private P2PConnection.OnP2PConnectionListener mOnP2PConnectionListener = new P2PConnection.OnP2PConnectionListener() { @Override public void onRetrievedAddress(final String address, final int port) { if (DEBUG) { Log.d(TAG, "WebServer#onRetrievedAddress=" + address + ":" + port); } onNotifySignaling(createSignaling(mContext, mP2PConnection.getConnectionId(), address, port)); } @Override public void onConnected(final String address, final int port) { if (DEBUG) { Log.i(TAG, "WebServer#onConnected: " + address + ":" + port); } Executors.newSingleThreadExecutor().submit(new Runnable() { @Override public void run() { relayHttpRequest(); } }); } @Override public void onReceivedData(final byte[] data, final String address, final int port) { if (DEBUG) { Log.i(TAG, "WebServer#onReceivedData: " + address + ":" + port + " " + data.length); } try { mSocket.getOutputStream().write(data); } catch (IOException e) { if (DEBUG) { Log.e(TAG, "WebServer#onReceivedData:", e); } } } @Override public void onDisconnected(final String address, final int port) { if (DEBUG) { Log.i(TAG, "WebServer#onDisconnected: " + address + ":" + port); } try { mSocket.close(); } catch (IOException e) { if (DEBUG) { Log.e(TAG, "WebServer#onDisconnected:", e); } } mStopFlag = true; } @Override public void onTimeout() { if (DEBUG) { Log.i(TAG, "WebServer#onTimeout:"); } sendFailedToConnect(); try { mSocket.close(); } catch (IOException e) { if (DEBUG) { Log.e(TAG, "WebServer#onTimeout:", e); } } mStopFlag = true; } }; } }