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