001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package org.apache.harmony.jndi.provider.ldap;
019:
020: import java.io.IOException;
021: import java.io.InputStream;
022: import java.io.OutputStream;
023: import java.net.Socket;
024: import java.net.UnknownHostException;
025: import java.util.ArrayList;
026: import java.util.Hashtable;
027: import java.util.List;
028:
029: import javax.naming.CommunicationException;
030: import javax.naming.ConfigurationException;
031: import javax.naming.Context;
032: import javax.naming.NamingException;
033: import javax.naming.ldap.Control;
034: import javax.naming.ldap.StartTlsRequest;
035: import javax.net.SocketFactory;
036: import javax.net.ssl.SSLSocketFactory;
037:
038: import org.apache.harmony.jndi.internal.nls.Messages;
039: import org.apache.harmony.jndi.provider.ldap.LdapContextImpl.UnsolicitedListener;
040: import org.apache.harmony.jndi.provider.ldap.asn1.ASN1Decodable;
041: import org.apache.harmony.jndi.provider.ldap.asn1.ASN1Encodable;
042: import org.apache.harmony.jndi.provider.ldap.asn1.LdapASN1Constant;
043: import org.apache.harmony.jndi.provider.ldap.event.ECNotificationControl;
044: import org.apache.harmony.jndi.provider.ldap.event.PersistentSearchControl;
045: import org.apache.harmony.jndi.provider.ldap.event.PersistentSearchResult;
046: import org.apache.harmony.security.asn1.ASN1Integer;
047:
048: /**
049: * LdapClient is the actual class used to communicate with Ldap Server.
050: *
051: */
052: public class LdapClient {
053: /**
054: * Socket used to communicate with Ldap Server.
055: */
056: private Socket socket;
057:
058: /**
059: * Input stream of socket.
060: */
061: private InputStream in;
062:
063: /**
064: * Output stream of socket.
065: */
066: private OutputStream out;
067:
068: /**
069: * Address of connection
070: */
071: private String address;
072:
073: /**
074: * port of connection
075: */
076: private int port;
077:
078: /**
079: * blocked requests list which wait for response
080: */
081: private Hashtable<Integer, Element> requests = new Hashtable<Integer, Element>();
082:
083: /**
084: * the max time to wait server response in milli-second
085: */
086: private long MAX_WAIT_TIME = 30 * 1000;
087:
088: /**
089: * responsible for dispatching received messages
090: */
091: private Dispatcher dispatcher;
092:
093: /**
094: * registered UnsolicitedListener
095: */
096: private List<UnsolicitedListener> unls = new ArrayList<UnsolicitedListener>();
097:
098: // constructor for test
099: public LdapClient() {
100: // do nothing
101: }
102:
103: /**
104: * Constructor for LdapClient.
105: *
106: * @param factory
107: * used to construct socket through its factory method
108: * @param address
109: * the Internet Protocol (IP) address of ldap server
110: * @param port
111: * the port number of ldap server
112: * @throws UnknownHostException
113: * if the host cannot be resolved
114: * @throws IOException
115: * if an error occurs while instantiating the socket
116: */
117: public LdapClient(SocketFactory factory, String address, int port)
118: throws UnknownHostException, IOException {
119: this .address = address;
120: this .port = port;
121: socket = factory.createSocket(address, port);
122: // FIXME: Use of InputStreamWrap here is to deal with a potential bug of
123: // RI.
124: in = new InputStreamWrap(socket.getInputStream());
125: out = socket.getOutputStream();
126: dispatcher = new Dispatcher();
127: dispatcher.start();
128: }
129:
130: /**
131: * The instance of the class is daemon thread, which read messages from
132: * server and dispatch to corresponding thread.
133: */
134: class Dispatcher extends Thread {
135:
136: private boolean isStopped = false;
137:
138: public Dispatcher() {
139: /**
140: * must be daemon thread, otherwrise can't destory by gc
141: */
142: setDaemon(true);
143: }
144:
145: public boolean isStopped() {
146: return isStopped;
147: }
148:
149: public void setStopped(boolean isStopped) {
150: this .isStopped = isStopped;
151: }
152:
153: @Override
154: public void run() {
155: while (!isStopped) {
156: try {
157: // set response op to null, load later
158: LdapMessage response = new LdapMessage(null) {
159:
160: /**
161: * Dispatcher can't know which response operation should
162: * be used until messageId had determined.
163: *
164: * @return response according messageId
165: */
166: @Override
167: public ASN1Decodable getResponseOp() {
168: // responseOp has been load, just return it
169: if (super .getResponseOp() != null) {
170: return super .getResponseOp();
171: }
172:
173: int messageId = getMessageId();
174:
175: // Unsolicited Notification
176: if (messageId == 0) {
177: return new UnsolicitedNotificationImpl();
178: }
179:
180: // get response operation according messageId
181: Element element = requests.get(Integer
182: .valueOf(messageId));
183: if (element != null) {
184: return element.response.getResponseOp();
185: }
186:
187: /*
188: * FIXME: if messageId not find in request list,
189: * what should we do?
190: */
191: return null;
192: }
193: };
194:
195: Exception ex = null;
196: /**
197: * TODO read message data by ourselves then decode, this
198: * would be robust
199: */
200: try {
201: // read next message
202: response.decode(in);
203: } catch (IOException e) {
204: // may socket has problem or decode occurs error
205: ex = e;
206: } catch (RuntimeException e) {
207: // may socket has problem or decode occurs error
208: ex = e;
209: }
210:
211: processResponse(response, ex);
212:
213: } catch (Exception e) {
214: // may never reach
215: e.printStackTrace();
216: }
217: }
218:
219: }
220:
221: private void processResponse(LdapMessage response, Exception ex) {
222: // unsolicited notification
223: if (response.getMessageId() == 0) {
224: notifyUnls(response);
225: return;
226: }
227:
228: Element element = requests.get(Integer.valueOf(response
229: .getMessageId()));
230:
231: if (element != null) {
232: element.response = response;
233: element.ex = ex;
234: // persistent search response
235: if (element.lock == null) {
236:
237: notifyPersistenSearchListener(element);
238:
239: } else {
240: /*
241: * notify the thread which send request and wait for
242: * response
243: */
244: if (element.response.getOperationIndex() == LdapASN1Constant.OP_EXTENDED_RESPONSE
245: && ((ExtendedOp) element.response
246: .getResponseOp())
247: .getExtendedRequest().getID()
248: .equals(StartTlsRequest.OID)) {
249: /*
250: * When establishing TLS by StartTls extended operation,
251: * no
252: */
253: isStopped = true;
254: }
255:
256: synchronized (element.lock) {
257: element.lock.notify();
258: }
259: } // end of if (element.lock == null) else
260: } // end of if (element != null)
261:
262: else if (ex != null) {
263: /*
264: * may asn1 decode error or socket problem, can get message id,
265: * so couldn't know which thread should be notified
266: */
267: // FIXME: any better way?
268: close();
269: }
270: // FIXME message id not found and no exception, what shoud we do?
271:
272: } // end of processResponse
273: } // Dispatcher
274:
275: private void notifyUnls(LdapMessage response) {
276: UnsolicitedNotificationImpl un = (UnsolicitedNotificationImpl) response
277: .getResponseOp();
278: for (UnsolicitedListener listener : unls) {
279: listener.receiveNotification(un, response.getControls());
280: }
281: }
282:
283: /**
284: * Carry out the ldap operation encapsulated in operation with controls.
285: *
286: * @param operation
287: * the ldap operation
288: * @param controls
289: * extra controls for some ldap operations
290: * @return the encapsulated response message from ldap server
291: * @throws IOException
292: */
293: public LdapMessage doOperation(LdapOperation operation,
294: Control[] controls) throws IOException {
295: return doOperation(operation.getRequestId(), operation
296: .getRequest(), operation.getResponse(), controls);
297: }
298:
299: /**
300: * Send out the ldap operation in request with controls, and decode response
301: * into LdapMessage.
302: *
303: * @param opIndex
304: * @param request
305: * the ldap request
306: * @param response
307: * the ldap response
308: * @param controls
309: * extra controls for some ldap operations
310: * @return the encapsulated response message from ldap server
311: * @throws IOException
312: */
313: public LdapMessage doOperation(int opIndex, ASN1Encodable request,
314: ASN1Decodable response, Control[] controls)
315: throws IOException {
316:
317: if (opIndex == LdapASN1Constant.OP_SEARCH_REQUEST) {
318: return doSearchOperation(request, response, controls);
319: }
320:
321: LdapMessage requestMsg = new LdapMessage(opIndex, request,
322: controls);
323:
324: Integer messageID = Integer.valueOf(requestMsg.getMessageId());
325:
326: Object lock = new Object();
327: requests.put(messageID, new Element(lock, new LdapMessage(
328: response)));
329:
330: try {
331: out.write(requestMsg.encode());
332: out.flush();
333: return waitResponse(messageID, lock);
334:
335: } finally {
336: // remove request from list
337: requests.remove(messageID);
338: }
339:
340: }
341:
342: /**
343: * Block the current thread until get response from server or occurs error
344: *
345: * @param messageID
346: * id of request message, is same as id of response message
347: * @param response
348: * decoder of the response
349: * @return response message, may not be null
350: *
351: * @throws Exception
352: */
353: private LdapMessage waitResponse(Integer messageID, Object lock)
354: throws IOException {
355: Element element = requests.get(messageID);
356:
357: /*
358: * test if dispatcher has not received response message from server,
359: * wait response
360: */
361: if (element.response.getMessageId() != messageID.intValue()) {
362:
363: synchronized (lock) {
364: try {
365: lock.wait(MAX_WAIT_TIME);
366: } catch (InterruptedException e) {
367: // ignore
368: }
369: }
370: }
371:
372: element = requests.get(messageID);
373:
374: // wait time out
375: if (element.response.getMessageId() != messageID.intValue()) {
376: // ldap.31=Read LDAP response message time out
377: throw new IOException(Messages.getString("ldap.31")); //$NON-NLS-1$
378: }
379:
380: // error occurs when read response
381: if (element.ex != null) {
382: // socket is not connected
383: if (!socket.isConnected()) {
384: close();
385: }
386: // element.ex must be one of IOException or RuntimeException
387: if (element.ex instanceof IOException) {
388: throw (IOException) element.ex;
389: }
390:
391: throw (RuntimeException) element.ex;
392: }
393:
394: return element.response;
395:
396: }
397:
398: private LdapMessage doSearchOperation(ASN1Encodable request,
399: ASN1Decodable response, Control[] controls)
400: throws IOException {
401: LdapMessage requestMsg = new LdapMessage(
402: LdapASN1Constant.OP_SEARCH_REQUEST, request, controls);
403:
404: Integer messageID = Integer.valueOf(requestMsg.getMessageId());
405:
406: Object lock = new Object();
407: requests.put(messageID, new Element(lock, new LdapMessage(
408: response)));
409:
410: try {
411: out.write(requestMsg.encode());
412: out.flush();
413: LdapMessage responseMsg = waitResponse(messageID, lock);
414:
415: while (responseMsg.getOperationIndex() != LdapASN1Constant.OP_SEARCH_RESULT_DONE) {
416: responseMsg = waitResponse(messageID, lock);
417: }
418:
419: return responseMsg;
420: } finally {
421: // remove request from list
422: requests.remove(messageID);
423: }
424:
425: }
426:
427: public void abandon(final int messageId, Control[] controls)
428: throws IOException {
429: doOperationWithoutResponse(LdapASN1Constant.OP_ABANDON_REQUEST,
430: new ASN1Encodable() {
431:
432: public void encodeValues(Object[] values) {
433: values[0] = ASN1Integer.fromIntValue(messageId);
434: }
435:
436: }, controls);
437: }
438:
439: public void doOperationWithoutResponse(int opIndex,
440: ASN1Encodable op, Control[] controls) throws IOException {
441: LdapMessage request = new LdapMessage(opIndex, op, controls);
442: out.write(request.encode());
443: out.flush();
444: }
445:
446: public int addPersistentSearch(SearchOp op) throws IOException {
447: LdapMessage request = new LdapMessage(
448: LdapASN1Constant.OP_SEARCH_REQUEST, op.getRequest(),
449: new Control[] { new PersistentSearchControl() });
450:
451: Integer messageID = Integer.valueOf(request.getMessageId());
452:
453: // set lock to null, indicate this is persistent search
454: requests.put(messageID, new Element(null, new LdapMessage(op
455: .getResponse())));
456: try {
457: out.write(request.encode());
458: out.flush();
459: return request.getMessageId();
460: } catch (IOException e) {
461: // send request faild, remove request from list
462: requests.remove(messageID);
463: throw e;
464: }
465:
466: }
467:
468: public void removePersistentSearch(int messageId, Control[] controls)
469: throws IOException {
470: requests.remove(Integer.valueOf(messageId));
471: abandon(messageId, controls);
472: }
473:
474: /**
475: * Close network connection, stop dispather thread, and release all other
476: * resources
477: *
478: * NOTE: invoke this method should be careful when this
479: * <code>LdapClient</code> instance is shared by multi
480: * <code>LdapContext</code>
481: *
482: */
483: public void close() {
484: // close socket
485: if (socket != null) {
486: try {
487: socket.close();
488: } catch (IOException e) {
489: // ignore
490: }
491: }
492:
493: socket = null;
494: in = null;
495: out = null;
496:
497: // try to stop dispather
498: if (dispatcher != null) {
499: dispatcher.setStopped(true);
500: dispatcher.interrupt();
501: }
502:
503: // notify all blocked thread
504: if (requests != null) {
505: for (Element element : requests.values()) {
506: if (element.lock != null) {
507: synchronized (element.lock) {
508: element.lock.notify();
509: }
510: } else {
511: // TODO notify persistent search listeners
512: }
513: }
514: requests.clear();
515: requests = null;
516: }
517:
518: }
519:
520: /**
521: * Get new instance of LdapClient according environment variable
522: *
523: * @param envmt
524: * @return
525: * @throws NamingException
526: */
527: public static LdapClient newInstance(String host, int port,
528: Hashtable<?, ?> envmt) throws NamingException {
529: String factoryName = (String) envmt
530: .get("java.naming.ldap.factory.socket");
531:
532: SocketFactory factory = null;
533: if (factoryName == null || "".equals(factoryName)) {
534: if ("ssl".equalsIgnoreCase((String) envmt
535: .get(Context.SECURITY_PROTOCOL))) {
536: factory = SSLSocketFactory.getDefault();
537: } else {
538: factory = SocketFactory.getDefault();
539: }
540: } else {
541:
542: try {
543: factory = (SocketFactory) classForName(factoryName)
544: .newInstance();
545: } catch (Exception e) {
546: ConfigurationException ex = new ConfigurationException();
547: ex.setRootCause(e);
548: throw ex;
549: }
550: }
551: // TODO: get LdapClient from pool first.
552:
553: try {
554: return new LdapClient(factory, host, port);
555:
556: } catch (IOException e) {
557: CommunicationException ex = new CommunicationException();
558: ex.setRootCause(e);
559: throw ex;
560: }
561: }
562:
563: private static Class<?> classForName(final String className)
564: throws ClassNotFoundException {
565:
566: Class<?> cls = null;
567: // try thread context class loader first
568: try {
569: cls = Class.forName(className, true, Thread.currentThread()
570: .getContextClassLoader());
571: } catch (ClassNotFoundException e) {
572: // Ignored.
573: }
574: // try system class loader second
575: try {
576: cls = Class.forName(className, true, ClassLoader
577: .getSystemClassLoader());
578: } catch (ClassNotFoundException e1) {
579: // Ignored.
580: }
581:
582: if (cls == null) {
583: // jndi.1C=class {0} not found
584: throw new ClassNotFoundException(Messages.getString(
585: "jndi.1C", className)); //$NON-NLS-1$
586: }
587:
588: return cls;
589: }
590:
591: /**
592: * struct for holding necessary info to add to requests list
593: */
594: static class Element {
595: Object lock;
596:
597: LdapMessage response;
598:
599: Exception ex;
600:
601: public Element(Object lock, LdapMessage response) {
602: this .lock = lock;
603: this .response = response;
604: }
605: }
606:
607: // TODO: This class is used to deal with a potential bug of RI, may be
608: // removed in the future.
609: /**
610: * When use <code>InputStream</code> from SSL Socket, if invoke
611: * <code>InputStream.read(byte[])</code> with byte array of zero length,
612: * the method will be blocked. Seems it's bug of ri.
613: *
614: * This wrap class delegate all request to wrapped instance, except
615: * returning immediately when the invoke
616: * <code>InputStream.read(byte[])</code> with byte array of zero length.
617: */
618: static class InputStreamWrap extends InputStream {
619: InputStream in;
620:
621: public InputStreamWrap(InputStream in) {
622: this .in = in;
623: }
624:
625: @Override
626: public int read() throws IOException {
627: return in.read();
628: }
629:
630: @Override
631: public int read(byte[] bs, int offset, int len)
632: throws IOException {
633: if (len == 0) {
634: return 0;
635: }
636: return in.read(bs, offset, len);
637: }
638:
639: @Override
640: public void reset() throws IOException {
641: in.reset();
642:
643: }
644:
645: @Override
646: public int available() throws IOException {
647: return in.available();
648: }
649:
650: @Override
651: public void close() throws IOException {
652: in.close();
653: }
654:
655: @Override
656: public void mark(int readlimit) {
657: in.mark(readlimit);
658: }
659:
660: @Override
661: public boolean markSupported() {
662: return in.markSupported();
663: }
664:
665: @Override
666: public int read(byte[] b) throws IOException {
667: return in.read(b);
668: }
669:
670: @Override
671: public long skip(long n) throws IOException {
672: return in.skip(n);
673: }
674: }
675:
676: public String getAddress() {
677: return address;
678: }
679:
680: public int getPort() {
681: return port;
682: }
683:
684: @Override
685: protected void finalize() {
686: close();
687: }
688:
689: public Socket getSocket() {
690: return this .socket;
691: }
692:
693: public void setSocket(Socket socket) throws IOException {
694: this .socket = socket;
695: this .in = new InputStreamWrap(socket.getInputStream());
696: this .out = socket.getOutputStream();
697: if (dispatcher != null) {
698: dispatcher.setStopped(true);
699: dispatcher.interrupt();
700: }
701: this .dispatcher = new Dispatcher();
702: this .dispatcher.start();
703: }
704:
705: public void addUnsolicitedListener(UnsolicitedListener listener) {
706: if (unls == null) {
707: unls = new ArrayList<UnsolicitedListener>();
708: }
709:
710: if (!unls.contains(listener)) {
711: unls.add(listener);
712: }
713: }
714:
715: private void notifyPersistenSearchListener(Element element) {
716: PersistentSearchResult psr = (PersistentSearchResult) ((SearchOp) element.response
717: .getResponseOp()).getSearchResult();
718: // test error
719: if (psr.getResult() != null) {
720: psr.receiveNotificationHook(psr.getResult());
721: }
722:
723: // notify listener
724: Control[] cs = element.response.getControls();
725: if (cs != null) {
726: for (int i = 0; i < cs.length; i++) {
727: Control control = cs[i];
728: if (ECNotificationControl.OID.equals(control.getID())) {
729: psr
730: .receiveNotificationHook(new ECNotificationControl(
731: control.getEncodedValue()));
732: }
733: }
734: }
735: }
736:
737: }
|