diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..5819449
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/de/ioexception/www/http/impl/BasicHttpResponse.java b/src/de/ioexception/www/http/impl/BasicHttpResponse.java
index 00526d0..3692d31 100644
--- a/src/de/ioexception/www/http/impl/BasicHttpResponse.java
+++ b/src/de/ioexception/www/http/impl/BasicHttpResponse.java
@@ -1,12 +1,23 @@
package de.ioexception.www.http.impl;
+import de.ioexception.www.Http;
import de.ioexception.www.http.HttpResponse;
import de.ioexception.www.http.HttpStatusCode;
+import de.ioexception.www.http.HttpVersion;
+import java.util.HashMap;
public class BasicHttpResponse extends BasicHttpMessage implements HttpResponse
{
HttpStatusCode statusCode;
+ public BasicHttpResponse()
+ {
+ setHeaders(new HashMap());
+ getHeaders().put(Http.CONTENT_LENGTH, "0");
+ setEntity(null);
+ setVersion(HttpVersion.VERSION_1_1);
+ }
+
@Override
public HttpStatusCode getStatusCode()
{
diff --git a/src/de/ioexception/www/server/HttpServer.java b/src/de/ioexception/www/server/HttpServer.java
index 55e1fda..ab1c239 100644
--- a/src/de/ioexception/www/server/HttpServer.java
+++ b/src/de/ioexception/www/server/HttpServer.java
@@ -2,6 +2,8 @@
import java.net.Socket;
+import de.ioexception.www.server.log.AccessLogger;
+
/**
* A basic HTTP server interface.
*
@@ -33,5 +35,12 @@ public interface HttpServer
* @return
*/
public String getServerSignature();
+
+ /**
+ * Returns the signature of the webserver.
+ *
+ * @return
+ */
+ public AccessLogger getAccessLogger();
}
diff --git a/src/de/ioexception/www/server/HttpWorker.java b/src/de/ioexception/www/server/HttpWorker.java
index bbc7f2c..2d992ba 100644
--- a/src/de/ioexception/www/server/HttpWorker.java
+++ b/src/de/ioexception/www/server/HttpWorker.java
@@ -1,5 +1,6 @@
package de.ioexception.www.server;
+import de.ioexception.www.Http;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -54,10 +55,12 @@ public Void call() throws Exception
}
else
{
- response.getHeaders().put("Connection", "close");
+ response.getHeaders().put(Http.CONNECTION, "close");
sendResponse(response, socket.getOutputStream());
socket.close();
}
+ //Log
+ server.getAccessLogger().log(socket.getInetAddress().getHostAddress(), request, response);
// We do not return anything here.
return null;
diff --git a/src/de/ioexception/www/server/cache/Cache.java b/src/de/ioexception/www/server/cache/Cache.java
new file mode 100644
index 0000000..94cbc02
--- /dev/null
+++ b/src/de/ioexception/www/server/cache/Cache.java
@@ -0,0 +1,7 @@
+package de.ioexception.www.server.cache;
+
+public interface Cache
+{
+ public V put(K key, V value);
+ public V get(K key);
+}
diff --git a/src/de/ioexception/www/server/cache/EntityCacheEntry.java b/src/de/ioexception/www/server/cache/EntityCacheEntry.java
new file mode 100644
index 0000000..f04a2d7
--- /dev/null
+++ b/src/de/ioexception/www/server/cache/EntityCacheEntry.java
@@ -0,0 +1,8 @@
+package de.ioexception.www.server.cache;
+
+public interface EntityCacheEntry
+{
+ byte[] getEntity();
+ String getETag();
+ String getContentType();
+}
diff --git a/src/de/ioexception/www/server/cache/impl/EntityCacheEntryImpl.java b/src/de/ioexception/www/server/cache/impl/EntityCacheEntryImpl.java
new file mode 100644
index 0000000..d895d39
--- /dev/null
+++ b/src/de/ioexception/www/server/cache/impl/EntityCacheEntryImpl.java
@@ -0,0 +1,37 @@
+package de.ioexception.www.server.cache.impl;
+
+import de.ioexception.www.server.cache.EntityCacheEntry;
+
+public class EntityCacheEntryImpl implements EntityCacheEntry
+{
+ private final byte[] entity;
+ private final String eTag;
+ private final String contentType;
+
+ public EntityCacheEntryImpl(byte[] entity, String eTag, String contentType)
+ {
+ super();
+ this.entity = entity;
+ this.eTag = eTag;
+ this.contentType = contentType;
+ }
+
+ @Override
+ public byte[] getEntity()
+ {
+ return entity;
+ }
+
+ @Override
+ public String getETag()
+ {
+ return eTag;
+ }
+
+ @Override
+ public String getContentType()
+ {
+ return contentType;
+ }
+
+}
diff --git a/src/de/ioexception/www/server/cache/impl/LRUCache.java b/src/de/ioexception/www/server/cache/impl/LRUCache.java
new file mode 100644
index 0000000..371394b
--- /dev/null
+++ b/src/de/ioexception/www/server/cache/impl/LRUCache.java
@@ -0,0 +1,54 @@
+package de.ioexception.www.server.cache.impl;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import de.ioexception.www.server.cache.Cache;
+
+/**
+ * A thread-safe LRU cache implementation based on internal LinkedHashMap.
+ *
+ * @author Benjamin Erb
+ *
+ * @param
+ * Entry Key Type
+ * @param
+ * Entry Value Type
+ */
+public class LRUCache implements Cache
+{
+ public static final int DEFAULT_MAX_SIZE = 1000;
+
+ private final Map internalMap;
+
+ public LRUCache()
+ {
+ this(DEFAULT_MAX_SIZE);
+ }
+
+ public LRUCache(final int maxSize)
+ {
+ this.internalMap = (Map) Collections.synchronizedMap(new LinkedHashMap(maxSize + 1, .75F, true)
+ {
+ private static final long serialVersionUID = 5369285290965670135L;
+
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest)
+ {
+ return size() > maxSize;
+ }
+ });
+ }
+
+ public V put(K key, V value)
+ {
+ return internalMap.put(key, value);
+ }
+
+ public V get(K key)
+ {
+ return internalMap.get(key);
+ }
+
+}
\ No newline at end of file
diff --git a/src/de/ioexception/www/server/impl/BasicAuthHttpWorker.java b/src/de/ioexception/www/server/impl/BasicAuthHttpWorker.java
new file mode 100644
index 0000000..1b0bc82
--- /dev/null
+++ b/src/de/ioexception/www/server/impl/BasicAuthHttpWorker.java
@@ -0,0 +1,60 @@
+package de.ioexception.www.server.impl;
+
+import de.ioexception.www.Http;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.Map;
+
+import util.Base64;
+import de.ioexception.www.http.HttpRequest;
+import de.ioexception.www.http.HttpResponse;
+import de.ioexception.www.http.HttpStatusCode;
+import de.ioexception.www.http.HttpVersion;
+import de.ioexception.www.http.impl.BasicHttpResponse;
+
+/**
+ * @author Benjamin Erb
+ *
+ */
+public class BasicAuthHttpWorker extends BasicHttpWorker
+{
+ private static final Map authentications;
+ private static final String realm = "Protected Area";
+
+ static
+ {
+ authentications = new HashMap();
+ authentications.put("test", "secret");
+ authentications.put("user", "1234");
+ };
+
+ public BasicAuthHttpWorker(Socket socket, BasicHttpServer server)
+ {
+ super(socket, server);
+ }
+
+ @Override
+ protected HttpResponse handleRequest(HttpRequest request)
+ {
+ if (request.getHeaders().containsKey(Http.AUTHORIZATION))
+ {
+ String authValue = request.getHeaders().get(Http.AUTHORIZATION);
+ String[] authValues = authValue.split(" ", 2);
+ String type = authValues[0];
+ String values = authValues[1];
+ if (type.equalsIgnoreCase("Basic"))
+ {
+ String auth = new String(Base64.decode(values));
+ String[] authentication = auth.split(":", 2);
+ if (authentications.containsKey(authentication[0]) && authentications.get(authentication[0]).equals(authentication[1]))
+ {
+ return super.handleRequest(request);
+ }
+ }
+ }
+ BasicHttpResponse response = new BasicHttpResponse();
+ response.setStatusCode(HttpStatusCode.UNAUTHORIZED);
+ response.getHeaders().put(Http.WWW_AUTHENTICATE, "Basic realm=\"" + realm + "\"");
+ return response;
+ }
+}
\ No newline at end of file
diff --git a/src/de/ioexception/www/server/impl/BasicHttpServer.java b/src/de/ioexception/www/server/impl/BasicHttpServer.java
index ddca550..7e9d776 100644
--- a/src/de/ioexception/www/server/impl/BasicHttpServer.java
+++ b/src/de/ioexception/www/server/impl/BasicHttpServer.java
@@ -1,5 +1,6 @@
package de.ioexception.www.server.impl;
+import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
@@ -8,6 +9,12 @@
import java.util.concurrent.Executors;
import de.ioexception.www.server.HttpServer;
+import de.ioexception.www.server.cache.Cache;
+import de.ioexception.www.server.cache.EntityCacheEntry;
+import de.ioexception.www.server.cache.impl.LRUCache;
+import de.ioexception.www.server.log.AccessLogger;
+import de.ioexception.www.server.log.impl.BufferedFileAccessLogger;
+import de.ioexception.www.server.log.impl.ConsoleAccessLogger;
/**
* A simple HTTP server implementation.
@@ -26,8 +33,13 @@ public class BasicHttpServer implements HttpServer
private final ExecutorService workerPool;
private final ExecutorService dispatcherService;
+ private final ExecutorService loggingService;
private final ServerSocket serverSocket;
+
+ private final Cache cache = new LRUCache(100);
+ private final AccessLogger accessLogger;
+
/**
* Creates a new HTTP server.
@@ -51,6 +63,9 @@ public BasicHttpServer(int port)
serverSocket = new ServerSocket(port);
workerPool = Executors.newFixedThreadPool(16);
dispatcherService = Executors.newSingleThreadExecutor();
+ loggingService = Executors.newSingleThreadExecutor();
+// accessLogger = new BufferedFileAccessLogger(new File("log/access.log"));
+ accessLogger = new ConsoleAccessLogger();
}
catch (IOException e)
{
@@ -62,13 +77,16 @@ public BasicHttpServer(int port)
@Override
public void dispatchRequest(Socket socket)
{
- workerPool.submit(new BasicHttpWorker(socket, this));
+ workerPool.submit(new BasicAuthHttpWorker(socket, this));
+// workerPool.submit(new BasicHttpWorker(socket, this));
+// workerPool.submit(new CachingHttpWorker(socket, this,cache));
}
@Override
public void start()
{
running = true;
+ loggingService.submit(accessLogger);
// Initiate the main server loop accepting incoming connections.
dispatcherService.submit(new Runnable()
{
@@ -123,4 +141,10 @@ public String getServerSignature()
return BasicHttpServer.SERVER_SIGNATURE;
}
+ @Override
+ public AccessLogger getAccessLogger()
+ {
+ return accessLogger;
+ }
+
}
diff --git a/src/de/ioexception/www/server/impl/BasicHttpWorker.java b/src/de/ioexception/www/server/impl/BasicHttpWorker.java
index 519e03b..9763401 100644
--- a/src/de/ioexception/www/server/impl/BasicHttpWorker.java
+++ b/src/de/ioexception/www/server/impl/BasicHttpWorker.java
@@ -80,10 +80,9 @@ protected HttpResponse handleRequest(HttpRequest request)
BasicHttpResponse response = new BasicHttpResponse();
- response.setHeaders(new HashMap());
response.getHeaders().put(Http.SERVER, server.getServerSignature());
- response.setVersion(request.getHttpVersion());
-
+ response.setVersion(request.getHttpVersion());
+
String requestUri = request.getRequestUri();
if (requestUri.equals("/"))
{
diff --git a/src/de/ioexception/www/server/impl/CachingHttpWorker.java b/src/de/ioexception/www/server/impl/CachingHttpWorker.java
new file mode 100644
index 0000000..79b527a
--- /dev/null
+++ b/src/de/ioexception/www/server/impl/CachingHttpWorker.java
@@ -0,0 +1,72 @@
+package de.ioexception.www.server.impl;
+
+import java.net.Socket;
+import java.util.HashMap;
+
+import de.ioexception.www.Http;
+import de.ioexception.www.http.HttpRequest;
+import de.ioexception.www.http.HttpResponse;
+import de.ioexception.www.http.HttpStatusCode;
+import de.ioexception.www.http.impl.BasicHttpResponse;
+import de.ioexception.www.server.cache.Cache;
+import de.ioexception.www.server.cache.EntityCacheEntry;
+import de.ioexception.www.server.cache.impl.EntityCacheEntryImpl;
+
+public class CachingHttpWorker extends BasicHttpWorker
+{
+ private final Cache cache;
+
+ public CachingHttpWorker(Socket socket, BasicHttpServer server, Cache cache)
+ {
+ super(socket, server);
+ this.cache = cache;
+ }
+
+ @Override
+ protected HttpResponse handleRequest(HttpRequest request)
+ {
+ //TODO how and when do modified entities get 'put' again to the cache?
+
+ EntityCacheEntry cacheEntry = cache.get(request.getRequestUri());
+
+ if(null == cacheEntry)
+ {
+ HttpResponse response = super.handleRequest(request);
+ if(response.getStatusCode().equals(HttpStatusCode.OK) && response.getEntity() != null && response.getEntity().length > 0)
+ {
+ EntityCacheEntry entry = new EntityCacheEntryImpl(response.getEntity(), response.getHeaders().get(Http.ETAG), response.getHeaders().get(Http.CONTENT_TYPE));
+ cache.put(request.getRequestUri(), entry);
+ response.getHeaders().put(Http.ETAG, new Integer(entry.hashCode()).toString());
+ }
+ return response;
+ }
+ else
+ {
+ if (request.getHeaders().containsKey(Http.IF_NONE_MATCH))
+ {
+ if (Integer.parseInt(request.getHeaders().get(Http.IF_NONE_MATCH)) == cacheEntry.hashCode())
+ {
+ BasicHttpResponse response = new BasicHttpResponse();
+ response.getHeaders().put(Http.SERVER, server.getServerSignature());
+ response.setVersion(request.getHttpVersion());
+ response.setStatusCode(HttpStatusCode.NOT_MODIFIED);
+ return response;
+ }
+ }
+
+ BasicHttpResponse response = new BasicHttpResponse();
+ response.getHeaders().put(Http.SERVER, server.getServerSignature());
+ response.setVersion(request.getHttpVersion());
+ response.getHeaders().put(Http.CONTENT_LENGTH, ""+cacheEntry.getEntity().length);
+ response.getHeaders().put(Http.ETAG, new Integer(cacheEntry.hashCode()).toString());
+ if(null != cacheEntry.getContentType())
+ {
+ response.getHeaders().put(Http.CONTENT_TYPE, cacheEntry.getContentType());
+ }
+ response.setEntity(cacheEntry.getEntity());
+ response.setStatusCode(HttpStatusCode.OK);
+ return response;
+ }
+ }
+
+}
diff --git a/src/de/ioexception/www/server/log/AccessLogger.java b/src/de/ioexception/www/server/log/AccessLogger.java
new file mode 100644
index 0000000..662389b
--- /dev/null
+++ b/src/de/ioexception/www/server/log/AccessLogger.java
@@ -0,0 +1,12 @@
+package de.ioexception.www.server.log;
+
+import java.util.concurrent.Callable;
+
+import de.ioexception.www.http.HttpRequest;
+import de.ioexception.www.http.HttpResponse;
+
+public interface AccessLogger extends Callable
+{
+ public void log(String clientHost, HttpRequest request, HttpResponse response);
+ public void log(String s);
+}
diff --git a/src/de/ioexception/www/server/log/impl/BufferedFileAccessLogger.java b/src/de/ioexception/www/server/log/impl/BufferedFileAccessLogger.java
new file mode 100644
index 0000000..c5f4cd0
--- /dev/null
+++ b/src/de/ioexception/www/server/log/impl/BufferedFileAccessLogger.java
@@ -0,0 +1,25 @@
+package de.ioexception.www.server.log.impl;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+/**
+ * A File-based Logger using the "Combined Log Format". It buffers log entries
+ * and writes them into the logfile when a certain amount of chunks is
+ * available. This decreases the overhead of i/o operations, but will cause loss
+ * of entries in case of ungrateful server shutdowns.
+ *
+ * @author Benjamin Erb
+ *
+ */
+public class BufferedFileAccessLogger extends GenericAccessLogger
+{
+
+ public BufferedFileAccessLogger(File logFile) throws IOException
+ {
+ super(new BufferedWriter(new FileWriter(logFile, true)));
+ }
+
+}
diff --git a/src/de/ioexception/www/server/log/impl/ConsoleAccessLogger.java b/src/de/ioexception/www/server/log/impl/ConsoleAccessLogger.java
new file mode 100644
index 0000000..9091b42
--- /dev/null
+++ b/src/de/ioexception/www/server/log/impl/ConsoleAccessLogger.java
@@ -0,0 +1,32 @@
+package de.ioexception.www.server.log.impl;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * A logger for console output. Flushes every entry immediately.
+ *
+ * @author Benjamin Erb
+ *
+ */
+public class ConsoleAccessLogger extends GenericAccessLogger
+{
+ public ConsoleAccessLogger()
+ {
+ super(new PrintWriter(System.out));
+ }
+
+ @Override
+ public void log(String logline)
+ {
+ super.log(logline);
+ try
+ {
+ super.flush();
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/de/ioexception/www/server/log/impl/FileAccessLogger.java b/src/de/ioexception/www/server/log/impl/FileAccessLogger.java
new file mode 100644
index 0000000..7ff433f
--- /dev/null
+++ b/src/de/ioexception/www/server/log/impl/FileAccessLogger.java
@@ -0,0 +1,23 @@
+package de.ioexception.www.server.log.impl;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+/**
+ * A File-based Logger using the "Combined Log Format". It directly writes each
+ * log entry into file and thus leads to poor i/o efficiency under heavy server
+ * load.
+ *
+ * @author Benjamin Erb
+ *
+ */
+public class FileAccessLogger extends GenericAccessLogger
+{
+
+ public FileAccessLogger(File logFile) throws IOException
+ {
+ super(new FileWriter(logFile, true));
+ }
+
+}
diff --git a/src/de/ioexception/www/server/log/impl/GenericAccessLogger.java b/src/de/ioexception/www/server/log/impl/GenericAccessLogger.java
new file mode 100644
index 0000000..e29dad3
--- /dev/null
+++ b/src/de/ioexception/www/server/log/impl/GenericAccessLogger.java
@@ -0,0 +1,107 @@
+package de.ioexception.www.server.log.impl;
+
+import java.io.Flushable;
+import java.io.IOException;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import de.ioexception.www.http.HttpRequest;
+import de.ioexception.www.http.HttpResponse;
+import de.ioexception.www.server.log.AccessLogger;
+
+/**
+ * An abstract logger class with provides basic functionalities for logging
+ * access, using the "Combined Log Format".
+ *
+ * @author Benjamin Erb
+ */
+public abstract class GenericAccessLogger implements AccessLogger, Flushable
+{
+ private final SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MMM/yyyy:HH:mm:ss Z", Locale.ENGLISH);
+
+ private volatile boolean running = true;
+ private final LinkedBlockingQueue logQueue = new LinkedBlockingQueue();
+ private final Writer writer;
+
+ /**
+ * The writer instance to be used.
+ *
+ * @param writer
+ */
+ public GenericAccessLogger(Writer writer)
+ {
+ this.writer = writer;
+ }
+
+ @Override
+ public void log(String logline)
+ {
+ try
+ {
+ logQueue.put(logline);
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public Void call() throws Exception
+ {
+ try
+ {
+ while (running || logQueue.size() > 0)
+ {
+ writer.write(logQueue.take());
+ }
+ writer.flush();
+ return null;
+ }
+ finally
+ {
+ writer.close();
+ }
+ }
+
+ public void shutdown()
+ {
+ try
+ {
+ writer.flush();
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+ running = false;
+ }
+
+
+ @Override
+ public void log(String clientHost, HttpRequest request, HttpResponse response)
+ {
+ StringBuilder s = new StringBuilder();
+ s.append(clientHost + " ");
+ s.append("- ");
+ s.append("- ");
+ s.append("[" + dateFormat.format(new Date()) + "] ");
+ s.append("\"" + request.getHttpMethod().toString() + " " + request.getRequestUri() + " " + request.getHttpVersion().toString() + "\" ");
+ s.append(response.getStatusCode().getCode() + " ");
+ s.append((response.getEntity() != null ? response.getEntity().length : 0) + " ");
+ s.append("\"" + (request.getHeaders().containsKey("Referer") ? request.getHeaders().get("Referer") : "") + "\" ");
+ s.append("\"" + (request.getHeaders().containsKey("User-Agent") ? request.getHeaders().get("User-Agent") : "") + "\"");
+ s.append("\n");
+ log(s.toString());
+ }
+
+ @Override
+ public void flush() throws IOException
+ {
+ writer.flush();
+ }
+
+}