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 return new DigestResult(headerInfo.getHeader(), response.getContent(), getRealm(), user, password, algorithm, nonce, clientQOP, opaque);
103 }
104
105 private Map<String, String> parseParameters(String wwwAuthenticate)
106 {
107 Map<String, String> result = new HashMap<>();
108 List<String> parts = splitParams(wwwAuthenticate);
109 for (String part : parts)
110 {
111 Matcher matcher = PARAM_PATTERN.matcher(part);
112 if (matcher.matches())
113 {
114 String name = matcher.group(1).trim().toLowerCase(Locale.ENGLISH);
115 String value = matcher.group(2).trim();
116 if (value.startsWith("\"") && value.endsWith("\""))
117 value = value.substring(1, value.length() - 1);
118 result.put(name, value);
119 }
120 }
121 return result;
122 }
123
124 private List<String> splitParams(String paramString)
125 {
126 List<String> result = new ArrayList<>();
127 int start = 0;
128 for (int i = 0; i < paramString.length(); ++i)
129 {
130 int quotes = 0;
131 char ch = paramString.charAt(i);
132 switch (ch)
133 {
134 case '\\':
135 ++i;
136 break;
137 case '"':
138 ++quotes;
139 break;
140 case ',':
141 if (quotes % 2 == 0)
142 {
143 String element = paramString.substring(start, i).trim();
144 if (element.length() > 0)
145 result.add(element);
146 start = i + 1;
147 }
148 break;
149 default:
150 break;
151 }
152 }
153 result.add(paramString.substring(start, paramString.length()).trim());
154 return result;
155 }
156
157 private MessageDigest getMessageDigest(String algorithm)
158 {
159 try
160 {
161 return MessageDigest.getInstance(algorithm);
162 }
163 catch (NoSuchAlgorithmException x)
164 {
165 return null;
166 }
167 }
168
169 private class DigestResult implements Result
170 {
171 private final AtomicInteger nonceCount = new AtomicInteger();
172 private final HttpHeader header;
173 private final byte[] content;
174 private final String realm;
175 private final String user;
176 private final String password;
177 private final String algorithm;
178 private final String nonce;
179 private final String qop;
180 private final String opaque;
181
182 public DigestResult(HttpHeader header, byte[] content, String realm, String user, String password, String algorithm, String nonce, String qop, String opaque)
183 {
184 this.header = header;
185 this.content = content;
186 this.realm = realm;
187 this.user = user;
188 this.password = password;
189 this.algorithm = algorithm;
190 this.nonce = nonce;
191 this.qop = qop;
192 this.opaque = opaque;
193 }
194
195 @Override
196 public URI getURI()
197 {
198 return DigestAuthentication.this.getURI();
199 }
200
201 @Override
202 public void apply(Request request)
203 {
204 MessageDigest digester = getMessageDigest(algorithm);
205 if (digester == null)
206 return;
207
208 String A1 = user + ":" + realm + ":" + password;
209 String hashA1 = toHexString(digester.digest(A1.getBytes(StandardCharsets.ISO_8859_1)));
210
211 URI uri = request.getURI();
212 String A2 = request.getMethod() + ":" + uri;
213 if ("auth-int".equals(qop))
214 A2 += ":" + toHexString(digester.digest(content));
215 String hashA2 = toHexString(digester.digest(A2.getBytes(StandardCharsets.ISO_8859_1)));
216
217 String nonceCount;
218 String clientNonce;
219 String A3;
220 if (qop != null)
221 {
222 nonceCount = nextNonceCount();
223 clientNonce = newClientNonce();
224 A3 = hashA1 + ":" + nonce + ":" + nonceCount + ":" + clientNonce + ":" + qop + ":" + hashA2;
225 }
226 else
227 {
228 nonceCount = null;
229 clientNonce = null;
230 A3 = hashA1 + ":" + nonce + ":" + hashA2;
231 }
232 String hashA3 = toHexString(digester.digest(A3.getBytes(StandardCharsets.ISO_8859_1)));
233
234 StringBuilder value = new StringBuilder("Digest");
235 value.append(" username=\"").append(user).append("\"");
236 value.append(", realm=\"").append(realm).append("\"");
237 value.append(", nonce=\"").append(nonce).append("\"");
238 if (opaque != null)
239 value.append(", opaque=\"").append(opaque).append("\"");
240 value.append(", algorithm=\"").append(algorithm).append("\"");
241 value.append(", uri=\"").append(uri).append("\"");
242 if (qop != null)
243 {
244 value.append(", qop=\"").append(qop).append("\"");
245 value.append(", nc=\"").append(nonceCount).append("\"");
246 value.append(", cnonce=\"").append(clientNonce).append("\"");
247 }
248 value.append(", response=\"").append(hashA3).append("\"");
249
250 request.header(header, value.toString());
251 }
252
253 private String nextNonceCount()
254 {
255 String padding = "00000000";
256 String next = Integer.toHexString(nonceCount.incrementAndGet()).toLowerCase(Locale.ENGLISH);
257 return padding.substring(0, padding.length() - next.length()) + next;
258 }
259
260 private String newClientNonce()
261 {
262 Random random = new Random();
263 byte[] bytes = new byte[8];
264 random.nextBytes(bytes);
265 return toHexString(bytes);
266 }
267
268 private String toHexString(byte[] bytes)
269 {
270 return TypeUtil.toHexString(bytes).toLowerCase(Locale.ENGLISH);
271 }
272 }
273 }