1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.jetty.client.util;
20
21 import java.net.URI;
22 import java.nio.charset.StandardCharsets;
23 import java.security.MessageDigest;
24 import java.security.NoSuchAlgorithmException;
25 import java.util.ArrayList;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Locale;
29 import java.util.Map;
30 import java.util.Random;
31 import java.util.concurrent.atomic.AtomicInteger;
32 import java.util.regex.Matcher;
33 import java.util.regex.Pattern;
34
35 import org.eclipse.jetty.client.HttpClient;
36 import org.eclipse.jetty.client.api.AuthenticationStore;
37 import org.eclipse.jetty.client.api.ContentResponse;
38 import org.eclipse.jetty.client.api.Request;
39 import org.eclipse.jetty.http.HttpHeader;
40 import org.eclipse.jetty.util.Attributes;
41 import org.eclipse.jetty.util.StringUtil;
42 import org.eclipse.jetty.util.TypeUtil;
43
44
45
46
47
48
49
50
51 public class DigestAuthentication extends AbstractAuthentication
52 {
53 private static final Pattern PARAM_PATTERN = Pattern.compile("([^=]+)=(.*)");
54
55 private final String user;
56 private final String password;
57
58
59
60
61
62
63
64 public DigestAuthentication(URI uri, String realm, String user, String password)
65 {
66 super(uri, realm);
67 this.user = user;
68 this.password = password;
69 }
70
71 @Override
72 public String getType()
73 {
74 return "Digest";
75 }
76
77 @Override
78 public Result authenticate(Request request, ContentResponse response, HeaderInfo headerInfo, Attributes context)
79 {
80 Map<String, String> params = parseParameters(headerInfo.getParameters());
81 String nonce = params.get("nonce");
82 if (nonce == null || nonce.length() == 0)
83 return null;
84 String opaque = params.get("opaque");
85 String algorithm = params.get("algorithm");
86 if (algorithm == null)
87 algorithm = "MD5";
88 MessageDigest digester = getMessageDigest(algorithm);
89 if (digester == null)
90 return null;
91 String serverQOP = params.get("qop");
92 String clientQOP = null;
93 if (serverQOP != null)
94 {
95 List<String> serverQOPValues = StringUtil.csvSplit(null,serverQOP,0,serverQOP.length());
96 if (serverQOPValues.contains("auth"))
97 clientQOP = "auth";
98 else if (serverQOPValues.contains("auth-int"))
99 clientQOP = "auth-int";
100 }
101
102 String realm = getRealm();
103 if (ANY_REALM.equals(realm))
104 realm = headerInfo.getRealm();
105 return new DigestResult(headerInfo.getHeader(), response.getContent(), realm, user, password, algorithm, nonce, clientQOP, opaque);
106 }
107
108 private Map<String, String> parseParameters(String wwwAuthenticate)
109 {
110 Map<String, String> result = new HashMap<>();
111 List<String> parts = splitParams(wwwAuthenticate);
112 for (String part : parts)
113 {
114 Matcher matcher = PARAM_PATTERN.matcher(part);
115 if (matcher.matches())
116 {
117 String name = matcher.group(1).trim().toLowerCase(Locale.ENGLISH);
118 String value = matcher.group(2).trim();
119 if (value.startsWith("\"") && value.endsWith("\""))
120 value = value.substring(1, value.length() - 1);
121 result.put(name, value);
122 }
123 }
124 return result;
125 }
126
127 private List<String> splitParams(String paramString)
128 {
129 List<String> result = new ArrayList<>();
130 int start = 0;
131 for (int i = 0; i < paramString.length(); ++i)
132 {
133 int quotes = 0;
134 char ch = paramString.charAt(i);
135 switch (ch)
136 {
137 case '\\':
138 ++i;
139 break;
140 case '"':
141 ++quotes;
142 break;
143 case ',':
144 if (quotes % 2 == 0)
145 {
146 String element = paramString.substring(start, i).trim();
147 if (element.length() > 0)
148 result.add(element);
149 start = i + 1;
150 }
151 break;
152 default:
153 break;
154 }
155 }
156 result.add(paramString.substring(start, paramString.length()).trim());
157 return result;
158 }
159
160 private MessageDigest getMessageDigest(String algorithm)
161 {
162 try
163 {
164 return MessageDigest.getInstance(algorithm);
165 }
166 catch (NoSuchAlgorithmException x)
167 {
168 return null;
169 }
170 }
171
172 private class DigestResult implements Result
173 {
174 private final AtomicInteger nonceCount = new AtomicInteger();
175 private final HttpHeader header;
176 private final byte[] content;
177 private final String realm;
178 private final String user;
179 private final String password;
180 private final String algorithm;
181 private final String nonce;
182 private final String qop;
183 private final String opaque;
184
185 public DigestResult(HttpHeader header, byte[] content, String realm, String user, String password, String algorithm, String nonce, String qop, String opaque)
186 {
187 this.header = header;
188 this.content = content;
189 this.realm = realm;
190 this.user = user;
191 this.password = password;
192 this.algorithm = algorithm;
193 this.nonce = nonce;
194 this.qop = qop;
195 this.opaque = opaque;
196 }
197
198 @Override
199 public URI getURI()
200 {
201 return DigestAuthentication.this.getURI();
202 }
203
204 @Override
205 public void apply(Request request)
206 {
207 MessageDigest digester = getMessageDigest(algorithm);
208 if (digester == null)
209 return;
210
211 String A1 = user + ":" + realm + ":" + password;
212 String hashA1 = toHexString(digester.digest(A1.getBytes(StandardCharsets.ISO_8859_1)));
213
214 URI uri = request.getURI();
215 String A2 = request.getMethod() + ":" + uri;
216 if ("auth-int".equals(qop))
217 A2 += ":" + toHexString(digester.digest(content));
218 String hashA2 = toHexString(digester.digest(A2.getBytes(StandardCharsets.ISO_8859_1)));
219
220 String nonceCount;
221 String clientNonce;
222 String A3;
223 if (qop != null)
224 {
225 nonceCount = nextNonceCount();
226 clientNonce = newClientNonce();
227 A3 = hashA1 + ":" + nonce + ":" + nonceCount + ":" + clientNonce + ":" + qop + ":" + hashA2;
228 }
229 else
230 {
231 nonceCount = null;
232 clientNonce = null;
233 A3 = hashA1 + ":" + nonce + ":" + hashA2;
234 }
235 String hashA3 = toHexString(digester.digest(A3.getBytes(StandardCharsets.ISO_8859_1)));
236
237 StringBuilder value = new StringBuilder("Digest");
238 value.append(" username=\"").append(user).append("\"");
239 value.append(", realm=\"").append(realm).append("\"");
240 value.append(", nonce=\"").append(nonce).append("\"");
241 if (opaque != null)
242 value.append(", opaque=\"").append(opaque).append("\"");
243 value.append(", algorithm=\"").append(algorithm).append("\"");
244 value.append(", uri=\"").append(uri).append("\"");
245 if (qop != null)
246 {
247 value.append(", qop=\"").append(qop).append("\"");
248 value.append(", nc=\"").append(nonceCount).append("\"");
249 value.append(", cnonce=\"").append(clientNonce).append("\"");
250 }
251 value.append(", response=\"").append(hashA3).append("\"");
252
253 request.header(header, value.toString());
254 }
255
256 private String nextNonceCount()
257 {
258 String padding = "00000000";
259 String next = Integer.toHexString(nonceCount.incrementAndGet()).toLowerCase(Locale.ENGLISH);
260 return padding.substring(0, padding.length() - next.length()) + next;
261 }
262
263 private String newClientNonce()
264 {
265 Random random = new Random();
266 byte[] bytes = new byte[8];
267 random.nextBytes(bytes);
268 return toHexString(bytes);
269 }
270
271 private String toHexString(byte[] bytes)
272 {
273 return TypeUtil.toHexString(bytes).toLowerCase(Locale.ENGLISH);
274 }
275 }
276 }