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(); + } + +}