|
1 /* |
|
2 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. |
|
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
|
4 * |
|
5 * This code is free software; you can redistribute it and/or modify it |
|
6 * under the terms of the GNU General Public License version 2 only, as |
|
7 * published by the Free Software Foundation. |
|
8 * |
|
9 * This code is distributed in the hope that it will be useful, but WITHOUT |
|
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
|
11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
|
12 * version 2 for more details (a copy is included in the LICENSE file that |
|
13 * accompanied this code). |
|
14 * |
|
15 * You should have received a copy of the GNU General Public License version |
|
16 * 2 along with this work; if not, write to the Free Software Foundation, |
|
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
|
18 * |
|
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
|
20 * or visit www.oracle.com if you need additional information or have any |
|
21 * questions. |
|
22 */ |
|
23 |
|
24 import java.io.Closeable; |
|
25 import java.io.IOException; |
|
26 import java.io.ObjectInputStream; |
|
27 import java.io.ObjectOutputStream; |
|
28 import java.io.Serializable; |
|
29 import java.net.ServerSocket; |
|
30 import java.net.Socket; |
|
31 import java.net.UnknownHostException; |
|
32 import java.util.ArrayList; |
|
33 import java.util.Arrays; |
|
34 import java.util.HashMap; |
|
35 import java.util.Map; |
|
36 import java.util.StringJoiner; |
|
37 import javax.security.auth.callback.Callback; |
|
38 import javax.security.auth.callback.CallbackHandler; |
|
39 import javax.security.auth.callback.NameCallback; |
|
40 import javax.security.auth.callback.PasswordCallback; |
|
41 import javax.security.auth.callback.UnsupportedCallbackException; |
|
42 import javax.security.sasl.AuthorizeCallback; |
|
43 import javax.security.sasl.RealmCallback; |
|
44 import javax.security.sasl.RealmChoiceCallback; |
|
45 import javax.security.sasl.Sasl; |
|
46 import javax.security.sasl.SaslClient; |
|
47 import javax.security.sasl.SaslException; |
|
48 import javax.security.sasl.SaslServer; |
|
49 |
|
50 /* |
|
51 * @test |
|
52 * @bug 8049814 |
|
53 * @summary JAVA SASL server and client tests with CRAM-MD5 and |
|
54 * DIGEST-MD5 mechanisms. The tests try different QOP values on |
|
55 * client and server side. |
|
56 * @modules java.security.sasl/javax.security.sasl |
|
57 */ |
|
58 public class ClientServerTest { |
|
59 |
|
60 private static final int DELAY = 100; |
|
61 private static final String LOCALHOST = "localhost"; |
|
62 private static final String DIGEST_MD5 = "DIGEST-MD5"; |
|
63 private static final String CRAM_MD5 = "CRAM-MD5"; |
|
64 private static final String PROTOCOL = "saslservice"; |
|
65 private static final String USER_ID = "sasltester"; |
|
66 private static final String PASSWD = "password"; |
|
67 private static final String QOP_AUTH = "auth"; |
|
68 private static final String QOP_AUTH_CONF = "auth-conf"; |
|
69 private static final String QOP_AUTH_INT = "auth-int"; |
|
70 private static final String AUTHID_SASL_TESTER = "sasl_tester"; |
|
71 private static final ArrayList<String> SUPPORT_MECHS = new ArrayList<>(); |
|
72 |
|
73 static { |
|
74 SUPPORT_MECHS.add(DIGEST_MD5); |
|
75 SUPPORT_MECHS.add(CRAM_MD5); |
|
76 } |
|
77 |
|
78 public static void main(String[] args) throws Exception { |
|
79 String[] allQops = { QOP_AUTH_CONF, QOP_AUTH_INT, QOP_AUTH }; |
|
80 String[] twoQops = { QOP_AUTH_INT, QOP_AUTH }; |
|
81 String[] authQop = { QOP_AUTH }; |
|
82 String[] authIntQop = { QOP_AUTH_INT }; |
|
83 String[] authConfQop = { QOP_AUTH_CONF }; |
|
84 String[] emptyQop = {}; |
|
85 |
|
86 boolean success = true; |
|
87 |
|
88 success &= runTest("", CRAM_MD5, new String[] { QOP_AUTH }, |
|
89 new String[] { QOP_AUTH }, false); |
|
90 success &= runTest("", DIGEST_MD5, new String[] { QOP_AUTH }, |
|
91 new String[] { QOP_AUTH }, false); |
|
92 success &= runTest(AUTHID_SASL_TESTER, DIGEST_MD5, |
|
93 new String[] { QOP_AUTH }, new String[] { QOP_AUTH }, false); |
|
94 success &= runTest("", DIGEST_MD5, allQops, authQop, false); |
|
95 success &= runTest("", DIGEST_MD5, allQops, authIntQop, false); |
|
96 success &= runTest("", DIGEST_MD5, allQops, authConfQop, false); |
|
97 success &= runTest("", DIGEST_MD5, twoQops, authQop, false); |
|
98 success &= runTest("", DIGEST_MD5, twoQops, authIntQop, false); |
|
99 success &= runTest("", DIGEST_MD5, twoQops, authConfQop, true); |
|
100 success &= runTest("", DIGEST_MD5, authIntQop, authQop, true); |
|
101 success &= runTest("", DIGEST_MD5, authConfQop, authQop, true); |
|
102 success &= runTest("", DIGEST_MD5, authConfQop, emptyQop, true); |
|
103 success &= runTest("", DIGEST_MD5, authIntQop, emptyQop, true); |
|
104 success &= runTest("", DIGEST_MD5, authQop, emptyQop, true); |
|
105 |
|
106 if (!success) { |
|
107 throw new RuntimeException("At least one test case failed"); |
|
108 } |
|
109 |
|
110 System.out.println("Test passed"); |
|
111 } |
|
112 |
|
113 private static boolean runTest(String authId, String mech, |
|
114 String[] clientQops, String[] serverQops, boolean expectException) |
|
115 throws Exception { |
|
116 |
|
117 System.out.println("AuthId:" + authId |
|
118 + " mechanism:" + mech |
|
119 + " clientQops: " + Arrays.toString(clientQops) |
|
120 + " serverQops: " + Arrays.toString(serverQops) |
|
121 + " expect exception:" + expectException); |
|
122 |
|
123 try (Server server = Server.start(LOCALHOST, authId, serverQops)) { |
|
124 new Client(LOCALHOST, server.getPort(), mech, authId, clientQops) |
|
125 .run(); |
|
126 if (expectException) { |
|
127 System.out.println("Expected exception not thrown"); |
|
128 return false; |
|
129 } |
|
130 } catch (SaslException e) { |
|
131 if (!expectException) { |
|
132 System.out.println("Unexpected exception: " + e); |
|
133 return false; |
|
134 } |
|
135 System.out.println("Expected exception: " + e); |
|
136 } |
|
137 |
|
138 return true; |
|
139 } |
|
140 |
|
141 static enum SaslStatus { |
|
142 SUCCESS, FAILURE, CONTINUE |
|
143 } |
|
144 |
|
145 static class Message implements Serializable { |
|
146 |
|
147 private final SaslStatus status; |
|
148 private final byte[] data; |
|
149 |
|
150 public Message(SaslStatus status, byte[] data) { |
|
151 this.status = status; |
|
152 this.data = data; |
|
153 } |
|
154 |
|
155 public SaslStatus getStatus() { |
|
156 return status; |
|
157 } |
|
158 |
|
159 public byte[] getData() { |
|
160 return data; |
|
161 } |
|
162 } |
|
163 |
|
164 static class SaslPeer { |
|
165 |
|
166 final String host; |
|
167 final String mechanism; |
|
168 final String qop; |
|
169 final CallbackHandler callback; |
|
170 |
|
171 SaslPeer(String host, String authId, String... qops) { |
|
172 this(host, null, authId, qops); |
|
173 } |
|
174 |
|
175 SaslPeer(String host, String mechanism, String authId, String... qops) { |
|
176 this.host = host; |
|
177 this.mechanism = mechanism; |
|
178 |
|
179 StringJoiner sj = new StringJoiner(","); |
|
180 for (String q : qops) { |
|
181 sj.add(q); |
|
182 } |
|
183 qop = sj.toString(); |
|
184 |
|
185 callback = new TestCallbackHandler(USER_ID, PASSWD, host, authId); |
|
186 } |
|
187 |
|
188 Message getMessage(Object ob) { |
|
189 if (!(ob instanceof Message)) { |
|
190 throw new RuntimeException("Expected an instance of Message"); |
|
191 } |
|
192 return (Message) ob; |
|
193 } |
|
194 } |
|
195 |
|
196 static class Server extends SaslPeer implements Runnable, Closeable { |
|
197 |
|
198 private volatile boolean ready = false; |
|
199 private volatile ServerSocket ssocket; |
|
200 |
|
201 static Server start(String host, String authId, String[] serverQops) |
|
202 throws UnknownHostException { |
|
203 Server server = new Server(host, authId, serverQops); |
|
204 Thread thread = new Thread(server); |
|
205 thread.setDaemon(true); |
|
206 thread.start(); |
|
207 |
|
208 while (!server.ready) { |
|
209 try { |
|
210 Thread.sleep(DELAY); |
|
211 } catch (InterruptedException e) { |
|
212 throw new RuntimeException(e); |
|
213 } |
|
214 } |
|
215 |
|
216 return server; |
|
217 } |
|
218 |
|
219 Server(String host, String authId, String... qops) { |
|
220 super(host, authId, qops); |
|
221 } |
|
222 |
|
223 int getPort() { |
|
224 return ssocket.getLocalPort(); |
|
225 } |
|
226 |
|
227 private void processConnection(SaslEndpoint endpoint) |
|
228 throws SaslException, IOException, ClassNotFoundException { |
|
229 System.out.println("process connection"); |
|
230 endpoint.send(SUPPORT_MECHS); |
|
231 Object o = endpoint.receive(); |
|
232 if (!(o instanceof String)) { |
|
233 throw new RuntimeException("Received unexpected object: " + o); |
|
234 } |
|
235 String mech = (String) o; |
|
236 SaslServer saslServer = createSaslServer(mech); |
|
237 Message msg = getMessage(endpoint.receive()); |
|
238 while (!saslServer.isComplete()) { |
|
239 byte[] data = processData(msg.getData(), endpoint, |
|
240 saslServer); |
|
241 if (saslServer.isComplete()) { |
|
242 System.out.println("server is complete"); |
|
243 endpoint.send(new Message(SaslStatus.SUCCESS, data)); |
|
244 } else { |
|
245 System.out.println("server continues"); |
|
246 endpoint.send(new Message(SaslStatus.CONTINUE, data)); |
|
247 msg = getMessage(endpoint.receive()); |
|
248 } |
|
249 } |
|
250 } |
|
251 |
|
252 private byte[] processData(byte[] data, SaslEndpoint endpoint, |
|
253 SaslServer server) throws SaslException, IOException { |
|
254 try { |
|
255 return server.evaluateResponse(data); |
|
256 } catch (SaslException e) { |
|
257 endpoint.send(new Message(SaslStatus.FAILURE, null)); |
|
258 System.out.println("Error while processing data"); |
|
259 throw e; |
|
260 } |
|
261 } |
|
262 |
|
263 private SaslServer createSaslServer(String mechanism) |
|
264 throws SaslException { |
|
265 Map<String, String> props = new HashMap<>(); |
|
266 props.put(Sasl.QOP, qop); |
|
267 return Sasl.createSaslServer(mechanism, PROTOCOL, host, props, |
|
268 callback); |
|
269 } |
|
270 |
|
271 @Override |
|
272 public void run() { |
|
273 try (ServerSocket ss = new ServerSocket(0)) { |
|
274 ssocket = ss; |
|
275 System.out.println("server started on port " + getPort()); |
|
276 ready = true; |
|
277 Socket socket = ss.accept(); |
|
278 try (SaslEndpoint endpoint = new SaslEndpoint(socket)) { |
|
279 System.out.println("server accepted connection"); |
|
280 processConnection(endpoint); |
|
281 } |
|
282 } catch (Exception e) { |
|
283 // ignore it for now, client will throw an exception |
|
284 } |
|
285 } |
|
286 |
|
287 @Override |
|
288 public void close() throws IOException { |
|
289 if (!ssocket.isClosed()) { |
|
290 ssocket.close(); |
|
291 } |
|
292 } |
|
293 } |
|
294 |
|
295 static class Client extends SaslPeer { |
|
296 |
|
297 private final int port; |
|
298 |
|
299 Client(String host, int port, String mech, String authId, |
|
300 String... qops) { |
|
301 super(host, mech, authId, qops); |
|
302 this.port = port; |
|
303 } |
|
304 |
|
305 public void run() throws Exception { |
|
306 System.out.println("Host:" + host + " port: " |
|
307 + port); |
|
308 try (SaslEndpoint endpoint = SaslEndpoint.create(host, port)) { |
|
309 negotiateMechanism(endpoint); |
|
310 SaslClient client = createSaslClient(); |
|
311 byte[] data = new byte[0]; |
|
312 if (client.hasInitialResponse()) { |
|
313 data = client.evaluateChallenge(data); |
|
314 } |
|
315 endpoint.send(new Message(SaslStatus.CONTINUE, data)); |
|
316 Message msg = getMessage(endpoint.receive()); |
|
317 while (!client.isComplete() |
|
318 && msg.getStatus() != SaslStatus.FAILURE) { |
|
319 switch (msg.getStatus()) { |
|
320 case CONTINUE: |
|
321 System.out.println("client continues"); |
|
322 data = client.evaluateChallenge(msg.getData()); |
|
323 endpoint.send(new Message(SaslStatus.CONTINUE, |
|
324 data)); |
|
325 msg = getMessage(endpoint.receive()); |
|
326 break; |
|
327 case SUCCESS: |
|
328 System.out.println("client succeeded"); |
|
329 data = client.evaluateChallenge(msg.getData()); |
|
330 if (data != null) { |
|
331 throw new SaslException("data should be null"); |
|
332 } |
|
333 break; |
|
334 default: |
|
335 throw new RuntimeException("Wrong status:" |
|
336 + msg.getStatus()); |
|
337 } |
|
338 } |
|
339 |
|
340 if (msg.getStatus() == SaslStatus.FAILURE) { |
|
341 throw new RuntimeException("Status is FAILURE"); |
|
342 } |
|
343 } |
|
344 |
|
345 System.out.println("Done"); |
|
346 } |
|
347 |
|
348 private SaslClient createSaslClient() throws SaslException { |
|
349 Map<String, String> props = new HashMap<>(); |
|
350 props.put(Sasl.QOP, qop); |
|
351 return Sasl.createSaslClient(new String[] {mechanism}, USER_ID, |
|
352 PROTOCOL, host, props, callback); |
|
353 } |
|
354 |
|
355 private void negotiateMechanism(SaslEndpoint endpoint) |
|
356 throws ClassNotFoundException, IOException { |
|
357 Object o = endpoint.receive(); |
|
358 if (o instanceof ArrayList) { |
|
359 ArrayList list = (ArrayList) o; |
|
360 if (!list.contains(mechanism)) { |
|
361 throw new RuntimeException( |
|
362 "Server does not support specified mechanism:" |
|
363 + mechanism); |
|
364 } |
|
365 } else { |
|
366 throw new RuntimeException( |
|
367 "Expected an instance of ArrayList, but received " + o); |
|
368 } |
|
369 |
|
370 endpoint.send(mechanism); |
|
371 } |
|
372 |
|
373 } |
|
374 |
|
375 static class SaslEndpoint implements AutoCloseable { |
|
376 |
|
377 private final Socket socket; |
|
378 private ObjectInputStream input; |
|
379 private ObjectOutputStream output; |
|
380 |
|
381 static SaslEndpoint create(String host, int port) throws IOException { |
|
382 return new SaslEndpoint(new Socket(host, port)); |
|
383 } |
|
384 |
|
385 SaslEndpoint(Socket socket) throws IOException { |
|
386 this.socket = socket; |
|
387 } |
|
388 |
|
389 private ObjectInputStream getInput() throws IOException { |
|
390 if (input == null && socket != null) { |
|
391 input = new ObjectInputStream(socket.getInputStream()); |
|
392 } |
|
393 return input; |
|
394 } |
|
395 |
|
396 private ObjectOutputStream getOutput() throws IOException { |
|
397 if (output == null && socket != null) { |
|
398 output = new ObjectOutputStream(socket.getOutputStream()); |
|
399 } |
|
400 return output; |
|
401 } |
|
402 |
|
403 public Object receive() throws IOException, ClassNotFoundException { |
|
404 return getInput().readObject(); |
|
405 } |
|
406 |
|
407 public void send(Object obj) throws IOException { |
|
408 getOutput().writeObject(obj); |
|
409 getOutput().flush(); |
|
410 } |
|
411 |
|
412 @Override |
|
413 public void close() throws IOException { |
|
414 if (socket != null && !socket.isClosed()) { |
|
415 socket.close(); |
|
416 } |
|
417 } |
|
418 |
|
419 } |
|
420 |
|
421 static class TestCallbackHandler implements CallbackHandler { |
|
422 |
|
423 private final String userId; |
|
424 private final char[] passwd; |
|
425 private final String realm; |
|
426 private String authId; |
|
427 |
|
428 TestCallbackHandler(String userId, String passwd, String realm, |
|
429 String authId) { |
|
430 this.userId = userId; |
|
431 this.passwd = passwd.toCharArray(); |
|
432 this.realm = realm; |
|
433 this.authId = authId; |
|
434 } |
|
435 |
|
436 @Override |
|
437 public void handle(Callback[] callbacks) throws IOException, |
|
438 UnsupportedCallbackException { |
|
439 for (Callback callback : callbacks) { |
|
440 if (callback instanceof NameCallback) { |
|
441 System.out.println("NameCallback"); |
|
442 ((NameCallback) callback).setName(userId); |
|
443 } else if (callback instanceof PasswordCallback) { |
|
444 System.out.println("PasswordCallback"); |
|
445 ((PasswordCallback) callback).setPassword(passwd); |
|
446 } else if (callback instanceof RealmCallback) { |
|
447 System.out.println("RealmCallback"); |
|
448 ((RealmCallback) callback).setText(realm); |
|
449 } else if (callback instanceof RealmChoiceCallback) { |
|
450 System.out.println("RealmChoiceCallback"); |
|
451 RealmChoiceCallback choice = (RealmChoiceCallback) callback; |
|
452 if (realm == null) { |
|
453 choice.setSelectedIndex(choice.getDefaultChoice()); |
|
454 } else { |
|
455 String[] choices = choice.getChoices(); |
|
456 for (int j = 0; j < choices.length; j++) { |
|
457 if (realm.equals(choices[j])) { |
|
458 choice.setSelectedIndex(j); |
|
459 break; |
|
460 } |
|
461 } |
|
462 } |
|
463 } else if (callback instanceof AuthorizeCallback) { |
|
464 System.out.println("AuthorizeCallback"); |
|
465 ((AuthorizeCallback) callback).setAuthorized(true); |
|
466 if (authId == null || authId.trim().length() == 0) { |
|
467 authId = userId; |
|
468 } |
|
469 ((AuthorizeCallback) callback).setAuthorizedID(authId); |
|
470 } else { |
|
471 throw new UnsupportedCallbackException(callback); |
|
472 } |
|
473 } |
|
474 } |
|
475 } |
|
476 |
|
477 } |