/*
*/
/**
*
* @author Sebastien Riou
*/
package uk.co.nimp.scard;
import com.atolsystems.atolutilities.AStringUtilities;
import com.atolsystems.atolutilities.StopRequestFromUserException;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.smartcardio.CardException;
import javax.smartcardio.CardNotPresentException;
import javax.swing.JOptionPane;
import uk.co.nimp.scard.log.ScardPrintStreamLogHandler;
import uk.co.nimp.smartcard.AnswerToReset;
import uk.co.nimp.smartcard.Apdu;
import uk.co.nimp.smartcard.UnexpectedCardResponseException;
public abstract class GenericTerminal implements WithBasicLoggers {
public final static int PROTOCOL_UNDEFINED= 0x00000000;
public final static int PROTOCOL_T_0= 0x00000001;
public final static int PROTOCOL_T_1= 0x00000002;
public final static int PROTOCOL_T_15= 0x00008000;
public final static int PROTOCOL_T_CL_A= 0x00010000;
public final static int PROTOCOL_T_CL_B= 0x00020000;
public final static int PROTOCOL_DIRECT= 0x08000000;
public final static int PROTOCOL_ANY_STD_CL= 0x00030000;
public final static int PROTOCOL_ANY_STD_CONTACT= 0x00000003;
public final static int PROTOCOL_ANY_STD= 0x00030003;
public final static int PROTOCOL_ANY= 0x0FFFFFFF;
public final static String PROTOCOL_NAME_UNDEFINED="T=Undefined";
public final static String PROTOCOL_NAME_T_0="T=0";
public final static String PROTOCOL_NAME_T_1="T=1";
public final static String PROTOCOL_NAME_T_15="T=15";
public final static String PROTOCOL_NAME_T_CL_A="T=CLA";
public final static String PROTOCOL_NAME_T_CL_B="T=CLB";
public final static String PROTOCOL_NAME_DIRECT="T=Direct";
public final static String PROTOCOL_NAME_ANY_STD_CL="T=CL";
public final static String PROTOCOL_NAME_ANY="*";
public final static int ACTIVATION_FORCE_COLD = 0x01;
public final static int ACTIVATION_FORCE_WARM = 0x02;
public final static int ACTIVATION_ANY = 0x04;
public final static int COM_RUN = 0x01;
public final static int COM_STEP = 0x02;
protected boolean autoGetResponse =false;
boolean unsupportedExceptionEnabled;
Set<ScardLogHandler> logHandlers;
final protected String name;
protected State state;
protected AnswerToReset atr;
protected int protocol;
protected boolean connected;
protected int activationCounter=0;;
protected boolean negociateComSpeed = true;
protected Map<String,String> env;
public GenericTerminal(String name) {
this.unsupportedExceptionEnabled = true;
this.logHandlers = new HashSet<ScardLogHandler>();
this.name = name;
state=State.UNDEFINED;
protocol=PROTOCOL_UNDEFINED;
logsEnabled=true;
player.setCheckConnection(false);
env=new HashMap<String,String>();
}
public Map<String, String> getEnv() {
return env;
}
public String getEnvVariable(String name){
String out=env.get(name);
if(null==out) out="";
return out;
}
public int getEnvSize() {
return env.size();
}
public String removeEnvVariable(String name) {
return env.remove(name);
}
public String putEnvVariable(String name, String value) {
return env.put(name, value);
}
public boolean containsEnvVariable(String name) {
return env.containsKey(name);
}
public void clearEnv() {
env.clear();
}
//Terminal with timers should override this method to have better accuracy
public void delay(long delayNs){
long start=System.nanoTime();
long now;
do{
now=System.nanoTime();
}while(now-start<delayNs);
}
/**
* Get the connection counter for this terminal.
* The counter is incremented each time the card is activated
* @return the value of the counter
*/
public int getActivationCounter() {
return activationCounter;
}
static public String getProtocolName(int protocol){
switch(protocol){
case PROTOCOL_UNDEFINED:
return PROTOCOL_NAME_UNDEFINED;
case PROTOCOL_T_0:
return PROTOCOL_NAME_T_0;
case PROTOCOL_T_1:
return PROTOCOL_NAME_T_1;
case PROTOCOL_T_15:
return PROTOCOL_NAME_T_15;
case PROTOCOL_T_CL_A:
return PROTOCOL_NAME_T_CL_A;
case PROTOCOL_T_CL_B:
return PROTOCOL_NAME_T_CL_B;
case PROTOCOL_DIRECT:
return PROTOCOL_NAME_DIRECT;
case PROTOCOL_ANY_STD_CL:
return PROTOCOL_NAME_ANY_STD_CL;
case PROTOCOL_ANY:
return PROTOCOL_NAME_ANY;
default:
throw new IllegalArgumentException("protocol = "+Integer.toHexString(protocol));
}
}
static public int getProtocolCode(String protocol){
if(protocol.equals(PROTOCOL_NAME_T_0))
return PROTOCOL_T_0;
if(protocol.equals(PROTOCOL_NAME_T_1))
return PROTOCOL_T_1;
if(protocol.equals(PROTOCOL_NAME_T_15))
return PROTOCOL_T_15;
if(protocol.equals(PROTOCOL_NAME_T_CL_A))
return PROTOCOL_T_CL_A;
if(protocol.equals(PROTOCOL_NAME_T_CL_B))
return PROTOCOL_T_CL_B;
if(protocol.equals(PROTOCOL_NAME_DIRECT))
return PROTOCOL_DIRECT;
if(protocol.equals(PROTOCOL_NAME_ANY_STD_CL))
return PROTOCOL_ANY_STD_CL;
if(protocol.equals(PROTOCOL_NAME_ANY))
return PROTOCOL_ANY;
throw new IllegalArgumentException("protocol = "+protocol);
}
/**
* Returns the unique name of this terminal.
*
* @return the unique name of this terminal.
*/
public String getName() {
return name;
}
public AnswerToReset getAtr() {
return atr;
}
public int getProtocol(){
return protocol;
}
/**
* Returns whether a card is present in this terminal.
*
* @return whether a card is present in this terminal.
*
* @throws CardException if the status could not be determined
*/
public abstract boolean isCardPresent() throws ScardException;
/**
* Waits until a card is present in this terminal or the timeout
* expires. If the method returns due to an expired timeout, it returns
* false. Otherwise it return true.
*
* <P>If a card is present in this terminal when this
* method is called, it returns immediately.
*
* @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 IllegalArgumentException if timeout is negative
* @throws CardException if the operation failed
*/
public abstract boolean waitForCardPresent(long timeout) throws ScardException;
/**
* Waits until a card is absent in this terminal or the timeout
* expires. If the method returns due to an expired timeout, it returns
* false. Otherwise it return true.
*
* <P>If no card is present in this terminal when this
* method is called, it returns immediately.
*
* @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 IllegalArgumentException if timeout is negative
* @throws CardException if the operation failed
*/
public abstract boolean waitForCardAbsent(long timeout) throws ScardException;
public synchronized void coldConnect()throws ScardException, CardNotPresentException{
connect(PROTOCOL_ANY, ACTIVATION_FORCE_COLD);
}
public synchronized void warmConnect()throws ScardException, CardNotPresentException{
connect(PROTOCOL_ANY, ACTIVATION_FORCE_WARM);
}
public synchronized void connect(int activation)throws ScardException,CardNotPresentException{
connect(PROTOCOL_ANY, activation);
}
public synchronized void connect(String protocol)throws ScardException,CardNotPresentException{
connect(getProtocolCode(protocol), ACTIVATION_FORCE_COLD);
}
//cold/warm reset
public abstract void connectImpl(int protocol, int activation)throws ScardException, CardNotPresentException ;
public synchronized void connect(int protocol, int activation)throws ScardException, CardNotPresentException {
if((GenericTerminal.ACTIVATION_FORCE_WARM==activation) && (false==connected))
throw new ScardException("Warm reset cannot be performed when the card is not activated already.");
beforeConnectScript.play();
boolean localConnected=false;
switch(activation){
case ACTIVATION_FORCE_COLD:
logLine(ScardLogHandler.LOG_CARD_EVENT,"COLD CONNECT");
break;
case ACTIVATION_FORCE_WARM:
logLine(ScardLogHandler.LOG_CARD_EVENT,"WARM CONNECT");
break;
default:
logLine(ScardLogHandler.LOG_CARD_EVENT,"CONNECT");
}
try{
connectImpl(protocol, activation);
activationCounter++;
localConnected=true;
} catch(ScardException e){
logLine(ScardLogHandler.LOG_ERROR,"Failed to connect: "+e.getMessage());
if(null!=e.getCause())
logLine(ScardLogHandler.LOG_ERROR,e.getCause().getMessage());
throw e;
} catch(CardNotPresentException e){
logLine(ScardLogHandler.LOG_ERROR,"Failed to connect: "+e.getMessage());
if(null!=e.getCause())
logLine(ScardLogHandler.LOG_ERROR,e.getCause().getMessage());
throw e;
} catch(Exception e){
logLine(ScardLogHandler.LOG_ERROR,"Failed to connect: "+e.getMessage());
if(null!=e.getCause())
logLine(ScardLogHandler.LOG_ERROR,e.getCause().getMessage());
throw new RuntimeException(e);
} finally {
connected = localConnected;
}
afterConnectScript.play();
}
public boolean isNegociateComSpeed() {
return negociateComSpeed;
}
public void setNegociateComSpeed(boolean autoPps) {
this.negociateComSpeed = autoPps;
}
class DummyStatusDisplayer implements StatusDisplayer{
public void setText(String msg) {
//do nothing
}
}
ScriptPlayer player=new ScriptPlayer(new DummyStatusDisplayer());
public boolean isUserCancelled() {
return player.isUserCancelled();
}
boolean errorOccured=false;
public boolean isErrorOccured() {
return player.isErrorOccured()|errorOccured;
}
final EventScript beforeDisconnectScript=new EventScript();//can be used to dump chip state before disconnection or set a parameter for next session
final EventScript beforeConnectScript=new EventScript();//can be used to set voltage and frequency
final EventScript afterConnectScript=new EventScript();//can be used to init the chip with different conditions before running a test
final EventScript unexpectedCardResponseScript=new EventScript();//allow diagnostic
//final EventScript unexpectedCardResponseResumingScript=new EventScript();//allow to resume operations
final EventScript apduTimeoutScript=new EventScript();//will be launched after forced disconnection
final EventScript apduMiscErrorScript=new EventScript();//can be use to resume operation after an error in one apdu. script must first establish connection.
public void setBeforeDisconnectScript(List<? extends Object> script){
beforeDisconnectScript.setScript(script);
}
public void setBeforeConnectScript(List<? extends Object> script){
beforeConnectScript.setScript(script);
}
public void setAfterConnectScript(List<? extends Object> script){
afterConnectScript.setScript(script);
}
public void setUnexpectedCardResponseScript(List<? extends Object> script){
unexpectedCardResponseScript.setScript(script);
}
/*public void setUnexpectedCardResponseResumingScript(List<? extends Object> script){
unexpectedCardResponseResumingScript.setScript(script);
}*/
public void setApduTimeoutScript(List<? extends Object> script){
apduTimeoutScript.setScript(script);
}
public void setApduMiscErrorScript(List<? extends Object> script){
apduMiscErrorScript.setScript(script);
}
public void playUnexpectedCardResponseScript() throws ScardException, StopRequestFromUserException, UnexpectedCardResponseException, CardNotPresentException {
unexpectedCardResponseScript.play();
}
class EventScript{
List<? extends Object> script;
boolean running;
void setScript(List<? extends Object> script){
while(running){
Thread.yield();
}
this.script=script;
}
void play() throws ScardException, StopRequestFromUserException, UnexpectedCardResponseException, CardNotPresentException{
if(running){
//System.out.println("EventScript already running! "+this.getClass());
return;
}
running=true;
try{
//System.out.println("Start EventScript "+this.getClass());
player.play(GenericTerminal.this, script);
//System.out.println("End of EventScript "+this.getClass());
}catch (IOException ex) {
throw new RuntimeException(ex);
}finally{
running=false;
}
}
List<? extends Object> getScript() {
return script;
}
}
protected void playEventScript(List<? extends Object> eventScript){
}
//power off
abstract protected void disconnectImpl() throws ScardException;
public synchronized void disconnect() throws ScardException, StopRequestFromUserException, UnexpectedCardResponseException, CardNotPresentException{
beforeDisconnectScript.play();
log(ScardLogHandler.LOG_CARD_EVENT,"DISCONNECT\n");
connected=false;
disconnectImpl();
}
abstract protected void forceDisconnectImpl() throws ScardException;
public void forceDisconnection() throws ScardException{
log(ScardLogHandler.LOG_CARD_EVENT,"FORCED DISCONNECTION\n");
connected=false;
forceDisconnectImpl();
}
Long lastApduTotalTime;
public long getLastApduTotalTime(){
return lastApduTotalTime;
}
abstract public void sendApduImpl(Apdu apdu)throws ScardException, UnexpectedCardResponseException;
public /*synchronized*/ void sendApdu(Apdu apdu)throws ScardException, UnexpectedCardResponseException, CardNotPresentException{
boolean localErrorHappened=true;
try{
try{
if(apdu.getExpectedLe()>0x100)
throw new RuntimeException("le>0x100 not supported yet");
logCommand(apdu);
Timer apduTimer=null;
ApduTimeoutTask apduTimeoutTask=null;
if(timeoutMs!=0){
apduTimer=new Timer();
apduTimeoutTask=new ApduTimeoutTask();
apduTimer.schedule(apduTimeoutTask, timeoutMs);
}
long start=System.nanoTime();
try{
sendApduImpl(apdu);
}catch(Throwable e){
throw new ScardException("Exception occured during "+apdu,e);
}
long end=System.nanoTime();
boolean log=true;
if(timeoutMs!=0){
apduTimer.cancel();
if(apduTimeoutTask.expired){
if(null==apduTimeoutScript)
throw new ScardException("Timeout for this apdu expired (timeout was "+timeoutMs+"ms)");
log=false;
apduTimeoutScript.play();
}
}
if(log){
lastApduTotalTime=new Long(end-start);
logResponse(apdu);
}
}catch(ScardException e){
log(e);
throw e;
}catch(RuntimeException e){
log(e);
throw e;
}
try {
apdu.checkResponse();
} catch (UnexpectedCardResponseException e) {
logUnexpectedResponse(apdu,e);
unexpectedCardResponseScript.play();
//if(null==unexpectedCardResponseResumingScript)
throw e;
//unexpectedCardResponseResumingScript.play();
}
localErrorHappened=false;
}finally{
if(localErrorHappened){
errorOccured=true;
if(null!=apduMiscErrorScript.getScript()){
apduMiscErrorScript.play();
return;//Trick to resume operations by cancelling any thrown exception
}
}
}
}
protected int timeoutMs=0;
//abstract void setApduTimeoutImpl(int timeoutMs)throws ScardException;
public void setApduTimeout(int timeoutMs)throws ScardException{
if(timeoutMs<0)
throw new IllegalArgumentException("timeoutNs<0");
this.timeoutMs=timeoutMs;
}
public int getApduTimeout(){
return timeoutMs;
}
/*public void sendApdu(Apdu apdu, int timeoutMs)throws ScardException, UnexpectedCardResponseException{
if(apdu.getExpectedLe()>0x100)
throw new RuntimeException("le>0x100 not supported yet");
logCommand(apdu);
Timer apduTimer=new Timer();
ApduTimeoutTask apduTimeoutTask=new ApduTimeoutTask();
apduTimer.schedule(apduTimeoutTask, timeoutMs);
long start=System.nanoTime();
sendApduImpl(apdu);
long end=System.nanoTime();
apduTimer.cancel();
lastApduTotalTime=new Long(end-start);
if(apduTimeoutTask.expired)
throw new ScardException("Timeout for this apdu expired (timeout was "+timeoutMs+"ms)");
logResponse(apdu);
try {
apdu.checkResponse();
} catch (UnexpectedCardResponseException e) {
logUnexpectedResponse(apdu,e);
throw e;
}
}*/
class ApduTimeoutTask extends TimerTask{
public boolean expired=false;
@Override
public void run() {
expired=true;
try {
forceDisconnection();
} catch (ScardException ex) {
Logger.getLogger(GenericTerminal.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
/**
* As all terminals do not have the same capabilities, some test actions might not be
* supported by a given terminal. In this case, the terminal should send a
* <code>UnsupportedOperationException</code>.
* This function allows to choose the policy in case of such exception
* @param unsupportedExceptionEnabled if true, exception are raised normally. if false, no <code>unsupportedException</code>
* will be generated but will be logged with the WARNING level
*/
public void setUnsupportedExceptionEnabled(boolean unsupportedExceptionEnabled) {
this.unsupportedExceptionEnabled = unsupportedExceptionEnabled;
}
public boolean isUnsupportedExceptionEnabled() {
return unsupportedExceptionEnabled;
}
public void handleUnsupportedOperation(){
UnsupportedOperationException e=new UnsupportedOperationException();
if(unsupportedExceptionEnabled)
throw e;
logAsWarning(e);
}
synchronized public void logEnv(String linePrefix) {
for(String varName:env.keySet()){
for(ScardLogHandler h : logHandlers){
h.logln(ScardLogHandler.LOG_INFO, linePrefix+varName+": "+env.get(varName));
}
}
}
synchronized public void log(UnexpectedCardResponseException e){
for(ScardLogHandler h : logHandlers){
h.log(ScardLogHandler.LOG_ERROR, e.getMessage());
}
}
synchronized public void logAtr(AnswerToReset atr){
for(ScardLogHandler h : logHandlers){
h.logAtr(atr);
}
}
synchronized public void logCommand(Apdu apdu){
for(ScardLogHandler h : logHandlers){
h.logCommand(apdu);
}
}
synchronized public void logResponse(Apdu apdu){
for(ScardLogHandler h : logHandlers){
h.logResponse(apdu);
}
}
synchronized public void logExpectedResponse(Apdu apdu){
for(ScardLogHandler h : logHandlers){
h.logExpectedResponse(apdu);
}
}
synchronized public void logUnexpectedResponse(Apdu apdu, UnexpectedCardResponseException e) {
for(ScardLogHandler h : logHandlers){
h.logUnexpectedResponse(apdu,e);
}
}
@Override
synchronized public void log(Throwable e) {
for(ScardLogHandler h : logHandlers){
h.log(ScardLogHandler.LOG_ERROR, e);
}
}
@Override
synchronized public void logAsWarning(Throwable e) {
for(ScardLogHandler h : logHandlers){
h.log(ScardLogHandler.LOG_WARNING, e);
}
}
@Override
synchronized public void log(int eventType, String msg){
for(ScardLogHandler h : logHandlers){
h.log(eventType, msg);
}
}
@Override
synchronized public void logLine(int eventType, String msg){
for(ScardLogHandler h : logHandlers){
//h.log(eventType, msg+"\n");
h.logln(eventType, msg);
}
}
synchronized public void addLogHandler(ScardLogHandler log) {
log.open();
logHandlers.add(log);
}
synchronized public void closeLog(ScardLogHandler log) throws IOException{
log.close();
logHandlers.remove(log);
}
public synchronized void removeLog(ScardLogHandler log) throws IOException{
logHandlers.remove(log);
}
@Override
public synchronized void closeAllLogs() throws IOException{
for(ScardLogHandler h : logHandlers){
h.close();
}
logHandlers.clear();
}
@Override
public synchronized void flushAllLogs() throws IOException{
for(ScardLogHandler h : logHandlers){
h.flush();
}
}
@Override
public synchronized boolean isLoggingOnSystemOut(){
for(ScardLogHandler log:logHandlers){
if(log instanceof ScardPrintStreamLogHandler){
ScardPrintStreamLogHandler scardPrintStreamLogHandler=(ScardPrintStreamLogHandler) log;
if(System.out==scardPrintStreamLogHandler.getPrintStream())
return true;
}
}
return false;
}
protected boolean logsEnabled;
@Override
public boolean areLogsEnabled() {
return logsEnabled;
}
@Override
public synchronized void setLogsEnabled(boolean enable) {
for(ScardLogHandler h : logHandlers){
h.enabled=enable;
}
this.logsEnabled = enable;
}
public synchronized Set<ScardLogHandler> getLogs(){
return logHandlers;
}
public boolean isConnected() {
return connected;
}
@Override
public String toString(){
return name;
}
@Override
public synchronized void clearLogs() {
for(ScardLogHandler h : logHandlers){
h.clear();
}
}
transient long deadTime;
public long getDeadTime() {
return deadTime+player.getDeadTime();
}
public synchronized void checkConnection() throws ScardException, CardNotPresentException{
int choice = JOptionPane.YES_OPTION;
String nl=AStringUtilities.systemNewLine;
if(false==this.isCardPresent()){
String msg = "Card not detected !"+nl+nl;
msg += "Do you want wait for the card and activate it using first proposed protocol ?";
long start=System.nanoTime();
choice = JOptionPane.showConfirmDialog(null, msg, "Card not detected", JOptionPane.YES_NO_CANCEL_OPTION);
if (JOptionPane.YES_OPTION == choice) {
this.waitForCardPresent(0);
deadTime+=System.nanoTime()-start;
this.coldConnect();
}else
deadTime+=System.nanoTime()-start;
}else if(false==this.isConnected()){
String msg = "Card not activated yet !"+nl+nl;
msg += "Do you want to activate the card using first proposed protocol ?";
long start=System.nanoTime();
choice = JOptionPane.showConfirmDialog(null, msg, "Card not activated", JOptionPane.YES_NO_CANCEL_OPTION);
deadTime+=System.nanoTime()-start;
if (JOptionPane.YES_OPTION == choice) {
this.coldConnect();
}
}
if (JOptionPane.CANCEL_OPTION == choice) {
throw new StopRequestFromUserException("Operation cancelled by user.");
}
}
/**
* Enumeration of attributes of a CardTerminal.
*
*/
public static enum State {
/**
* any state.
*/
ALL,
/**
* CardTerminals in which a card is present.
*/
CARD_PRESENT,
/**
* CardTerminals in which a card is not present.
*/
CARD_ABSENT,
/**
* CardTerminals for which a card insertion was detected during the
* latest call to {@linkplain GenericTerminalManager list()}
* call.
*/
CARD_INSERTION,
/**
* CardTerminals for which a card removal was detected during the
* latest call to {@linkplain GenericTerminalManager list()}
* call.
*/
CARD_REMOVAL,
/**
* The state of the terminal is not known yet.
*/
UNDEFINED,
}
}