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