Skip to content

Commit beb6b1a

Browse files
committed
Digest auth isn't working, close AsyncHttpClient#847
1 parent ae78f83 commit beb6b1a

File tree

3 files changed

+136
-80
lines changed

3 files changed

+136
-80
lines changed

src/main/java/com/ning/http/client/Realm.java

Lines changed: 115 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
package com.ning.http.client;
1818

1919
import static com.ning.http.util.MiscUtils.isNonEmpty;
20-
import static java.nio.charset.StandardCharsets.ISO_8859_1;
21-
import static java.nio.charset.StandardCharsets.UTF_8;
20+
import static java.nio.charset.StandardCharsets.*;
2221

2322
import com.ning.http.client.uri.Uri;
23+
import com.ning.http.util.AuthenticatorUtils;
2424
import com.ning.http.util.StringUtils;
2525

2626
import java.nio.charset.Charset;
@@ -33,6 +33,7 @@
3333
*/
3434
public class Realm {
3535

36+
private static final String EMPTY_ENTITY_MD5 = "d41d8cd98f00b204e9800998ecf8427e";
3637
private static final String DEFAULT_NC = "00000001";
3738

3839
private final String principal;
@@ -274,24 +275,24 @@ public static class RealmBuilder {
274275
// This code is already Apache licenced.
275276
//
276277

277-
private String principal = "";
278-
private String password = "";
278+
private String principal;
279+
private String password;
279280
private AuthScheme scheme = AuthScheme.NONE;
280-
private String realmName = "";
281-
private String nonce = "";
282-
private String algorithm = "MD5";
283-
private String response = "";
284-
private String opaque = "";
285-
private String qop = "auth";
281+
private String realmName;
282+
private String nonce;
283+
private String algorithm;
284+
private String response;
285+
private String opaque;
286+
private String qop;
286287
private String nc = DEFAULT_NC;
287-
private String cnonce = "";
288+
private String cnonce;
288289
private Uri uri;
289290
private String methodName = "GET";
290291
private boolean usePreemptive = false;
291292
private String ntlmDomain = System.getProperty("http.auth.ntlm.domain", "");
292293
private Charset charset = UTF_8;
293294
private String ntlmHost = "localhost";
294-
private boolean useAbsoluteURI = true;
295+
private boolean useAbsoluteURI = false;
295296
private boolean omitQuery = false;
296297
private boolean targetProxy = false;
297298

@@ -402,7 +403,9 @@ public String getQop() {
402403
}
403404

404405
public RealmBuilder setQop(String qop) {
405-
this.qop = qop;
406+
if (isNonEmpty(qop)) {
407+
this.qop = qop;
408+
}
406409
return this;
407410
}
408411

@@ -469,6 +472,27 @@ public RealmBuilder setTargetProxy(boolean targetProxy) {
469472
return this;
470473
}
471474

475+
private String parseRawQop(String rawQop) {
476+
String[] rawServerSupportedQops = rawQop.split(",");
477+
String[] serverSupportedQops = new String[rawServerSupportedQops.length];
478+
for (int i = 0; i < rawServerSupportedQops.length; i++) {
479+
serverSupportedQops[i] = rawServerSupportedQops[i].trim();
480+
}
481+
482+
// prefer auth over auth-int
483+
for (String rawServerSupportedQop: serverSupportedQops) {
484+
if (rawServerSupportedQop.equals("auth"))
485+
return rawServerSupportedQop;
486+
}
487+
488+
for (String rawServerSupportedQop: serverSupportedQops) {
489+
if (rawServerSupportedQop.equals("auth-int"))
490+
return rawServerSupportedQop;
491+
}
492+
493+
return null;
494+
}
495+
472496
public RealmBuilder parseWWWAuthenticateHeader(String headerLine) {
473497
setRealmName(match(headerLine, "realm"));
474498
setNonce(match(headerLine, "nonce"));
@@ -477,7 +501,12 @@ public RealmBuilder parseWWWAuthenticateHeader(String headerLine) {
477501
setAlgorithm(algorithm);
478502
}
479503
setOpaque(match(headerLine, "opaque"));
480-
setQop(match(headerLine, "qop"));
504+
505+
String rawQop = match(headerLine, "qop");
506+
if (rawQop != null) {
507+
setQop(parseRawQop(rawQop));
508+
}
509+
481510
if (isNonEmpty(getNonce())) {
482511
setScheme(AuthScheme.DIGEST);
483512
} else {
@@ -490,6 +519,7 @@ public RealmBuilder parseProxyAuthenticateHeader(String headerLine) {
490519
setRealmName(match(headerLine, "realm"));
491520
setNonce(match(headerLine, "nonce"));
492521
setOpaque(match(headerLine, "opaque"));
522+
// FIXME what about algorithm and opaque?
493523
setQop(match(headerLine, "qop"));
494524
if (isNonEmpty(getNonce())) {
495525
setScheme(AuthScheme.DIGEST);
@@ -501,25 +531,24 @@ public RealmBuilder parseProxyAuthenticateHeader(String headerLine) {
501531
}
502532

503533
public RealmBuilder clone(Realm clone) {
504-
setRealmName(clone.getRealmName());
505-
setAlgorithm(clone.getAlgorithm());
506-
setMethodName(clone.getMethodName());
507-
setNc(clone.getNc());
508-
setNonce(clone.getNonce());
509-
setPassword(clone.getPassword());
510-
setPrincipal(clone.getPrincipal());
511-
setCharset(clone.getCharset());
512-
setOpaque(clone.getOpaque());
513-
setQop(clone.getQop());
514-
setScheme(clone.getScheme());
515-
setUri(clone.getUri());
516-
setUsePreemptiveAuth(clone.getUsePreemptiveAuth());
517-
setNtlmDomain(clone.getNtlmDomain());
518-
setNtlmHost(clone.getNtlmHost());
519-
setUseAbsoluteURI(clone.isUseAbsoluteURI());
520-
setOmitQuery(clone.isOmitQuery());
521-
setTargetProxy(clone.isTargetProxy());
522-
return this;
534+
return setRealmName(clone.getRealmName())//
535+
.setAlgorithm(clone.getAlgorithm())//
536+
.setMethodName(clone.getMethodName())//
537+
.setNc(clone.getNc())//
538+
.setNonce(clone.getNonce())//
539+
.setPassword(clone.getPassword())//
540+
.setPrincipal(clone.getPrincipal())//
541+
.setCharset(clone.getCharset())//
542+
.setOpaque(clone.getOpaque())//
543+
.setQop(clone.getQop())//
544+
.setScheme(clone.getScheme())//
545+
.setUri(clone.getUri())//
546+
.setUsePreemptiveAuth(clone.getUsePreemptiveAuth())//
547+
.setNtlmDomain(clone.getNtlmDomain())//
548+
.setNtlmHost(clone.getNtlmHost())//
549+
.setUseAbsoluteURI(clone.isUseAbsoluteURI())//
550+
.setOmitQuery(clone.isOmitQuery())//
551+
.setTargetProxy(clone.isTargetProxy());
523552
}
524553

525554
private void newCnonce(MessageDigest md) {
@@ -534,11 +563,11 @@ private void newCnonce(MessageDigest md) {
534563
*/
535564
private String match(String headerLine, String token) {
536565
if (headerLine == null) {
537-
return "";
566+
return null;
538567
}
539568

540569
int match = headerLine.indexOf(token);
541-
if (match <= 0) return "";
570+
if (match <= 0) return null;
542571

543572
// = to skip
544573
match += token.length() + 1;
@@ -557,46 +586,64 @@ public RealmBuilder setCharset(Charset charset) {
557586
return this;
558587
}
559588

560-
private void newResponse(MessageDigest md) {
561-
// BEWARE: compute first as it used the cached StringBuilder
562-
String url = uri.toUrl();
563-
564-
StringBuilder sb = StringUtils.stringBuilder();
565-
sb.append(principal)
566-
.append(":")
567-
.append(realmName)
568-
.append(":")
569-
.append(password);
589+
private byte[] md5FromRecycledStringBuilder(StringBuilder sb, MessageDigest md) {
570590
md.update(StringUtils.charSequence2ByteBuffer(sb, ISO_8859_1));
571591
sb.setLength(0);
572-
byte[] ha1 = md.digest();
592+
return md.digest();
593+
}
594+
595+
private byte[] secretDigest(StringBuilder sb, MessageDigest md) {
573596

574-
sb.append(methodName)
575-
.append(':')
576-
.append(url);
597+
sb.append(principal).append(':').append(realmName).append(':').append(password);
598+
byte[] ha1 = md5FromRecycledStringBuilder(sb, md);
599+
600+
if (algorithm == null || algorithm.equals("MD5")) {
601+
return ha1;
602+
} else if ("MD5-sess".equals(algorithm)) {
603+
appendBase16(sb, ha1);
604+
sb.append(':').append(nonce).append(':').append(cnonce);
605+
return md5FromRecycledStringBuilder(sb, md);
606+
}
607+
608+
throw new UnsupportedOperationException("Digest algorithm not supported: " + algorithm);
609+
}
610+
611+
private byte[] dataDigest(StringBuilder sb, String digestUri, MessageDigest md) {
577612

578-
md.update(StringUtils.charSequence2ByteBuffer(sb, ISO_8859_1));
579-
sb.setLength(0);
580-
byte[] ha2 = md.digest();
613+
sb.append(methodName).append(':').append(digestUri);
614+
if ("auth-int".equals(qop)) {
615+
sb.append(':').append(EMPTY_ENTITY_MD5);
581616

582-
appendBase16(sb, ha1);
583-
sb.append(':').append(nonce).append(':');
617+
} else if (qop != null && !qop.equals("auth")) {
618+
throw new UnsupportedOperationException("Digest qop not supported: " + qop);
619+
}
584620

585-
if (isNonEmpty(qop)) {
586-
//qop ="auth" or "auth-int"
587-
sb.append(nc)//
588-
.append(':')//
589-
.append(cnonce)//
590-
.append(':')//
591-
.append(qop)//
592-
.append(':');
621+
return md5FromRecycledStringBuilder(sb, md);
622+
}
623+
624+
private void appendDataBase(StringBuilder sb) {
625+
sb.append(':').append(nonce).append(':');
626+
if ("auth".equals(qop) || "auth-int".equals(qop)) {
627+
sb.append(nc).append(':').append(cnonce).append(':').append(qop).append(':');
593628
}
594-
595-
appendBase16(sb, ha2);
596-
md.update(StringUtils.charSequence2ByteBuffer(sb, ISO_8859_1));
597-
sb.setLength(0);
598-
byte[] digest = md.digest();
599-
response = toHexString(digest);
629+
}
630+
631+
private void newResponse(MessageDigest md) {
632+
// BEWARE: compute first as it used the cached StringBuilder
633+
String digestUri = AuthenticatorUtils.computeRealmURI(uri, useAbsoluteURI, omitQuery);
634+
635+
StringBuilder sb = StringUtils.stringBuilder();
636+
637+
// WARNING: DON'T MOVE, BUFFER IS RECYCLED!!!!
638+
byte[] secretDigest = secretDigest(sb, md);
639+
byte[] dataDigest = dataDigest(sb, digestUri, md);
640+
641+
appendBase16(sb, secretDigest);
642+
appendDataBase(sb);
643+
appendBase16(sb, dataDigest);
644+
645+
byte[] responseDigest = md5FromRecycledStringBuilder(sb, md);
646+
response = toHexString(responseDigest);
600647
}
601648

602649
private static String toHexString(byte[] data) {

src/main/java/com/ning/http/util/AuthenticatorUtils.java

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,16 @@ private static String computeBasicAuthentication(String principal, String passwo
3737
return "Basic " + Base64.encode(s.getBytes(charset));
3838
}
3939

40-
private static String computeRealmURI(Realm realm) {
41-
Uri uri = realm.getUri();
42-
if (realm.isUseAbsoluteURI()) {
43-
return realm.isOmitQuery() && MiscUtils.isNonEmpty(uri.getQuery()) ? uri.withNewQuery(null).toUrl() : uri.toUrl();
40+
public static String computeRealmURI(Realm realm) {
41+
return computeRealmURI(realm.getUri(), realm.isUseAbsoluteURI(), realm.isOmitQuery());
42+
}
43+
44+
public static String computeRealmURI(Uri uri, boolean useAbsoluteURI, boolean omitQuery) {
45+
if (useAbsoluteURI) {
46+
return omitQuery && MiscUtils.isNonEmpty(uri.getQuery()) ? uri.withNewQuery(null).toUrl() : uri.toUrl();
4447
} else {
4548
String path = getNonEmptyPath(uri);
46-
return realm.isOmitQuery() || !MiscUtils.isNonEmpty(uri.getQuery()) ? path : path + "?" + uri.getQuery();
49+
return omitQuery || !MiscUtils.isNonEmpty(uri.getQuery()) ? path : path + "?" + uri.getQuery();
4750
}
4851
}
4952

@@ -54,14 +57,20 @@ public static String computeDigestAuthentication(Realm realm) {
5457
append(builder, "realm", realm.getRealmName(), true);
5558
append(builder, "nonce", realm.getNonce(), true);
5659
append(builder, "uri", computeRealmURI(realm), true);
57-
append(builder, "algorithm", realm.getAlgorithm(), false);
60+
if (isNonEmpty(realm.getAlgorithm()))
61+
append(builder, "algorithm", realm.getAlgorithm(), false);
5862

5963
append(builder, "response", realm.getResponse(), true);
60-
if (isNonEmpty(realm.getOpaque()))
64+
65+
if (realm.getOpaque() != null)
6166
append(builder, "opaque", realm.getOpaque(), true);
62-
append(builder, "qop", realm.getQop(), false);
63-
append(builder, "nc", realm.getNc(), false);
64-
append(builder, "cnonce", realm.getCnonce(), true);
67+
68+
if (realm.getQop() != null) {
69+
append(builder, "qop", realm.getQop(), false);
70+
// nc and cnonce only sent if server sent qop
71+
append(builder, "nc", realm.getNc(), false);
72+
append(builder, "cnonce", realm.getCnonce(), true);
73+
}
6574
builder.setLength(builder.length() - 2); // remove tailing ", "
6675

6776
// FIXME isn't there a more efficient way?

src/test/java/com/ning/http/client/RealmTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ private void testOldDigest(String qop) {
7575
Realm orig = builder.build();
7676

7777
String ha1 = getMd5(user + ":" + realm + ":" + pass);
78-
String ha2 = getMd5(method + ":" + uri);
78+
String ha2 = getMd5(method + ":" + uri.getPath());
7979
String expectedResponse = getMd5(ha1 + ":" + nonce + ":" + ha2);
8080

8181
Assert.assertEquals(expectedResponse, orig.getResponse());
@@ -103,7 +103,7 @@ public void testStrongDigest() {
103103
String nc = orig.getNc();
104104
String cnonce = orig.getCnonce();
105105
String ha1 = getMd5(user + ":" + realm + ":" + pass);
106-
String ha2 = getMd5(method + ":" + uri);
106+
String ha2 = getMd5(method + ":" + uri.getPath());
107107
String expectedResponse = getMd5(ha1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + ha2);
108108

109109
Assert.assertEquals(expectedResponse, orig.getResponse());

0 commit comments

Comments
 (0)