comparison src/luan/modules/url/LuanUrl.java @ 1317:c286c1e36b81

add client digest authentication
author Franklin Schmidt <fschmidt@gmail.com>
date Fri, 01 Feb 2019 03:46:56 -0700
parents 5763597ca5c0
children 35a6a195819f
comparison
equal deleted inserted replaced
1316:11d3640e739d 1317:c286c1e36b81
9 import java.io.UnsupportedEncodingException; 9 import java.io.UnsupportedEncodingException;
10 import java.net.URL; 10 import java.net.URL;
11 import java.net.URLConnection; 11 import java.net.URLConnection;
12 import java.net.HttpURLConnection; 12 import java.net.HttpURLConnection;
13 import java.net.URLEncoder; 13 import java.net.URLEncoder;
14 import java.security.MessageDigest;
15 import java.security.NoSuchAlgorithmException;
14 import java.util.Map; 16 import java.util.Map;
15 import java.util.HashMap; 17 import java.util.HashMap;
16 import java.util.List; 18 import java.util.List;
17 import java.util.Base64; 19 import java.util.Base64;
20 import luan.lib.parser.ParseException;
18 import luan.Luan; 21 import luan.Luan;
19 import luan.LuanState; 22 import luan.LuanState;
20 import luan.LuanTable; 23 import luan.LuanTable;
21 import luan.LuanJavaFunction; 24 import luan.LuanJavaFunction;
22 import luan.LuanException; 25 import luan.LuanException;
28 31
29 private static enum Method { GET, POST, DELETE } 32 private static enum Method { GET, POST, DELETE }
30 33
31 private URL url; 34 private URL url;
32 private Method method = Method.GET; 35 private Method method = Method.GET;
33 private Map headers; 36 private final Map<String,Object> headers = new HashMap<String,Object>();
34 private String content = ""; 37 private String content = "";
35 private MultipartClient multipart = null; 38 private MultipartClient multipart = null;
36 private int timeout = 0; 39 private int timeout = 0;
40 private String authUser = null;
41 private String authPassword = null;
37 42
38 public LuanUrl(URL url,LuanTable options) throws LuanException { 43 public LuanUrl(URL url,LuanTable options) throws LuanException {
39 this.url = url; 44 this.url = url;
40 if( options != null ) { 45 if( options != null ) {
41 Map map = options.asMap(); 46 Map map = options.asMap();
48 throw new LuanException( "invalid method: "+methodStr ); 53 throw new LuanException( "invalid method: "+methodStr );
49 } 54 }
50 } 55 }
51 Map headerMap = getMap(map,"headers"); 56 Map headerMap = getMap(map,"headers");
52 if( headerMap != null ) { 57 if( headerMap != null ) {
53 headers = new HashMap();
54 for( Object hack : headerMap.entrySet() ) { 58 for( Object hack : headerMap.entrySet() ) {
55 Map.Entry entry = (Map.Entry)hack; 59 Map.Entry entry = (Map.Entry)hack;
56 String name = (String)entry.getKey(); 60 String name = (String)entry.getKey();
57 Object val = entry.getValue(); 61 Object val = entry.getValue();
58 if( val instanceof String ) { 62 if( val instanceof String ) {
70 Map auth = getMap(map,"authorization"); 74 Map auth = getMap(map,"authorization");
71 if( auth != null ) { 75 if( auth != null ) {
72 if( headers!=null && headers.containsKey("authorization") ) 76 if( headers!=null && headers.containsKey("authorization") )
73 throw new LuanException( "can't define authorization with header 'authorization' defined" ); 77 throw new LuanException( "can't define authorization with header 'authorization' defined" );
74 String user = getString(auth,"user"); 78 String user = getString(auth,"user");
79 if( user==null ) user = "";
75 String password = getString(auth,"password"); 80 String password = getString(auth,"password");
81 if( password==null ) password = "";
82 String type = getString(auth,"type");
76 if( !auth.isEmpty() ) 83 if( !auth.isEmpty() )
77 throw new LuanException( "unrecognized authorization options: "+auth ); 84 throw new LuanException( "unrecognized authorization options: "+auth );
78 StringBuilder sb = new StringBuilder(); 85 if( type != null ) {
79 if( user != null ) 86 if( !type.toLowerCase().equals("basic") )
80 sb.append(user); 87 throw new LuanException( "authorization type can only be 'basic' or nil" );
81 sb.append(':'); 88 String val = basicAuth(user,password);
82 if( password != null ) 89 headers.put("authorization",val);
83 sb.append(password); 90 } else {
84 String val = "Basic " + Base64.getEncoder().encodeToString(sb.toString().getBytes()); 91 authUser = user;
85 if( headers == null ) 92 authPassword = password;
86 headers = new HashMap(); 93 }
87 headers.put("authorization",val);
88 } 94 }
89 Map params = getMap(map,"parameters"); 95 Map params = getMap(map,"parameters");
90 String enctype = getString(map,"enctype"); 96 String enctype = getString(map,"enctype");
91 if( enctype != null ) { 97 if( enctype != null ) {
92 if( !enctype.equals("multipart/form-data") ) 98 if( !enctype.equals("multipart/form-data") )
189 LuanTable t = getTable(map,key); 195 LuanTable t = getTable(map,key);
190 return t==null ? null : t.asMap(); 196 return t==null ? null : t.asMap();
191 } 197 }
192 198
193 @Override public InputStream inputStream(LuanState luan) throws IOException, LuanException { 199 @Override public InputStream inputStream(LuanState luan) throws IOException, LuanException {
200 try {
201 return inputStream(luan,null);
202 } catch(AuthException e) {
203 try {
204 return inputStream(luan,e.authorization);
205 } catch(AuthException e2) {
206 throw new RuntimeException(e2); // never
207 }
208 }
209 }
210
211 private InputStream inputStream(LuanState luan,String authorization)
212 throws IOException, LuanException, AuthException
213 {
194 URLConnection con = url.openConnection(); 214 URLConnection con = url.openConnection();
195 if( timeout != 0 ) { 215 if( timeout != 0 ) {
196 con.setConnectTimeout(timeout); 216 con.setConnectTimeout(timeout);
197 con.setReadTimeout(timeout); 217 con.setReadTimeout(timeout);
198 } 218 }
199 if( headers != null ) { 219 for( Map.Entry<String,Object> entry : headers.entrySet() ) {
200 for( Object hack : headers.entrySet() ) { 220 String key = entry.getKey();
201 Map.Entry entry = (Map.Entry)hack; 221 Object val = entry.getValue();
202 String key = (String)entry.getKey(); 222 if( val instanceof String ) {
203 Object val = entry.getValue(); 223 con.addRequestProperty(key,(String)val);
204 if( val instanceof String ) { 224 } else {
205 con.addRequestProperty(key,(String)val); 225 List list = (List)val;
206 } else { 226 for( Object obj : list ) {
207 List list = (List)val; 227 con.addRequestProperty(key,(String)obj);
208 for( Object obj : list ) { 228 }
209 con.addRequestProperty(key,(String)obj); 229 }
210 } 230 }
211 } 231 if( authorization != null )
212 } 232 con.addRequestProperty("Authorization",authorization);
213 }
214 if( !(con instanceof HttpURLConnection) ) { 233 if( !(con instanceof HttpURLConnection) ) {
215 if( method!=Method.GET ) 234 if( method!=Method.GET )
216 throw new LuanException("method must be GET but is "+method); 235 throw new LuanException("method must be GET but is "+method);
217 return con.getInputStream(); 236 return con.getInputStream();
218 } 237 }
219 238
220 HttpURLConnection httpCon = (HttpURLConnection)con; 239 HttpURLConnection httpCon = (HttpURLConnection)con;
221 240
222 if( method==Method.GET ) { 241 if( method==Method.GET ) {
223 return getInputStream(luan,httpCon); 242 return getInputStream(luan,httpCon,authorization);
224 } 243 }
225 244
226 if( method==Method.DELETE ) { 245 if( method==Method.DELETE ) {
227 httpCon.setRequestMethod("DELETE"); 246 httpCon.setRequestMethod("DELETE");
228 return getInputStream(luan,httpCon); 247 return getInputStream(luan,httpCon,authorization);
229 } 248 }
230 249
231 // POST 250 // POST
232 251
233 // httpCon.setRequestProperty("content-type","application/x-www-form-urlencoded"); 252 // httpCon.setRequestProperty("content-type","application/x-www-form-urlencoded");
243 out = httpCon.getOutputStream(); 262 out = httpCon.getOutputStream();
244 out.write(post); 263 out.write(post);
245 } 264 }
246 out.flush(); 265 out.flush();
247 try { 266 try {
248 return getInputStream(luan,httpCon); 267 return getInputStream(luan,httpCon,authorization);
249 } finally { 268 } finally {
250 out.close(); 269 out.close();
251 } 270 }
252 } 271 }
253 272
254 private static InputStream getInputStream(LuanState luan,HttpURLConnection httpCon) throws IOException, LuanException { 273 private InputStream getInputStream(LuanState luan,HttpURLConnection httpCon,String authorization)
274 throws IOException, LuanException, AuthException
275 {
255 try { 276 try {
256 return httpCon.getInputStream(); 277 return httpCon.getInputStream();
257 } catch(FileNotFoundException e) { 278 } catch(FileNotFoundException e) {
258 throw e; 279 throw e;
259 } catch(IOException e) { 280 } catch(IOException e) {
260 int responseCode = httpCon.getResponseCode(); 281 int responseCode = httpCon.getResponseCode();
282 if( responseCode == 401 && authUser != null && authorization==null ) {
283 String authStr = httpCon.getHeaderField("www-authenticate");
284 //System.out.println("auth = "+authStr);
285 try {
286 WwwAuthenticate auth = new WwwAuthenticate(authStr);
287 if( auth.type.equals("Basic") ) {
288 String val = basicAuth(authUser,authPassword);
289 throw new AuthException(val);
290 } else if( auth.type.equals("Digest") ) {
291 String realm = auth.options.get("realm");
292 if(realm==null) throw new RuntimeException("missing realm");
293 String algorithm = auth.options.get("algorithm");
294 if( algorithm!=null && !algorithm.equals("MD5") )
295 throw new LuanException("unsupported digest algorithm: "+algorithm);
296 String qop = auth.options.get("qop");
297 if( qop!=null && !qop.equals("auth") )
298 throw new LuanException("unsupported digest qop: "+qop);
299 String nonce = auth.options.get("nonce");
300 if(nonce==null) throw new RuntimeException("missing nonce");
301 String uri = fullPath(url);
302 String a1 = authUser + ':' + realm + ':' + authPassword;
303 String a2 = "" + method + ':' + uri;
304 String nc = "00000001";
305 String cnonce = "7761faf2daa45b3b"; // who cares?
306 String response = md5(a1) + ':' + nonce;
307 if( qop != null ) {
308 response += ':' + nc + ':' + cnonce + ':' + qop;
309 }
310 response += ':' + md5(a2);
311 response = md5(response);
312 String val = "Digest";
313 val += " username=\"" + authUser + "\"";
314 val += ", realm=\"" + realm + "\"";
315 val += ", uri=\"" + uri + "\"";
316 val += ", nonce=\"" + nonce + "\"";
317 val += ", response=\"" + response + "\"";
318 if( qop != null ) {
319 val += ", qop=" + qop;
320 val += ", nc=" + nc;
321 val += ", cnonce=\"" + cnonce + "\"";
322 }
323 //System.out.println("val = "+val);
324 throw new AuthException(val);
325 } else
326 throw new RuntimeException(auth.type);
327 } catch(ParseException pe) {
328 throw new LuanException(pe);
329 }
330 }
261 String responseMessage = httpCon.getResponseMessage(); 331 String responseMessage = httpCon.getResponseMessage();
262 InputStream is = httpCon.getErrorStream(); 332 InputStream is = httpCon.getErrorStream();
263 if( is == null ) 333 if( is == null )
264 throw e; 334 throw e;
265 Reader in = new InputStreamReader(is); 335 Reader in = new InputStreamReader(is);
279 349
280 @Override public String to_uri_string() { 350 @Override public String to_uri_string() {
281 return url.toString(); 351 return url.toString();
282 } 352 }
283 353
354 private static String basicAuth(String user,String password) {
355 String s = user + ':' + password;
356 return "Basic " + Base64.getEncoder().encodeToString(s.getBytes());
357 }
358
359 private final class AuthException extends Exception {
360 final String authorization;
361
362 AuthException(String authorization) {
363 this.authorization = authorization;
364 }
365 }
366
367 // retarded java api lacks this
368 public static String fullPath(URL url) {
369 String path = url.getPath();
370 String query = url.getQuery();
371 if( query != null )
372 path += "?" + query;
373 return path;
374 }
375
376 // retarded java api lacks this
377 public static String md5(String s) {
378 try {
379 byte[] md5 = MessageDigest.getInstance("MD5").digest(s.getBytes());
380 StringBuffer sb = new StringBuffer();
381 for( byte b : md5 ) {
382 sb.append( String.format("%02x",b) );
383 }
384 return sb.toString();
385 } catch(NoSuchAlgorithmException e) {
386 throw new RuntimeException(e);
387 }
388 }
389
284 } 390 }