Java tutorial
/* * Copyright (C) 2011-2013 GUIGUI Simon, fyhertz@gmail.com * * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/) * * Spydroid 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 source code 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 source code; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Based on that: http://hc.apache.org/httpcomponents-core-ga/examples.html. * */ package net.kseek.http; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.BindException; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; import java.util.Date; import java.util.LinkedList; import java.util.Map; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.X509KeyManager; import org.apache.http.ConnectionClosedException; import org.apache.http.HttpException; import org.apache.http.HttpServerConnection; import org.apache.http.impl.DefaultConnectionReuseStrategy; import org.apache.http.impl.DefaultHttpResponseFactory; import org.apache.http.impl.DefaultHttpServerConnection; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.CoreConnectionPNames; import org.apache.http.params.CoreProtocolPNames; import org.apache.http.params.HttpParams; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.BasicHttpProcessor; import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpRequestHandler; import org.apache.http.protocol.HttpRequestHandlerRegistry; import org.apache.http.protocol.ResponseConnControl; import org.apache.http.protocol.ResponseContent; import org.apache.http.protocol.ResponseDate; import org.apache.http.protocol.ResponseServer; import org.apache.http.protocol.UriPatternMatcher; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Binder; import android.os.IBinder; import android.preference.PreferenceManager; import android.util.Log; /** * * A Service that contains a HTTP server and a HTTPS server. * * Check out the github page of this project for further information. * * You may add some logic to this server with {@link #addRequestHandler(String, HttpRequestHandler)}. * By default it serves files from /assets/www. * */ public class TinyHttpServer extends Service { /** The tag used by the server. */ public static final String TAG = "TinyHttpServer"; /** Default port for HTTP. */ public final static int DEFAULT_HTTP_PORT = 8080; /** Default port for HTTPS. */ public final static int DEFAULT_HTTPS_PORT = 8443; /** Port already in use. */ public final static int ERROR_HTTP_BIND_FAILED = 0x00; /** Port already in use. */ public final static int ERROR_HTTPS_BIND_FAILED = 0x01; /** You need to add the class {@link ModSSL} to the package {@link net.kseek.http}. */ public final static int ERROR_HTTPS_NOT_SUPPORTED = 0x02; /** An error occured with the HTTPS server :( */ public final static int ERROR_HTTPS_SERVER_CRASHED = 0x03; /** Key used in the SharedPreferences to store whether the HTTP server is enabled or not. */ public final static String KEY_HTTP_ENABLED = "http_enabled"; /** Key used in the SharedPreferences to store whether the HTTPS server is enabled or not. */ public final static String KEY_HTTPS_ENABLED = "https_enabled"; /** Key used in the SharedPreferences for the port used by the HTTP server. */ public final static String KEY_HTTP_PORT = "http_port"; /** Key used in the SharedPreferences for the port used by the HTTPS server. */ public final static String KEY_HTTPS_PORT = "https_port"; /** Key used in the SharedPreferences for storing the password of the keystore. */ public final static String KEY_PASSWORD = "https_password"; /** Name of the file used to store the keystore containing the certificates for HTTPS. */ public final static String KEYSTORE_FILE_NAME = "keystore.jks"; /** * Common name that will appear in the root certificate. * You might want to extend {@link TinyHttpServer} to change that. **/ protected String mCACommonName = "TinyHttpServer CA"; protected String[] MODULES = new String[] { "ModAssetServer", "ModInternationalization" }; protected int mHttpPort = DEFAULT_HTTP_PORT; protected int mHttpsPort = DEFAULT_HTTPS_PORT; protected boolean mHttpEnabled = true, mHttpsEnabled = false; protected LinkedList<CallbackListener> mListeners = new LinkedList<CallbackListener>(); private BasicHttpProcessor mHttpProcessor; private HttpParams mParams; private HttpRequestListener mHttpRequestListener = null; private HttpsRequestListener mHttpsRequestListener = null; private SharedPreferences mSharedPreferences; private boolean mHttpsUpdate = false, mHttpUpdate = false; Date mLastModified; MHttpRequestHandlerRegistry mRegistry; Context mContext; /** Be careful: those callbacks won't necessarily be called from the ui thread ! */ public interface CallbackListener { /** Called when an error occurs. */ void onError(TinyHttpServer server, Exception e, int error); void onMessage(TinyHttpServer server, int message); } /** * See {@link TinyHttpServer.CallbackListener} to check out what events will be fired once you set up a listener. * @param listener The listener */ public void addCallbackListener(CallbackListener listener) { synchronized (mListeners) { mListeners.add(listener); } } /** * Removes the listener. * @param listener The listener */ public void removeCallbackListener(CallbackListener listener) { synchronized (mListeners) { mListeners.remove(listener); } } /** * You may add some HttpRequestHandler to modify the default behavior of the server. * @param pattern Patterns may have three formats: * or *<uri> or <uri>* * @param handler A HttpRequestHandler */ protected void addRequestHandler(String pattern, HttpRequestHandler handler) { mRegistry.register(pattern, handler); } /** * Sets the port for the HTTP server to use. * @param port The port to use */ public void setHttpPort(int port) { Editor editor = mSharedPreferences.edit(); editor.putString(KEY_HTTP_PORT, String.valueOf(port)); editor.commit(); } /** * Sets the port for the HTTPS server to use. * @param port The port to use */ public void setHttpsPort(int port) { Editor editor = mSharedPreferences.edit(); editor.putString(KEY_HTTPS_PORT, String.valueOf(port)); editor.commit(); } /** Enables the HTTP server. */ public void setHttpEnabled(boolean enable) { Editor editor = mSharedPreferences.edit(); editor.putBoolean(KEY_HTTP_ENABLED, enable); editor.commit(); } /** Enables the HTTPS server. */ public void setHttpsEnabled(boolean enable) { Editor editor = mSharedPreferences.edit(); editor.putBoolean(KEY_HTTPS_ENABLED, enable); editor.commit(); } /** Returns the port used by the HTTP server. */ public int getHttpPort() { return mHttpPort; } /** Returns the port used by the HTTPS server. */ public int getHttpsPort() { return mHttpsPort; } /** Indicates whether or not the HTTP server is enabled. */ public boolean isHttpEnabled() { return mHttpEnabled; } /** Indicates whether or not the HTTPS server is enabled. */ public boolean isHttpsEnabled() { return mHttpsEnabled; } /** Starts (or restart if needed) the HTTP server. */ public void start() { // Stops the HTTP server if it has been disabled or if it needs to be restarted if ((!mHttpEnabled || mHttpUpdate) && mHttpRequestListener != null) { mHttpRequestListener.kill(); mHttpRequestListener = null; } // Stops the HTTPS server if it has been disabled or if it needs to be restarted if ((!mHttpsEnabled || mHttpsUpdate) && mHttpsRequestListener != null) { mHttpsRequestListener.kill(); mHttpsRequestListener = null; } // Starts the HTTP server if needed if (mHttpEnabled && mHttpRequestListener == null) { try { mHttpRequestListener = new HttpRequestListener(mHttpPort); } catch (Exception e) { mHttpRequestListener = null; } } // Starts the HTTPS server if needed if (mHttpsEnabled && mHttpsRequestListener == null) { try { mHttpsRequestListener = new HttpsRequestListener(mHttpsPort); } catch (Exception e) { mHttpsRequestListener = null; } } mHttpUpdate = false; mHttpsUpdate = false; } /** Stops the HTTP server and/or the HTTPS server but not the Android service. */ public void stop() { if (mHttpRequestListener != null) { // Stops the HTTP server mHttpRequestListener.kill(); mHttpRequestListener = null; } if (mHttpsRequestListener != null) { // Stops the HTTPS server mHttpsRequestListener.kill(); mHttpsRequestListener = null; } } @Override public void onCreate() { super.onCreate(); mContext = getApplicationContext(); mRegistry = new MHttpRequestHandlerRegistry(); mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); mParams = new BasicHttpParams(); mParams.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 5000) .setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, 8 * 1024) .setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, false) .setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true) .setParameter(CoreProtocolPNames.ORIGIN_SERVER, "MajorKernelPanic HTTP Server"); // Set up the HTTP protocol processor mHttpProcessor = new BasicHttpProcessor(); mHttpProcessor.addInterceptor(new ResponseDate()); mHttpProcessor.addInterceptor(new ResponseServer()); mHttpProcessor.addInterceptor(new ResponseContent()); mHttpProcessor.addInterceptor(new ResponseConnControl()); // Will be used in the "Last-Modifed" entity-header field try { String packageName = mContext.getPackageName(); mLastModified = new Date(mContext.getPackageManager().getPackageInfo(packageName, 0).lastUpdateTime); } catch (NameNotFoundException e) { mLastModified = new Date(0); } // Restores the state of the service mHttpPort = Integer.parseInt(mSharedPreferences.getString(KEY_HTTP_PORT, String.valueOf(mHttpPort))); mHttpsPort = Integer.parseInt(mSharedPreferences.getString(KEY_HTTPS_PORT, String.valueOf(mHttpsPort))); mHttpEnabled = mSharedPreferences.getBoolean(KEY_HTTP_ENABLED, mHttpEnabled); mHttpsEnabled = mSharedPreferences.getBoolean(KEY_HTTPS_ENABLED, mHttpsEnabled); // If the configuration is modified, the server will adjust mSharedPreferences.registerOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener); // Loads plugins available in the package net.kseek.http for (int i = 0; i < MODULES.length; i++) { try { Class<?> pluginClass = Class .forName(TinyHttpServer.class.getPackage().getName() + "." + MODULES[i]); Constructor<?> pluginConstructor = pluginClass.getConstructor(new Class[] { TinyHttpServer.class }); addRequestHandler((String) pluginClass.getField("PATTERN").get(null), (HttpRequestHandler) pluginConstructor.newInstance(this)); } catch (ClassNotFoundException ignore) { // Module disabled } catch (Exception e) { Log.e(TAG, "Bad module: " + MODULES[i]); e.printStackTrace(); } } start(); } @Override public void onDestroy() { stop(); mSharedPreferences.unregisterOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener); } @Override public int onStartCommand(Intent intent, int flags, int startId) { //Log.d(TAG,"TinyServerHttp started !"); return START_STICKY; } private OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key.equals(KEY_HTTP_PORT)) { int port = Integer.parseInt(sharedPreferences.getString(KEY_HTTP_PORT, String.valueOf(mHttpPort))); if (port != mHttpPort) { mHttpPort = port; mHttpUpdate = true; start(); } } else if (key.equals(KEY_HTTPS_PORT)) { int port = Integer .parseInt(sharedPreferences.getString(KEY_HTTPS_PORT, String.valueOf(mHttpsPort))); if (port != mHttpsPort) { mHttpsPort = port; mHttpsUpdate = true; start(); } } else if (key.equals(KEY_HTTPS_ENABLED)) { mHttpsEnabled = sharedPreferences.getBoolean(KEY_HTTPS_ENABLED, true); start(); } else if (key.equals(KEY_HTTP_ENABLED)) { mHttpEnabled = sharedPreferences.getBoolean(KEY_HTTP_ENABLED, true); start(); } } }; /** The Binder you obtain when a connection with the Service is established. */ public class LocalBinder extends Binder { public TinyHttpServer getService() { return TinyHttpServer.this; } } /** See {@link TinyHttpServer.LocalBinder}. */ @Override public IBinder onBind(Intent intent) { return mBinder; } private final IBinder mBinder = new LocalBinder(); protected void postError(Exception exception, int id) { synchronized (mListeners) { if (mListeners.size() > 0) { for (CallbackListener cl : mListeners) { cl.onError(this, exception, id); } } } } protected void postMessage(int id) { synchronized (mListeners) { if (mListeners.size() > 0) { for (CallbackListener cl : mListeners) { cl.onMessage(this, id); } } } } protected class HttpRequestListener extends RequestListener { public HttpRequestListener(final int port) throws Exception { try { ServerSocket serverSocket = new ServerSocket(port); construct(serverSocket); Log.i(TAG, "HTTP server listening on port " + serverSocket.getLocalPort()); } catch (BindException e) { postError(e, ERROR_HTTP_BIND_FAILED); throw e; } } protected void kill() { super.kill(); Log.i(TAG, "HTTP server stopped !"); } } protected class HttpsRequestListener extends RequestListener { private X509KeyManager mKeyManager = null; private char[] mPassword; private boolean mNotSupported = false; private final String mClasspath = TinyHttpServer.class.getPackage().getName() + ".ModSSL$X509KeyManager"; public HttpsRequestListener(final int port) throws Exception { SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(TinyHttpServer.this); if (!settings.contains(KEY_PASSWORD)) { // Generates a password for the keystore // TODO: entropy of Math.random() ? String password = Integer.toString((int) (Math.random() * Integer.MAX_VALUE), 36); Editor editor = settings.edit(); editor.putString(KEY_PASSWORD, password); editor.commit(); mPassword = password.toCharArray(); mContext.deleteFile(KEYSTORE_FILE_NAME); } else { mPassword = settings.getString(KEY_PASSWORD, "XX").toCharArray(); } // We create the X509KeyManager through reflexion so that SSL support can easily be removed if not needed try { Class<?> X509KeyManager = Class.forName(mClasspath); Method loadFromKeyStore = X509KeyManager.getDeclaredMethod("loadFromKeyStore", InputStream.class, char[].class); try { InputStream is = mContext.openFileInput(KEYSTORE_FILE_NAME); mKeyManager = (X509KeyManager) loadFromKeyStore.invoke(null, is, mPassword); } catch (FileNotFoundException e) { } catch (Exception e) { Log.e(TAG, "Could not open keystore, a new one will be created..."); e.printStackTrace(); } if (mKeyManager == null) { Constructor<?> constructor = X509KeyManager .getConstructor(new Class[] { char[].class, String.class }); mKeyManager = (javax.net.ssl.X509KeyManager) constructor.newInstance(mPassword, mCACommonName); } SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(new KeyManager[] { mKeyManager }, null, null); SSLServerSocket serverSocket = (SSLServerSocket) sslContext.getServerSocketFactory() .createServerSocket(port); serverSocket.setUseClientMode(false); serverSocket.setEnableSessionCreation(true); serverSocket.setWantClientAuth(false); Log.d(TAG, "Protocol: " + sslContext.getProtocol()); Log.d(TAG, "Provider: " + sslContext.getProvider()); Log.d(TAG, "Cipher suites: " + arrToString(serverSocket.getEnabledCipherSuites())); Log.d(TAG, "Protocols enabled: " + arrToString(serverSocket.getEnabledProtocols())); serverSocket.setEnabledProtocols(new String[] { "TLSv1" }); serverSocket.setEnabledCipherSuites(new String[] { "TLS_RSA_WITH_AES_128_CBC_SHA" }); Log.d(TAG, "Cipher suites: " + arrToString(serverSocket.getEnabledCipherSuites())); Log.d(TAG, "Protocols enabled: " + arrToString(serverSocket.getEnabledProtocols())); construct(serverSocket); Log.i(TAG, "HTTPS server listening on port " + serverSocket.getLocalPort()); } catch (NoSuchMethodException e) { // HTTPS support disabled ! Log.e(TAG, "HTTPS not supported !"); postError(e, ERROR_HTTPS_NOT_SUPPORTED); throw e; } catch (BindException e) { postError(e, ERROR_HTTPS_BIND_FAILED); throw e; } catch (Exception e) { Log.e(TAG, "HTTPS server crashed !"); e.printStackTrace(); postError(e, ERROR_HTTPS_SERVER_CRASHED); throw e; } } private String arrToString(String[] list) { String str = ""; for (int i = 0; i < list.length; i++) { str += list[i] + ";"; } return str; } /** Stops the {@link TinyHttpServer.RequestListener} */ protected void kill() { if (!mNotSupported) { super.kill(); // Saves all the certificates generated by the our KeyManager in a keystore try { Method saveToKeyStore = Class.forName(mClasspath).getDeclaredMethod("saveToKeyStore", OutputStream.class, char[].class); // Prevents concurrent write operation in the keystore OutputStream os = mContext.openFileOutput(KEYSTORE_FILE_NAME, Context.MODE_PRIVATE); saveToKeyStore.invoke(mKeyManager, os, mPassword); } catch (NoSuchMethodException e) { // HTTPS support disabled ! Log.e(TAG, "HTTPS not supported !"); postError(e, ERROR_HTTPS_NOT_SUPPORTED); } catch (Exception e) { System.out.println("An error occured while saving the KeyStore"); e.printStackTrace(); } Log.i(TAG, "HTTPS server stopped !"); } } } private class RequestListener extends Thread { private ServerSocket mServerSocket; private final org.apache.http.protocol.HttpService mHttpService; protected RequestListener() throws Exception { mHttpService = new org.apache.http.protocol.HttpService(mHttpProcessor, new DefaultConnectionReuseStrategy(), new DefaultHttpResponseFactory()); mHttpService.setHandlerResolver(mRegistry); mHttpService.setParams(mParams); } protected void construct(ServerSocket serverSocket) { mServerSocket = serverSocket; start(); } protected void kill() { try { mServerSocket.close(); } catch (IOException ignore) { } try { this.join(); } catch (InterruptedException ignore) { } } public void run() { while (!Thread.interrupted()) { try { // Set up HTTP connection Socket socket = this.mServerSocket.accept(); DefaultHttpServerConnection conn = new DefaultHttpServerConnection(); Log.d(TAG, "Incoming connection from " + socket.getInetAddress()); conn.bind(socket, mParams); // Start worker thread Thread t = new WorkerThread(this.mHttpService, conn, socket); t.setDaemon(true); t.start(); } catch (SocketException e) { break; } catch (InterruptedIOException ex) { Log.e(TAG, "Interrupted !"); break; } catch (IOException e) { Log.d(TAG, "I/O error initialising connection thread: " + e.getMessage()); break; } } } } static class WorkerThread extends Thread { private final org.apache.http.protocol.HttpService httpservice; private final HttpServerConnection conn; private final Socket socket; public WorkerThread(final org.apache.http.protocol.HttpService httpservice, final HttpServerConnection conn, final Socket socket) { super(); this.httpservice = httpservice; this.conn = conn; this.socket = socket; } public void run() { Log.d(TAG, "New connection thread"); HttpContext context = new MHttpContext(socket); try { while (!Thread.interrupted() && this.conn.isOpen()) { try { this.httpservice.handleRequest(this.conn, context); } catch (UnsupportedOperationException e) { e.printStackTrace(); // shutdownOutput is not implemented by SSLSocket, and it is called in the implementation // of org.apache.http.impl.SocketHttpServerConnection.close(). } } } catch (ConnectionClosedException e) { Log.d(TAG, "Client closed connection"); e.printStackTrace(); } catch (SocketTimeoutException e) { Log.d(TAG, "Socket timeout"); } catch (IOException e) { Log.e(TAG, "I/O error: " + e.getMessage()); } catch (HttpException e) { Log.e(TAG, "Unrecoverable HTTP protocol violation: " + e.getMessage()); } finally { try { OutputStream sockOutOStream = socket.getOutputStream(); sockOutOStream.write(new byte[0]); sockOutOStream.flush(); socket.close(); } catch (IOException e) { } try { this.conn.shutdown(); } catch (Exception ignore) { } } } } /** Little modification of BasicHttpContext to add access to the underlying tcp socket. */ public static class MHttpContext extends BasicHttpContext { private Socket socket; public MHttpContext(Socket socket) { super(null); this.socket = socket; } /** Returns a reference to the underlying socket of the connection. */ public Socket getSocket() { return socket; } } /** * A slightly modified version of org.apache.http.protocol.HttpRequestHandlerRegistry * that allows the registry to be modified while some threads may be using it. * Recent versions of this file are thread-safe, but Android seems to be using an old version. */ public static class MHttpRequestHandlerRegistry extends HttpRequestHandlerRegistry { private final UriPatternMatcher matcher; public MHttpRequestHandlerRegistry() { matcher = new UriPatternMatcher(); } public synchronized void register(final String pattern, final HttpRequestHandler handler) { matcher.register(pattern, handler); } public synchronized void unregister(final String pattern) { matcher.unregister(pattern); } public synchronized void setHandlers(@SuppressWarnings("rawtypes") final Map map) { matcher.setHandlers(map); } public synchronized HttpRequestHandler lookup(final String requestURI) { // This is the only function that will often be called by threads of the HTTP server // and it seems like a rather small crtical section to me, so it should not slow things down much return (HttpRequestHandler) matcher.lookup(requestURI); } } }