Added JsonPage and finished implementation of digest auth page

This commit is contained in:
Ziver Koc 2016-11-04 16:52:10 +01:00
parent 2c7a9a6eff
commit 26c09452f3
10 changed files with 244 additions and 52 deletions

View file

@ -114,6 +114,7 @@ public class IOUtil {
while((line = in.readLine()) != null){ while((line = in.readLine()) != null){
str.append(line).append("\n"); str.append(line).append("\n");
} }
if (str.length() > 0)
str.delete(str.length()-1, str.length()); // remove last new line str.delete(str.length()-1, str.length()); // remove last new line
if (close) reader.close(); if (close) reader.close();

View file

@ -32,12 +32,12 @@ import java.util.Iterator;
public class HttpHeader { public class HttpHeader {
// HTTP info // HTTP info
private boolean request; private boolean request = true;
private String type; private String type = "GET";
private String url; private String url = "/";
private HashMap<String, String> urlAttributes; private HashMap<String, String> urlAttributes;
private float version; private float version = 1.0f;
private int httpCode; private int httpCode = 200;
private InputStream in; private InputStream in;
// Parameters // Parameters

View file

@ -31,7 +31,7 @@ import zutil.parser.URLDecoder;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -44,7 +44,7 @@ public class HttpHeaderParser {
private InputStream in; private InputStream in;
private boolean readStatusLine; private boolean skipStatusLine;
/** /**
@ -54,7 +54,7 @@ public class HttpHeaderParser {
*/ */
public HttpHeaderParser(InputStream in){ public HttpHeaderParser(InputStream in){
this.in = in; this.in = in;
this.readStatusLine = true; this.skipStatusLine = false;
} }
/** /**
@ -72,7 +72,7 @@ public class HttpHeaderParser {
String line; String line;
// First line // First line
if (readStatusLine) { if (!skipStatusLine) {
if( (line=IOUtil.readLine(in)) != null && !line.isEmpty() ) if( (line=IOUtil.readLine(in)) != null && !line.isEmpty() )
parseStatusLine(header, line); parseStatusLine(header, line);
else 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){ public void skipStatusLine(boolean skipStatusLine){
this.readStatusLine = readStatusLine; this.skipStatusLine = skipStatusLine;
} }
/** /**
@ -153,9 +153,25 @@ public class HttpHeaderParser {
public static void parseCookieValues(Map<String,String> map, String cookieValue){ public static void parseCookieValues(Map<String,String> map, String cookieValue){
parseHeaderValues(map, cookieValue, ";"); parseHeaderValues(map, cookieValue, ";");
} }
/** /**
* Parses a header value string that contains key and value paired data and * 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<String, String> parseHeaderValues(String headerValue, String delimiter){
HashMap<String, String> 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. * will be set as a empty string.
* *
* TODO: method is not quote aware * TODO: method is not quote aware

View file

@ -84,7 +84,7 @@ public class HttpPrintStream extends OutputStream{
*/ */
public HttpPrintStream(OutputStream out, HttpMessageType type) { public HttpPrintStream(OutputStream out, HttpMessageType type) {
this.out = new PrintStream(out); this.out = new PrintStream(out);
this.httpVersion = "1.1"; this.httpVersion = "1.0";
this.message_type = type; this.message_type = type;
this.res_status_code = 200; this.res_status_code = 200;
this.headers = new HashMap<String, String>(); this.headers = new HashMap<String, String>();

View file

@ -98,15 +98,18 @@ public class HttpServer extends ThreadedTCPNetworkServer{
private class SessionGarbageCollector extends TimerTask { private class SessionGarbageCollector extends TimerTask {
public void run(){ public void run(){
Object[] keys = sessions.keySet().toArray(); Object[] keys = sessions.keySet().toArray();
int count = 0;
for(Object key : keys){ for(Object key : keys){
Map<String,Object> session = sessions.get(key); Map<String,Object> session = sessions.get(key);
// Check if session is still valid // Check if session is still valid
if((Long)session.get(SESSION_TTL_KEY) < System.currentTimeMillis()){ if((Long)session.get(SESSION_TTL_KEY) < System.currentTimeMillis()){
sessions.remove(key); 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; return null;
} }
private static int noOfConnections = 0;
/** /**
* Internal class that handles all the requests * Internal class that handles all the requests
* *
@ -160,8 +162,6 @@ public class HttpServer extends ThreadedTCPNetworkServer{
} }
public void run(){ public void run(){
//logger.finest("New Connection: "+socket.getInetAddress()+" (Ongoing connections: "+(++noOfConnections)+")");
try { try {
//**************************** PARSE REQUEST ********************************* //**************************** PARSE REQUEST *********************************
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
@ -171,7 +171,6 @@ public class HttpServer extends ThreadedTCPNetworkServer{
logger.finer("No header received"); logger.finer("No header received");
return; return;
} }
String tmp = null;
//******* Read in the post data if available //******* Read in the post data if available
if (header.getHeader("Content-Length") != null && if (header.getHeader("Content-Length") != null &&
@ -213,7 +212,6 @@ public class HttpServer extends ThreadedTCPNetworkServer{
out.setStatusCode(200); out.setStatusCode(200);
out.setHeader("Server", SERVER_VERSION); out.setHeader("Server", SERVER_VERSION);
out.setHeader("Content-Type", "text/html"); out.setHeader("Content-Type", "text/html");
//out.setHeader("Connection", "keep-alive");
out.setCookie(SESSION_ID_KEY, "" + session.get(SESSION_ID_KEY)); out.setCookie(SESSION_ID_KEY, "" + session.get(SESSION_ID_KEY));
if (header.getRequestURL() != null && pages.containsKey(header.getRequestURL())) { if (header.getRequestURL() != null && pages.containsKey(header.getRequestURL())) {
@ -252,7 +250,6 @@ public class HttpServer extends ThreadedTCPNetworkServer{
out.close(); out.close();
in.close(); in.close();
socket.close(); socket.close();
//logger.finest("Connection Closed: "+socket.getInetAddress()+" (Ongoing connections: "+(--noOfConnections)+")");
} catch( Exception e ) { } catch( Exception e ) {
logger.log(Level.WARNING, "Could not close connection", e); logger.log(Level.WARNING, "Could not close connection", e);
} }

View file

@ -1,14 +1,18 @@
package zutil.net.http.page; package zutil.net.http.page;
import zutil.Encrypter; import sun.net.www.HeaderParser;
import zutil.Hasher; import zutil.Hasher;
import zutil.log.LogUtil;
import zutil.net.http.HttpHeader; import zutil.net.http.HttpHeader;
import zutil.net.http.HttpHeaderParser;
import zutil.net.http.HttpPage; import zutil.net.http.HttpPage;
import zutil.net.http.HttpPrintStream; import zutil.net.http.HttpPrintStream;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.logging.Logger;
/** /**
* A abstract page that requires HTTP Digest authentication * A abstract page that requires HTTP Digest authentication
@ -18,7 +22,9 @@ import java.util.Map;
* @author Ziver * @author Ziver
*/ */
public abstract class HttpDigestAuthPage implements HttpPage{ 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_AUTH_HEADER = "WWW-Authenticate";
private static final String HTTP_CLIENT_HEADER = "Authorization"; private static final String HTTP_CLIENT_HEADER = "Authorization";
private static final String AUTH_TYPE = "Digest"; 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_USERNAME = "username";
private static final String AUTH_URI = "uri"; private static final String AUTH_URI = "uri";
private static final String AUTH_RESPONSE = "response"; 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<String,String> userMap = new HashMap<>();
private SecureRandom secRandom = new SecureRandom(); private SecureRandom secRandom = new SecureRandom();
@ -40,6 +48,12 @@ public abstract class HttpDigestAuthPage implements HttpPage{
this.realm = realm; 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 @Override
@ -49,17 +63,51 @@ public abstract class HttpDigestAuthPage implements HttpPage{
Map<String, String> cookie, Map<String, String> cookie,
Map<String, String> request) throws IOException { Map<String, String> 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()); session.put(AUTH_NONCE, generateNonce());
out.setStatusCode(401); out.setStatusCode(401);
out.setHeader(HTTP_AUTH_HEADER, generateAuthHeader((String) session.get(AUTH_NONCE))); 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{ else{
HashMap<String,String> 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); 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){ private String generateAuthHeader(String nonce){
StringBuilder str = new StringBuilder(); StringBuilder str = new StringBuilder();
str.append(AUTH_TYPE).append(' '); 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) { 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 // If the algorithm directive's value is "MD5" or unspecified, then HA1 is
// HA1=MD5(username:realm:password) // HA1=MD5(username:realm:password)
ha1 = Hasher.MD5(username +":"+ realm +":"+ password); ha1 = Hasher.MD5(username +":"+ realm +":"+ password);
@ -111,4 +159,5 @@ public abstract class HttpDigestAuthPage implements HttpPage{
Map<String, Object> session, Map<String, Object> session,
Map<String, String> cookie, Map<String, String> cookie,
Map<String, String> request) throws IOException; Map<String, String> request) throws IOException;
} }

View file

@ -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<String, Object> session,
Map<String, String> cookie,
Map<String, String> 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<String, Object> session,
Map<String, String> cookie,
Map<String, String> request);
}

View file

@ -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<String,Object> 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();
}
}

View file

@ -1,11 +1,16 @@
package zutil.net.http.page; package zutil.net.http.page;
import static org.hamcrest.CoreMatchers.*;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import zutil.Hasher; import zutil.Hasher;
import zutil.io.StringOutputStream; import zutil.io.IOUtil;
import zutil.net.http.HttpHeader; import zutil.net.http.HttpHeader;
import zutil.net.http.HttpHeaderParser; import zutil.net.http.HttpHeaderParser;
import zutil.net.http.HttpPrintStream; import zutil.net.http.HttpPrintStream;
import zutil.net.http.HttpTestUtil;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
@ -18,10 +23,22 @@ import static org.junit.Assert.*;
*/ */
public class HttpDigestAuthPageTest { public class HttpDigestAuthPageTest {
private static final String PAGE_CONTENT = "Hello World!"; 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 @Test
public void cleanRequest() throws IOException { public void cleanRequest() throws IOException {
HttpHeader rspHeader = makeRequest(new HttpHeader()); HttpHeader rspHeader = HttpTestUtil.makeRequest(page);
assertEquals(401, rspHeader.getHTTPCode()); assertEquals(401, rspHeader.getHTTPCode());
assertTrue(rspHeader.getHeader("WWW-Authenticate") != null); assertTrue(rspHeader.getHeader("WWW-Authenticate") != null);
@ -29,47 +46,60 @@ public class HttpDigestAuthPageTest {
Map<String,String> authHeader = parseAuthHeader(rspHeader); Map<String,String> authHeader = parseAuthHeader(rspHeader);
assertTrue(authHeader.containsKey("realm")); assertTrue(authHeader.containsKey("realm"));
assertTrue(authHeader.containsKey("nonce")); assertTrue(authHeader.containsKey("nonce"));
assertThat(IOUtil.readContentAsString(rspHeader.getInputStream()),
not(containsString(PAGE_CONTENT)));
} }
@Test @Test
public void authenticate() throws IOException { 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 reqHeader = new HttpHeader();
HttpHeader rspHeader = makeRequest(reqHeader); HttpHeader rspHeader = HttpTestUtil.makeRequest(page, reqHeader);
Map<String,String> authHeader = parseAuthHeader(rspHeader); Map<String,String> authHeader = parseAuthHeader(rspHeader);
reqHeader = new HttpHeader(); reqHeader = new HttpHeader();
String realm = authHeader.get("realm"); String realm = authHeader.get("realm");
String nonce = authHeader.get("nonce"); String nonce = authHeader.get("nonce");
String uri = "/login"; String uri = "/login";
String ha1 = Hasher.MD5(username+":"+realm+":"+password);
String ha1 = Hasher.MD5("username:password"); String ha2 = Hasher.MD5("MD5:" +uri);
String ha2 = Hasher.MD5("MD5:/" +uri);
String response = Hasher.MD5(ha1 +":"+ nonce +":"+ ha2); String response = Hasher.MD5(ha1 +":"+ nonce +":"+ ha2);
reqHeader.setHeader("Authorization", "Digest username=\"username\", " + reqHeader.setRequestURL(uri);
reqHeader.setHeader("Authorization", "Digest " +
"username=\""+username+"\", " +
"realm=\""+realm+"\", " + "realm=\""+realm+"\", " +
"nonce=\""+nonce+"\", " + "nonce=\""+nonce+"\", " +
"uri=\""+uri+"\", " + "uri=\""+uri+"\", " +
"response=\""+response+"\""); "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){ public static String parseAuthType(HttpHeader headers){
String tmp = headers.getHeader("WWW-Authenticate"); String tmp = headers.getHeader("WWW-Authenticate");
return tmp.substring(0, tmp.indexOf(' ')); return tmp.substring(0, tmp.indexOf(' '));

View file

@ -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<String, Object> session, Map<String, String> cookie, Map<String, String> 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()));
}
}