From 45f514fc278d0870c2236f26bf1139d0d018c643 Mon Sep 17 00:00:00 2001 From: Ziver Koc Date: Sun, 31 Jan 2010 18:10:00 +0000 Subject: [PATCH] Abstracted the HTTP server with TCP Network classes and added an SSDP service --- exemple.gif | Bin 0 -> 78776 bytes src/zutil/FileFinder.java | 89 ++-- src/zutil/History.java | 59 --- src/zutil/MultiPrintStream.java | 11 +- src/zutil/db/MySQLConnection.java | 24 +- src/zutil/image/ImageUtil.java | 384 ++---------------- src/zutil/image/RAWImageUtil.java | 372 +++++++++++++++++ src/zutil/image/filters/BlurFilter.java | 10 +- .../filters/ContrastBrightnessFilter.java | 14 +- .../image/filters/ConvolutionFilter.java | 4 +- src/zutil/image/filters/DitheringFilter.java | 12 +- src/zutil/image/filters/MedianFilter.java | 4 +- src/zutil/image/filters/SpotLightFilter.java | 8 +- src/zutil/network/SSDPServer.java | 323 +++++++++++++++ src/zutil/network/http/HTTPHeaderParser.java | 237 +++++++++++ src/zutil/network/http/HttpPrintStream.java | 131 ++++-- src/zutil/network/http/HttpServer.java | 244 +++-------- src/zutil/network/nio/NioNetwork.java | 2 +- .../threaded/ThreadedTCPNetworkServer.java | 115 ++++++ .../ThreadedTCPNetworkServerThread.java | 12 + .../network/threaded/ThreadedUDPNetwork.java | 103 +++++ .../threaded/ThreadedUDPNetworkThread.java | 21 + src/zutil/struct/HistoryList.java | 105 +++++ src/zutil/test/FileFinderHasherTest.java | 4 +- src/zutil/wrapper/StringOutputStream.java | 45 ++ 25 files changed, 1645 insertions(+), 688 deletions(-) create mode 100644 exemple.gif delete mode 100644 src/zutil/History.java create mode 100644 src/zutil/image/RAWImageUtil.java create mode 100644 src/zutil/network/SSDPServer.java create mode 100644 src/zutil/network/http/HTTPHeaderParser.java create mode 100644 src/zutil/network/threaded/ThreadedTCPNetworkServer.java create mode 100644 src/zutil/network/threaded/ThreadedTCPNetworkServerThread.java create mode 100644 src/zutil/network/threaded/ThreadedUDPNetwork.java create mode 100644 src/zutil/network/threaded/ThreadedUDPNetworkThread.java create mode 100644 src/zutil/struct/HistoryList.java create mode 100644 src/zutil/wrapper/StringOutputStream.java diff --git a/exemple.gif b/exemple.gif new file mode 100644 index 0000000000000000000000000000000000000000..7f8171aceab2832908ffa2ee6f4e9627ad42e8f8 GIT binary patch literal 78776 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 JR5Add^M5qfE9U?J literal 0 HcmV?d00001 diff --git a/src/zutil/FileFinder.java b/src/zutil/FileFinder.java index ead7cf3..494bf05 100644 --- a/src/zutil/FileFinder.java +++ b/src/zutil/FileFinder.java @@ -1,12 +1,16 @@ package zutil; +import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; +import java.io.InputStream; import java.net.URISyntaxException; import java.net.URL; -import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; import java.util.regex.Matcher; /** @@ -19,8 +23,8 @@ public class FileFinder { /** * Returns a String with a relative path from the given path * - * @param file The file to get a relative path from - * @param path The path + * @param file is the file to get a relative path from + * @param path is the path * @return A String with a relative path */ public static String relativePath(File file, String path){ @@ -36,11 +40,11 @@ public class FileFinder { } /** - * Returns the File object for the given file + * Returns the File object for the given file. + * Can not point to files in JAR files. * - * @param path The path to the file (no / if not absolute path) + * @param path is the path to the file (no / if not absolute path) * @return A File object for the file - * @throws URISyntaxException */ public static File find(String path){ try { @@ -50,17 +54,46 @@ public class FileFinder { } return new File(findURL(path).toURI()); } catch (Exception e) { - e.printStackTrace(); + //e.printStackTrace(MultiPrintStream.out); } return null; } + /** + * Returns the URL to the given file + * + * @param path is the path to the file (no / if not absolute path) + * @return A URL object for the file + * @throws URISyntaxException + */ + public static URL findURL(String path){ + return FileFinder.class.getClassLoader().getResource(path); + } + + /** + * Returns a InputStream from the path + * + * @param path is the path to the file (no / if not absolute path) + * @return A InputStream object for the file + */ + public static InputStream getInputStream(String path){ + try { + File file = new File(path); + if(file!=null && file.exists()){ + return new BufferedInputStream( new FileInputStream( file ) ); + } + return FileFinder.class.getClassLoader().getResourceAsStream(path); + } catch (Exception e) { + //e.printStackTrace(MultiPrintStream.out); + } + return null; + } /** * Reads and returns the content of a file as a String. * Or use FileUtils.readFileToString(file); * - * @param file The file to read + * @param file is the file to read * @return The file content * @throws IOException */ @@ -78,45 +111,35 @@ public class FileFinder { } /** - * Returns the URL to the given file + * Returns a ArrayList with all the files in a folder and sub folders * - * @param path The path to the file (no / if not absolute path) - * @return A URL object for the file - * @throws URISyntaxException + * @param dir is the directory to search in + * @return The ArrayList with the files */ - public static URL findURL(String path){ - return FileFinder.class.getClassLoader().getResource(path); + public static List search(File dir){ + return search(dir, new LinkedList(), true); } /** * Returns a ArrayList with all the files in a folder and sub folders * - * @param dir The directory to search in + * @param dir is the directory to search in + * @param fileList is the ArrayList to add the files to + * @param recursice is if the method should search the sub directories to. * @return The ArrayList with the files */ - public static ArrayList search(File dir){ - return search(dir, new ArrayList()); - } - - /** - * Returns a ArrayList with all the files in a folder and sub folders - * - * @param dir The directory to search in - * @param fileList The ArrayList to add the files to - * @return The ArrayList with the files - */ - public static ArrayList search(File dir, ArrayList fileList){ + public static List search(File dir, List fileList, boolean recursive){ String[] temp = dir.list(); File file; if(temp != null){ for(int i=0; i { - public int history_length = 10; - private LinkedList history; - private int historyIndex = 0; - - public History(int histlength){ - history_length = histlength; - history = new LinkedList(); - } - - public void addToHistory(T url){ - while(historyIndex < history.size()-1){ - history.removeLast(); - } - history.addLast(url); - if(history_length < history.size()){ - history.removeFirst(); - } - - historyIndex = history.size()-1; - } - - public T getBackHistory(){ - if(historyIndex > 0){ - historyIndex -= 1; - } - else{ - historyIndex = 0; - } - return history.get(historyIndex); - } - - public T getForwHistory(){ - if(forwHistoryExist()){ - historyIndex += 1; - } - else{ - historyIndex = history.size()-1; - } - return history.get(historyIndex); - } - - public T getCurrentHistory(){ - return history.get(historyIndex); - } - - public boolean forwHistoryExist(){ - if(historyIndex < history.size()-1){ - return true; - } - else{ - return false; - } - } -} diff --git a/src/zutil/MultiPrintStream.java b/src/zutil/MultiPrintStream.java index c95ce6b..91cc65e 100644 --- a/src/zutil/MultiPrintStream.java +++ b/src/zutil/MultiPrintStream.java @@ -332,9 +332,11 @@ public class MultiPrintStream extends PrintStream { for ( int i=0; i)return true; - else if(o instanceof Map)return true; + else if(o instanceof Collection)return true; + else if(o instanceof Map)return true; else if(o instanceof InputStream)return true; else if(o instanceof Reader)return true; else if(o instanceof Dumpable)return true; diff --git a/src/zutil/db/MySQLConnection.java b/src/zutil/db/MySQLConnection.java index 89af2d4..e5f9885 100644 --- a/src/zutil/db/MySQLConnection.java +++ b/src/zutil/db/MySQLConnection.java @@ -21,6 +21,7 @@ public class MySQLConnection { public MySQLConnection(String url, String db, String user, String password) throws SQLException, InstantiationException, IllegalAccessException, ClassNotFoundException{ Class.forName ("com.mysql.jdbc.Driver").newInstance(); + DriverManager.setLoginTimeout(10); conn = DriverManager.getConnection ("jdbc:mysql://"+url+"/"+db, user, password); } @@ -48,8 +49,11 @@ public class MySQLConnection { Statement s = conn.createStatement (); s.executeQuery(sql); ResultSet result = s.getResultSet(); - if(result.next()) - return result.getString(0); + if(result.next()){ + String tmp = result.getString(1); + result.close(); + return tmp; + } return null; } @@ -66,6 +70,22 @@ public class MySQLConnection { return ret; } + /** + * @return the last inserted id or -1 if there was an error + * @throws SQLException + */ + public int getLastInsertID() throws SQLException{ + Statement s = conn.createStatement (); + s.executeQuery("SELECT LAST_INSERT_ID()"); + ResultSet result = s.getResultSet(); + if(result.next()){ + int tmp = result.getInt(1); + result.close(); + return tmp; + } + return -1; + } + /** * Runs a Prepared Statement.
* NOTE: Don't forget to close the PreparedStatement or it can lead to memory leak diff --git a/src/zutil/image/ImageUtil.java b/src/zutil/image/ImageUtil.java index 3c13e2f..7ff9405 100644 --- a/src/zutil/image/ImageUtil.java +++ b/src/zutil/image/ImageUtil.java @@ -1,372 +1,48 @@ package zutil.image; +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; + /** - * Some util methods for image processing + * This is a static class containing image utilities + * * @author Ziver - * */ public class ImageUtil { - - /** - * Returns the peek value in the image - * - * @param data The image data - * @param startX is the x pixel of the image to start from - * @param startY is the y pixel of the image to start from - * @param stopX is the x pixel of the image to stop - * @param stopY is the y pixel of the image to stop - * @return The peak value of the image - */ - public static int getPeakValue(int[][][] data) { - return getPeakValue(data, 0, 0, data[0].length, data.length); - } - - /** - * Returns the peek value in the image - * - * @param data The image data - * @param startX is the x pixel of the image to start from - * @param startY is the y pixel of the image to start from - * @param stopX is the x pixel of the image to stop - * @param stopY is the y pixel of the image to stop - * @return The peak value of the image - */ - public static int getPeakValue(int[][][] data, int startX, int startY, int stopX, int stopY) { - int peak = 0; - for(int y=startY; y peak) peak = data[y][x][1]; - if(data[y][x][2] > peak) peak = data[y][x][2]; - if(data[y][x][3] > peak) peak = data[y][x][3]; - } - } - return peak; - } /** - * Normalizes the image data by the given scale + * Resizes an BufferedImage * - * @param data The image data - * @param startX is the x pixel of the image to start from - * @param startY is the y pixel of the image to start from - * @param stopX is the x pixel of the image to stop - * @param stopY is the y pixel of the image to stop - * @param scale The scale to normalize the image by + * @param source is the image to resize + * @param width is the wanted width + * @param height is the wanted height + * @param keep_aspect is if the aspect ratio of the image should be kept + * @return the resized image */ - public static void normalize(int[][][] data, int startX, int startY, int stopX, int stopY, double scale) { - for(int y=startY; y height){ + scale_width = scale_height; + }else{ + scale_height = scale_width; } } - int meanSquare = (int)(accum/pixelCount); - int rms = (int)(Math.sqrt(meanSquare)); - return rms; - } - /** - * Multiplies the given image data by the given value - * - * @param data is the image data - * @param startX is the x pixel of the image to start from - * @param startY is the y pixel of the image to start from - * @param stopX is the x pixel of the image to stop - * @param stopY is the y pixel of the image to stop - * @param scale is the number to scale the image color by - */ - public static void scale(int[][][] data, int startX, int startY, int stopX, int stopY, double scale){ - for(int y=startY; y 255) - return 255; - else - return color; + AffineTransform at = AffineTransform.getScaleInstance(scale_width, scale_height); + g2d.drawRenderedImage(source, at); + g2d.dispose(); + return tmp; } } diff --git a/src/zutil/image/RAWImageUtil.java b/src/zutil/image/RAWImageUtil.java new file mode 100644 index 0000000..b5887ce --- /dev/null +++ b/src/zutil/image/RAWImageUtil.java @@ -0,0 +1,372 @@ +package zutil.image; + +/** + * Some util methods for image processing + * @author Ziver + * + */ +public class RAWImageUtil { + + /** + * Returns the peek value in the image + * + * @param data The image data + * @param startX is the x pixel of the image to start from + * @param startY is the y pixel of the image to start from + * @param stopX is the x pixel of the image to stop + * @param stopY is the y pixel of the image to stop + * @return The peak value of the image + */ + public static int getPeakValue(int[][][] data) { + return getPeakValue(data, 0, 0, data[0].length, data.length); + } + + /** + * Returns the peek value in the image + * + * @param data The image data + * @param startX is the x pixel of the image to start from + * @param startY is the y pixel of the image to start from + * @param stopX is the x pixel of the image to stop + * @param stopY is the y pixel of the image to stop + * @return The peak value of the image + */ + public static int getPeakValue(int[][][] data, int startX, int startY, int stopX, int stopY) { + int peak = 0; + for(int y=startY; y peak) peak = data[y][x][1]; + if(data[y][x][2] > peak) peak = data[y][x][2]; + if(data[y][x][3] > peak) peak = data[y][x][3]; + } + } + return peak; + } + + /** + * Normalizes the image data by the given scale + * + * @param data The image data + * @param startX is the x pixel of the image to start from + * @param startY is the y pixel of the image to start from + * @param stopX is the x pixel of the image to stop + * @param stopY is the y pixel of the image to stop + * @param scale The scale to normalize the image by + */ + public static void normalize(int[][][] data, int startX, int startY, int stopX, int stopY, double scale) { + for(int y=startY; y 255) + return 255; + else + return color; + } +} diff --git a/src/zutil/image/filters/BlurFilter.java b/src/zutil/image/filters/BlurFilter.java index 9e0d9b3..96f7e04 100644 --- a/src/zutil/image/filters/BlurFilter.java +++ b/src/zutil/image/filters/BlurFilter.java @@ -3,7 +3,7 @@ package zutil.image.filters; import java.awt.image.BufferedImage; import zutil.image.ImageFilterProcessor; -import zutil.image.ImageUtil; +import zutil.image.RAWImageUtil; import zutil.math.ZMath; public class BlurFilter extends ImageFilterProcessor{ @@ -29,10 +29,10 @@ public class BlurFilter extends ImageFilterProcessor{ @Override public int[][][] process(int[][][] data, int startX, int startY, int stopX, int stopY) { - int inputPeak = ImageUtil.getPeakValue(data); + int inputPeak = RAWImageUtil.getPeakValue(data); int[][][] tmpData = new int[data.length][data[0].length][4]; - int[][][] output = ImageUtil.copyArray(data); + int[][][] output = RAWImageUtil.copyArray(data); //Perform the convolution one or more times in succession int redSum, greenSum, blueSum, outputPeak; for(int i=0; i 0) - output[y+1][x-1][i] = ImageUtil.clip( output[y+1][x-1][i] + (error*3)/16 ); - output[y+1][x+0][i] = ImageUtil.clip( output[y+1][x+0][i] + (error*5)/16 ); + output[y+1][x-1][i] = RAWImageUtil.clip( output[y+1][x-1][i] + (error*3)/16 ); + output[y+1][x+0][i] = RAWImageUtil.clip( output[y+1][x+0][i] + (error*5)/16 ); if (x + 1 < data[0].length) - output[y+1][x+1][i] = ImageUtil.clip( output[y+1][x+1][i] + (error*1)/16 ); + output[y+1][x+1][i] = RAWImageUtil.clip( output[y+1][x+1][i] + (error*1)/16 ); } } } diff --git a/src/zutil/image/filters/MedianFilter.java b/src/zutil/image/filters/MedianFilter.java index 0bd9443..0cad9b8 100644 --- a/src/zutil/image/filters/MedianFilter.java +++ b/src/zutil/image/filters/MedianFilter.java @@ -4,7 +4,7 @@ import java.awt.image.BufferedImage; import zutil.algo.sort.sortable.SortableDataList; import zutil.image.ImageFilterProcessor; -import zutil.image.ImageUtil; +import zutil.image.RAWImageUtil; import zutil.math.ZMath; /** @@ -62,7 +62,7 @@ public class MedianFilter extends ImageFilterProcessor{ */ @Override public int[][][] process(int[][][] data, int startX, int startY, int stopX, int stopY) { - int[][][] tmpData = ImageUtil.copyArray(data); + int[][][] tmpData = RAWImageUtil.copyArray(data); int edgeX = windowSize / 2; int edgeY = windowSize / 2; diff --git a/src/zutil/image/filters/SpotLightFilter.java b/src/zutil/image/filters/SpotLightFilter.java index 5ce549b..101d598 100644 --- a/src/zutil/image/filters/SpotLightFilter.java +++ b/src/zutil/image/filters/SpotLightFilter.java @@ -3,7 +3,7 @@ package zutil.image.filters; import java.awt.image.BufferedImage; import zutil.image.ImageFilterProcessor; -import zutil.image.ImageUtil; +import zutil.image.RAWImageUtil; import zutil.math.ZMath; public class SpotLightFilter extends ImageFilterProcessor{ @@ -64,9 +64,9 @@ public class SpotLightFilter extends ImageFilterProcessor{ } output[y][x][0] = data[y][x][0]; - output[y][x][1] = ImageUtil.clip((int)(scale * data[y][x][1])); - output[y][x][2] = ImageUtil.clip((int)(scale * data[y][x][2])); - output[y][x][3] = ImageUtil.clip((int)(scale * data[y][x][3])); + output[y][x][1] = RAWImageUtil.clip((int)(scale * data[y][x][1])); + output[y][x][2] = RAWImageUtil.clip((int)(scale * data[y][x][2])); + output[y][x][3] = RAWImageUtil.clip((int)(scale * data[y][x][3])); } } return output; diff --git a/src/zutil/network/SSDPServer.java b/src/zutil/network/SSDPServer.java new file mode 100644 index 0000000..442d68d --- /dev/null +++ b/src/zutil/network/SSDPServer.java @@ -0,0 +1,323 @@ +package zutil.network; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.util.HashMap; +import java.util.Timer; +import java.util.TimerTask; +import java.util.UUID; + +import zutil.MultiPrintStream; +import zutil.network.http.HTTPHeaderParser; +import zutil.network.http.HttpPrintStream; +import zutil.network.threaded.ThreadedUDPNetworkThread; +import zutil.network.threaded.ThreadedUDPNetwork; +import zutil.wrapper.StringOutputStream; + +/** + * A Server class that announces an service by the SSDP + * protocol specified at: + * http://coherence.beebits.net/chrome/site/draft-cai-ssdp-v1-03.txt + * ftp://ftp.pwg.org/pub/pwg/www/hypermail/ps/att-0188/01-psi_SSDP.pdf + * + * @author Ziver + * + * ********* Message clarification: + * ****** Incoming: + * ST: Search Target, this is object of the discovery request, (e.g., ssdp:all, etc.) + * HOST: This is the SSDP multicast address + * MAN: Description of packet type, (e.g., "ssdp:discover", ) + * MX: Wait these few seconds and then send response + * + * ****** Outgoing: + * EXT: required by HTTP - not used with SSDP + * SERVER: informational + * LOCATION: This is the URL to request the QueryEndpointsInterface endpoint + * USN: advertisement UUID + * CACHE-CONTROL: max-age = seconds until advertisement expires + * NT: Notify target same as ST + * NTS: same as Man but for Notify messages + */ +public class SSDPServer extends ThreadedUDPNetwork implements ThreadedUDPNetworkThread{ + public static final String SERVER_INFO = "SSDP Java Server by Ziver Koc"; + public static final int DEFAULT_CACHE_TIME = 60*30; // 30 min + public static final int BUFFER_SIZE = 512; + public static final String SSDP_MULTICAST_ADDR = "239.255.255.250"; + public static final int SSDP_PORT = 1900; + + // instance specific values + private int cache_time; + private NotifyTimer notifyTimer = null; + /** HashMap that contains services as < SearchTargetName, Location > */ + private HashMap services; + /** A Map of all the used USN:s */ + private HashMap usn_map; + + + public static void main(String[] args) throws IOException{ + SSDPServer ssdp = new SSDPServer(); + ssdp.addService("upnp:rootdevice", "nowhere"); + ssdp.start(); + MultiPrintStream.out.println("SSDP Server running"); + } + + public SSDPServer() throws IOException{ + super( null, SSDP_PORT, SSDP_MULTICAST_ADDR ); + super.setThread( this ); + + services = new HashMap(); + usn_map = new HashMap(); + + setChacheTime( DEFAULT_CACHE_TIME ); + enableNotify( true ); + } + + /** + * Adds an service that will be announced. + * + * @param searchTarget is the ST value in SSDP + * @param location is the location of the service + */ + public void addService(String searchTarget, String location){ + services.put( searchTarget, location ); + } + /** + * Remove a service from being announced. This function will + * send out an byebye message to the clients that the service is down. + * + * @param searchTarget is the ST value in SSDP + */ + public void removeService(String searchTarget){ + sendByeBye( searchTarget ); + services.remove( searchTarget ); + } + + /** + * Sets the cache time that will be sent to + * the clients. If notification is enabled then an + * notification message will be sent every cache_time/2 seconds + * + * @param time is the time in seconds + */ + public void setChacheTime(int time){ + cache_time = time; + if( isNotifyEnabled() ){ + enableNotify(false); + enableNotify(true); + } + } + + /** + * Enable or disable notification messages to clients + * every cache_time/2 seconds + */ + public void enableNotify(boolean enable){ + if( enable && notifyTimer==null ){ + notifyTimer = new NotifyTimer(); + Timer timer = new Timer(); + timer.schedule(new NotifyTimer(), 0, cache_time*1000/2); + }else if( !enable && notifyTimer!=null ){ + notifyTimer.cancel(); + notifyTimer = null; + } + } + /** + * @return if notification messages is enabled + */ + public boolean isNotifyEnabled(){ + return notifyTimer != null; + } + + /** + * Handles the incoming packets like this: + * + * ***** REQUEST: + * M-SEARCH * HTTP/1.1 + * S: uuid:ijklmnop-7dec-11d0-a765-00a0c91e6bf6 + * Host: 239.255.255.250:reservedSSDPport + * Man: "ssdp:discover" + * ST: ge:fridge + * MX: 3 + * + * ***** RESPONSE; + * HTTP/1.1 200 OK + * S: uuid:ijklmnop-7dec-11d0-a765-00a0c91e6bf6 + * Ext: + * Cache-Control: no-cache="Ext", max-age = 5000 + * ST: ge:fridge + * USN: uuid:abcdefgh-7dec-11d0-a765-00a0c91e6bf6 + * AL: + * + */ + public void receivedPacket(DatagramPacket packet, ThreadedUDPNetwork network) { + try { + String msg = new String( packet.getData() ); + + HTTPHeaderParser header = new HTTPHeaderParser( msg ); + MultiPrintStream.out.println(header); + + // ******* Respond + // Check that the message is an ssdp discovery message + if( header.getRequestType().equalsIgnoreCase("M-SEARCH") ){ + String man = header.getHTTPAttribute("Man").replace("\"", ""); + String st = header.getHTTPAttribute("ST"); + // Check that its the correct URL and that its an ssdp:discover message + if( header.getRequestURL().equals("*") && man.equalsIgnoreCase("ssdp:discover") ){ + // Check if the requested service exists + if( services.containsKey( st ) ){ + // Generate the SSDP response + StringOutputStream response = new StringOutputStream(); + HttpPrintStream http = new HttpPrintStream( response ); + http.setStatusCode(200); + http.setHeader("Server", SERVER_INFO ); + http.setHeader("ST", st ); + http.setHeader("Location", services.get(st) ); + http.setHeader("EXT", "" ); + http.setHeader("Cache-Control", "max-age = "+cache_time ); + http.setHeader("USN", getUSN(st) ); + + http.close(); + MultiPrintStream.out.println("\n"+response); + byte[] data = response.toString().getBytes(); + packet = new DatagramPacket( + data, data.length, + packet.getAddress(), + packet.getPort()); + network.send( packet ); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + + /** + * This thread is a timer task that sends an + * notification message to the network every + * cache_time/2 seconds. + * + * @author Ziver + */ + private class NotifyTimer extends TimerTask { + public void run(){ + sendNotify(); + } + } + /** + * Sends keepalive messages to update the cache of the clients + */ + public void sendNotify(){ + for(String st : services.keySet()){ + sendNotify( st ); + } + } + /** + * Sends an keepalive message to update the cache of the clients + * + * @param searchTarget is the ST value of the service + * + * ********** Message ex: + * NOTIFY * HTTP/1.1 + * Host: 239.255.255.250:reservedSSDPport + * NT: blenderassociation:blender + * NTS: ssdp:alive + * USN: someunique:idscheme3 + * AL: + * Cache-Control: max-age = 7393 + */ + public void sendNotify(String searchTarget){ + try { + // Generate the SSDP response + StringOutputStream msg = new StringOutputStream(); + HttpPrintStream http = new HttpPrintStream( msg, HttpPrintStream.HTTPMessageType.REQUEST ); + http.setRequestType("NOTIFY"); + http.setRequestURL("*"); + http.setHeader("Server", SERVER_INFO ); + http.setHeader("Host", SSDP_MULTICAST_ADDR+":"+SSDP_PORT ); + http.setHeader("NT", searchTarget ); + http.setHeader("NTS", "ssdp:alive" ); + http.setHeader("Location", services.get(searchTarget) ); + http.setHeader("Cache-Control", "max-age = "+cache_time ); + http.setHeader("USN", getUSN(searchTarget) ); + + http.close(); + MultiPrintStream.out.println("\n"+msg); + byte[] data = msg.toString().getBytes(); + DatagramPacket packet = new DatagramPacket( + data, data.length, + InetAddress.getByName( SSDP_MULTICAST_ADDR ), + SSDP_PORT ); + super.send( packet ); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + + /** + * Shutdown message is sent to the clients that all + * the service is shutting down. + */ + public void sendByeBye(){ + for(String st : services.keySet()){ + sendByeBye( st ); + } + } + /** + * Shutdown message is sent to the clients that the service is shutting down + * + * @param searchTarget is the ST value of the service + * + * ********** Message ex: + * NOTIFY * HTTP/1.1 + * Host: 239.255.255.250:reservedSSDPport + * NT: someunique:idscheme3 + * NTS: ssdp:byebye + * USN: someunique:idscheme3 + */ + public void sendByeBye(String searchTarget){ + try { + // Generate the SSDP response + StringOutputStream msg = new StringOutputStream(); + HttpPrintStream http = new HttpPrintStream( msg, HttpPrintStream.HTTPMessageType.REQUEST ); + http.setRequestType("NOTIFY"); + http.setRequestURL("*"); + http.setHeader("Server", SERVER_INFO ); + http.setHeader("Host", SSDP_MULTICAST_ADDR+":"+SSDP_PORT ); + http.setHeader("NT", searchTarget ); + http.setHeader("NTS", "ssdp:byebye" ); + http.setHeader("USN", getUSN(searchTarget) ); + + http.close(); + MultiPrintStream.out.println("\n"+msg); + byte[] data = msg.toString().getBytes(); + DatagramPacket packet = new DatagramPacket( + data, data.length, + InetAddress.getByName( SSDP_MULTICAST_ADDR ), + SSDP_PORT ); + super.send( packet ); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Generates an unique USN for the service + * + * @param searchTarget is the service ST name + * @return an unique string that corresponds to the service + */ + public String getUSN(String searchTarget){ + if( !usn_map.containsKey( searchTarget ) ){ + String usn = "uuid:" + UUID.nameUUIDFromBytes( (searchTarget+services.get(searchTarget)).getBytes() ); + usn_map.put( searchTarget, usn ); + return usn; + } + return usn_map.get( searchTarget ); + } +} diff --git a/src/zutil/network/http/HTTPHeaderParser.java b/src/zutil/network/http/HTTPHeaderParser.java new file mode 100644 index 0000000..1aaaf7f --- /dev/null +++ b/src/zutil/network/http/HTTPHeaderParser.java @@ -0,0 +1,237 @@ +package zutil.network.http; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Scanner; +import java.util.regex.Pattern; + +public class HTTPHeaderParser { + // Some Cached regexes + private static final Pattern colonPattern = Pattern.compile(":"); + private static final Pattern equalPattern = Pattern.compile("="); + private static final Pattern andPattern = Pattern.compile("&"); + private static final Pattern semiColonPattern = Pattern.compile(";"); + + // HTTP info + private String type; + private String url; + private HashMap url_attr; + private float version; + + // params + private HashMap attributes; + private HashMap cookies; + + /** + * Parses the HTTP header information from the stream + * + * @param in is the stream + * @throws IOException + */ + public HTTPHeaderParser(BufferedReader in) throws IOException{ + url_attr = new HashMap(); + attributes = new HashMap(); + cookies = new HashMap(); + + String tmp = null; + if( (tmp=in.readLine()) != null && !tmp.isEmpty() ){ + parseStartLine( tmp ); + while( (tmp=in.readLine()) != null && !tmp.isEmpty() ){ + parseLine( tmp ); + } + } + parseCookies(); + } + + /** + * Parses the HTTP header information from an String + * + * @param in is the string + */ + public HTTPHeaderParser(String in){ + url_attr = new HashMap(); + attributes = new HashMap(); + cookies = new HashMap(); + + Scanner sc = new Scanner(in); + sc.useDelimiter("\n"); + String tmp = null; + if( sc.hasNext() && !(tmp=sc.next()).isEmpty() ){ + parseStartLine( tmp ); + while( sc.hasNext() && !(tmp=sc.next()).isEmpty() ){ + parseLine( tmp ); + } + } + parseCookies(); + } + + /** + * Parses the first header line and ads the values to + * the map and returns the file name and path + * + * @param header The header String + * @param map The HashMap to put the variables to + * @return The path and file name as a String + */ + protected void parseStartLine(String line){ + type = (line.substring(0, line.indexOf(" "))).trim(); + version = Float.parseFloat( line.substring(line.lastIndexOf("HTTP/")+5 , line.length()).trim() ); + line = (line.substring(type.length()+1, line.lastIndexOf("HTTP/"))).trim(); + + // parse URL and attributes + if(line.indexOf('?') > -1){ + url = line.substring(0, line.indexOf('?')); + line = line.substring(line.indexOf('?')+1, line.length()); + parseUrlAttributes(line, url_attr); + } + else{ + url = line; + } + } + + /** + * Parses a String with variables from a get or post + * that was sent from a client and puts the data into a HashMap + * + * @param header is the String containing all the attributes + * @param map is the HashMap to put all the values into + */ + public static void parseUrlAttributes(String attributes, HashMap map){ + String[] tmp; + // get the variables + String[] data = andPattern.split( attributes ); + for(String element : data){ + tmp = equalPattern.split(element, 2); + map.put( + tmp[0].trim(), // Key + (tmp.length>1 ? tmp[1] : "").trim()); //Value + } + } + + /** + * Parses the rest of the header + * + * @param line is the next line in the header + */ + protected void parseLine(String line){ + String[] data = colonPattern.split( line, 2 ); + attributes.put( + data[0].trim().toUpperCase(), // Key + (data.length>1 ? data[1] : "").trim()); //Value + } + + /** + * Parses the attribute "Cookie" and returns a HashMap + * with the values + * + * @return a HashMap with cookie values + */ + protected void parseCookies(){ + if( attributes.containsKey("COOKIE") ){ + String[] tmp = semiColonPattern.split( attributes.get("COOKIE") ); + String[] tmp2; + for(String cookie : tmp){ + tmp2 = equalPattern.split(cookie, 2); + cookies.put( + tmp2[0].trim(), // Key + (tmp2.length>1 ? tmp2[1] : "").trim()); //Value + } + } + } + + /** + * @return the HTTP message type( ex. GET,POST...) + */ + public String getRequestType(){ + return type; + } + /** + * @return the HTTP version of this header + */ + public float getHTTPVersion(){ + return version; + } + /** + * @return the URL that the client sent the server + */ + public String getRequestURL(){ + return url; + } + /** + * Returns the URL attribute value of the given name, + * returns null if there is no such attribute + */ + public String getURLAttribute(String name){ + return url_attr.get( name ); + } + /** + * Returns the HTTP attribute value of the given name, + * returns null if there is no such attribute + */ + public String getHTTPAttribute(String name){ + return attributes.get( name.toUpperCase() ); + } + /** + * Returns the cookie value of the given name, + * returns null if there is no such attribute + */ + public String getCookie(String name){ + return cookies.get( name ); + } + + + /** + * @return athe parsed cookies + */ + public HashMap getCookies(){ + return cookies; + } + /** + * @return the parsed URL values + */ + public HashMap getURLAttributes(){ + return url_attr; + } + /** + * @return the parsed header attributes + */ + public HashMap getAttributes(){ + return attributes; + } + + + public String toString(){ + StringBuffer tmp = new StringBuffer(); + tmp.append("\nType: "); + tmp.append(type); + tmp.append("\nHTTP Version: HTTP/"); + tmp.append(version); + + tmp.append("\nURL: "); + tmp.append(url); + + for( String key : url_attr.keySet() ){ + tmp.append("\nURL Attr: "); + tmp.append(key); + tmp.append("="); + tmp.append( url_attr.get(key) ); + } + + for( String key : attributes.keySet() ){ + tmp.append("\nHTTP Attr: "); + tmp.append(key); + tmp.append("="); + tmp.append( attributes.get(key) ); + } + + for( String key : cookies.keySet() ){ + tmp.append("\nCookie: "); + tmp.append(key); + tmp.append("="); + tmp.append( cookies.get(key) ); + } + + return tmp.toString(); + } +} diff --git a/src/zutil/network/http/HttpPrintStream.java b/src/zutil/network/http/HttpPrintStream.java index 8d934d6..d3c7c21 100644 --- a/src/zutil/network/http/HttpPrintStream.java +++ b/src/zutil/network/http/HttpPrintStream.java @@ -11,17 +11,51 @@ import java.util.HashMap; * @author Ziver * */ -public class HttpPrintStream extends PrintStream{ - private Integer status_code; - private HashMap header; - private HashMap cookie; - private StringBuffer buffer; - private boolean buffer_enabled; +public class HttpPrintStream extends PrintStream{ + // Defines the type of message + public enum HTTPMessageType{ + REQUEST, + RESPONSE + } + // This defines the type of message that will be generated + private HTTPMessageType message_type; + // The status code of the message, ONLY for response + private Integer res_status_code; + // The request type of the message ONLY for request + private String req_type; + // The requesting url ONLY for request + private String req_url; + // An Map of all the header values + private HashMap header; + // An Map of all the cookies + private HashMap cookie; + // The buffered header + private StringBuffer buffer; + // If the header buffering is enabled + private boolean buffer_enabled; + + /** + * Creates an new instance of HttpPrintStream with + * message type of RESPONSE and buffering disabled. + * + * @param out is the OutputStream to send the message + */ public HttpPrintStream(OutputStream out) { + this( out, HTTPMessageType.RESPONSE ); + } + /** + * Creates an new instance of HttpPrintStream with + * message type buffering disabled. + * + * @param out is the OutputStream to send the message + * @param type is the type of message + */ + public HttpPrintStream(OutputStream out, HTTPMessageType type) { super(out); - status_code = 0; + this.message_type = type; + res_status_code = 0; header = new HashMap(); cookie = new HashMap(); buffer = new StringBuffer(); @@ -49,9 +83,9 @@ public class HttpPrintStream extends PrintStream{ * @param value is the value of the cookie * @throws Exception Throws exception if the header has already been sent */ - public void setCookie(String key, String value) throws Exception{ + public void setCookie(String key, String value) throws RuntimeException{ if(cookie == null) - throw new Exception("Header already sent!!!"); + throw new RuntimeException("Header already sent!!!"); cookie.put(key, value); } @@ -62,22 +96,51 @@ public class HttpPrintStream extends PrintStream{ * @param value is the value of the header * @throws Exception Throws exception if the header has already been sent */ - public void setHeader(String key, String value) throws Exception{ + public void setHeader(String key, String value) throws RuntimeException{ if(header == null) - throw new Exception("Header already sent!!!"); + throw new RuntimeException("Header already sent!!!"); header.put(key, value); } - + /** - * Sets the return status code + * Sets the status code of the message, ONLY available in HTTP RESPONSE * * @param code the code from 100 up to 599 - * @throws Exception Throws exception if the header has already been sent + * @throws RuntimeException if the header has already been sent or the message type is wrong */ - public void setStatusCode(int code) throws Exception{ - if(status_code == null) - throw new Exception("Header already sent!!!"); - status_code = code; + public void setStatusCode(int code) throws RuntimeException{ + if( res_status_code == null ) + throw new RuntimeException("Header already sent!!!"); + if( message_type != HTTPMessageType.RESPONSE ) + throw new RuntimeException("Status Code is only available in HTTP RESPONSE!!!"); + res_status_code = code; + } + + /** + * Sets the request type of the message, ONLY available in HTTP REQUEST + * + * @param req_type is the type of the message, e.g. GET, POST... + * @throws RuntimeException if the header has already been sent or the message type is wrong + */ + public void setRequestType(String req_type) throws RuntimeException{ + if( req_type == null ) + throw new RuntimeException("Header already sent!!!"); + if( message_type != HTTPMessageType.REQUEST ) + throw new RuntimeException("Request Message Type is only available in HTTP REQUEST!!!"); + this.req_type = req_type; + } + /** + * Sets the requesting URL of the message, ONLY available in HTTP REQUEST + * + * @param req_url is the URL + * @throws RuntimeException if the header has already been sent or the message type is wrong + */ + public void setRequestURL(String req_url) throws RuntimeException{ + if( req_url == null ) + throw new RuntimeException("Header already sent!!!"); + if( message_type != HTTPMessageType.REQUEST ) + throw new RuntimeException("Request URL is only available in HTTP REQUEST!!!"); + this.req_url = req_url; } /** @@ -102,10 +165,15 @@ public class HttpPrintStream extends PrintStream{ buffer.append(s); } else{ - if(status_code != null){ - super.print("HTTP/1.0 "+status_code+" "+getStatusString(status_code)); + if(res_status_code != null){ + if( message_type==HTTPMessageType.REQUEST ) + super.print(req_type+" "+req_url+" HTTP/1.1"); + else + super.print("HTTP/1.1 "+res_status_code+" "+getStatusString(res_status_code)); super.println(); - status_code = null; + res_status_code = null; + req_type = null; + req_url = null; } if(header != null){ for(String key : header.keySet()){ @@ -115,9 +183,20 @@ public class HttpPrintStream extends PrintStream{ header = null; } if(cookie != null){ - for(String key : cookie.keySet()){ - super.print("Set-Cookie: "+key+"="+cookie.get(key)+";"); - super.println(); + if( !cookie.isEmpty() ){ + if( message_type==HTTPMessageType.REQUEST ){ + super.print("Cookie: "); + for(String key : cookie.keySet()){ + super.print(key+"="+cookie.get(key)+"; "); + } + super.println(); + } + else{ + for(String key : cookie.keySet()){ + super.print("Set-Cookie: "+key+"="+cookie.get(key)+";"); + super.println(); + } + } } super.println(); cookie = null; @@ -136,7 +215,7 @@ public class HttpPrintStream extends PrintStream{ buffer.delete(0, buffer.length()); buffer_enabled = true; } - else if(status_code != null || header != null || cookie != null){ + else if(res_status_code != null || header != null || cookie != null){ printOrBuffer(""); } super.flush(); @@ -165,7 +244,7 @@ public class HttpPrintStream extends PrintStream{ public void print(int x){ printOrBuffer(String.valueOf(x));} public void print(long x){ printOrBuffer(String.valueOf(x));} public void print(Object x){ printOrBuffer(String.valueOf(x));} - + /* public void write(int b) { print((char)b);} public void write(byte buf[], int off, int len){ diff --git a/src/zutil/network/http/HttpServer.java b/src/zutil/network/http/HttpServer.java index 1dfbc9d..c58d015 100644 --- a/src/zutil/network/http/HttpServer.java +++ b/src/zutil/network/http/HttpServer.java @@ -4,22 +4,16 @@ import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; -import java.net.ServerSocket; import java.net.Socket; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.cert.CertificateException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Timer; import java.util.TimerTask; -import java.util.regex.Pattern; - -import javax.net.ssl.SSLServerSocketFactory; import zutil.MultiPrintStream; +import zutil.network.threaded.ThreadedTCPNetworkServer; +import zutil.network.threaded.ThreadedTCPNetworkServerThread; /** @@ -28,7 +22,7 @@ import zutil.MultiPrintStream; * * @author Ziver */ -public class HttpServer extends Thread{ +public class HttpServer extends ThreadedTCPNetworkServer{ public static final boolean DEBUG = false; public static final String SERVER_VERSION = "StaticInt HttpServer 1.0"; public static final int COOKIE_TTL = 200; @@ -36,8 +30,6 @@ public class HttpServer extends Thread{ public final String server_url; public final int server_port; - private File keyStore; - private String keyStorePass; private HashMap pages; private HttpPage defaultPage; @@ -64,10 +56,9 @@ public class HttpServer extends Thread{ * @param sslCert If this is not null then the server will use a SSL connection with the given certificate */ public HttpServer(String url, int port, File keyStore, String keyStorePass){ + super( port, keyStore, keyStorePass ); this.server_url = url; this.server_port = port; - this.keyStorePass = keyStorePass; - this.keyStore = keyStore; pages = new HashMap(); sessions = Collections.synchronizedMap(new HashMap>()); @@ -75,6 +66,8 @@ public class HttpServer extends Thread{ Timer timer = new Timer(); timer.schedule(new GarbageCollector(), 0, SESSION_TTL / 2); + + MultiPrintStream.out.println("HTTP"+(keyStore==null?"":"S")+" Server ready!"); } /** @@ -119,49 +112,13 @@ public class HttpServer extends Thread{ defaultPage = page; } - public void run(){ - try{ - ServerSocket ss; - if(keyStorePass != null && keyStore != null){ - registerCertificate(keyStore, keyStorePass); - ss = initSSL(server_port); - MultiPrintStream.out.println("Https Server Running!!!"); - } - else{ - ss = new ServerSocket(server_port); - MultiPrintStream.out.println("Http Server Running!!!"); - } - - while(true){ - new HttpServerThread(ss.accept()); - } - } catch (Exception e) { - e.printStackTrace(); + protected ThreadedTCPNetworkServerThread getThreadInstance( Socket s ){ + try { + return new HttpServerThread( s ); + } catch (IOException e) { + e.printStackTrace( MultiPrintStream.out ); } - } - - /** - * Initiates a SSLServerSocket - * - * @param port The port to listen to - * @return The SSLServerSocket - * @throws IOException - */ - private ServerSocket initSSL(int port) throws IOException{ - SSLServerSocketFactory sslserversocketfactory = - (SSLServerSocketFactory) SSLServerSocketFactory.getDefault(); - return sslserversocketfactory.createServerSocket(port); - - } - - /** - * Registers the given cert file to the KeyStore - * - * @param certFile The cert file - */ - protected void registerCertificate(File keyStore, String keyStorePass) throws CertificateException, IOException, KeyStoreException, NoSuchProviderException, NoSuchAlgorithmException{ - System.setProperty("javax.net.ssl.keyStore", keyStore.getAbsolutePath()); - System.setProperty("javax.net.ssl.keyStorePassword", keyStorePass); + return null; } /** @@ -170,7 +127,7 @@ public class HttpServer extends Thread{ * @author Ziver * */ - class HttpServerThread extends Thread{ + protected class HttpServerThread implements ThreadedTCPNetworkServerThread{ private HttpPrintStream out; private BufferedReader in; private Socket socket; @@ -179,16 +136,11 @@ public class HttpServer extends Thread{ out = new HttpPrintStream(socket.getOutputStream()); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); this.socket = socket; - start(); - if(DEBUG)MultiPrintStream.out.println("New Connection!!! "+socket.getInetAddress().getHostName()); + if(DEBUG) MultiPrintStream.out.println("New Connection!!! "+socket.getInetAddress().getHostName()); } public void run(){ String tmp = null; - String[] tmpArray, tmpArray2; - Pattern colonPattern = Pattern.compile(":"); - Pattern semiColonPattern = Pattern.compile(";"); - Pattern equalPattern = Pattern.compile("="); String page_url = ""; HashMap client_info = new HashMap(); @@ -197,45 +149,19 @@ public class HttpServer extends Thread{ //**************************** REQUEST ********************************* try { - if(DEBUG)MultiPrintStream.out.println("Reciving Http Request!!!"); - while((tmp=in.readLine()) != null && !tmp.isEmpty()){ - //System.err.println(tmp); - //*********** Handling Get variables - if(tmp.startsWith("GET")){ - // Gets the file URL and get values - tmp = (tmp.substring(5, tmp.indexOf("HTTP/"))).trim(); - page_url = parseHttpHeader(tmp, request); - } - //********* Handling Post variable data - else if(tmp.startsWith("POST")){ - // Gets the file URL and get values - tmp = (tmp.substring(6, tmp.indexOf("HTTP/"))).trim(); - page_url = parseHttpHeader(tmp, request); - } - //********* Handling Cookies - else if(tmp.startsWith("Cookie")){ - tmp = colonPattern.split(tmp)[1]; - tmpArray = semiColonPattern.split(tmp); - for(String e : tmpArray){ - tmpArray2 = equalPattern.split(e); - cookie.put( - tmpArray2[0].trim(), // Key - (tmpArray2.length>1 ? tmpArray2[1] : "").trim()); //Value - } - } - //********* Handling Client info - else{ - tmpArray = colonPattern.split(tmp); - client_info.put( - tmpArray[0].trim(), // Key - (tmpArray.length>1 ? tmpArray[1] : "").trim()); //Value - } - } + if(DEBUG) MultiPrintStream.out.println("Reciving Http Request!!!"); + + HTTPHeaderParser parser = new HTTPHeaderParser(in); + if(DEBUG) MultiPrintStream.out.println(parser); + client_info = parser.getAttributes(); + request = parser.getURLAttributes(); + cookie = parser.getCookies(); + //******* Read in the post data if available - if(client_info.containsKey("Content-Length")){ + if( parser.getHTTPAttribute("Content-Length")!=null ){ // Reads the post data size - tmp = client_info.get("Content-Length"); + tmp = parser.getHTTPAttribute("Content-Length"); int post_data_length = Integer.parseInt( tmp ); // read the data StringBuffer tmpb = new StringBuffer(); @@ -244,19 +170,20 @@ public class HttpServer extends Thread{ tmpb.append((char)in.read()); } - if(client_info.get("Content-Type").contains("application/x-www-form-urlencoded")){ + tmp = parser.getHTTPAttribute("Content-Type"); + if( tmp.contains("application/x-www-form-urlencoded") ){ // get the variables - parseVariables(tmpb.toString(), request); + HTTPHeaderParser.parseUrlAttributes( tmpb.toString(), request ); } - else if(client_info.get("Content-Type").contains("application/soap+xml") || - client_info.get("Content-Type").contains("text/xml") || - client_info.get("Content-Type").contains("text/plain")){ + else if( tmp.contains("application/soap+xml" ) || + tmp.contains("text/xml") || + tmp.contains("text/plain") ){ // save the variables - request.put("" , tmpb.toString()); + request.put( "" , tmpb.toString() ); } - else if(client_info.get("Content-Type").contains("multipart/form-data")){ + else if( tmp.contains("multipart/form-data") ){ // TODO: File upload - throw new Exception("\"multipart-form-data\" Not implemented!!!"); + throw new Exception( "\"multipart-form-data\" Not implemented!!!" ); } } @@ -264,118 +191,71 @@ public class HttpServer extends Thread{ // Get the client session or create one Map client_session; long ttl_time = System.currentTimeMillis()+SESSION_TTL; - if(cookie.containsKey("session_id") && sessions.containsKey(cookie.get("session_id"))){ - client_session = sessions.get(cookie.get("session_id")); + if( cookie.containsKey("session_id") && sessions.containsKey(cookie.get("session_id")) ){ + client_session = sessions.get( cookie.get("session_id") ); // Check if session is still valid - if((Long)client_session.get("ttl") < System.currentTimeMillis()){ + if( (Long)client_session.get("ttl") < System.currentTimeMillis() ){ int session_id = (Integer)client_session.get("session_id"); client_session = Collections.synchronizedMap(new HashMap()); - client_session.put("session_id", session_id); - sessions.put(""+session_id, client_session); + client_session.put( "session_id", session_id); + sessions.put( ""+session_id, client_session); } // renew the session TTL - client_session.put("ttl", ttl_time); + client_session.put( "ttl", ttl_time ); } else{ client_session = Collections.synchronizedMap(new HashMap()); - client_session.put("session_id", nextSessionId); - client_session.put("ttl", ttl_time); - sessions.put(""+nextSessionId, client_session); + client_session.put( "session_id", nextSessionId ); + client_session.put( "ttl", ttl_time ); + sessions.put( ""+nextSessionId, client_session ); nextSessionId++; } // Debug if(DEBUG){ - MultiPrintStream.out.println("# page_url: "+page_url); - MultiPrintStream.out.println("# cookie: "+cookie); - MultiPrintStream.out.println("# client_session: "+client_session); - MultiPrintStream.out.println("# client_info: "+client_info); - MultiPrintStream.out.println("# request: "+request); + MultiPrintStream.out.println( "# page_url: "+page_url ); + MultiPrintStream.out.println( "# cookie: "+cookie ); + MultiPrintStream.out.println( "# client_session: "+client_session ); + MultiPrintStream.out.println( "# client_info: "+client_info ); + MultiPrintStream.out.println( "# request: "+request ); } //**************************** RESPONSE ************************************ - if(DEBUG)MultiPrintStream.out.println("Sending Http Response!!!"); + if(DEBUG) MultiPrintStream.out.println("Sending Http Response!!!"); out.setStatusCode(200); - out.setHeader("Server", SERVER_VERSION); - out.setHeader("Content-Type", "text/html"); - out.setCookie("session_id", ""+client_session.get("session_id")); + out.setHeader( "Server", SERVER_VERSION ); + out.setHeader( "Content-Type", "text/html" ); + out.setCookie( "session_id", ""+client_session.get("session_id") ); - if(!page_url.isEmpty() && pages.containsKey(page_url)){ + if( !page_url.isEmpty() && pages.containsKey(page_url) ){ pages.get(page_url).respond(out, client_info, client_session, cookie, request); } - else if(defaultPage != null){ + else if( defaultPage != null ){ defaultPage.respond(out, client_info, client_session, cookie, request); } else{ - out.setStatusCode(404); - out.println("404 Page Not Found"); + out.setStatusCode( 404 ); + out.println( "404 Page Not Found" ); } //******************************************************************************** } catch (Exception e) { - e.printStackTrace(MultiPrintStream.out); + e.printStackTrace( MultiPrintStream.out ); try { - out.setStatusCode(500); + out.setStatusCode( 500 ); } catch (Exception e1) {} if(e.getMessage() != null) - out.println("500 Internal Server Error(Header: "+tmp+"): "+e.getMessage()); + out.println( "500 Internal Server Error: "+e.getMessage() ); else{ - out.println("500 Internal Server Error(Header: "+tmp+"): "+e.getCause().getMessage()); + out.println( "500 Internal Server Error: "+e.getCause().getMessage() ); } } try{ - if(DEBUG)MultiPrintStream.out.println("Conection Closed!!!"); + if(DEBUG) MultiPrintStream.out.println("Conection Closed!!!"); out.close(); in.close(); socket.close(); - } catch (Exception e) { - e.printStackTrace(MultiPrintStream.out); - } - } - } - - /** - * Parses the first header line and ads the values to - * the map and returns the file name and path - * - * @param header The header String - * @param map The HashMap to put the variables to - * @return The path and file name as a String - */ - private String parseHttpHeader(String header, HashMap map){ - String page_url = ""; - // cut out the page name - if(header.indexOf('?') > -1){ - page_url = header.substring(0, header.indexOf('?')); - header = header.substring(header.indexOf('?')+1, header.length()); - parseVariables(header, map); - } - else{ - page_url = header; - } - - return page_url; - } - - /** - * Parses a String with variables from a get or post - * from a client and puts the data into a HashMap - * - * @param header A String with all the variables - * @param map The HashMap to put all the variables into - */ - private void parseVariables(String header, HashMap map){ - int tmp; - // get the variables - String[] data = header.split("&"); - for(String element : data){ - tmp = element.indexOf('='); - if(tmp > 0){ - map.put( - element.substring(0, tmp ).trim(), // Key - element.substring(tmp+1, element.length() ).trim() ); //Value - } - else{ - map.put(element, ""); + } catch( Exception e ) { + e.printStackTrace( MultiPrintStream.out ); } } } diff --git a/src/zutil/network/nio/NioNetwork.java b/src/zutil/network/nio/NioNetwork.java index b3899a3..4c32e77 100644 --- a/src/zutil/network/nio/NioNetwork.java +++ b/src/zutil/network/nio/NioNetwork.java @@ -36,7 +36,7 @@ public abstract class NioNetwork implements Runnable { * 2 = message debug * 3 = selector debug */ - public static int DEBUG = 2; + public static final int DEBUG = 2; public static enum NetworkType {SERVER, CLIENT}; private NetworkType type; diff --git a/src/zutil/network/threaded/ThreadedTCPNetworkServer.java b/src/zutil/network/threaded/ThreadedTCPNetworkServer.java new file mode 100644 index 0000000..6496f1e --- /dev/null +++ b/src/zutil/network/threaded/ThreadedTCPNetworkServer.java @@ -0,0 +1,115 @@ +package zutil.network.threaded; + +import java.io.File; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.cert.CertificateException; + +import javax.net.ssl.SSLServerSocketFactory; + +import zutil.MultiPrintStream; + + +/** + * A simple web server that handles both cookies and + * sessions for all the clients + * + * @author Ziver + */ +public abstract class ThreadedTCPNetworkServer extends Thread{ + public final int port; + private File keyStore; + private String keyStorePass; + + /** + * Creates a new instance of the sever + * + * @param port The port that the server should listen to + */ + public ThreadedTCPNetworkServer(int port){ + this(port, null, null); + } + /** + * Creates a new instance of the sever + * + * @param port The port that the server should listen to + * @param sslCert If this is not null then the server will use SSL connection with this keyStore file path + * @param sslCert If this is not null then the server will use a SSL connection with the given certificate + */ + public ThreadedTCPNetworkServer(int port, File keyStore, String keyStorePass){ + this.port = port; + this.keyStorePass = keyStorePass; + this.keyStore = keyStore; + } + + + public void run(){ + ServerSocket ss = null; + try{ + if(keyStorePass != null && keyStore != null){ + registerCertificate(keyStore, keyStorePass); + ss = initSSL( port ); + } + else{ + ss = new ServerSocket( port ); + } + + while(true){ + Socket s = ss.accept(); + ThreadedTCPNetworkServerThread t = getThreadInstance( s ); + if( t!=null ) + new Thread( t ).start(); + else{ + MultiPrintStream.out.println("Unable to instantiate ThreadedTCPNetworkServerThread, closing connection!"); + s.close(); + } + } + } catch(Exception e) { + e.printStackTrace( MultiPrintStream.out ); + } + + if( ss!=null ){ + try{ + ss.close(); + }catch(IOException e){ e.printStackTrace( MultiPrintStream.out ); } + } + } + + /** + * This method returns an new instance of the ThreadedTCPNetworkServerThread + * that will handle the newly made connection, if an null value is returned + * then the ThreadedTCPNetworkServer will close the new connection. + * + * @param s is an new connection to an host + * @return a new instance of an thread or null + */ + protected abstract ThreadedTCPNetworkServerThread getThreadInstance( Socket s ); + + /** + * Initiates a SSLServerSocket + * + * @param port The port to listen to + * @return The SSLServerSocket + * @throws IOException + */ + private ServerSocket initSSL(int port) throws IOException{ + SSLServerSocketFactory sslserversocketfactory = + (SSLServerSocketFactory) SSLServerSocketFactory.getDefault(); + return sslserversocketfactory.createServerSocket(port); + + } + + /** + * Registers the given cert file to the KeyStore + * + * @param certFile The cert file + */ + protected void registerCertificate(File keyStore, String keyStorePass) throws CertificateException, IOException, KeyStoreException, NoSuchProviderException, NoSuchAlgorithmException{ + System.setProperty("javax.net.ssl.keyStore", keyStore.getAbsolutePath()); + System.setProperty("javax.net.ssl.keyStorePassword", keyStorePass); + } +} \ No newline at end of file diff --git a/src/zutil/network/threaded/ThreadedTCPNetworkServerThread.java b/src/zutil/network/threaded/ThreadedTCPNetworkServerThread.java new file mode 100644 index 0000000..21f9ddb --- /dev/null +++ b/src/zutil/network/threaded/ThreadedTCPNetworkServerThread.java @@ -0,0 +1,12 @@ +package zutil.network.threaded; + +/** + * The class that will handle the TCP connection will incclude + * this interface + * + * @author Ziver + * + */ +public interface ThreadedTCPNetworkServerThread extends Runnable{ + +} diff --git a/src/zutil/network/threaded/ThreadedUDPNetwork.java b/src/zutil/network/threaded/ThreadedUDPNetwork.java new file mode 100644 index 0000000..cbaefb7 --- /dev/null +++ b/src/zutil/network/threaded/ThreadedUDPNetwork.java @@ -0,0 +1,103 @@ +package zutil.network.threaded; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.MulticastSocket; +import java.net.SocketException; + + + +/** + * A simple web server that handles both cookies and + * sessions for all the clients + * + * @author Ziver + */ +public class ThreadedUDPNetwork extends Thread{ + public static final int BUFFER_SIZE = 512; + + // Type of UDP socket + enum UDPType{ + MULTICAST, + UNICAST + } + protected final UDPType type; + protected final int port; + protected DatagramSocket socket; + protected ThreadedUDPNetworkThread thread = null; + + + /** + * Creates a new unicast instance of the sever + * + * @param thread is the class that will handle incoming packets + * @param port is the port that the server should listen to + * @throws SocketException + */ + public ThreadedUDPNetwork(ThreadedUDPNetworkThread thread, int port) throws SocketException{ + this.type = UDPType.UNICAST; + this.port = port; + setThread( thread ); + + socket = new DatagramSocket( port ); + } + + /** + * Creates a new multicast instance of the sever + * + * @param thread is the class that will handle incoming packets + * @param port is the port that the server should listen to + * @param multicast_addr is the multicast address that the server will listen on + * @throws IOException + */ + public ThreadedUDPNetwork(ThreadedUDPNetworkThread thread, int port, String multicast_addr ) throws IOException{ + this.type = UDPType.MULTICAST; + this.port = port; + setThread( thread ); + + // init udp socket + MulticastSocket msocket = new MulticastSocket( port ); + InetAddress group = InetAddress.getByName( multicast_addr ); + msocket.joinGroup( group ); + + socket = msocket; + } + + + public void run(){ + try{ + while(true){ + byte[] buf = new byte[BUFFER_SIZE]; + DatagramPacket packet = new DatagramPacket(buf, buf.length); + socket.receive( packet ); + if( thread!=null ) + thread.receivedPacket( packet, this ); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Sends the given packet + * + * @param packet is the packet to send + * @throws IOException + */ + public synchronized void send( DatagramPacket packet ) throws IOException{ + socket.send(packet); + } + + /** + * Sets the thread that will handle the incoming packets + * + * @param thread is the thread + */ + public void setThread(ThreadedUDPNetworkThread thread){ + this.thread = thread; + } + + +} \ No newline at end of file diff --git a/src/zutil/network/threaded/ThreadedUDPNetworkThread.java b/src/zutil/network/threaded/ThreadedUDPNetworkThread.java new file mode 100644 index 0000000..48a381a --- /dev/null +++ b/src/zutil/network/threaded/ThreadedUDPNetworkThread.java @@ -0,0 +1,21 @@ +package zutil.network.threaded; + +import java.net.DatagramPacket; + +/** + * This interface is for processing received packets + * from the TNetworkUDPServer + * + * @author Ziver + * + */ +public interface ThreadedUDPNetworkThread extends Runnable{ + + /** + * Packet will be processed in this method + * + * @param packet is the received packet + * @param network is the network class that received the packet + */ + public void receivedPacket(DatagramPacket packet, ThreadedUDPNetwork network); +} diff --git a/src/zutil/struct/HistoryList.java b/src/zutil/struct/HistoryList.java new file mode 100644 index 0000000..54702dc --- /dev/null +++ b/src/zutil/struct/HistoryList.java @@ -0,0 +1,105 @@ +package zutil.struct; + +import java.util.Iterator; +import java.util.LinkedList; + + +public class HistoryList implements Iterable{ + public int history_length; + private LinkedList history; + private int historyIndex = 0; + + /** + * Creates an HistoryList object + */ + public HistoryList(){ + this( Integer.MAX_VALUE ); + } + + /** + * Creates an HistoryList object with an max size + * + * @param histlength the maximum size of the list + */ + public HistoryList(int histlength){ + history_length = histlength; + history = new LinkedList(); + } + + /** + * Returns the item in the given index + * + * @param i is the index + * @return item in that index + */ + public T get(int i){ + return history.get( i ); + } + + /** + * Adds an item to the list and removes the last if + * the list is bigger than the max length. + * + * @param item is the item to add + */ + public void add(T item){ + while(historyIndex < history.size()-1){ + history.removeLast(); + } + history.addLast(item); + if(history_length < history.size()){ + history.removeFirst(); + } + + historyIndex = history.size()-1; + } + + /** + * @return the previous item in the list + */ + public T getPrevious(){ + if(historyIndex > 0){ + historyIndex -= 1; + } + else{ + historyIndex = 0; + } + return history.get(historyIndex); + } + + /** + * @return the next item in the list + */ + public T getNext(){ + if(next()){ + historyIndex += 1; + } + else{ + historyIndex = history.size()-1; + } + return history.get(historyIndex); + } + + public T getCurrent(){ + return history.get(historyIndex); + } + + /** + * @return if there are items newer than the current + */ + public boolean next(){ + if(historyIndex < history.size()-1){ + return true; + } + else{ + return false; + } + } + + /** + * @return an iterator of the list + */ + public Iterator iterator(){ + return history.iterator(); + } +} diff --git a/src/zutil/test/FileFinderHasherTest.java b/src/zutil/test/FileFinderHasherTest.java index bd79c0e..1c74d73 100644 --- a/src/zutil/test/FileFinderHasherTest.java +++ b/src/zutil/test/FileFinderHasherTest.java @@ -2,7 +2,7 @@ package zutil.test; import java.io.File; import java.net.URISyntaxException; -import java.util.ArrayList; +import java.util.List; import zutil.FileFinder; import zutil.Hasher; @@ -12,7 +12,7 @@ public class FileFinderHasherTest { String relativePath = "zutil/test"; File path = FileFinder.find(relativePath); - ArrayList files = FileFinder.search(path); + List files = FileFinder.search(path); for(int i=0; i