/**
* Copyright (C) 2012 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.cellbots.logger.localServer;
import android.os.Environment;
import android.util.Log;
import org.apache.http.ConnectionClosedException;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpServerConnection;
import org.apache.http.HttpStatus;
import org.apache.http.MethodNotSupportedException;
import org.apache.http.ProtocolVersion;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentProducer;
import org.apache.http.entity.EntityTemplate;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.message.BasicHttpResponse;
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.HttpService;
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.util.EntityUtils;
import java.io.File;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
/**
* This is the HTTP server that runs locally and listens on the specified port.
* Accessing it directly will result in a dump of the current state of the
* sensors. We plan to add other functionality such as serving out an HTML
* interface and exposing logging controls through this local server. This code
* is based off of
* https://cellbots.googlecode.com/svn/trunk/android/java/cellbots/src/com/cellbots/httpserver/HttpCommandServer.java
* originally created by chaitanyag@google.com (Chaitanya Gharpure).
*
* @author clchen@google.com (Charles L. Chen)
*/
public class LocalHttpServer {
private class ResponseResource<T> {
public T resource;
public String contentType;
public ResponseResource(T res, String ct) {
resource = res;
contentType = ct;
}
}
private class UrlParams {
public String[] keys;
public String[] values;
public UrlParams(String url) {
int index = url.indexOf('?');
if (index >= 0) {
String paramStr = url.substring(url.indexOf('?') + 1);
String[] toks = paramStr.split("&");
keys = new String[toks.length];
values = new String[toks.length];
int i = 0;
for (String tok : toks) {
String[] tmp = tok.split("=");
if (tmp.length > 0) {
keys[i] = tmp[0];
if (tmp.length > 1)
values[i] = tmp[1];
} else {
keys[i] = tok;
}
i++;
}
}
}
}
private static final String TAG = "LocalHttpServer";
private static final String EXTERNAL_STORAGE_PATH =
Environment.getExternalStorageDirectory() + "/";
private int mPort = 8080;
private String rootDir;
private RequestListenerThread listenerThread;
private boolean running = true;
HashMap<String, ResponseResource<byte[]>> dataMap =
new HashMap<String, ResponseResource<byte[]>>();
HashMap<String, ResponseResource<String>> resourceMap =
new HashMap<String, ResponseResource<String>>();
private HttpCommandServerListener serverListener;
public LocalHttpServer(String root, int port, HttpCommandServerListener listener) {
serverListener = listener;
mPort = port;
setRoot(root);
try {
listenerThread = new RequestListenerThread(mPort);
listenerThread.setDaemon(false);
listenerThread.start();
} catch (IOException e) {
Log.e(TAG, "Error starting HTTP server: " + e.getMessage());
}
}
public void setRoot(String root) {
rootDir = EXTERNAL_STORAGE_PATH + root;
File dir = new File(EXTERNAL_STORAGE_PATH + root);
if (!dir.exists()) {
dir.mkdirs();
}
}
public void addResponseByName(String name, byte[] data, String contentType) {
if (name == null || data == null)
return;
dataMap.put(name, new ResponseResource<byte[]>(data, contentType));
}
public void addResponseByName(String name, String path, String contentType) {
if (name == null || path == null)
return;
resourceMap.put(name, new ResponseResource<String>(path, contentType));
}
public byte[] getResponseByName(String name) {
return dataMap.containsKey(name) ? dataMap.get(name).resource : null;
}
public void stopServer() {
running = false;
if (listenerThread == null)
return;
listenerThread.stopServer();
try {
listenerThread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private String getResourceNameFromTarget(String target) {
int lastPos = target.indexOf('?');
return target.substring(1, lastPos >= 0 ? lastPos : target.length());
}
public void handle(final HttpServerConnection conn, final HttpContext context)
throws HttpException, IOException {
HttpRequest request = conn.receiveRequestHeader();
HttpResponse response = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1),
HttpStatus.SC_OK, "OK");
String method = request.getRequestLine().getMethod().toUpperCase(Locale.ENGLISH);
if (!method.equals("GET") &&
!method.equals("HEAD") &&
!method.equals("POST") &&
!method.equals("PUT")) {
throw new MethodNotSupportedException(method + " method not supported");
}
// Get the requested target. This is the string after the domain name in
// the URL. If the full URL was http://mydomain.com/test.html, target
// will be /test.html.
String target = request.getRequestLine().getUri();
// Log.w(TAG, "*** Request target: " + target);
// Gets the requested resource name. For example, if the full URL was
// http://mydomain.com/test.html?x=1&y=2, resource name will be
// test.html
final String resName = getResourceNameFromTarget(target);
UrlParams params = new UrlParams(target);
// Log.w(TAG, "*** Request LINE: " +
// request.getRequestLine().toString());
// Log.w(TAG, "*** Request resource: " + resName);
if (method.equals("POST") || method.equals("PUT")) {
byte[] entityContent = null;
// Gets the content if the request has an entity.
if (request instanceof HttpEntityEnclosingRequest) {
conn.receiveRequestEntity((HttpEntityEnclosingRequest) request);
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
if (entity != null) {
entityContent = EntityUtils.toByteArray(entity);
}
}
response.setStatusCode(HttpStatus.SC_OK);
if (serverListener != null) {
serverListener.onRequest(resName, params.keys, params.values, entityContent);
}
} else if (dataMap.containsKey(resName)) { // The requested resource is
// a byte array
response.setStatusCode(HttpStatus.SC_OK);
response.setHeader("Content-Type", dataMap.get(resName).contentType);
response.setEntity(new ByteArrayEntity(dataMap.get(resName).resource));
} else { // Return sensor readings
String contentType = resourceMap.containsKey(resName) ?
resourceMap.get(resName).contentType : "text/html";
response.setStatusCode(HttpStatus.SC_OK);
EntityTemplate body = new EntityTemplate(new ContentProducer() {
@Override
public void writeTo(final OutputStream outstream) throws IOException {
OutputStreamWriter writer = new OutputStreamWriter(outstream, "UTF-8");
writer.write(serverListener.getLoggerStatus());
writer.flush();
}
});
body.setContentType(contentType);
response.setEntity(body);
}
conn.sendResponseHeader(response);
conn.sendResponseEntity(response);
conn.flush();
conn.shutdown();
}
/**
* This thread listens to HTTP requests and delegates it to worker threads.
*/
class RequestListenerThread extends Thread {
private final ServerSocket serversocket;
private final HttpParams params;
public RequestListenerThread(int port) throws IOException {
this.serversocket = new ServerSocket(port);
this.params = new BasicHttpParams();
this.params
.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, "HttpComponents/1.1");
// Set up the HTTP protocol processor
BasicHttpProcessor httpproc = new BasicHttpProcessor();
httpproc.addInterceptor(new ResponseDate());
httpproc.addInterceptor(new ResponseServer());
httpproc.addInterceptor(new ResponseContent());
httpproc.addInterceptor(new ResponseConnControl());
// Set up the HTTP service
new HttpService(httpproc, new DefaultConnectionReuseStrategy(),
new DefaultHttpResponseFactory());
}
public void stopServer() {
try {
this.serversocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
Log.d(TAG, "*** Listening on port " + this.serversocket.getLocalPort());
while (running) {
try {
// Set up HTTP connection
Socket socket = this.serversocket.accept();
DefaultHttpServerConnection conn = new DefaultHttpServerConnection();
conn.bind(socket, this.params);
// Start worker thread
Thread t = new WorkerThread(conn);
t.setDaemon(true);
t.start();
} catch (InterruptedIOException ex) {
break;
} catch (IOException e) {
Log.e(TAG, "I/O error initialising connection thread: " + e.getMessage());
break;
}
}
try {
this.serversocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* This thread handles the HTTP requests.
*/
class WorkerThread extends Thread {
private final HttpServerConnection conn;
public WorkerThread(final HttpServerConnection conn) {
super();
this.conn = conn;
}
@Override
public void run() {
HttpContext context = new BasicHttpContext(null);
try {
while (!Thread.interrupted() && this.conn.isOpen()) {
LocalHttpServer.this.handle(conn, context);
}
} catch (ConnectionClosedException ex) {
Log.e(TAG, "Client closed connection");
} catch (IOException ex) {
ex.printStackTrace();
Log.e(TAG, "I/O error: " + ex.getMessage());
} catch (HttpException ex) {
Log.e(TAG, "Unrecoverable HTTP protocol violation: " + ex.getMessage());
} finally {
try {
this.conn.shutdown();
} catch (IOException ignore) {
}
}
}
}
/**
* Implement this interface to receive callback when an HTTP PUT/POST
* request is received.
*/
public interface HttpCommandServerListener {
public void onRequest(String req, String[] keys, String[] values, byte[] data);
public String getLoggerStatus();
}
/**
* Returns the IP addresses of this device.
*
* @return IP addresses as a String. Addresses are separated by newline.
*/
public static String getLocalIpAddresses() {
String ipAddresses = "";
try {
for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces();
en.hasMoreElements();) {
NetworkInterface intf = en.nextElement();
for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses();
enumIpAddr.hasMoreElements();) {
InetAddress inetAddress = enumIpAddr.nextElement();
if (!inetAddress.isLoopbackAddress()) {
String ipAddress = inetAddress.getHostAddress().toString();
if (ipAddress.split("\\.").length == 4) {
ipAddresses = ipAddresses + ipAddress + ":8080\n";
}
}
}
}
} catch (SocketException ex) {
Log.e("", ex.toString());
}
return ipAddresses;
}
}