forked from public/pysim
pySim: Add support for PC/SC readers
Support for PC/SC can be used by simply specifying the '-p 0' option at the command line (indicating PC/SC reader 0, i.e. the first)
This commit is contained in:
172
pySim.py
172
pySim.py
@@ -8,6 +8,7 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Sylvain Munaut <tnt@246tNt.com>
|
# Copyright (C) 2009 Sylvain Munaut <tnt@246tNt.com>
|
||||||
|
# Copyright (C) 2010 Harald Welte <laforge@gnumonks.org>
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
@@ -27,6 +28,11 @@ import time
|
|||||||
import serial
|
import serial
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from smartcard.Exceptions import NoCardException
|
||||||
|
from smartcard.System import readers
|
||||||
|
from smartcard.CardConnectionObserver import ConsoleCardConnectionObserver
|
||||||
|
from smartcard.util import toBytes
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# Utils
|
# Utils
|
||||||
@@ -38,6 +44,9 @@ def h2b(s):
|
|||||||
def b2h(s):
|
def b2h(s):
|
||||||
return ''.join(['%02x'%ord(x) for x in s])
|
return ''.join(['%02x'%ord(x) for x in s])
|
||||||
|
|
||||||
|
def i2h(s):
|
||||||
|
return ''.join(['%02x'%(x) for x in s])
|
||||||
|
|
||||||
def swap_nibbles(s):
|
def swap_nibbles(s):
|
||||||
return ''.join([x+y for x,y in zip(s[1::2], s[0::2])])
|
return ''.join([x+y for x,y in zip(s[1::2], s[0::2])])
|
||||||
|
|
||||||
@@ -52,7 +61,7 @@ def scan_all(base, start=0, end=65536):
|
|||||||
s.select_file(base)
|
s.select_file(base)
|
||||||
for v in range(start, end):
|
for v in range(start, end):
|
||||||
try:
|
try:
|
||||||
data, sw = s.send_apdu('a0a4000002%04x' % v)
|
data, sw = s._tp.send_apdu('a0a4000002%04x' % v)
|
||||||
if sw == '9000':
|
if sw == '9000':
|
||||||
rv.append( (v, data, sw) )
|
rv.append( (v, data, sw) )
|
||||||
s.select_file(base)
|
s.select_file(base)
|
||||||
@@ -66,7 +75,7 @@ def scan_all(base, start=0, end=65536):
|
|||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# Main serial communication
|
# Transport Link for serial (RS232) based readers included with simcard
|
||||||
# -------------------------------------------------------------------------{{{
|
# -------------------------------------------------------------------------{{{
|
||||||
|
|
||||||
import exceptions
|
import exceptions
|
||||||
@@ -78,7 +87,6 @@ class ProtocolError(exceptions.Exception):
|
|||||||
|
|
||||||
|
|
||||||
class SerialSimLink(object):
|
class SerialSimLink(object):
|
||||||
|
|
||||||
def __init__(self, device='/dev/ttyUSB0', baudrate=9600, rst='-rts', debug=False):
|
def __init__(self, device='/dev/ttyUSB0', baudrate=9600, rst='-rts', debug=False):
|
||||||
self._sl = serial.Serial(
|
self._sl = serial.Serial(
|
||||||
port = device,
|
port = device,
|
||||||
@@ -260,10 +268,98 @@ class SerialSimLink(object):
|
|||||||
raise RuntimeError("SW match failed ! Expected %s and got %s." % (sw.lower(), rv[1]))
|
raise RuntimeError("SW match failed ! Expected %s and got %s." % (sw.lower(), rv[1]))
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Transport Link for a standard PC/SC reader
|
||||||
|
# -------------------------------------------------------------------------{{{
|
||||||
|
|
||||||
|
class PcscSimLink(object):
|
||||||
|
def __init__(self, reader_number=0, observer=0):
|
||||||
|
r = readers();
|
||||||
|
try:
|
||||||
|
self._con = r[reader_number].createConnection()
|
||||||
|
if (observer):
|
||||||
|
observer = ConsoleCardConnectionObserver()
|
||||||
|
self._con.addObserver(observer)
|
||||||
|
self._con.connect()
|
||||||
|
#print r[reader_number], b2h(self._con.getATR())
|
||||||
|
except NoCardException:
|
||||||
|
raise NoCardError()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self._con.disconnect()
|
||||||
|
return
|
||||||
|
|
||||||
|
def reset_card(self):
|
||||||
|
self._con.disconnect()
|
||||||
|
try:
|
||||||
|
self._con.connect()
|
||||||
|
except NoCardException:
|
||||||
|
raise NoCardError()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def send_apdu_raw(self, pdu):
|
||||||
|
"""send_apdu_raw(pdu): Sends an APDU with minimal processing
|
||||||
|
|
||||||
|
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
|
||||||
|
return : tuple(data, sw), where
|
||||||
|
data : string (in hex) of returned data (ex. "074F4EFFFF")
|
||||||
|
sw : string (in hex) of status word (ex. "9000")
|
||||||
|
"""
|
||||||
|
apdu = toBytes(pdu)
|
||||||
|
|
||||||
|
data, sw1, sw2 = self._con.transmit(apdu)
|
||||||
|
|
||||||
|
sw = [sw1, sw2]
|
||||||
|
|
||||||
|
# Return value
|
||||||
|
return i2h(data), i2h(sw)
|
||||||
|
|
||||||
|
def send_apdu(self, pdu):
|
||||||
|
"""send_apdu(pdu): Sends an APDU and auto fetch response data
|
||||||
|
|
||||||
|
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
|
||||||
|
return : tuple(data, sw), where
|
||||||
|
data : string (in hex) of returned data (ex. "074F4EFFFF")
|
||||||
|
sw : string (in hex) of status word (ex. "9000")
|
||||||
|
"""
|
||||||
|
data, sw = self.send_apdu_raw(pdu)
|
||||||
|
|
||||||
|
if (sw is not None) and (sw[0:2] == '9f'):
|
||||||
|
pdu_gr = pdu[0:2] + 'c00000' + sw[2:4]
|
||||||
|
data, sw = self.send_apdu_raw(pdu_gr)
|
||||||
|
|
||||||
|
return data, sw
|
||||||
|
|
||||||
|
def send_apdu_checksw(self, pdu, sw="9000"):
|
||||||
|
"""send_apdu_checksw(pdu,sw): Sends an APDU and check returned SW
|
||||||
|
|
||||||
|
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
|
||||||
|
sw : string of 4 hexadecimal characters (ex. "9000")
|
||||||
|
return : tuple(data, sw), where
|
||||||
|
data : string (in hex) of returned data (ex. "074F4EFFFF")
|
||||||
|
sw : string (in hex) of status word (ex. "9000")
|
||||||
|
"""
|
||||||
|
rv = self.send_apdu(pdu)
|
||||||
|
if sw.lower() != rv[1]:
|
||||||
|
raise RuntimeError("SW match failed ! Expected %s and got %s." % (sw.lower(), rv[1]))
|
||||||
|
return rv
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# SIM Card commands according to ISO 7816-4 and TS 11.11
|
||||||
|
# -------------------------------------------------------------------------{{{
|
||||||
|
|
||||||
|
class SimCardCommands(object):
|
||||||
|
def __init__(self, transport):
|
||||||
|
self._tp = transport;
|
||||||
|
|
||||||
def select_file(self, dir_list):
|
def select_file(self, dir_list):
|
||||||
rv = []
|
rv = []
|
||||||
for i in dir_list:
|
for i in dir_list:
|
||||||
data, sw = self.send_apdu_checksw("a0a4000002" + i)
|
data, sw = self._tp.send_apdu_checksw("a0a4000002" + i)
|
||||||
rv.append(data)
|
rv.append(data)
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
@@ -274,14 +370,14 @@ class SerialSimLink(object):
|
|||||||
if length is None:
|
if length is None:
|
||||||
length = int(r[-1][4:8], 16) - offset
|
length = int(r[-1][4:8], 16) - offset
|
||||||
pdu = 'a0b0%04x%02x' % (offset, (min(256, length) & 0xff))
|
pdu = 'a0b0%04x%02x' % (offset, (min(256, length) & 0xff))
|
||||||
return self.send_apdu(pdu)
|
return self._tp.send_apdu(pdu)
|
||||||
|
|
||||||
def update_binary(self, ef, data, offset=0):
|
def update_binary(self, ef, data, offset=0):
|
||||||
if not hasattr(type(ef), '__iter__'):
|
if not hasattr(type(ef), '__iter__'):
|
||||||
ef = [ef]
|
ef = [ef]
|
||||||
self.select_file(ef)
|
self.select_file(ef)
|
||||||
pdu = 'a0d6%04x%02x' % (offset, len(data)/2) + data
|
pdu = 'a0d6%04x%02x' % (offset, len(data)/2) + data
|
||||||
return self.send_apdu(pdu)
|
return self._tp.send_apdu(pdu)
|
||||||
|
|
||||||
def read_record(self, ef, rec_no):
|
def read_record(self, ef, rec_no):
|
||||||
if not hasattr(type(ef), '__iter__'):
|
if not hasattr(type(ef), '__iter__'):
|
||||||
@@ -289,7 +385,7 @@ class SerialSimLink(object):
|
|||||||
r = self.select_file(ef)
|
r = self.select_file(ef)
|
||||||
rec_length = int(r[-1][28:30], 16)
|
rec_length = int(r[-1][28:30], 16)
|
||||||
pdu = 'a0b2%02x04%02x' % (rec_no, rec_length)
|
pdu = 'a0b2%02x04%02x' % (rec_no, rec_length)
|
||||||
return self.send_apdu(pdu)
|
return self._tp.send_apdu(pdu)
|
||||||
|
|
||||||
def update_record(self, ef, rec_no, data, force_len=False):
|
def update_record(self, ef, rec_no, data, force_len=False):
|
||||||
if not hasattr(type(ef), '__iter__'):
|
if not hasattr(type(ef), '__iter__'):
|
||||||
@@ -302,7 +398,7 @@ class SerialSimLink(object):
|
|||||||
else:
|
else:
|
||||||
rec_length = len(data)/2
|
rec_length = len(data)/2
|
||||||
pdu = ('a0dc%02x04%02x' % (rec_no, rec_length)) + data
|
pdu = ('a0dc%02x04%02x' % (rec_no, rec_length)) + data
|
||||||
return self.send_apdu(pdu)
|
return self._tp.send_apdu(pdu)
|
||||||
|
|
||||||
def record_size(self, ef):
|
def record_size(self, ef):
|
||||||
r = self.select_file(ef)
|
r = self.select_file(ef)
|
||||||
@@ -316,8 +412,10 @@ class SerialSimLink(object):
|
|||||||
if len(rand) != 32:
|
if len(rand) != 32:
|
||||||
raise ValueError('Invalid rand')
|
raise ValueError('Invalid rand')
|
||||||
self.select_file(['3f00', '7f20'])
|
self.select_file(['3f00', '7f20'])
|
||||||
return self.send_apdu('a088000010' + rand)
|
return self._tp.send_apdu('a088000010' + rand)
|
||||||
|
|
||||||
|
def reset_card(self):
|
||||||
|
return self._tp.reset_card()
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
@@ -327,8 +425,8 @@ class SerialSimLink(object):
|
|||||||
|
|
||||||
class Card(object):
|
class Card(object):
|
||||||
|
|
||||||
def __init__(self, sl):
|
def __init__(self, scc):
|
||||||
self._sl = sl
|
self._scc = scc
|
||||||
|
|
||||||
def _e_iccid(self, iccid):
|
def _e_iccid(self, iccid):
|
||||||
return swap_nibbles(iccid)
|
return swap_nibbles(iccid)
|
||||||
@@ -345,7 +443,7 @@ class Card(object):
|
|||||||
return swap_nibbles(lpad('%d' % mcc, 3) + lpad('%d' % mnc, 3))
|
return swap_nibbles(lpad('%d' % mcc, 3) + lpad('%d' % mnc, 3))
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self._sl.reset_card()
|
self._scc.reset_card()
|
||||||
|
|
||||||
|
|
||||||
class _MagicSimBase(Card):
|
class _MagicSimBase(Card):
|
||||||
@@ -369,17 +467,17 @@ class _MagicSimBase(Card):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def autodetect(kls, sl):
|
def autodetect(kls, scc):
|
||||||
try:
|
try:
|
||||||
for p, l, t in kls._files.values():
|
for p, l, t in kls._files.values():
|
||||||
if not t:
|
if not t:
|
||||||
continue
|
continue
|
||||||
if sl.record_size(['3f00', '7f4d', p]) != l:
|
if scc.record_size(['3f00', '7f4d', p]) != l:
|
||||||
return None
|
return None
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return kls(sl)
|
return kls(scc)
|
||||||
|
|
||||||
def _get_count(self):
|
def _get_count(self):
|
||||||
"""
|
"""
|
||||||
@@ -388,7 +486,7 @@ class _MagicSimBase(Card):
|
|||||||
"""
|
"""
|
||||||
f = self._files['name']
|
f = self._files['name']
|
||||||
|
|
||||||
r = self._sl.select_file(['3f00', '7f4d', f[0]])
|
r = self._scc.select_file(['3f00', '7f4d', f[0]])
|
||||||
rec_len = int(r[-1][28:30], 16)
|
rec_len = int(r[-1][28:30], 16)
|
||||||
tlen = int(r[-1][4:8],16)
|
tlen = int(r[-1][4:8],16)
|
||||||
rec_cnt = (tlen / rec_len) - 1;
|
rec_cnt = (tlen / rec_len) - 1;
|
||||||
@@ -400,13 +498,13 @@ class _MagicSimBase(Card):
|
|||||||
|
|
||||||
def program(self, p):
|
def program(self, p):
|
||||||
# Go to dir
|
# Go to dir
|
||||||
self._sl.select_file(['3f00', '7f4d'])
|
self._scc.select_file(['3f00', '7f4d'])
|
||||||
|
|
||||||
# Home PLMN in PLMN_Sel format
|
# Home PLMN in PLMN_Sel format
|
||||||
hplmn = self._e_plmn(p['mcc'], p['mnc'])
|
hplmn = self._e_plmn(p['mcc'], p['mnc'])
|
||||||
|
|
||||||
# Operator name ( 3f00/7f4d/8f0c )
|
# Operator name ( 3f00/7f4d/8f0c )
|
||||||
self._sl.update_record(self._files['name'][0], 2,
|
self._scc.update_record(self._files['name'][0], 2,
|
||||||
rpad(b2h(p['name']), 32) + ('%02x' % len(p['name'])) + '01'
|
rpad(b2h(p['name']), 32) + ('%02x' % len(p['name'])) + '01'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -430,7 +528,7 @@ class _MagicSimBase(Card):
|
|||||||
# PLMN_Sel
|
# PLMN_Sel
|
||||||
v+= '6f30' + '18' + rpad(hplmn, 36)
|
v+= '6f30' + '18' + rpad(hplmn, 36)
|
||||||
|
|
||||||
self._sl.update_record(self._files['b_ef'][0], 1,
|
self._scc.update_record(self._files['b_ef'][0], 1,
|
||||||
rpad(v, self._files['b_ef'][1]*2)
|
rpad(v, self._files['b_ef'][1]*2)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -438,11 +536,11 @@ class _MagicSimBase(Card):
|
|||||||
# FIXME
|
# FIXME
|
||||||
|
|
||||||
# Write PLMN_Sel forcefully as well
|
# Write PLMN_Sel forcefully as well
|
||||||
r = self._sl.select_file(['3f00', '7f20', '6f30'])
|
r = self._scc.select_file(['3f00', '7f20', '6f30'])
|
||||||
tl = int(r[-1][4:8], 16)
|
tl = int(r[-1][4:8], 16)
|
||||||
|
|
||||||
hplmn = self._e_plmn(p['mcc'], p['mnc'])
|
hplmn = self._e_plmn(p['mcc'], p['mnc'])
|
||||||
self._sl.update_binary('6f30', hplmn + 'ff' * (tl-3))
|
self._scc.update_binary('6f30', hplmn + 'ff' * (tl-3))
|
||||||
|
|
||||||
def erase(self):
|
def erase(self):
|
||||||
# Dummy
|
# Dummy
|
||||||
@@ -458,7 +556,7 @@ class _MagicSimBase(Card):
|
|||||||
# Write
|
# Write
|
||||||
for n in range(0,self._get_count()):
|
for n in range(0,self._get_count()):
|
||||||
for k, (msg, ofs) in df.iteritems():
|
for k, (msg, ofs) in df.iteritems():
|
||||||
self._sl.update_record(['3f00', '7f4d', k], n + ofs, msg)
|
self._scc.update_record(['3f00', '7f4d', k], n + ofs, msg)
|
||||||
|
|
||||||
|
|
||||||
class SuperSim(_MagicSimBase):
|
class SuperSim(_MagicSimBase):
|
||||||
@@ -497,14 +595,14 @@ class FakeMagicSim(Card):
|
|||||||
name = 'fakemagicsim'
|
name = 'fakemagicsim'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def autodetect(kls, sl):
|
def autodetect(kls, scc):
|
||||||
try:
|
try:
|
||||||
if sl.record_size(['3f00', '000c']) != 0x5a:
|
if scc.record_size(['3f00', '000c']) != 0x5a:
|
||||||
return None
|
return None
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return kls(sl)
|
return kls(scc)
|
||||||
|
|
||||||
def _get_infos(self):
|
def _get_infos(self):
|
||||||
"""
|
"""
|
||||||
@@ -512,7 +610,7 @@ class FakeMagicSim(Card):
|
|||||||
and entry size
|
and entry size
|
||||||
"""
|
"""
|
||||||
|
|
||||||
r = self._sl.select_file(['3f00', '000c'])
|
r = self._scc.select_file(['3f00', '000c'])
|
||||||
rec_len = int(r[-1][28:30], 16)
|
rec_len = int(r[-1][28:30], 16)
|
||||||
tlen = int(r[-1][4:8],16)
|
tlen = int(r[-1][4:8],16)
|
||||||
rec_cnt = (tlen / rec_len) - 1;
|
rec_cnt = (tlen / rec_len) - 1;
|
||||||
@@ -524,11 +622,11 @@ class FakeMagicSim(Card):
|
|||||||
|
|
||||||
def program(self, p):
|
def program(self, p):
|
||||||
# Home PLMN
|
# Home PLMN
|
||||||
r = self._sl.select_file(['3f00', '7f20', '6f30'])
|
r = self._scc.select_file(['3f00', '7f20', '6f30'])
|
||||||
tl = int(r[-1][4:8], 16)
|
tl = int(r[-1][4:8], 16)
|
||||||
|
|
||||||
hplmn = self._e_plmn(p['mcc'], p['mnc'])
|
hplmn = self._e_plmn(p['mcc'], p['mnc'])
|
||||||
self._sl.update_binary('6f30', hplmn + 'ff' * (tl-3))
|
self._scc.update_binary('6f30', hplmn + 'ff' * (tl-3))
|
||||||
|
|
||||||
# Get total number of entries and entry size
|
# Get total number of entries and entry size
|
||||||
rec_cnt, rec_len = self._get_infos()
|
rec_cnt, rec_len = self._get_infos()
|
||||||
@@ -544,7 +642,7 @@ class FakeMagicSim(Card):
|
|||||||
rpad(p['smsp'], 20) + # 10b SMSP (padded with ff if needed)
|
rpad(p['smsp'], 20) + # 10b SMSP (padded with ff if needed)
|
||||||
10*'f' # 5b (unknown ...)
|
10*'f' # 5b (unknown ...)
|
||||||
)
|
)
|
||||||
self._sl.update_record('000c', 1, entry)
|
self._scc.update_record('000c', 1, entry)
|
||||||
|
|
||||||
def erase(self):
|
def erase(self):
|
||||||
# Get total number of entries and entry size
|
# Get total number of entries and entry size
|
||||||
@@ -553,7 +651,7 @@ class FakeMagicSim(Card):
|
|||||||
# Erase all entries
|
# Erase all entries
|
||||||
entry = 'ff' * rec_len
|
entry = 'ff' * rec_len
|
||||||
for i in range(0, rec_cnt):
|
for i in range(0, rec_cnt):
|
||||||
self._sl.update_record('000c', 1+i, entry)
|
self._scc.update_record('000c', 1+i, entry)
|
||||||
|
|
||||||
|
|
||||||
# In order for autodetection ...
|
# In order for autodetection ...
|
||||||
@@ -580,6 +678,10 @@ def parse_options():
|
|||||||
help="Serial Device for SIM access [default: %default]",
|
help="Serial Device for SIM access [default: %default]",
|
||||||
default="/dev/ttyUSB0",
|
default="/dev/ttyUSB0",
|
||||||
)
|
)
|
||||||
|
parser.add_option("-p", "--pcsc-device", dest="pcsc_dev", metavar="PCSC",
|
||||||
|
help="Which PC/SC reader number for SIM access",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
parser.add_option("-b", "--baud", dest="baudrate", type="int", metavar="BAUD",
|
parser.add_option("-b", "--baud", dest="baudrate", type="int", metavar="BAUD",
|
||||||
help="Baudrate used for SIM access [default: %default]",
|
help="Baudrate used for SIM access [default: %default]",
|
||||||
default=9600,
|
default=9600,
|
||||||
@@ -777,7 +879,11 @@ if __name__ == '__main__':
|
|||||||
print_parameters(cp)
|
print_parameters(cp)
|
||||||
|
|
||||||
# Connect to the card
|
# Connect to the card
|
||||||
sl = SerialSimLink(device=opts.device, baudrate=opts.baudrate)
|
if opts.pcsc_dev is None:
|
||||||
|
sl = SerialSimLink(device=opts.device, baudrate=opts.baudrate)
|
||||||
|
else:
|
||||||
|
sl = PcscSimLink(0, observer=0)
|
||||||
|
scc = SimCardCommands(transport=sl)
|
||||||
|
|
||||||
# Detect type if needed
|
# Detect type if needed
|
||||||
card = None
|
card = None
|
||||||
@@ -785,7 +891,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
if opts.type == "auto":
|
if opts.type == "auto":
|
||||||
for kls in _cards_classes:
|
for kls in _cards_classes:
|
||||||
card = kls.autodetect(sl)
|
card = kls.autodetect(scc)
|
||||||
if card:
|
if card:
|
||||||
print "Autodetected card type %s" % card.name
|
print "Autodetected card type %s" % card.name
|
||||||
card.reset()
|
card.reset()
|
||||||
@@ -796,7 +902,7 @@ if __name__ == '__main__':
|
|||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
elif opts.type in ctypes:
|
elif opts.type in ctypes:
|
||||||
card = ctypes[opts.type](sl)
|
card = ctypes[opts.type](scc)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print "Unknown card type %s" % opts.type
|
print "Unknown card type %s" % opts.type
|
||||||
|
|||||||
Reference in New Issue
Block a user