/* * Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package org.poreid.security.pcscforjava; import org.poreid.pcscforjava.CardTerminal; import org.poreid.pcscforjava.CardTerminals; import org.poreid.pcscforjava.PCSCErrorValues; import org.poreid.pcscforjava.CardException; import org.poreid.pcscforjava.TerminalFactory; import org.poreid.pcscforjava.CardTerminalsEvent; import java.util.*; import java.lang.ref.*; import static org.poreid.pcscforjava.CardTerminals.State.*; import static org.poreid.security.pcscforjava.PCSC.*; import static org.poreid.security.pcscforjava.PCSCDefines.*; /** * TerminalFactorySpi implementation class. * * @since 1.6 * @author Andreas Sterbenz * @author Matthieu Leromain */ final class PCSCTerminals extends CardTerminals { /** * SCARDCONTEXT, currently shared between all threads/terminals */ protected static long contextId; /** * Terminal state used by waitForCard() */ private Map<String,ReaderState> stateMap; /** * Plug & Play class Thread */ static private PCSCPnPThread m_cardTerminalsThread = null; /** * Plug & Play thread */ static private Thread m_thread; /** * Last state for the list method */ private State m_state; /** * List of CardTerminal objects. */ private List<CardTerminal> list; /** * The current list. */ private Object m_currentList; /** * Constructs a new CardTerminals object. * * <p>This constructor is called by subclasses only. Application should * call {@linkplain TerminalFactory#terminals} * to obtain a CardTerminals object. */ PCSCTerminals() { // empty } /** * Initializes the PCSC context. * @throws PCSCException if a PCSC exception occurs. */ static synchronized void initContext() throws PCSCException { if (contextId == 0) { contextId = SCardEstablishContext(SCARD_SCOPE_SYSTEM); } } /** * Releases the PCSC context. * @throws PCSCException if a PCSC exception occurs. */ static synchronized void releaseContext() throws PCSCException { SCardReleaseContext(contextId); contextId = 0; } /** * The Hashmap of terminals. */ private static final Map<String,Reference<TerminalImpl>> terminals = new HashMap<String,Reference<TerminalImpl>>(); /** * Gets the terminal. * @param name the name of the terminal. * @return the terminal. */ private static synchronized TerminalImpl implGetTerminal(String name) { Reference<TerminalImpl> ref = terminals.get(name); TerminalImpl terminal = (ref != null) ? ref.get() : null; if (terminal != null) { terminal.setContextId(contextId); return terminal; } terminal = new TerminalImpl(contextId, name); terminals.put(name, new WeakReference<TerminalImpl>(terminal)); return terminal; } /** * Returns an unmodifiable list of all terminals matching the specified * state.<br /> * * <p>If state is {@link State#ALL State.ALL}, this method returns * all CardTerminals encapsulated by this object. * If state is {@link State#CARD_PRESENT State.CARD_PRESENT} or * {@link State#CARD_ABSENT State.CARD_ABSENT}, it returns all * CardTerminals where a card is currently present or absent, respectively. * * <p>If state is {@link State#CARD_INSERTION State.CARD_INSERTION} or * {@link State#CARD_REMOVAL State.CARD_REMOVAL}, it returns all * CardTerminals for which an insertion (or removal, respectively) * was detected during the last call to {@linkplain #waitForChange}. * If <code>waitForChange()</code> has not been called on this object, * <code>CARD_INSERTION</code> is equivalent to <code>CARD_PRESENT</code> * and <code>CARD_REMOVAL</code> is equivalent to <code>CARD_ABSENT</code>. * For an example of the use of <code>CARD_INSERTION</code>, * see {@link #waitForChange}. * * @param state the State * @return an unmodifiable list of all terminals matching the specified * attribute. * * @throws NullPointerException if attr is null * @throws CardException if the card operation failed */ public synchronized List<CardTerminal> list(State state) throws CardException { if (state == null) { throw new NullPointerException(); } try { // Be sure that the context is well established initContext(); } catch (PCSCException ex) { this.startPnPThread(); this.m_currentList = Collections.emptyList(); return (List<CardTerminal>) this.m_currentList; } if(contextId == 0) { this.startPnPThread(); this.m_currentList = Collections.emptyList(); return (List<CardTerminal>) this.m_currentList; } this.m_state = state; try { String[] readerNames = SCardListReaders(contextId); list = new ArrayList<CardTerminal>(readerNames.length); if (stateMap == null) { // If waitForChange() has never been called, treat event // queries as status queries. if (state == CARD_INSERTION) { state = CARD_PRESENT; } else if (state == CARD_REMOVAL) { state = CARD_ABSENT; } } updateTerminalsHashMap(readerNames); for (String readerName : readerNames) { CardTerminal terminal = implGetTerminal(readerName); ReaderState readerState; switch (state) { case ALL: list.add(terminal); // Just to be sure that the card terminal is real try { terminal.isCardPresent(); } catch(Exception ex){ list.remove(terminal); } break; case CARD_PRESENT: if (terminal.isCardPresent()) { list.add(terminal); } break; case CARD_ABSENT: if (terminal.isCardPresent() == false) { list.add(terminal); } break; case CARD_INSERTION: readerState = stateMap.get(readerName); if ((readerState != null) && readerState.isInsertion()) { list.add(terminal); } break; case CARD_REMOVAL: readerState = stateMap.get(readerName); if ((readerState != null) && readerState.isRemoval()) { list.add(terminal); } break; default: throw new CardException("org.poreid.pcscforjava." + "PCSCTerminals.list " + "PCSCException: SCARD_F_UNKNOWN_ERROR " + "Unknown state: " + state); } } this.startPnPThread(); this.m_currentList = Collections.unmodifiableList(list); } catch (PCSCException e) { if(e.getMessage().contains("SCARD_E_NO_READERS_AVAILABLE")) { this.startPnPThread(); this.m_currentList = Collections.emptyList(); } else throw new CardException("org.poreid.pcscforjava." + "PCSCTerminals.list " + "PCSCException: " + e.getMessage(), e); } return (List<CardTerminal>) this.m_currentList; } private void updateTerminalsHashMap(String[] readerNames) { int _iNbModification = 0; Set _keys = terminals.keySet(); Iterator _it = _keys.iterator(); String _key = null; int _iNbKeys = _keys.size(), _i = 0; while(_i < _iNbKeys) { boolean _bFound = false; try { _key = (String) _it.next(); for (String readerName : readerNames) { if(readerName.equalsIgnoreCase(_key)) { _bFound = true; break; } } } catch(Exception ex) {// If exception remove it } if(!_bFound) { _iNbModification++; ((TerminalImpl)terminals.get(_key).get()).notifyDisconnection(); terminals.remove(_key); _it = _keys.iterator(); _iNbKeys = _keys.size(); _i = -1; } _i++; } // If there is no disconnection it is possible that a reader has just // restarted or new connection so in each case the last reader has no // correct card. if((_iNbModification == 0) && (_key != null)) ((TerminalImpl)terminals.get(_key).get()).notifyDisconnection(); } /** * Check if the current resource manager context is in valid state or not. * <br />It is a good way to be sure that the current resources are always * available and up to date. * * @return false if the current resource manager context is no more valid, * true otherwise. */ @Override public boolean isValidContext() { try { SCardIsValidContext(contextId); return true; } catch (PCSCException ex) { return false; } } /** * Closes an established resource manager context. * It is a good way to finish the use of the smart card API. * * @throws CardException if the card operation failed */ @Override public void closeContext() throws CardException { if(m_cardTerminalsThread != null) { m_cardTerminalsThread.stop(); m_cardTerminalsThread = null; } try { SCardReleaseContext(contextId); contextId = 0; } catch(PCSCException ex) { throw new CardException("org.poreid.pcscforjava." + "PCSCTerminals.closeContext " + "PCSCException: " + ex.getMessage(), ex); } } /** * Indicates if the Plug & Play is supported by the library or not. * @param lContextId * @return true if it is supported.\n * false otherwise. */ private native boolean SCardIsPlugAndPlaySupported(long lContextId) throws PCSCException; /** * Returns if the {@linkg CardTerminals CardTerminals} object is managed * by Plug & Play or not. * @return false if the object does not support Plug & Play mode. * <br /> true otherwise. * @throws CardException if a card operation failed */ public boolean isPlugAndPlaySupported() throws CardException { try { if(contextId != 0) return SCardIsPlugAndPlaySupported(contextId); } catch(PCSCException ex) { throw new CardException("org.poreid.pcscforjava." + "PCSCTerminals.isPlugAndPlaySupported " + "PCSCException: " + ex.getMessage(), ex); } return false; } /** * Starts the Plug & Play thread. */ private void startPnPThread() { if(m_cardTerminalsThread == null) { m_cardTerminalsThread = PCSCPnPThread.getInstance(this); m_thread = m_cardTerminalsThread.start(); m_cardTerminalsThread.addObserver(this); } else { //m_cardTerminalsThread.addObserver(this); } } /** * Updates the card terminals list when an event of type card terminal * insertion / removal occurs. * * <p>This method is called as a callback by subclasses only. Application * should call {@linkplain TerminalFactory#terminals} which launch the * detection thread and call this method.</p> * * @throws CardException if a card operation falied. */ public void updateCardTerminalsListByEvent() throws CardException { if (this.m_state == null) this.m_state = ALL; try { // Be sure that the context is well established initContext(); } catch (PCSCException ex) {return;} try { String[] readerNames = SCardListReaders(contextId); List<CardTerminal> _tmpList = new ArrayList<CardTerminal>(readerNames.length); updateTerminalsHashMap(readerNames); for (String readerName : readerNames) { CardTerminal terminal = implGetTerminal(readerName); ReaderState readerState; switch (this.m_state) { case ALL: _tmpList.add(terminal); break; case CARD_PRESENT: if (terminal.isCardPresent()) { _tmpList.add(terminal); } break; case CARD_ABSENT: if (terminal.isCardPresent() == false) { _tmpList.add(terminal); } break; case CARD_INSERTION: readerState = stateMap.get(readerName); if ((readerState != null) && readerState.isInsertion()) { _tmpList.add(terminal); } break; case CARD_REMOVAL: readerState = stateMap.get(readerName); if ((readerState != null) && readerState.isRemoval()) { _tmpList.add(terminal); } break; default: throw new CardException("org.poreid.pcscforjava." + "PCSCTerminals.updateCardTerminalsListByEvent " + "PCSCException: SCARD_F_UNKNOWN_ERROR " + "Unknown state: " + this.m_state); } } this.m_currentList = _tmpList; } catch (PCSCException e) { if(e.getMessage().contains("SCARD_E_NO_READERS_AVAILABLE")) { this.m_currentList = Collections.emptyList(); } else throw new CardException("org.poreid.pcscforjava." + "PCSCTerminals.updateCardTerminalsListByEvent " + "PCSCException: " + e.getMessage(), e); } List _tmp = TerminalFactory.getPnPCallbacks(); if(_tmp != null) { for(int _i = 0; _i < _tmp.size(); _i++) ((CardTerminalsEvent)_tmp.get(_i)). updateCardTerminalsListByEvent((List<CardTerminal>) this.m_currentList); } } /** * The reader state class. */ private static class ReaderState { private int current, previous; ReaderState() { current = SCARD_STATE_UNAWARE; previous = SCARD_STATE_UNAWARE; } int get() { return current; } void update(int newState) { previous = current; current = newState; } boolean isInsertion() { return !present(previous) && present(current); } boolean isRemoval() { return present(previous) && !present(current); } static boolean present(int state) { return (state & SCARD_STATE_PRESENT) != 0; } } /** * Waits for card insertion or removal in any of the terminals of this * object or until the timeout expires. * * <p>This method examines each CardTerminal of this object. * If a card was inserted into or removed from a CardTerminal since the * previous call to <code>waitForChange()</code>, it returns * immediately. * Otherwise, or if this is the first call to <code>waitForChange()</code> * on this object, it blocks until a card is inserted into or removed from * a CardTerminal. * * <p>If <code>timeout</code> is greater than 0, the method returns after * <code>timeout</code> milliseconds even if there is no change in state. * In that case, this method returns <code>false</code>; otherwise it * returns <code>true</code>. * * <p>This method is often used in a loop in combination with * {@link #list(CardTerminals.State) list(State.CARD_INSERTION)}, * for example: * <pre> * TerminalFactory factory = ...; * CardTerminals terminals = factory.terminals(); * while (true) { * for (CardTerminal terminal : terminals.list(CARD_INSERTION)) { * // examine Card in terminal, return if it matches * } * terminals.waitForChange(); * }</pre> * * @param timeout if positive, block for up to <code>timeout</code> * milliseconds; if zero, block indefinitely; must not be negative * @return false if the method returns due to an expired timeout, * true otherwise. * * @throws IllegalStateException if this <code>CardTerminals</code> * object does not contain any terminals * @throws IllegalArgumentException if timeout is negative * @throws CardException if the card operation failed */ public synchronized boolean waitForChange(long timeout) throws CardException { if (timeout < 0) { throw new IllegalArgumentException ("Timeout must not be negative: " + timeout); } if (stateMap == null) { // We need to initialize the state database. // Do that with a recursive call, which will return immediately // because we pass SCARD_STATE_UNAWARE. // After that, proceed with the real call. stateMap = new HashMap<String,ReaderState>(); waitForChange(0); } if (timeout == 0) { timeout = TIMEOUT_INFINITE; } try { String[] readerNames = SCardListReaders(contextId); int n = readerNames.length; if (n == 0) { throw new IllegalStateException("No terminals available"); } int[] status = new int[n]; ReaderState[] readerStates = new ReaderState[n]; for (int i = 0; i < readerNames.length; i++) { String name = readerNames[i]; ReaderState state = stateMap.get(name); if (state == null) { state = new ReaderState(); } readerStates[i] = state; status[i] = state.get(); } status = SCardGetStatusChange(contextId, timeout, status, readerNames); stateMap.clear(); // remove any readers that are no longer available for (int i = 0; i < n; i++) { ReaderState state = readerStates[i]; if(status != null) state.update(status[i]); stateMap.put(readerNames[i], state); } return true; } catch (PCSCException e) { if (e.code == PCSCErrorValues.SCARD_E_TIMEOUT) { return false; } else { throw new CardException("org.poreid.pcscforjava." + "PCSCTerminals.waitForChange " + "PCSCException: " + e.getMessage(), e); } } } static List<CardTerminal> waitForCards( List<? extends CardTerminal> terminals, long timeout, boolean wantPresent) throws CardException { // the argument sanity checks are performed in // javax.smartcardio.TerminalFactory or TerminalImpl long thisTimeout; if (timeout == 0) { timeout = TIMEOUT_INFINITE; thisTimeout = TIMEOUT_INFINITE; } else { // if timeout is not infinite, do the initial call that retrieves // the status with a 0 timeout. Otherwise, we might get incorrect // timeout exceptions (seen on Solaris with PC/SC shim) thisTimeout = 0; } String[] names = new String[terminals.size()]; int i = 0; for (CardTerminal terminal : terminals) { if (terminal instanceof TerminalImpl == false) { throw new IllegalArgumentException ("Invalid terminal type: " + terminal.getClass().getName()); } TerminalImpl impl = (TerminalImpl)terminal; names[i++] = impl.name; } int[] status = new int[names.length]; Arrays.fill(status, SCARD_STATE_UNAWARE); try { while (true) { // note that we pass "timeout" on each native PC/SC call // that means that if we end up making multiple (more than 2) // calls, we might wait too long. // for now assume that is unlikely and not a problem. status = SCardGetStatusChange(contextId, thisTimeout, status, names); thisTimeout = timeout; List<CardTerminal> results = null; for (i = 0; i < names.length; i++) { boolean nowPresent = (status[i] & SCARD_STATE_PRESENT) != 0; if (nowPresent == wantPresent) { if (results == null) { results = new ArrayList<CardTerminal>(); } results.add(implGetTerminal(names[i])); } } if (results != null) { return Collections.unmodifiableList(results); } } } catch (PCSCException e) { if (e.code == PCSCErrorValues.SCARD_E_TIMEOUT) { return Collections.emptyList(); } else { throw new CardException("org.poreid.pcscforjava." + "PCSCTerminals.waitForCard " + "PCSCException: " + e.getMessage(), e); } } } }