add connect response reading
This commit is contained in:
@@ -46,3 +46,13 @@ class InvalidType(Exception):
|
|||||||
'''
|
'''
|
||||||
Exception.__init__(self, message)
|
Exception.__init__(self, message)
|
||||||
|
|
||||||
|
class InvalidSize(Exception):
|
||||||
|
'''
|
||||||
|
raise when invalid size is present in packet type occured
|
||||||
|
'''
|
||||||
|
def __init__(self, message = ""):
|
||||||
|
'''
|
||||||
|
constructor with message
|
||||||
|
@param message: message show when exception is raised
|
||||||
|
'''
|
||||||
|
Exception.__init__(self, message)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
import struct
|
import struct
|
||||||
|
from copy import deepcopy
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
from error import InvalidValue, InvalidType
|
from error import InvalidValue, InvalidType
|
||||||
|
|
||||||
@@ -240,6 +241,28 @@ class CompositeType(Type):
|
|||||||
size += sizeof(self.__dict__[name])
|
size += sizeof(self.__dict__[name])
|
||||||
return size
|
return size
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
'''
|
||||||
|
compare each properties which are Type inheritance
|
||||||
|
if one is different then not equal
|
||||||
|
@param other: CompositeType
|
||||||
|
@return: True if each subtype are equals
|
||||||
|
'''
|
||||||
|
if self._typeName != other._typeName:
|
||||||
|
return False
|
||||||
|
for name in self._typeName:
|
||||||
|
if self.__dict__[name] != other.__dict__[name]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
'''
|
||||||
|
return not equal result operator
|
||||||
|
@param other: CompositeType
|
||||||
|
@return: False if each subtype are equals
|
||||||
|
'''
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
class UInt8(SimpleType):
|
class UInt8(SimpleType):
|
||||||
'''
|
'''
|
||||||
unsigned byte
|
unsigned byte
|
||||||
@@ -460,15 +483,29 @@ class Stream(StringIO):
|
|||||||
self.writeType(element)
|
self.writeType(element)
|
||||||
return
|
return
|
||||||
value.write(self)
|
value.write(self)
|
||||||
|
|
||||||
def write_unistr(self, value):
|
def CheckValueOnRead(cls):
|
||||||
for c in value:
|
'''
|
||||||
self.write_uint8(ord(c))
|
wrap read method of class
|
||||||
self.write_uint8(0)
|
to check value on read
|
||||||
self.write_uint8(0)
|
if new value is different from old value
|
||||||
self.write_uint8(0)
|
raise InvalidValue
|
||||||
|
@param cls: class that inherit from Type
|
||||||
|
'''
|
||||||
|
oldRead = cls.read
|
||||||
|
def read(self, s):
|
||||||
|
old = deepcopy(self)
|
||||||
|
oldRead(self, s)
|
||||||
|
if self != old:
|
||||||
|
raise InvalidValue("CheckValueOnRead %s != %s"%(self, old))
|
||||||
|
cls.read = read
|
||||||
|
return cls
|
||||||
|
|
||||||
def hexDump(src, length=16):
|
def hexDump(src, length=16):
|
||||||
|
'''
|
||||||
|
print hex representation of sr
|
||||||
|
@param src: string
|
||||||
|
'''
|
||||||
FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)])
|
FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)])
|
||||||
for c in xrange(0, len(src), length):
|
for c in xrange(0, len(src), length):
|
||||||
chars = src[c:c+length]
|
chars = src[c:c+length]
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
'''
|
'''
|
||||||
from rdpy.protocol.network.type import UInt8, UInt16Be, UInt32Be, String
|
from rdpy.protocol.network.type import UInt8, UInt16Be, UInt32Be, String
|
||||||
from rdpy.utils.const import ConstAttributes
|
from rdpy.utils.const import ConstAttributes
|
||||||
from rdpy.protocol.network.error import InvalidExpectedDataException
|
from rdpy.protocol.network.error import InvalidExpectedDataException,\
|
||||||
|
InvalidSize
|
||||||
|
|
||||||
@ConstAttributes
|
@ConstAttributes
|
||||||
class BerPc(object):
|
class BerPc(object):
|
||||||
@@ -228,9 +229,8 @@ def readEnumerated(s):
|
|||||||
'''
|
'''
|
||||||
if not readUniversalTag(s, Tag.BER_TAG_ENUMERATED, False):
|
if not readUniversalTag(s, Tag.BER_TAG_ENUMERATED, False):
|
||||||
raise InvalidExpectedDataException("invalid ber tag")
|
raise InvalidExpectedDataException("invalid ber tag")
|
||||||
size = readLength(s)
|
if readLength(s) != 1:
|
||||||
if size != UInt32Be(1):
|
raise InvalidSize("enumerate size is wrong")
|
||||||
raise InvalidExpectedDataException("enumerate size is wrong")
|
|
||||||
enumer = UInt8()
|
enumer = UInt8()
|
||||||
s.readType(enumer)
|
s.readType(enumer)
|
||||||
return enumer.value
|
return enumer.value
|
||||||
|
|||||||
@@ -2,28 +2,30 @@
|
|||||||
@author: sylvain
|
@author: sylvain
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from rdpy.utils.const import ConstAttributes
|
from rdpy.utils.const import ConstAttributes, TypeAttributes
|
||||||
from rdpy.protocol.network.layer import LayerAutomata
|
from rdpy.protocol.network.layer import LayerAutomata
|
||||||
from rdpy.protocol.network.type import sizeof, Stream, UInt8
|
from rdpy.protocol.network.type import sizeof, Stream, UInt8
|
||||||
from rdpy.protocol.rdp.ber import writeLength
|
from rdpy.protocol.rdp.ber import writeLength
|
||||||
|
from rdpy.protocol.network.error import InvalidExpectedDataException, InvalidValue, InvalidSize
|
||||||
|
|
||||||
import ber, gcc
|
import ber, gcc
|
||||||
|
|
||||||
@ConstAttributes
|
@ConstAttributes
|
||||||
|
@TypeAttributes(UInt8)
|
||||||
class Message(object):
|
class Message(object):
|
||||||
'''
|
'''
|
||||||
message type
|
message type
|
||||||
'''
|
'''
|
||||||
MCS_TYPE_CONNECT_INITIAL = UInt8(0x65)
|
MCS_TYPE_CONNECT_INITIAL = 0x65
|
||||||
MCS_TYPE_CONNECT_RESPONSE = UInt8(0x66)
|
MCS_TYPE_CONNECT_RESPONSE = 0x66
|
||||||
MCS_EDRQ = UInt8(1)
|
MCS_EDRQ = 1
|
||||||
MCS_DPUM = UInt8(8)
|
MCS_DPUM = 8
|
||||||
MCS_AURQ = UInt8(10)
|
MCS_AURQ = 10
|
||||||
MCS_AUCF = UInt8(11)
|
MCS_AUCF = 11
|
||||||
MCS_CJRQ = UInt8(14)
|
MCS_CJRQ = 14
|
||||||
MCS_CJCF = UInt8(15)
|
MCS_CJCF = 15
|
||||||
MCS_SDRQ = UInt8(25)
|
MCS_SDRQ = 25
|
||||||
MCS_SDIN = UInt8(26)
|
MCS_SDIN = 26
|
||||||
|
|
||||||
class Channel:
|
class Channel:
|
||||||
MCS_GLOBAL_CHANNEL = 1003
|
MCS_GLOBAL_CHANNEL = 1003
|
||||||
@@ -66,6 +68,8 @@ class MCS(LayerAutomata):
|
|||||||
self.writeDomainParams(0xffff, 0xfc17, 0xffff, 0xffff),
|
self.writeDomainParams(0xffff, 0xfc17, 0xffff, 0xffff),
|
||||||
ber.writeOctetstring(ccReqStream.getvalue()))
|
ber.writeOctetstring(ccReqStream.getvalue()))
|
||||||
self._transport.send((ber.writeApplicationTag(Message.MCS_TYPE_CONNECT_INITIAL, sizeof(tmp)), tmp))
|
self._transport.send((ber.writeApplicationTag(Message.MCS_TYPE_CONNECT_INITIAL, sizeof(tmp)), tmp))
|
||||||
|
#we must receive a connect response
|
||||||
|
self.setNextState(self.recvConnectResponse)
|
||||||
|
|
||||||
def writeDomainParams(self, maxChannels, maxUsers, maxTokens, maxPduSize):
|
def writeDomainParams(self, maxChannels, maxUsers, maxTokens, maxPduSize):
|
||||||
'''
|
'''
|
||||||
@@ -81,5 +85,34 @@ class MCS(LayerAutomata):
|
|||||||
ber.writeInteger(1), ber.writeInteger(0), ber.writeInteger(1),
|
ber.writeInteger(1), ber.writeInteger(0), ber.writeInteger(1),
|
||||||
ber.writeInteger(maxPduSize), ber.writeInteger(2))
|
ber.writeInteger(maxPduSize), ber.writeInteger(2))
|
||||||
return (ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True), writeLength(sizeof(domainParam)), domainParam)
|
return (ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True), writeLength(sizeof(domainParam)), domainParam)
|
||||||
|
|
||||||
|
def readDomainParams(self, s):
|
||||||
|
'''
|
||||||
|
read domain params structure
|
||||||
|
'''
|
||||||
|
if not ber.readUniversalTag(s, ber.Tag.BER_TAG_SEQUENCE, True):
|
||||||
|
raise InvalidValue("bad BER tags")
|
||||||
|
length = ber.readLength(s)
|
||||||
|
max_channels = ber.readInteger(s)
|
||||||
|
max_users = ber.readInteger(s)
|
||||||
|
max_tokens = ber.readInteger(s)
|
||||||
|
ber.readInteger(s)
|
||||||
|
ber.readInteger(s)
|
||||||
|
ber.readInteger(s)
|
||||||
|
max_pdu_size = ber.readInteger(s)
|
||||||
|
ber.readInteger(s)
|
||||||
|
|
||||||
|
def recvConnectResponse(self, data):
|
||||||
|
ber.readApplicationTag(data, Message.MCS_TYPE_CONNECT_RESPONSE)
|
||||||
|
ber.readEnumerated(data)
|
||||||
|
ber.readInteger(data)
|
||||||
|
self.readDomainParams(data)
|
||||||
|
if not ber.readUniversalTag(data, ber.Tag.BER_TAG_OCTET_STRING, False):
|
||||||
|
raise InvalidExpectedDataException("invalid expected tag")
|
||||||
|
gccRequestLength = ber.readLength(data)
|
||||||
|
if data.dataLen() != gccRequestLength:
|
||||||
|
raise InvalidSize("gcc request have ")
|
||||||
|
from rdpy.protocol.network.type import hexDump
|
||||||
|
hexDump(data.getvalue())
|
||||||
|
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
@author: sylvain
|
@author: sylvain
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from rdpy.protocol.network.type import UInt8, UInt16Be, UInt32Be, String, Stream
|
from rdpy.protocol.network.type import UInt8, UInt16Be, UInt32Be, String
|
||||||
from rdpy.protocol.network.error import InvalidValue, InvalidExpectedDataException
|
from rdpy.protocol.network.error import InvalidValue, InvalidExpectedDataException
|
||||||
|
|
||||||
def readLength(s):
|
def readLength(s):
|
||||||
|
|||||||
@@ -4,37 +4,40 @@
|
|||||||
from rdpy.protocol.network.layer import LayerAutomata
|
from rdpy.protocol.network.layer import LayerAutomata
|
||||||
from rdpy.protocol.network.type import UInt8, UInt16Le, UInt16Be, UInt32Le, CompositeType, sizeof
|
from rdpy.protocol.network.type import UInt8, UInt16Le, UInt16Be, UInt32Le, CompositeType, sizeof
|
||||||
from rdpy.protocol.network.error import InvalidExpectedDataException, NegotiationFailure
|
from rdpy.protocol.network.error import InvalidExpectedDataException, NegotiationFailure
|
||||||
from rdpy.utils.const import ConstAttributes
|
from rdpy.utils.const import ConstAttributes, TypeAttributes
|
||||||
|
|
||||||
@ConstAttributes
|
@ConstAttributes
|
||||||
|
@TypeAttributes(UInt8)
|
||||||
class MessageType(object):
|
class MessageType(object):
|
||||||
'''
|
'''
|
||||||
message type
|
message type
|
||||||
'''
|
'''
|
||||||
X224_TPDU_CONNECTION_REQUEST = UInt8(0xE0)
|
X224_TPDU_CONNECTION_REQUEST = 0xE0
|
||||||
X224_TPDU_CONNECTION_CONFIRM = UInt8(0xD0)
|
X224_TPDU_CONNECTION_CONFIRM = 0xD0
|
||||||
X224_TPDU_DISCONNECT_REQUEST = UInt8(0x80)
|
X224_TPDU_DISCONNECT_REQUEST = 0x80
|
||||||
X224_TPDU_DATA = UInt8(0xF0)
|
X224_TPDU_DATA = 0xF0
|
||||||
X224_TPDU_ERROR = UInt8(0x70)
|
X224_TPDU_ERROR = 0x70
|
||||||
|
|
||||||
@ConstAttributes
|
@ConstAttributes
|
||||||
|
@TypeAttributes(UInt8)
|
||||||
class NegociationType(object):
|
class NegociationType(object):
|
||||||
'''
|
'''
|
||||||
negotiation header
|
negotiation header
|
||||||
'''
|
'''
|
||||||
TYPE_RDP_NEG_REQ = UInt8(0x01)
|
TYPE_RDP_NEG_REQ = 0x01
|
||||||
TYPE_RDP_NEG_RSP = UInt8(0x02)
|
TYPE_RDP_NEG_RSP = 0x02
|
||||||
TYPE_RDP_NEG_FAILURE = UInt8(0x03)
|
TYPE_RDP_NEG_FAILURE = 0x03
|
||||||
|
|
||||||
@ConstAttributes
|
@ConstAttributes
|
||||||
|
@TypeAttributes(UInt32Le)
|
||||||
class Protocols(object):
|
class Protocols(object):
|
||||||
'''
|
'''
|
||||||
protocols available for TPDU layer
|
protocols available for TPDU layer
|
||||||
'''
|
'''
|
||||||
PROTOCOL_RDP = UInt32Le(0x00000000)
|
PROTOCOL_RDP = 0x00000000
|
||||||
PROTOCOL_SSL = UInt32Le(0x00000001)
|
PROTOCOL_SSL = 0x00000001
|
||||||
PROTOCOL_HYBRID = UInt32Le(0x00000002)
|
PROTOCOL_HYBRID = 0x00000002
|
||||||
PROTOCOL_HYBRID_EX = UInt32Le(0x00000008)
|
PROTOCOL_HYBRID_EX = 0x00000008
|
||||||
|
|
||||||
class TPDUConnectHeader(CompositeType):
|
class TPDUConnectHeader(CompositeType):
|
||||||
'''
|
'''
|
||||||
@@ -46,6 +49,15 @@ class TPDUConnectHeader(CompositeType):
|
|||||||
self.code = code
|
self.code = code
|
||||||
self.padding = (UInt16Be(), UInt16Be(), UInt8())
|
self.padding = (UInt16Be(), UInt16Be(), UInt8())
|
||||||
|
|
||||||
|
class TPDUDataHeader(CompositeType):
|
||||||
|
'''
|
||||||
|
header send when tpdu exchange application data
|
||||||
|
'''
|
||||||
|
def __init__(self):
|
||||||
|
CompositeType.__init__(self)
|
||||||
|
self.header = UInt8(2)
|
||||||
|
self.messageType = MessageType.X224_TPDU_DATA
|
||||||
|
self.separator = UInt8(0x80)
|
||||||
|
|
||||||
class Negotiation(CompositeType):
|
class Negotiation(CompositeType):
|
||||||
'''
|
'''
|
||||||
@@ -69,7 +81,6 @@ class TPDU(LayerAutomata):
|
|||||||
@param presentation: MCS layer
|
@param presentation: MCS layer
|
||||||
'''
|
'''
|
||||||
LayerAutomata.__init__(self, presentation)
|
LayerAutomata.__init__(self, presentation)
|
||||||
|
|
||||||
#default protocol is SSl because is the only supported
|
#default protocol is SSl because is the only supported
|
||||||
#in this version of RDPY
|
#in this version of RDPY
|
||||||
self._protocol = Protocols.PROTOCOL_SSL
|
self._protocol = Protocols.PROTOCOL_SSL
|
||||||
@@ -103,9 +114,19 @@ class TPDU(LayerAutomata):
|
|||||||
LayerAutomata.connect(self)
|
LayerAutomata.connect(self)
|
||||||
|
|
||||||
def recvData(self, data):
|
def recvData(self, data):
|
||||||
print "TPDU data"
|
'''
|
||||||
from rdpy.protocol.network.type import hexDump
|
read data header from packet
|
||||||
hexDump(data.getvalue())
|
and pass to presentation layer
|
||||||
|
@param data: stream
|
||||||
|
'''
|
||||||
|
header = TPDUDataHeader()
|
||||||
|
data.readType(header)
|
||||||
|
if header.messageType == MessageType.X224_TPDU_DATA:
|
||||||
|
LayerAutomata.recv(self, data)
|
||||||
|
elif header.messageType == MessageType.X224_TPDU_ERROR:
|
||||||
|
raise Exception("receive error from tpdu layer")
|
||||||
|
else:
|
||||||
|
raise InvalidExpectedDataException("unknow tpdu code %s"%header.messageType)
|
||||||
|
|
||||||
def sendConnectionRequest(self):
|
def sendConnectionRequest(self):
|
||||||
'''
|
'''
|
||||||
@@ -121,7 +142,7 @@ class TPDU(LayerAutomata):
|
|||||||
write message packet for TPDU layer
|
write message packet for TPDU layer
|
||||||
add TPDU header
|
add TPDU header
|
||||||
'''
|
'''
|
||||||
self._transport.send((UInt8(2), MessageType.X224_TPDU_DATA, UInt8(0x80), message))
|
self._transport.send((TPDUDataHeader(), message))
|
||||||
|
|
||||||
def readNeg(self, data):
|
def readNeg(self, data):
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -3,58 +3,63 @@
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
from rdpy.protocol.network.type import UInt8, UInt16Be, UInt32Be, SInt32Be, String, CompositeType
|
from rdpy.protocol.network.type import UInt8, UInt16Be, UInt32Be, SInt32Be, String, CompositeType
|
||||||
from rdpy.utils.const import ConstAttributes
|
from rdpy.utils.const import ConstAttributes, TypeAttributes
|
||||||
|
|
||||||
@ConstAttributes
|
@ConstAttributes
|
||||||
|
@TypeAttributes(String)
|
||||||
class ProtocolVersion(object):
|
class ProtocolVersion(object):
|
||||||
'''
|
'''
|
||||||
different ptotocol version
|
different ptotocol version
|
||||||
'''
|
'''
|
||||||
UNKNOWN = String()
|
UNKNOWN = ""
|
||||||
RFB003003 = String("RFB 003.003\n")
|
RFB003003 = "RFB 003.003\n"
|
||||||
RFB003007 = String("RFB 003.007\n")
|
RFB003007 = "RFB 003.007\n"
|
||||||
RFB003008 = String("RFB 003.008\n")
|
RFB003008 = "RFB 003.008\n"
|
||||||
|
|
||||||
@ConstAttributes
|
@ConstAttributes
|
||||||
|
@TypeAttributes(UInt8)
|
||||||
class SecurityType(object):
|
class SecurityType(object):
|
||||||
'''
|
'''
|
||||||
security type supported
|
security type supported
|
||||||
(or will be supported)
|
(or will be supported)
|
||||||
by rdpy
|
by rdpy
|
||||||
'''
|
'''
|
||||||
INVALID = UInt8(0)
|
INVALID = 0
|
||||||
NONE = UInt8(1)
|
NONE = 1
|
||||||
VNC = UInt8(2)
|
VNC = 2
|
||||||
|
|
||||||
@ConstAttributes
|
@ConstAttributes
|
||||||
|
@TypeAttributes(UInt32Be)
|
||||||
class Pointer(object):
|
class Pointer(object):
|
||||||
'''
|
'''
|
||||||
mouse event code (which button)
|
mouse event code (which button)
|
||||||
actually in RFB specification only$
|
actually in RFB specification only$
|
||||||
three buttons are supported
|
three buttons are supported
|
||||||
'''
|
'''
|
||||||
BUTTON1 = UInt32Be(0x1)
|
BUTTON1 = 0x1
|
||||||
BUTTON2 = UInt32Be(0x2)
|
BUTTON2 = 0x2
|
||||||
BUTTON3 = UInt32Be(0x4)
|
BUTTON3 = 0x4
|
||||||
|
|
||||||
@ConstAttributes
|
@ConstAttributes
|
||||||
|
@TypeAttributes(SInt32Be)
|
||||||
class Encoding(object):
|
class Encoding(object):
|
||||||
'''
|
'''
|
||||||
encoding types
|
encoding types
|
||||||
'''
|
'''
|
||||||
RAW = SInt32Be(0)
|
RAW = 0
|
||||||
|
|
||||||
@ConstAttributes
|
@ConstAttributes
|
||||||
|
@TypeAttributes(UInt8)
|
||||||
class ClientToServerMessages(object):
|
class ClientToServerMessages(object):
|
||||||
'''
|
'''
|
||||||
messages types
|
messages types
|
||||||
'''
|
'''
|
||||||
PIXEL_FORMAT = UInt8(0)
|
PIXEL_FORMAT = 0
|
||||||
ENCODING = UInt8(2)
|
ENCODING = 2
|
||||||
FRAME_BUFFER_UPDATE_REQUEST = UInt8(3)
|
FRAME_BUFFER_UPDATE_REQUEST = 3
|
||||||
KEY_EVENT = UInt8(4)
|
KEY_EVENT = 4
|
||||||
POINTER_EVENT = UInt8(5)
|
POINTER_EVENT = 5
|
||||||
CUT_TEXT = UInt8(6)
|
CUT_TEXT = 6
|
||||||
|
|
||||||
class PixelFormat(CompositeType):
|
class PixelFormat(CompositeType):
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -33,16 +33,27 @@ class Constant(object):
|
|||||||
delete is forbidden on constant
|
delete is forbidden on constant
|
||||||
'''
|
'''
|
||||||
raise Exception("can't delete constant")
|
raise Exception("can't delete constant")
|
||||||
|
|
||||||
|
def TypeAttributes(typeClass):
|
||||||
|
'''
|
||||||
|
call typeClass ctor on each attributes
|
||||||
|
to uniform atributes type on class
|
||||||
|
@param typeClass: class use to construct each class attributes
|
||||||
|
@return: class decorator
|
||||||
|
'''
|
||||||
|
def wrapper(cls):
|
||||||
|
for c_name, c_value in cls.__dict__.iteritems():
|
||||||
|
if c_name[0] != '_' and not callable(c_value):
|
||||||
|
setattr(cls, c_name, typeClass(c_value))
|
||||||
|
return cls
|
||||||
|
return wrapper
|
||||||
|
|
||||||
def ConstAttributes(cls):
|
def ConstAttributes(cls):
|
||||||
'''
|
'''
|
||||||
|
copy on read attributes
|
||||||
transform all attributes of class
|
transform all attributes of class
|
||||||
in constant attribute
|
in constant attribute
|
||||||
only attributes which are not begining with '_' char
|
only attributes which are not begining with '_' char
|
||||||
and are not callable
|
and are not callable
|
||||||
'''
|
'''
|
||||||
for c_name, c_value in cls.__dict__.iteritems():
|
return TypeAttributes(Constant)(cls)
|
||||||
if c_name[0] != '_' and not callable(c_value):
|
|
||||||
setattr(cls, c_name, Constant(c_value))
|
|
||||||
return cls
|
|
||||||
Reference in New Issue
Block a user