package org.openintents.wifiserver.webserver;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.LinkedList;
import java.util.List;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.impl.nio.DefaultServerIOEventDispatch;
import org.apache.http.impl.nio.SSLServerIOEventDispatch;
import org.apache.http.impl.nio.reactor.DefaultListeningIOReactor;
import org.apache.http.nio.protocol.BufferingHttpServiceHandler;
import org.apache.http.nio.reactor.IOEventDispatch;
import org.apache.http.nio.reactor.IOReactorException;
import org.apache.http.nio.reactor.ListeningIOReactor;
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.BasicHttpProcessor;
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 android.util.Log;
/**
* The WebServer is the core of the server application. It handles the
* initialization and is used to start and stop the actual server component.
*
* @author Stanley Förster
*
*/
public class WebServer {
/**
* This enum includes all states, the server can have.
*
* @author Stanley Förster
*/
public enum Status {
STARTED,
STOPPED,
ERROR
}
private final static String TAG = WebServer.class.getSimpleName();
private int mPort = -1;
private ListeningIOReactor mIOReactor;
private IOEventDispatch mIOEventDispatch;
private HttpRequestHandlerRegistry mHandlerRegistry;
private BasicHttpProcessor mHttpProcessor;
private List<ServerStatusListener> mListeners;
/**
* Creates a new {@link WebServer} with SSL support.<br />
* When using this constructor the web server is initialized with SSL
* support using the given certificate. To load the key store a password is
* required. Also only BKS key stores are supported.<br />
* The server will listen for incoming requests only on the specified port.
*
* @param port
* Port on which the server will listen for incoming requests.
* @param enableSSL
* Indicates if communication should be SSL encrypted.
* @param certFile
* The certificate which is used to establish a SSL connection.
* @param password
* Password which is required to load the key store.
*/
public WebServer(int port, boolean enableSSL, InputStream certFile, char[] password) {
this.mPort = port;
this.mListeners = new LinkedList<ServerStatusListener>();
HttpParams httpParams = new BasicHttpParams();
httpParams.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 30000)
.setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, 8*1024)
.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, false)
.setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true)
.setParameter(CoreProtocolPNames.ORIGIN_SERVER, "OI Server");
mHttpProcessor = new BasicHttpProcessor();
mHttpProcessor.addInterceptor(new ResponseDate());
mHttpProcessor.addInterceptor(new ResponseServer());
mHttpProcessor.addInterceptor(new ResponseContent());
mHttpProcessor.addInterceptor(new ResponseConnControl());
BufferingHttpServiceHandler handler = new BufferingHttpServiceHandler(
mHttpProcessor,
new DefaultHttpResponseFactory(),
new DefaultConnectionReuseStrategy(),
httpParams);
mHandlerRegistry = new HttpRequestHandlerRegistry();
handler.setHandlerResolver(mHandlerRegistry);
if (enableSSL) {
mIOEventDispatch = new SSLServerIOEventDispatch(handler, createSSLContext(certFile, password), httpParams);
} else {
mIOEventDispatch = new DefaultServerIOEventDispatch(handler, httpParams);
}
try {
mIOReactor = new DefaultListeningIOReactor(2, httpParams);
} catch (IOReactorException e) {
e.printStackTrace();
}
}
/**
* Creates a web server without SSL support, which will listen on the
* specified port.
*
* @param port
* The port on which the server should listen for incoming
* requests.
*/
public WebServer(int port) {
this(port, false, null, null);
}
/**
* Adds a new request interceptor, which will be called before the request
* is handled by a specific {@link HttpRequestHandler}.
*
* @param interceptor
* The interceptor which should be added to the list of
* interceptors.
*/
public void addRequestInterceptor(HttpRequestInterceptor interceptor) {
mHttpProcessor.addInterceptor(interceptor);
}
/**
* Registers a new request handler which will be invoked when a request's
* URL matches the given URL pattern.
* If there are more than one matching pattern, the most specific one will
* be used.
*
* @param urlPattern
* The pattern that indicates which requests should be handled by
* the request handler.
* @param handler
* The handler which will be invoked if a request's URL matches
* the corresponding pattern.
*/
public void registerRequestHandler(String urlPattern, HttpRequestHandler handler) {
mHandlerRegistry.register(urlPattern, handler);
}
/**
* Creates a new SSL context by loading the given certificate from a key
* store. The only supported format is "BKS".
*
* @param certFile
* Certificate file which is required to create a SSL context.
* @param password
* The password is required to load the key store.
* @return A initializes SSL context using the given certificate.
*/
private SSLContext createSSLContext(InputStream certFile, char[] password) {
SSLContext sslContext = null;
try {
KeyStore keyStore;
keyStore = KeyStore.getInstance("BKS");
keyStore.load(certFile, password);
certFile.close();
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, password);
sslContext = SSLContext.getInstance("SSL");
sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
} catch (KeyStoreException e) {
Log.e(TAG, "There was an error with the keystore!", e);
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "No such algorithm!", e);
} catch (CertificateException e) {
Log.e(TAG, "Failed to initialize keystore!", e);
} catch (IOException e) {
Log.e(TAG, "Failed to read certification file!", e);
} catch (UnrecoverableKeyException e) {
Log.e(TAG, "Failed to init key manager factory!", e);
} catch (KeyManagementException e) {
Log.e(TAG, "Failed to init ssl context!", e);
}
return sslContext;
}
/**
* Starts the server which will then listen for incoming requests. This will
* run in a new thread. After the server started successfully the state will
* be changed to "STARTED".
*/
public void start() {
Thread server = new Thread(new Runnable() {
@Override
public void run() {
try {
mIOReactor.listen(new InetSocketAddress(mPort));
mIOReactor.execute(mIOEventDispatch);
} catch (IOException e) {
e.printStackTrace();
statusUpdate(Status.ERROR, e.toString());
}
}
});
server.setDaemon(true);
server.start();
statusUpdate(Status.STARTED);
}
/**
* Stops the server and sets its state to "STOPPED".
*/
public void stop() {
try {
mIOReactor.shutdown();
} catch (IOException e) {
e.printStackTrace();
}
statusUpdate(Status.STOPPED);
}
/**
* Returns the port which was specified in the constructor.
*
* @return The port, the server listens on.
*/
public int getPort() {
return mPort;
}
/**
* Adds a new listener which will be notified if the server changes its
* state.
*
* @param listener
* A new listener.
*/
public void addListener(ServerStatusListener listener) {
this.mListeners.add(listener);
}
/**
* Removes the specified listener, so it will be no longer invoked after the
* server changed its state.
*
* @param listener
* The listener which should be removed.
*/
public void removeListener(ServerStatusListener listener) {
this.mListeners.remove(listener);
}
/**
* Notifies all registered listeners if the server's state changed.
* An optional message can be given which includes additional information
* about the state change, like an error message.
*
* @param status
* The server's new state.
* @param msg
* An optional message.
*/
private void statusUpdate(Status status, String msg) {
for (ServerStatusListener listener : mListeners) {
listener.onStatusChanged(status, msg);
}
}
/**
* Notifies all registered listeners if the server's state changed.
*
* @param status
* The server's new state.
*/
private void statusUpdate(Status status) {
this.statusUpdate(status, null);
}
}