/**************************************************************************** * Copyright (C) 2014-2015 TU Darmstadt. * All rights reserved. * Contact: ecsec GmbH (info@ecsec.de) * * This file is part of the Open eCard App. * * GNU General Public License Usage * This file may be used under the terms of the GNU General Public * License version 3.0 as published by the Free Software Foundation * and appearing in the file LICENSE.GPL included in the packaging of * this file. Please review the following information to ensure the * GNU General Public License version 3.0 requirements will be met: * http://www.gnu.org/copyleft/gpl.html. * * Other Usage * Alternatively, this file may be used in accordance with the terms * and conditions contained in a signed written agreement between * you and ecsec GmbH. * ***************************************************************************/ package org.openecard.scio; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Queue; import javax.annotation.Nonnull; import javax.smartcardio.CardException; import javax.smartcardio.CardTerminal; import javax.smartcardio.CardTerminals; import org.openecard.common.ifd.scio.NoSuchTerminal; import org.openecard.common.ifd.scio.SCIOErrorCode; import org.openecard.common.ifd.scio.SCIOException; import org.openecard.common.ifd.scio.SCIOTerminal; import org.openecard.common.ifd.scio.SCIOTerminals; import org.openecard.common.ifd.scio.TerminalState; import org.openecard.common.ifd.scio.TerminalWatcher; import org.openecard.common.util.Pair; import static org.openecard.scio.PCSCExceptionExtractor.getCode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * PC/SC terminals implementation of the SCIOTerminals. * * @author Wael Alkhatib * @author Tobias Wich */ public class PCSCTerminals implements SCIOTerminals { private static final Logger logger = LoggerFactory.getLogger(PCSCTerminals.class); private static final long WAIT_DELTA = 1500; private final PCSCFactory terminalFactory; private CardTerminals terminals; PCSCTerminals(@Nonnull PCSCFactory terminalFactory) { this.terminalFactory = terminalFactory; loadTerminals(); } private void reloadFactory() { terminalFactory.reloadPCSC(); loadTerminals(); } private void loadTerminals() { terminals = terminalFactory.getRawFactory().terminals(); } @Override public List<SCIOTerminal> list() throws SCIOException { return list(State.ALL); } @Override public List<SCIOTerminal> list(State state) throws SCIOException { return list(state, true); } public List<SCIOTerminal> list(State state, boolean firstTry) throws SCIOException { logger.trace("Entering list()."); try { CardTerminals.State scState = convertState(state); // get terminals with the specified state from the SmartcardIO List<CardTerminal> scList = terminals.list(scState); ArrayList<SCIOTerminal> list = convertTerminals(scList); logger.trace("Leaving list()."); return Collections.unmodifiableList(list); } catch (CardException ex) { if (getCode(ex) == SCIOErrorCode.SCARD_E_NO_READERS_AVAILABLE) { logger.debug("No reader available exception."); return Collections.emptyList(); } else if (getCode(ex) == SCIOErrorCode.SCARD_E_NO_SERVICE) { if (firstTry) { logger.debug("No service available exception, reloading PCSC and trying again."); reloadFactory(); return list(state, false); } else { logger.debug("No service available exception, returning empty list."); return Collections.emptyList(); } } String msg = "Failed to retrieve list from terminals instance."; logger.error(msg, ex); throw new SCIOException(msg, getCode(ex), ex); } } private CardTerminals.State convertState(@Nonnull State state) { switch (state) { case ALL: return CardTerminals.State.ALL; case CARD_PRESENT: return CardTerminals.State.CARD_PRESENT; case CARD_ABSENT: return CardTerminals.State.CARD_ABSENT; default: logger.error("Unknown state type requested: {}", state); throw new IllegalArgumentException("Invalid state type requested."); } } private SCIOTerminal convertTerminal(@Nonnull CardTerminal scTerminal) { // TODO: check if we should only return the same instances (caching) here return new PCSCTerminal(scTerminal); } private ArrayList<SCIOTerminal> convertTerminals(List<CardTerminal> terminals) { ArrayList<SCIOTerminal> result = new ArrayList<>(terminals.size()); for (CardTerminal t : terminals) { result.add(convertTerminal(t)); } return result; } @Override public SCIOTerminal getTerminal(@Nonnull String name) throws NoSuchTerminal { CardTerminal t = terminals.getTerminal(name); if (t == null) { throw new NoSuchTerminal(String.format("Terminal '%s' does not exist in the system.", name)); } else { return convertTerminal(t); } } @Override public TerminalWatcher getWatcher() throws SCIOException { return new PCSCWatcher(this); } /// /// Terminal Watcher part /// private static class PCSCWatcher implements TerminalWatcher { private final PCSCTerminals parent; private final PCSCTerminals own; private Queue<StateChangeEvent> pendingEvents; private Collection<String> terminals; private Collection<String> cardPresent; public PCSCWatcher(@Nonnull PCSCTerminals parent) { this.parent = parent; this.own = new PCSCTerminals(parent.terminalFactory); } @Override public SCIOTerminals getTerminals() { // the terminal used to create the watcher return parent; } @Override public List<TerminalState> start() throws SCIOException { logger.trace("Entering start()."); if (pendingEvents != null) { throw new IllegalStateException("Trying to initialize already initialized watcher instance."); } pendingEvents = new LinkedList<>(); terminals = new HashSet<>(); cardPresent = new HashSet<>(); try { // call wait for change and directly afterwards get current list of cards // with a bit of luck no change has happened in between and the list is coherent own.terminals.waitForChange(1); List<CardTerminal> javaTerminals = own.terminals.list(); ArrayList<TerminalState> result = new ArrayList<>(javaTerminals.size()); // fill sets according to state of the terminals logger.debug("Detecting initial terminal status."); for (CardTerminal next : javaTerminals) { String name = next.getName(); boolean cardInserted = next.isCardPresent(); logger.debug("Terminal='{}' cardPresent={}", name, cardInserted); terminals.add(name); if (cardInserted) { cardPresent.add(name); result.add(new TerminalState(name, true)); } else { result.add(new TerminalState(name, false)); } } // return list of our terminals logger.trace("Leaving start() with {} states.", result.size()); return Collections.unmodifiableList(result); } catch (CardException ex) { if (getCode(ex) == SCIOErrorCode.SCARD_E_NO_READERS_AVAILABLE) { logger.debug("No reader available exception."); return Collections.emptyList(); } else if (getCode(ex) == SCIOErrorCode.SCARD_E_NO_SERVICE) { logger.debug("No service available exception, reloading PCSC and returning empty list."); parent.reloadFactory(); own.loadTerminals(); return Collections.emptyList(); } String msg = "Failed to retrieve status from the PCSC system."; logger.error(msg, ex); throw new SCIOException(msg, getCode(ex), ex); } catch (IllegalStateException ex) { logger.debug("No reader available exception."); return Collections.emptyList(); } } @Override public StateChangeEvent waitForChange() throws SCIOException { return waitForChange(0); } @Override public StateChangeEvent waitForChange(long timeout) throws SCIOException { logger.trace("Entering waitForChange()."); if (pendingEvents == null) { throw new IllegalStateException("Calling wait on uninitialized watcher instance."); } // try to return any present events first StateChangeEvent nextEvent = pendingEvents.poll(); if (nextEvent != null) { logger.trace("Leaving waitForChange() with queued event."); return nextEvent; } else { Pair<Boolean, Boolean> waitResult; try { waitResult = internalWait(timeout); } catch (CardException ex) { String msg = "Error while waiting for a state change in the terminals."; logger.error(msg, ex); throw new SCIOException(msg, getCode(ex), ex); } boolean changed = waitResult.p1; boolean error = waitResult.p2; if (! changed) { logger.trace("Leaving waitForChange() with no event."); return new StateChangeEvent(); } else { // something has changed, retrieve actual terminals from the system and see what has changed Collection<String> newTerminals = new HashSet<>(); Collection<String> newCardPresent = new HashSet<>(); // only ask for terminals if there is no error if (! error) { try { List<CardTerminal> newStates = own.terminals.list(); for (CardTerminal next : newStates) { String name = next.getName(); newTerminals.add(name); if (next.isCardPresent()) { newCardPresent.add(name); } } } catch (CardException ex) { String msg = "Failed to retrieve status of the observed terminals."; logger.error(msg, ex); throw new SCIOException(msg, getCode(ex), ex); } } // calculate what has actually happened // removed cards Collection<String> cardRemoved = subtract(cardPresent, newCardPresent); Collection<StateChangeEvent> crEvents = createEvents(EventType.CARD_REMOVED, cardRemoved); // removed terminals Collection<String> termRemoved = subtract(terminals, newTerminals); Collection<StateChangeEvent> trEvents = createEvents(EventType.TERMINAL_REMOVED, termRemoved); // added terminals Collection<String> termAdded = subtract(newTerminals, terminals); Collection<StateChangeEvent> taEvents = createEvents(EventType.TERMINAL_ADDED, termAdded); // added cards Collection<String> cardAdded = subtract(newCardPresent, cardPresent); Collection<StateChangeEvent> caEvents = createEvents(EventType.CARD_INSERTED, cardAdded); // update internal status with the calculated state terminals = newTerminals; cardPresent = newCardPresent; pendingEvents.addAll(crEvents); pendingEvents.addAll(trEvents); pendingEvents.addAll(taEvents); pendingEvents.addAll(caEvents); // use remove so we get an exception when no event has been recorded // this would mean our algorithm is corrupt logger.trace("Leaving waitForChange() with fresh event."); return pendingEvents.remove(); } } } private void sleep(long millis) throws SCIOException { try { Thread.sleep(millis); } catch (InterruptedException ex2) { String msg = "Wait interrupted by another thread."; throw new SCIOException(msg, SCIOErrorCode.SCARD_E_SERVICE_STOPPED); } } /** * Wait for events in the system. * The SmartcardIO wait function only reacts on card events, new and removed terminals go unseen. in order to * fix this, we wait only a short time and check the terminal list periodically. * * @param timeout Timeout values as in {@link #waitForChange(long)}. * @return The first value is the changed flag . It is {@code true} if a change the terminals happened, * {@code false} if a timeout occurred. <br> * The second value is the error flag. It is {@code true} if an error was used to indicate that no terminals * are connected, {@code false} otherwise. * @throws CardException Thrown if any error related to the SmartcardIO occured. * @throws SCIOException Thrown if the thread was interrupted. Contains the code * {@link SCIOErrorCode#SCARD_E_SERVICE_STOPPED}. */ private Pair<Boolean, Boolean> internalWait(long timeout) throws CardException, SCIOException { // the SmartcardIO wait function only reacts on card events, new and removed terminals go unseen // to fix this, we wait only a short time and check the terminal list periodically if (timeout < 0) { throw new IllegalArgumentException("Negative timeout value given."); } else if (timeout == 0) { timeout = Long.MAX_VALUE; } while (true) { if (timeout == 0) { // waited for all time and nothing happened return new Pair<>(false, false); } // calculate next wait slice long waitTime; if (timeout < WAIT_DELTA) { waitTime = timeout; timeout = 0; } else { timeout = timeout - WAIT_DELTA; waitTime = WAIT_DELTA; } try { // check if there is something new on the card side // due to the wait call blocking every other smartcard operation, we only wait for the actual events // very shortly and sleep for the rest of the time boolean change = own.terminals.waitForChange(1); if (change) { return new Pair<>(true, false); } sleep(waitTime); // try again after sleeping change = own.terminals.waitForChange(1); if (change) { return new Pair<>(true, false); } } catch (CardException ex) { switch (getCode(ex)) { case SCARD_E_NO_SERVICE: logger.debug("No service available exception, reloading PCSC."); parent.reloadFactory(); own.loadTerminals(); case SCARD_E_NO_READERS_AVAILABLE: // send events that everything is removed if there are any terminals connected right now if (! terminals.isEmpty()) { return new Pair<>(true, true); } else { logger.debug("Waiting for PCSC system to become available again."); // if nothing changed, wait a bit and try again sleep(waitTime); continue; } default: throw ex; } } catch (IllegalStateException ex) { // send events that everything is removed if there are any terminals connected right now if (! terminals.isEmpty()) { return new Pair<>(true, true); } else { logger.debug("Waiting for PCSC system to become available again."); // if nothing changed, wait a bit and try again sleep(waitTime); continue; } } // check if there is something new on the terminal side ArrayList<CardTerminal> currentTerms = new ArrayList<>(own.terminals.list()); if (currentTerms.size() != terminals.size()) { return new Pair<>(true, false); } // same size, but still compare terminal names HashSet<String> newTermNames = new HashSet<>(); for (CardTerminal next : currentTerms) { newTermNames.add(next.getName()); } int sizeBefore = newTermNames.size(); if (sizeBefore != terminals.size()) { return new Pair<>(false, false); } newTermNames.addAll(terminals); int sizeAfter = newTermNames.size(); if (sizeBefore != sizeAfter) { return new Pair<>(false, false); } } } private static <T> Collection<T> subtract(Collection<T> a, Collection<T> b) { HashSet<T> result = new HashSet<>(a); result.removeAll(b); return result; } private static Collection<StateChangeEvent> createEvents(EventType type, Collection<String> list) { Collection<StateChangeEvent> result = new ArrayList<>(list.size()); for (String next : list) { result.add(new StateChangeEvent(type, next)); } return result; } } }