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.Charset;
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, String wwwAuthenticate, Attributes context)
89 {
90
91 String type = "igest";
92 wwwAuthenticate = wwwAuthenticate.substring(wwwAuthenticate.indexOf(type) + type.length());
93
94 Map<String, String> params = parseParams(wwwAuthenticate);
95 String nonce = params.get("nonce");
96 if (nonce == null || nonce.length() == 0)
97 return null;
98 String opaque = params.get("opaque");
99 String algorithm = params.get("algorithm");
100 if (algorithm == null)
101 algorithm = "MD5";
102 MessageDigest digester = getMessageDigest(algorithm);
103 if (digester == null)
104 return null;
105 String serverQOP = params.get("qop");
106 String clientQOP = null;
107 if (serverQOP != null)
108 {
109 List<String> serverQOPValues = Arrays.asList(serverQOP.split(","));
110 if (serverQOPValues.contains("auth"))
111 clientQOP = "auth";
112 else if (serverQOPValues.contains("auth-int"))
113 clientQOP = "auth-int";
114 }
115
116 return new DigestResult(request.getURI(), response.getContent(), realm, user, password, algorithm, nonce, clientQOP, opaque);
117 }
118
119 private Map<String, String> parseParams(String wwwAuthenticate)
120 {
121 Map<String, String> result = new HashMap<>();
122 List<String> parts = splitParams(wwwAuthenticate);
123 for (String part : parts)
124 {
125 Matcher matcher = PARAM_PATTERN.matcher(part);
126 if (matcher.matches())
127 {
128 String name = matcher.group(1).trim().toLowerCase(Locale.ENGLISH);
129 String value = matcher.group(2).trim();
130 if (value.startsWith("\"") && value.endsWith("\""))
131 value = value.substring(1, value.length() - 1);
132 result.put(name, value);
133 }
134 }
135 return result;
136 }
137
138 private List<String> splitParams(String paramString)
139 {
140 List<String> result = new ArrayList<>();
141 int start = 0;
142 for (int i = 0; i < paramString.length(); ++i)
143 {
144 int quotes = 0;
145 char ch = paramString.charAt(i);
146 switch (ch)
147 {
148 case '\\':
149 ++i;
150 break;
151 case '"':
152 ++quotes;
153 break;
154 case ',':
155 if (quotes % 2 == 0)
156 {
157 result.add(paramString.substring(start, i).trim());
158 start = i + 1;
159 }
160 break;
161 default:
162 break;
163 }
164 }
165 result.add(paramString.substring(start, paramString.length()).trim());
166 return result;
167 }
168
169 private MessageDigest getMessageDigest(String algorithm)
170 {
171 try
172 {
173 return MessageDigest.getInstance(algorithm);
174 }
175 catch (NoSuchAlgorithmException x)
176 {
177 return null;
178 }
179 }
180
181 private class DigestResult implements Result
182 {
183 private final AtomicInteger nonceCount = new AtomicInteger();
184 private final URI uri;
185 private final byte[] content;
186 private final String realm;
187 private final String user;
188 private final String password;
189 private final String algorithm;
190 private final String nonce;
191 private final String qop;
192 private final String opaque;
193
194 public DigestResult(URI uri, byte[] content, String realm, String user, String password, String algorithm, String nonce, String qop, String opaque)
195 {
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 if (!request.getURI().toString().startsWith(uri.toString()))
217 return;
218
219 MessageDigest digester = getMessageDigest(algorithm);
220 if (digester == null)
221 return;
222
223 Charset charset = Charset.forName("ISO-8859-1");
224 String A1 = user + ":" + realm + ":" + password;
225 String hashA1 = toHexString(digester.digest(A1.getBytes(charset)));
226
227 String A2 = request.getMethod().asString() + ":" + request.getURI();
228 if ("auth-int".equals(qop))
229 A2 += ":" + toHexString(digester.digest(content));
230 String hashA2 = toHexString(digester.digest(A2.getBytes(charset)));
231
232 String nonceCount;
233 String clientNonce;
234 String A3;
235 if (qop != null)
236 {
237 nonceCount = nextNonceCount();
238 clientNonce = newClientNonce();
239 A3 = hashA1 + ":" + nonce + ":" + nonceCount + ":" + clientNonce + ":" + qop + ":" + hashA2;
240 }
241 else
242 {
243 nonceCount = null;
244 clientNonce = null;
245 A3 = hashA1 + ":" + nonce + ":" + hashA2;
246 }
247 String hashA3 = toHexString(digester.digest(A3.getBytes(charset)));
248
249 StringBuilder value = new StringBuilder("Digest");
250 value.append(" username=\"").append(user).append("\"");
251 value.append(", realm=\"").append(realm).append("\"");
252 value.append(", nonce=\"").append(nonce).append("\"");
253 if (opaque != null)
254 value.append(", opaque=\"").append(opaque).append("\"");
255 value.append(", algorithm=\"").append(algorithm).append("\"");
256 value.append(", uri=\"").append(request.getURI()).append("\"");
257 if (qop != null)
258 {
259 value.append(", qop=\"").append(qop).append("\"");
260 value.append(", nc=\"").append(nonceCount).append("\"");
261 value.append(", cnonce=\"").append(clientNonce).append("\"");
262 }
263 value.append(", response=\"").append(hashA3).append("\"");
264
265 request.header(HttpHeader.AUTHORIZATION.asString(), value.toString());
266 }
267
268 private String nextNonceCount()
269 {
270 String padding = "00000000";
271 String next = Integer.toHexString(nonceCount.incrementAndGet()).toLowerCase(Locale.ENGLISH);
272 return padding.substring(0, padding.length() - next.length()) + next;
273 }
274
275 private String newClientNonce()
276 {
277 Random random = new Random();
278 byte[] bytes = new byte[8];
279 random.nextBytes(bytes);
280 return toHexString(bytes);
281 }
282
283 private String toHexString(byte[] bytes)
284 {
285 return TypeUtil.toHexString(bytes).toLowerCase(Locale.ENGLISH);
286 }
287 }
288 }