From 26c09452f3b0a36981ea8cffbea0cd8a7feafe7a Mon Sep 17 00:00:00 2001 From: Ziver Koc Date: Fri, 4 Nov 2016 16:52:10 +0100 Subject: [PATCH] Added JsonPage and finished implementation of digest auth page --- src/zutil/io/IOUtil.java | 3 +- src/zutil/net/http/HttpHeader.java | 10 +-- src/zutil/net/http/HttpHeaderParser.java | 32 ++++++-- src/zutil/net/http/HttpPrintStream.java | 2 +- src/zutil/net/http/HttpServer.java | 11 +-- .../net/http/page/HttpDigestAuthPage.java | 61 +++++++++++++-- src/zutil/net/http/page/HttpJsonPage.java | 35 +++++++++ test/zutil/net/http/HttpTestUtil.java | 30 +++++++ .../net/http/page/HttpDigestAuthPageTest.java | 78 +++++++++++++------ .../zutil/net/http/page/HttpJsonPageTest.java | 34 ++++++++ 10 files changed, 244 insertions(+), 52 deletions(-) create mode 100755 src/zutil/net/http/page/HttpJsonPage.java create mode 100755 test/zutil/net/http/HttpTestUtil.java create mode 100755 test/zutil/net/http/page/HttpJsonPageTest.java diff --git a/src/zutil/io/IOUtil.java b/src/zutil/io/IOUtil.java index a8b56bc..e3f8962 100755 --- a/src/zutil/io/IOUtil.java +++ b/src/zutil/io/IOUtil.java @@ -114,7 +114,8 @@ public class IOUtil { while((line = in.readLine()) != null){ str.append(line).append("\n"); } - str.delete(str.length()-1, str.length()); // remove last new line + if (str.length() > 0) + str.delete(str.length()-1, str.length()); // remove last new line if (close) reader.close(); return str.toString(); diff --git a/src/zutil/net/http/HttpHeader.java b/src/zutil/net/http/HttpHeader.java index cc89e93..83b57f8 100755 --- a/src/zutil/net/http/HttpHeader.java +++ b/src/zutil/net/http/HttpHeader.java @@ -32,12 +32,12 @@ import java.util.Iterator; public class HttpHeader { // HTTP info - private boolean request; - private String type; - private String url; + private boolean request = true; + private String type = "GET"; + private String url = "/"; private HashMap urlAttributes; - private float version; - private int httpCode; + private float version = 1.0f; + private int httpCode = 200; private InputStream in; // Parameters diff --git a/src/zutil/net/http/HttpHeaderParser.java b/src/zutil/net/http/HttpHeaderParser.java index 994ee37..d4bd52c 100755 --- a/src/zutil/net/http/HttpHeaderParser.java +++ b/src/zutil/net/http/HttpHeaderParser.java @@ -31,7 +31,7 @@ import zutil.parser.URLDecoder; import java.io.IOException; import java.io.InputStream; -import java.util.List; +import java.util.HashMap; import java.util.Map; import java.util.regex.Pattern; @@ -44,7 +44,7 @@ public class HttpHeaderParser { private InputStream in; - private boolean readStatusLine; + private boolean skipStatusLine; /** @@ -54,7 +54,7 @@ public class HttpHeaderParser { */ public HttpHeaderParser(InputStream in){ this.in = in; - this.readStatusLine = true; + this.skipStatusLine = false; } /** @@ -72,7 +72,7 @@ public class HttpHeaderParser { String line; // First line - if (readStatusLine) { + if (!skipStatusLine) { if( (line=IOUtil.readLine(in)) != null && !line.isEmpty() ) parseStatusLine(header, line); else @@ -90,10 +90,10 @@ public class HttpHeaderParser { } /** - * @param readStatusLine indicates if the stream contains http status lines. (default: true) + * @param skipStatusLine indicates if the parser should expect a http status lines. (default: true) */ - public void setReadStatusLine(boolean readStatusLine){ - this.readStatusLine = readStatusLine; + public void skipStatusLine(boolean skipStatusLine){ + this.skipStatusLine = skipStatusLine; } /** @@ -153,9 +153,25 @@ public class HttpHeaderParser { public static void parseCookieValues(Map map, String cookieValue){ parseHeaderValues(map, cookieValue, ";"); } + + /** * Parses a header value string that contains key and value paired data and - * stores them in a HashMap. If a pair only contain a key the the value + * stores them in a HashMap. If a pair only contain a key then the value + * will be set as a empty string. + * + * TODO: method is not quote aware + * @param headerValue the raw header value String that will be parsed. + * @param delimiter the delimiter that separates key and value pairs (e.g. ';' for Cookies or ',' for Cache-Control) + */ + public static HashMap parseHeaderValues(String headerValue, String delimiter){ + HashMap map = new HashMap<>(); + parseHeaderValues(map, headerValue, delimiter); + return map; + } + /** + * Parses a header value string that contains key and value paired data and + * stores them in a HashMap. If a pair only contain a key then the value * will be set as a empty string. * * TODO: method is not quote aware diff --git a/src/zutil/net/http/HttpPrintStream.java b/src/zutil/net/http/HttpPrintStream.java index 80d3024..98cd56e 100755 --- a/src/zutil/net/http/HttpPrintStream.java +++ b/src/zutil/net/http/HttpPrintStream.java @@ -84,7 +84,7 @@ public class HttpPrintStream extends OutputStream{ */ public HttpPrintStream(OutputStream out, HttpMessageType type) { this.out = new PrintStream(out); - this.httpVersion = "1.1"; + this.httpVersion = "1.0"; this.message_type = type; this.res_status_code = 200; this.headers = new HashMap(); diff --git a/src/zutil/net/http/HttpServer.java b/src/zutil/net/http/HttpServer.java index be0fac5..33e0899 100755 --- a/src/zutil/net/http/HttpServer.java +++ b/src/zutil/net/http/HttpServer.java @@ -98,15 +98,18 @@ public class HttpServer extends ThreadedTCPNetworkServer{ private class SessionGarbageCollector extends TimerTask { public void run(){ Object[] keys = sessions.keySet().toArray(); + int count = 0; for(Object key : keys){ Map session = sessions.get(key); // Check if session is still valid if((Long)session.get(SESSION_TTL_KEY) < System.currentTimeMillis()){ sessions.remove(key); - logger.fine("Removing Session: "+key); + ++count; } } + if (count > 0) + logger.fine("Removed "+count+" old sessions"); } } @@ -141,7 +144,6 @@ public class HttpServer extends ThreadedTCPNetworkServer{ return null; } - private static int noOfConnections = 0; /** * Internal class that handles all the requests * @@ -160,8 +162,6 @@ public class HttpServer extends ThreadedTCPNetworkServer{ } public void run(){ - //logger.finest("New Connection: "+socket.getInetAddress()+" (Ongoing connections: "+(++noOfConnections)+")"); - try { //**************************** PARSE REQUEST ********************************* long time = System.currentTimeMillis(); @@ -171,7 +171,6 @@ public class HttpServer extends ThreadedTCPNetworkServer{ logger.finer("No header received"); return; } - String tmp = null; //******* Read in the post data if available if (header.getHeader("Content-Length") != null && @@ -213,7 +212,6 @@ public class HttpServer extends ThreadedTCPNetworkServer{ out.setStatusCode(200); out.setHeader("Server", SERVER_VERSION); out.setHeader("Content-Type", "text/html"); - //out.setHeader("Connection", "keep-alive"); out.setCookie(SESSION_ID_KEY, "" + session.get(SESSION_ID_KEY)); if (header.getRequestURL() != null && pages.containsKey(header.getRequestURL())) { @@ -252,7 +250,6 @@ public class HttpServer extends ThreadedTCPNetworkServer{ out.close(); in.close(); socket.close(); - //logger.finest("Connection Closed: "+socket.getInetAddress()+" (Ongoing connections: "+(--noOfConnections)+")"); } catch( Exception e ) { logger.log(Level.WARNING, "Could not close connection", e); } diff --git a/src/zutil/net/http/page/HttpDigestAuthPage.java b/src/zutil/net/http/page/HttpDigestAuthPage.java index 1662013..e6084cb 100755 --- a/src/zutil/net/http/page/HttpDigestAuthPage.java +++ b/src/zutil/net/http/page/HttpDigestAuthPage.java @@ -1,14 +1,18 @@ package zutil.net.http.page; -import zutil.Encrypter; +import sun.net.www.HeaderParser; import zutil.Hasher; +import zutil.log.LogUtil; import zutil.net.http.HttpHeader; +import zutil.net.http.HttpHeaderParser; import zutil.net.http.HttpPage; import zutil.net.http.HttpPrintStream; import java.io.IOException; import java.security.SecureRandom; +import java.util.HashMap; import java.util.Map; +import java.util.logging.Logger; /** * A abstract page that requires HTTP Digest authentication @@ -18,7 +22,9 @@ import java.util.Map; * @author Ziver */ public abstract class HttpDigestAuthPage implements HttpPage{ - private static final String DEAFULT_REALM = "Login"; + private static final Logger logger = LogUtil.getLogger(); + + private static final String DEFAULT_REALM = "Login"; private static final String HTTP_AUTH_HEADER = "WWW-Authenticate"; private static final String HTTP_CLIENT_HEADER = "Authorization"; private static final String AUTH_TYPE = "Digest"; @@ -29,9 +35,11 @@ public abstract class HttpDigestAuthPage implements HttpPage{ private static final String AUTH_USERNAME = "username"; private static final String AUTH_URI = "uri"; private static final String AUTH_RESPONSE = "response"; + private static final String AUTH_DELIMITER = ","; - private String realm = DEAFULT_REALM; + private String realm = DEFAULT_REALM; + private HashMap userMap = new HashMap<>(); private SecureRandom secRandom = new SecureRandom(); @@ -40,6 +48,12 @@ public abstract class HttpDigestAuthPage implements HttpPage{ this.realm = realm; } + public void addUser(String username, char[] password) { + userMap.put(username, new String(password)); // TODO: should use char[] for passwords + } + public void removeUser(String username) { + userMap.remove(username); + } @Override @@ -49,17 +63,51 @@ public abstract class HttpDigestAuthPage implements HttpPage{ Map cookie, Map request) throws IOException { - if (headers.getHeader(HTTP_CLIENT_HEADER) == null) { + if (headers.getHeader(HTTP_CLIENT_HEADER) == null || !session.containsKey(AUTH_NONCE)) { session.put(AUTH_NONCE, generateNonce()); out.setStatusCode(401); out.setHeader(HTTP_AUTH_HEADER, generateAuthHeader((String) session.get(AUTH_NONCE))); + out.println("401 Unauthorized"); + } + else if ( ! headers.getHeader(HTTP_CLIENT_HEADER).startsWith(AUTH_TYPE)){ + out.setStatusCode(501); + out.println("501 Not Implemented"); } else{ - authRespond(out, headers, session, cookie, request); + HashMap authMap = HttpHeaderParser.parseHeaderValues( + headers.getHeader(HTTP_CLIENT_HEADER).substring(AUTH_TYPE.length()+1), // Skip auth type + AUTH_DELIMITER); + if (authenticate( + authMap.get(AUTH_USERNAME), + headers.getRequestURL(), + (String)session.get(AUTH_NONCE), + authMap.get(AUTH_RESPONSE))) { + // Safe area, user authenticated + logger.fine("User '"+authMap.get(AUTH_USERNAME)+"' has been authenticated for realm '"+realm+"'"); + authRespond(out, headers, session, cookie, request); + } + else{ + out.setStatusCode(403); + out.println("403 Forbidden"); + } } } + private boolean authenticate(String username, String uri, String nonce, String clientResponse){ + if (!userMap.containsKey(username)) // do user exist? + return false; + + String generatedResponse = generateResponseHash( + generateH1(username, userMap.get(username), realm), + generateH2(uri), + nonce); + if (generatedResponse.equals(clientResponse)){ + return true; + } + return false; + } + private String generateAuthHeader(String nonce){ StringBuilder str = new StringBuilder(); str.append(AUTH_TYPE).append(' '); @@ -76,7 +124,7 @@ public abstract class HttpDigestAuthPage implements HttpPage{ } private static String generateH1(String username, String password, String realm) { - String ha1 = null; + String ha1; // If the algorithm directive's value is "MD5" or unspecified, then HA1 is // HA1=MD5(username:realm:password) ha1 = Hasher.MD5(username +":"+ realm +":"+ password); @@ -111,4 +159,5 @@ public abstract class HttpDigestAuthPage implements HttpPage{ Map session, Map cookie, Map request) throws IOException; + } diff --git a/src/zutil/net/http/page/HttpJsonPage.java b/src/zutil/net/http/page/HttpJsonPage.java new file mode 100755 index 0000000..3b5075c --- /dev/null +++ b/src/zutil/net/http/page/HttpJsonPage.java @@ -0,0 +1,35 @@ +package zutil.net.http.page; + +import zutil.net.http.HttpHeader; +import zutil.net.http.HttpPage; +import zutil.net.http.HttpPrintStream; +import zutil.parser.DataNode; +import zutil.parser.json.JSONWriter; + +import java.io.IOException; +import java.util.Map; + +/** + * @author Ziver on 2016-11-04. + */ +public abstract class HttpJsonPage implements HttpPage { + + @Override + public void respond(HttpPrintStream out, + HttpHeader headers, + Map session, + Map cookie, + Map request) throws IOException { + + out.setHeader("Content-Type", "application/json"); + JSONWriter writer = new JSONWriter(out); + writer.write(jsonRespond(headers, session, cookie, request)); + writer.close(); + } + + + protected abstract DataNode jsonRespond(HttpHeader headers, + Map session, + Map cookie, + Map request); +} diff --git a/test/zutil/net/http/HttpTestUtil.java b/test/zutil/net/http/HttpTestUtil.java new file mode 100755 index 0000000..bd975b1 --- /dev/null +++ b/test/zutil/net/http/HttpTestUtil.java @@ -0,0 +1,30 @@ +package zutil.net.http; + +import zutil.io.StringOutputStream; +import zutil.net.http.page.HttpDigestAuthPageTest; + +import java.io.IOException; +import java.util.HashMap; + +public class HttpTestUtil { + public static HashMap session = new HashMap(); + + /** + * Make a simple http request on the given page object + */ + public static HttpHeader makeRequest(HttpPage page) throws IOException { + return makeRequest(page, new HttpHeader()); + } + /** + * Make a simple http request on the given page object + */ + public static HttpHeader makeRequest(HttpPage page, HttpHeader headers) throws IOException { + StringOutputStream buff = new StringOutputStream(); + HttpPrintStream out = new HttpPrintStream(buff); + page.respond( + out, headers, session, new HashMap(), new HashMap()); + out.flush(); + HttpHeaderParser parser = new HttpHeaderParser(buff.toString()); + return parser.read(); + } +} \ No newline at end of file diff --git a/test/zutil/net/http/page/HttpDigestAuthPageTest.java b/test/zutil/net/http/page/HttpDigestAuthPageTest.java index 79134ed..36c7b5c 100755 --- a/test/zutil/net/http/page/HttpDigestAuthPageTest.java +++ b/test/zutil/net/http/page/HttpDigestAuthPageTest.java @@ -1,11 +1,16 @@ package zutil.net.http.page; +import static org.hamcrest.CoreMatchers.*; + +import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import zutil.Hasher; -import zutil.io.StringOutputStream; +import zutil.io.IOUtil; import zutil.net.http.HttpHeader; import zutil.net.http.HttpHeaderParser; import zutil.net.http.HttpPrintStream; +import zutil.net.http.HttpTestUtil; import java.io.IOException; import java.util.HashMap; @@ -18,10 +23,22 @@ import static org.junit.Assert.*; */ public class HttpDigestAuthPageTest { private static final String PAGE_CONTENT = "Hello World!"; + private static final String PAGE_USERNAME = "username"; + private static final String PAGE_PASSWORD = "password"; + + private HttpDigestTestPage page; + + @Before + public void init(){ + page = new HttpDigestTestPage(); + page.addUser(PAGE_USERNAME, PAGE_PASSWORD.toCharArray()); + } + + @Test public void cleanRequest() throws IOException { - HttpHeader rspHeader = makeRequest(new HttpHeader()); + HttpHeader rspHeader = HttpTestUtil.makeRequest(page); assertEquals(401, rspHeader.getHTTPCode()); assertTrue(rspHeader.getHeader("WWW-Authenticate") != null); @@ -29,47 +46,60 @@ public class HttpDigestAuthPageTest { Map authHeader = parseAuthHeader(rspHeader); assertTrue(authHeader.containsKey("realm")); assertTrue(authHeader.containsKey("nonce")); + assertThat(IOUtil.readContentAsString(rspHeader.getInputStream()), + not(containsString(PAGE_CONTENT))); } - @Test public void authenticate() throws IOException { + HttpHeader rspHeader = authenticate(PAGE_USERNAME, PAGE_PASSWORD); + assertEquals(200, rspHeader.getHTTPCode()); + assertThat(IOUtil.readContentAsString(rspHeader.getInputStream()), + containsString(PAGE_CONTENT)); + } + @Test + public void wrongUsername() throws IOException { + HttpHeader rspHeader = authenticate(PAGE_USERNAME+"wrong", PAGE_PASSWORD); + assertEquals(403, rspHeader.getHTTPCode()); + assertThat(IOUtil.readContentAsString(rspHeader.getInputStream()), + not(containsString(PAGE_CONTENT))); + } + @Test + public void wrongPassword() throws IOException { + HttpHeader rspHeader = authenticate(PAGE_USERNAME, PAGE_PASSWORD+"wrong"); + assertEquals(403, rspHeader.getHTTPCode()); + assertThat(IOUtil.readContentAsString(rspHeader.getInputStream()), + not(containsString(PAGE_CONTENT))); + } + + + + + + public HttpHeader authenticate(String username, String password) throws IOException { HttpHeader reqHeader = new HttpHeader(); - HttpHeader rspHeader = makeRequest(reqHeader); + HttpHeader rspHeader = HttpTestUtil.makeRequest(page, reqHeader); Map authHeader = parseAuthHeader(rspHeader); reqHeader = new HttpHeader(); String realm = authHeader.get("realm"); String nonce = authHeader.get("nonce"); String uri = "/login"; - - String ha1 = Hasher.MD5("username:password"); - String ha2 = Hasher.MD5("MD5:/" +uri); + String ha1 = Hasher.MD5(username+":"+realm+":"+password); + String ha2 = Hasher.MD5("MD5:" +uri); String response = Hasher.MD5(ha1 +":"+ nonce +":"+ ha2); - reqHeader.setHeader("Authorization", "Digest username=\"username\", " + + reqHeader.setRequestURL(uri); + reqHeader.setHeader("Authorization", "Digest " + + "username=\""+username+"\", " + "realm=\""+realm+"\", " + "nonce=\""+nonce+"\", " + "uri=\""+uri+"\", " + "response=\""+response+"\""); - rspHeader = makeRequest(reqHeader); - assertEquals(200, rspHeader.getHTTPCode()); + + return HttpTestUtil.makeRequest(page, reqHeader); } - - - - - public static HttpHeader makeRequest(HttpHeader headers) throws IOException { - StringOutputStream buff = new StringOutputStream(); - HttpPrintStream out = new HttpPrintStream(buff); - new HttpDigestTestPage().respond( - out, headers, new HashMap(), new HashMap(), new HashMap()); - out.flush(); - HttpHeaderParser parser = new HttpHeaderParser(buff.toString()); - return parser.read(); - } - public static String parseAuthType(HttpHeader headers){ String tmp = headers.getHeader("WWW-Authenticate"); return tmp.substring(0, tmp.indexOf(' ')); diff --git a/test/zutil/net/http/page/HttpJsonPageTest.java b/test/zutil/net/http/page/HttpJsonPageTest.java new file mode 100755 index 0000000..f5f7714 --- /dev/null +++ b/test/zutil/net/http/page/HttpJsonPageTest.java @@ -0,0 +1,34 @@ +package zutil.net.http.page; + +import org.junit.Test; +import zutil.io.IOUtil; +import zutil.net.http.HttpHeader; +import zutil.net.http.HttpTestUtil; +import zutil.parser.DataNode; + +import java.io.IOException; +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * @author Ziver on 2016-11-04. + */ +public class HttpJsonPageTest { + + private HttpJsonPage page = new HttpJsonPage() { + @Override + protected DataNode jsonRespond(HttpHeader headers, Map session, Map cookie, Map request) { + return new DataNode(DataNode.DataType.Map); + } + }; + + + + @Test + public void simpleResponse() throws IOException { + HttpHeader header = HttpTestUtil.makeRequest(page); + assertEquals("application/json", header.getHeader("Content-Type")); + assertEquals("{}", IOUtil.readContentAsString(header.getInputStream())); + } +} \ No newline at end of file