changeset 1317:c286c1e36b81

add client digest authentication
author Franklin Schmidt <fschmidt@gmail.com>
date Fri, 01 Feb 2019 03:46:56 -0700
parents 11d3640e739d
children 35a6a195819f
files src/luan/modules/url/LuanUrl.java src/luan/modules/url/WwwAuthenticate.java
diffstat 2 files changed, 190 insertions(+), 28 deletions(-) [+]
line wrap: on
line diff
--- a/src/luan/modules/url/LuanUrl.java	Thu Jan 31 04:26:23 2019 -0700
+++ b/src/luan/modules/url/LuanUrl.java	Fri Feb 01 03:46:56 2019 -0700
@@ -11,10 +11,13 @@
 import java.net.URLConnection;
 import java.net.HttpURLConnection;
 import java.net.URLEncoder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Base64;
+import luan.lib.parser.ParseException;
 import luan.Luan;
 import luan.LuanState;
 import luan.LuanTable;
@@ -30,10 +33,12 @@
 
 	private URL url;
 	private Method method = Method.GET;
-	private Map headers;
+	private final Map<String,Object> headers = new HashMap<String,Object>();
 	private String content = "";
 	private MultipartClient multipart = null;
 	private int timeout = 0;
+	private String authUser = null;
+	private String authPassword = null;
 
 	public LuanUrl(URL url,LuanTable options) throws LuanException {
 		this.url = url;
@@ -50,7 +55,6 @@
 			}
 			Map headerMap = getMap(map,"headers");
 			if( headerMap != null ) {
-				headers = new HashMap();
 				for( Object hack : headerMap.entrySet() ) {
 					Map.Entry entry = (Map.Entry)hack;
 					String name = (String)entry.getKey();
@@ -72,19 +76,21 @@
 				if( headers!=null && headers.containsKey("authorization") )
 					throw new LuanException( "can't define authorization with header 'authorization' defined" );
 				String user = getString(auth,"user");
+				if( user==null )  user = "";
 				String password = getString(auth,"password");
+				if( password==null )  password = "";
+				String type = getString(auth,"type");
 				if( !auth.isEmpty() )
 					throw new LuanException( "unrecognized authorization options: "+auth );
-				StringBuilder sb = new StringBuilder();
-				if( user != null )
-					sb.append(user);
-				sb.append(':');
-				if( password != null )
-					sb.append(password);
-				String val = "Basic " + Base64.getEncoder().encodeToString(sb.toString().getBytes());
-				if( headers == null )
-					headers = new HashMap();
-				headers.put("authorization",val);
+				if( type != null ) {
+					if( !type.toLowerCase().equals("basic") )
+						throw new LuanException( "authorization type can only be 'basic' or nil" );
+					String val = basicAuth(user,password);
+					headers.put("authorization",val);
+				} else {
+					authUser = user;
+					authPassword = password;
+				}
 			}
 			Map params = getMap(map,"parameters");
 			String enctype = getString(map,"enctype");
@@ -191,26 +197,39 @@
 	}
 
 	@Override public InputStream inputStream(LuanState luan) throws IOException, LuanException {
+		try {
+			return inputStream(luan,null);
+		} catch(AuthException e) {
+			try {
+				return inputStream(luan,e.authorization);
+			} catch(AuthException e2) {
+				throw new RuntimeException(e2);  // never
+			}
+		}
+	}
+
+	private InputStream inputStream(LuanState luan,String authorization)
+		throws IOException, LuanException, AuthException
+	{
 		URLConnection con = url.openConnection();
 		if( timeout != 0 ) {
 			con.setConnectTimeout(timeout);
 			con.setReadTimeout(timeout);
 		}
-		if( headers != null ) {
-			for( Object hack : headers.entrySet() ) {
-				Map.Entry entry = (Map.Entry)hack;
-				String key = (String)entry.getKey();
-				Object val = entry.getValue();
-				if( val instanceof String ) {
-					con.addRequestProperty(key,(String)val);
-				} else {
-					List list = (List)val;
-					for( Object obj : list ) {
-						con.addRequestProperty(key,(String)obj);
-					}
+		for( Map.Entry<String,Object> entry : headers.entrySet() ) {
+			String key = entry.getKey();
+			Object val = entry.getValue();
+			if( val instanceof String ) {
+				con.addRequestProperty(key,(String)val);
+			} else {
+				List list = (List)val;
+				for( Object obj : list ) {
+					con.addRequestProperty(key,(String)obj);
 				}
 			}
 		}
+		if( authorization != null )
+			con.addRequestProperty("Authorization",authorization);
 		if( !(con instanceof HttpURLConnection) ) {
 			if( method!=Method.GET )
 				throw new LuanException("method must be GET but is "+method);
@@ -220,12 +239,12 @@
 		HttpURLConnection httpCon = (HttpURLConnection)con;
 
 		if( method==Method.GET ) {
-			return getInputStream(luan,httpCon);
+			return getInputStream(luan,httpCon,authorization);
 		}
 
 		if( method==Method.DELETE ) {
 			httpCon.setRequestMethod("DELETE");
-			return getInputStream(luan,httpCon);
+			return getInputStream(luan,httpCon,authorization);
 		}
 
 		// POST
@@ -245,19 +264,70 @@
 		}
 		out.flush();
 		try {
-			return getInputStream(luan,httpCon);
+			return getInputStream(luan,httpCon,authorization);
 		} finally {
 			out.close();
 		}
 	}
 
-	private static InputStream getInputStream(LuanState luan,HttpURLConnection httpCon) throws IOException, LuanException {
+	private InputStream getInputStream(LuanState luan,HttpURLConnection httpCon,String authorization)
+		throws IOException, LuanException, AuthException
+	{
 		try {
 			return httpCon.getInputStream();
 		} catch(FileNotFoundException e) {
 			throw e;
 		} catch(IOException e) {
 			int responseCode = httpCon.getResponseCode();
+			if( responseCode == 401 && authUser != null && authorization==null ) {
+				String authStr = httpCon.getHeaderField("www-authenticate");
+				//System.out.println("auth = "+authStr);
+				try {
+					WwwAuthenticate auth = new WwwAuthenticate(authStr);
+					if( auth.type.equals("Basic") ) {
+						String val = basicAuth(authUser,authPassword);
+						throw new AuthException(val);
+					} else if( auth.type.equals("Digest") ) {
+						String realm = auth.options.get("realm");
+						if(realm==null) throw new RuntimeException("missing realm");
+						String algorithm = auth.options.get("algorithm");
+						if( algorithm!=null && !algorithm.equals("MD5") )
+							throw new LuanException("unsupported digest algorithm: "+algorithm);
+						String qop = auth.options.get("qop");
+						if( qop!=null && !qop.equals("auth") )
+							throw new LuanException("unsupported digest qop: "+qop);
+						String nonce = auth.options.get("nonce");
+						if(nonce==null) throw new RuntimeException("missing nonce");
+						String uri = fullPath(url);
+						String a1 = authUser + ':' + realm + ':' + authPassword;
+						String a2 = "" + method + ':' + uri;
+						String nc = "00000001";
+						String cnonce = "7761faf2daa45b3b";  // who cares?
+						String response = md5(a1) + ':' + nonce;
+						if( qop != null ) {
+							response += ':' + nc + ':' + cnonce + ':' + qop;
+						}
+						response += ':' + md5(a2);
+						response = md5(response);
+						String val = "Digest";
+						val += " username=\"" + authUser + "\"";
+						val += ", realm=\"" + realm + "\"";
+						val += ", uri=\"" + uri + "\"";
+						val += ", nonce=\"" + nonce + "\"";
+						val += ", response=\"" + response + "\"";
+						if( qop != null ) {
+							val += ", qop=" + qop;
+							val += ", nc=" + nc;
+							val += ", cnonce=\"" + cnonce + "\"";
+						}
+						//System.out.println("val = "+val);
+						throw new AuthException(val);
+					} else
+						throw new RuntimeException(auth.type);
+				} catch(ParseException pe) {
+					throw new LuanException(pe);
+				}
+			}
 			String responseMessage = httpCon.getResponseMessage();
 			InputStream is = httpCon.getErrorStream();
 			if( is == null )
@@ -281,4 +351,40 @@
 		return url.toString();
 	}
 
+	private static String basicAuth(String user,String password) {
+		String s = user + ':' + password;
+		return "Basic " + Base64.getEncoder().encodeToString(s.getBytes());
+	}
+
+	private final class AuthException extends Exception {
+		final String authorization;
+
+		AuthException(String authorization) {
+			this.authorization = authorization;
+		}
+	}
+
+	// retarded java api lacks this
+	public static String fullPath(URL url) {
+		String path = url.getPath();
+		String query = url.getQuery();
+		if( query != null )
+			path += "?" + query;
+		return path;
+	}
+
+	// retarded java api lacks this
+	public static String md5(String s) {
+		try {
+			byte[] md5 = MessageDigest.getInstance("MD5").digest(s.getBytes());
+			StringBuffer sb = new StringBuffer();
+			for( byte b : md5 ) {
+				sb.append( String.format("%02x",b) );
+			}
+			return sb.toString();
+		} catch(NoSuchAlgorithmException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/url/WwwAuthenticate.java	Fri Feb 01 03:46:56 2019 -0700
@@ -0,0 +1,56 @@
+package luan.modules.url;
+
+import java.util.Map;
+import java.util.HashMap;
+import luan.lib.parser.Parser;
+import luan.lib.parser.ParseException;
+
+
+public final class WwwAuthenticate {
+	public final String type;
+	public final Map<String,String> options = new HashMap<String,String>();
+	private final Parser parser;
+
+	public WwwAuthenticate(String header) throws ParseException {
+		parser = new Parser(header);
+		type = parseType();
+		if( !matchSpace() )
+			throw new ParseException(parser,"space expected");
+		do {
+			while( matchSpace() );
+			int start = parser.currentIndex();
+			while( parser.inCharRange('a','z') );
+			String name = parser.textFrom(start);
+			if( name.length() == 0 )
+				throw new ParseException(parser,"option name not found");
+			if( !parser.match('=') )
+				throw new ParseException(parser,"'=' expected");
+			if( !parser.match('"') )
+				throw new ParseException(parser,"'\"' expected");
+			start = parser.currentIndex();
+			while( !parser.test('"') ) {
+				if( !parser.anyChar() )
+					throw new ParseException(parser,"unexpected end of text");
+			}
+			String value = parser.textFrom(start);
+			if( !parser.match('"') )
+				throw new ParseException(parser,"'\"' expected");
+			options.put(name,value);
+			while( matchSpace() );
+		} while( parser.match(',') );
+		if( !parser.endOfInput() )
+			throw new ParseException(parser,"unexpected input");
+	}
+
+	private String parseType() throws ParseException {
+		if( parser.match("Basic") )
+			return "Basic";
+		if( parser.match("Digest") )
+			return "Digest";
+		throw new ParseException(parser,"invalid type");
+	}
+
+	private boolean matchSpace() {
+		return parser.anyOf(" \t\r\n");
+	}
+}
\ No newline at end of file