Mercurial Hosting > luan
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