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