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