Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
O
opcua-asyncio
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Nikola Balog
opcua-asyncio
Commits
a80a7be6
Commit
a80a7be6
authored
Dec 20, 2015
by
ORD
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #79 from alkor/message-chunks
Split messages into MessageChunks
parents
558fddd6
7105528f
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
272 additions
and
52 deletions
+272
-52
opcua/binary_client.py
opcua/binary_client.py
+40
-44
opcua/uaprocessor.py
opcua/uaprocessor.py
+11
-7
opcua/uaprotocol_hand.py
opcua/uaprotocol_hand.py
+198
-1
tests/tests.py
tests/tests.py
+23
-0
No files found.
opcua/binary_client.py
View file @
a80a7be6
...
...
@@ -24,7 +24,7 @@ class UASocketClient(object):
handle socket connection and send ua messages
timeout is the timeout used while waiting for an ua answer from server
"""
def
__init__
(
self
,
timeout
=
1
):
def
__init__
(
self
,
timeout
=
1
,
security_policy
=
ua
.
SecurityPolicy
()
):
self
.
logger
=
logging
.
getLogger
(
__name__
+
"Socket"
)
self
.
_thread
=
None
self
.
_lock
=
Lock
()
...
...
@@ -37,6 +37,8 @@ class UASocketClient(object):
self
.
_request_id
=
0
self
.
_request_handle
=
0
self
.
_callbackmap
=
{}
self
.
_security_policy
=
security_policy
self
.
_max_chunk_size
=
65536
def
start
(
self
):
"""
...
...
@@ -47,28 +49,43 @@ class UASocketClient(object):
self
.
_thread
=
Thread
(
target
=
self
.
_run
)
self
.
_thread
.
start
()
def
send_request
(
self
,
request
,
callback
=
None
,
timeout
=
1000
):
def
_send_request
(
self
,
request
,
callback
=
None
,
timeout
=
1000
,
message_type
=
ua
.
MessageType
.
SecureMessage
):
"""
send request to server
.
send request to server
, lower-level method
timeout is the timeout written in ua header
returns future
"""
with
self
.
_lock
:
request
.
RequestHeader
=
self
.
_create_request_header
(
timeout
)
try
:
cachedreq
=
CachedRequest
(
request
.
to_binary
()
)
binreq
=
request
.
to_binary
(
)
except
:
# reset reqeust handle if any error
# see self._create_request_header
self
.
_request_handle
-=
1
raise
hdr
=
ua
.
Header
(
ua
.
MessageType
.
SecureMessage
,
ua
.
ChunkType
.
Single
,
self
.
_security_token
.
ChannelId
)
symhdr
=
self
.
_create_sym_algo_header
()
seqhdr
=
self
.
_create_sequence_header
()
self
.
_request_id
+=
1
future
=
Future
()
if
callback
:
future
.
add_done_callback
(
callback
)
self
.
_callbackmap
[
seqhdr
.
RequestId
]
=
future
self
.
_write_socket
(
hdr
,
symhdr
,
seqhdr
,
cachedreq
)
self
.
_callbackmap
[
self
.
_request_id
]
=
future
for
chunk
in
ua
.
MessageChunk
.
message_to_chunks
(
self
.
_security_policy
,
binreq
,
self
.
_max_chunk_size
,
message_type
=
message_type
,
channel_id
=
self
.
_security_token
.
ChannelId
,
request_id
=
self
.
_request_id
,
token_id
=
self
.
_security_token
.
TokenId
):
self
.
_sequence_number
+=
1
chunk
.
SequenceHeader
.
SequenceNumber
=
self
.
_sequence_number
self
.
_socket
.
write
(
chunk
.
to_binary
())
return
future
def
send_request
(
self
,
request
,
callback
=
None
,
timeout
=
1000
,
message_type
=
ua
.
MessageType
.
SecureMessage
):
"""
send request to server.
timeout is the timeout written in ua header
returns response object if no callback is provided
"""
future
=
self
.
_send_request
(
request
,
callback
,
timeout
,
message_type
)
if
not
callback
:
data
=
future
.
result
(
self
.
timeout
)
self
.
check_answer
(
data
,
" in response to "
+
request
.
__class__
.
__name__
)
...
...
@@ -179,19 +196,6 @@ class UASocketClient(object):
hdr
.
TimeoutHint
=
timeout
return
hdr
def
_create_sym_algo_header
(
self
):
hdr
=
ua
.
SymmetricAlgorithmHeader
()
hdr
.
TokenId
=
self
.
_security_token
.
TokenId
return
hdr
def
_create_sequence_header
(
self
):
hdr
=
ua
.
SequenceHeader
()
self
.
_sequence_number
+=
1
hdr
.
SequenceNumber
=
self
.
_sequence_number
self
.
_request_id
+=
1
hdr
.
RequestId
=
self
.
_request_id
return
hdr
def
connect_socket
(
self
,
host
,
port
):
"""
connect to server socket and start receiving thread
...
...
@@ -215,22 +219,15 @@ class UASocketClient(object):
with
self
.
_lock
:
self
.
_callbackmap
[
0
]
=
future
self
.
_write_socket
(
header
,
hello
)
return
ua
.
Acknowledge
.
from_binary
(
future
.
result
(
self
.
timeout
))
ack
=
ua
.
Acknowledge
.
from_binary
(
future
.
result
(
self
.
timeout
))
self
.
_max_chunk_size
=
ack
.
SendBufferSize
# client shouldn't send chunks larger than this
return
ack
def
open_secure_channel
(
self
,
params
):
self
.
logger
.
info
(
"open_secure_channel"
)
with
self
.
_lock
:
request
=
ua
.
OpenSecureChannelRequest
()
request
.
Parameters
=
params
request
.
RequestHeader
=
self
.
_create_request_header
()
hdr
=
ua
.
Header
(
ua
.
MessageType
.
SecureOpen
,
ua
.
ChunkType
.
Single
,
self
.
_security_token
.
ChannelId
)
asymhdr
=
ua
.
AsymmetricAlgorithmHeader
()
seqhdr
=
self
.
_create_sequence_header
()
future
=
Future
()
self
.
_callbackmap
[
seqhdr
.
RequestId
]
=
future
self
.
_write_socket
(
hdr
,
asymhdr
,
seqhdr
,
request
)
request
=
ua
.
OpenSecureChannelRequest
()
request
.
Parameters
=
params
future
=
self
.
_send_request
(
request
,
message_type
=
ua
.
MessageType
.
SecureOpen
)
response
=
ua
.
OpenSecureChannelResponse
.
from_binary
(
future
.
result
(
self
.
timeout
))
response
.
ResponseHeader
.
ServiceResult
.
check
()
...
...
@@ -240,17 +237,16 @@ class UASocketClient(object):
def
close_secure_channel
(
self
):
"""
close secure channel. It seems to trigger a shutdown of socket
in most servers, so be prepare to reconnect
in most servers, so be prepare to reconnect.
OPC UA specs Part 6, 7.1.4 say that Server does not send a CloseSecureChannel response and should just close socket
"""
self
.
logger
.
info
(
"get_endpoint"
)
self
.
logger
.
info
(
"close_secure_channel"
)
request
=
ua
.
CloseSecureChannelRequest
()
future
=
self
.
_send_request
(
request
,
message_type
=
ua
.
MessageType
.
SecureClose
)
with
self
.
_lock
:
request
=
ua
.
CloseSecureChannelRequest
()
request
.
RequestHeader
=
self
.
_create_request_header
()
hdr
=
ua
.
Header
(
ua
.
MessageType
.
SecureClose
,
ua
.
ChunkType
.
Single
,
self
.
_security_token
.
ChannelId
)
symhdr
=
self
.
_create_sym_algo_header
()
seqhdr
=
self
.
_create_sequence_header
()
self
.
_write_socket
(
hdr
,
symhdr
,
seqhdr
,
request
)
# don't expect any more answers
future
.
cancel
()
self
.
_callbackmap
.
clear
()
# some servers send a response here, most do not ... so we ignore
...
...
opcua/uaprocessor.py
View file @
a80a7be6
...
...
@@ -28,17 +28,20 @@ class UAProcessor(object):
self
.
_socketlock
=
Lock
()
self
.
_datalock
=
Lock
()
self
.
_publishdata_queue
=
[]
self
.
_seq_number
=
1
self
.
_seq_number
=
0
self
.
_security_policy
=
ua
.
SecurityPolicy
()
self
.
_max_chunk_size
=
65536
def
send_response
(
self
,
requesthandle
,
algohdr
,
seqhdr
,
response
,
msgtype
=
ua
.
MessageType
.
SecureMessage
):
with
self
.
_socketlock
:
response
.
ResponseHeader
.
RequestHandle
=
requesthandle
seqhdr
.
SequenceNumber
=
self
.
_seq_number
self
.
_seq_number
+=
1
hdr
=
ua
.
Header
(
msgtype
,
ua
.
ChunkType
.
Single
,
self
.
channel
.
SecurityToken
.
ChannelId
)
if
isinstance
(
algohdr
,
ua
.
SymmetricAlgorithmHeader
):
algohdr
.
TokenId
=
self
.
channel
.
SecurityToken
.
TokenId
self
.
_write_socket
(
hdr
,
algohdr
,
seqhdr
,
response
)
for
chunk
in
ua
.
MessageChunk
.
message_to_chunks
(
self
.
_security_policy
,
response
.
to_binary
(),
self
.
_max_chunk_size
,
msgtype
,
channel_id
=
self
.
channel
.
SecurityToken
.
ChannelId
,
token_id
=
self
.
channel
.
SecurityToken
.
TokenId
,
request_id
=
seqhdr
.
RequestId
):
self
.
_seq_number
+=
1
chunk
.
SequenceHeader
.
SequenceNumber
=
self
.
_seq_number
self
.
socket
.
write
(
chunk
.
to_binary
())
def
_write_socket
(
self
,
hdr
,
*
args
):
alle
=
[]
...
...
@@ -81,6 +84,7 @@ class UAProcessor(object):
hello
=
ua
.
Hello
.
from_binary
(
body
)
hdr
=
ua
.
Header
(
ua
.
MessageType
.
Acknowledge
,
ua
.
ChunkType
.
Single
)
ack
=
ua
.
Acknowledge
()
self
.
_max_chunk_size
=
hello
.
ReceiveBufferSize
ack
.
ReceiveBufferSize
=
hello
.
ReceiveBufferSize
ack
.
SendBufferSize
=
hello
.
SendBufferSize
self
.
_write_socket
(
hdr
,
ack
)
...
...
opcua/uaprotocol_hand.py
View file @
a80a7be6
import
struct
import
logging
import
hashlib
import
opcua.uaprotocol_auto
as
auto
import
opcua.uatypes
as
uatypes
...
...
@@ -70,7 +71,7 @@ class ChunkType(object):
Invalid
=
b"0"
# FIXME check
Single
=
b"F"
Intermediate
=
b"C"
Final
=
b"A"
Abort
=
b"A"
# when an error occurred and the Message is aborted (body is ErrorMessage)
class
Header
(
uatypes
.
FrozenClass
):
...
...
@@ -107,6 +108,10 @@ class Header(uatypes.FrozenClass):
hdr
.
ChannelId
=
uatype_UInt32
.
unpack
(
data
.
read
(
4
))[
0
]
return
hdr
@
staticmethod
def
max_size
():
return
struct
.
calcsize
(
"<3scII"
)
def
__str__
(
self
):
return
"Header(type:{}, chunk_type:{}, body_size:{}, channel:{})"
.
format
(
self
.
MessageType
,
self
.
ChunkType
,
self
.
body_size
,
self
.
ChannelId
)
__repr__
=
__str__
...
...
@@ -207,6 +212,10 @@ class SymmetricAlgorithmHeader(uatypes.FrozenClass):
def
to_binary
(
self
):
return
uatype_UInt32
.
pack
(
self
.
TokenId
)
@
staticmethod
def
max_size
():
return
struct
.
calcsize
(
"<I"
)
def
__str__
(
self
):
return
"{}(TokenId:{} )"
.
format
(
self
.
__class__
.
__name__
,
self
.
TokenId
)
__repr__
=
__str__
...
...
@@ -232,10 +241,198 @@ class SequenceHeader(uatypes.FrozenClass):
b
.
append
(
uatype_UInt32
.
pack
(
self
.
RequestId
))
return
b""
.
join
(
b
)
@
staticmethod
def
max_size
():
return
struct
.
calcsize
(
"<II"
)
def
__str__
(
self
):
return
"{}(SequenceNumber:{}, RequestId:{} )"
.
format
(
self
.
__class__
.
__name__
,
self
.
SequenceNumber
,
self
.
RequestId
)
__repr__
=
__str__
class
CryptographyNone
:
"""
Base class for symmetric/asymmetric cryprography
"""
def
__init__
(
self
,
mode
=
auto
.
MessageSecurityMode
.
None_
):
pass
def
plain_block_size
(
self
):
"""
Size of plain text block for block cipher.
"""
return
1
def
encrypted_block_size
(
self
):
"""
Size of encrypted text block for block cipher.
"""
return
1
def
padding
(
self
,
size
):
"""
Create padding for a block of given size.
plain_size = size + len(padding) + signature_size()
plain_size = N * plain_block_size()
"""
return
b''
def
min_padding_size
(
self
):
return
0
def
signature_size
(
self
):
return
0
def
signature
(
self
,
data
):
return
b''
def
encrypt
(
self
,
data
):
return
data
def
decrypt
(
self
,
data
):
return
data
def
vsignature_size
(
self
):
return
0
def
verify
(
self
,
data
,
signature
):
"""
Verify signature and raise exception if signature is invalid
"""
pass
def
remove_padding
(
self
,
data
):
return
data
class
SecurityPolicy
:
"""
Base class for security policy
"""
def
__init__
(
self
):
self
.
asymmetric_cryptography
=
CryptographyNone
()
self
.
symmetric_cryptography
=
CryptographyNone
()
self
.
Mode
=
auto
.
MessageSecurityMode
.
None_
self
.
URI
=
"http://opcfoundation.org/UA/SecurityPolicy#None"
self
.
server_certificate
=
b""
self
.
client_certificate
=
b""
def
symmetric_key_size
(
self
):
return
0
def
make_symmetric_key
(
self
,
a
,
b
):
pass
class
MessageChunk
(
uatypes
.
FrozenClass
):
"""
Message Chunk, as described in OPC UA specs Part 6, 6.7.2.
"""
def
__init__
(
self
,
security_policy
,
body
=
b''
,
msg_type
=
MessageType
.
SecureMessage
,
chunk_type
=
ChunkType
.
Single
):
self
.
MessageHeader
=
Header
(
msg_type
,
chunk_type
)
if
msg_type
in
(
MessageType
.
SecureMessage
,
MessageType
.
SecureClose
):
self
.
SecurityHeader
=
SymmetricAlgorithmHeader
()
elif
msg_type
==
MessageType
.
SecureOpen
:
self
.
SecurityHeader
=
AsymmetricAlgorithmHeader
()
else
:
raise
Exception
(
"Unsupported message type: {}"
.
format
(
msg_type
))
self
.
SequenceHeader
=
SequenceHeader
()
self
.
Body
=
body
self
.
_security_policy
=
security_policy
@
staticmethod
def
from_binary
(
security_policy
,
data
):
h
=
Header
.
from_string
(
data
)
return
MessageChunk
.
from_header_and_body
(
security_policy
,
h
,
data
)
@
staticmethod
def
from_header_and_body
(
security_policy
,
header
,
data
):
if
header
.
MessageType
in
(
MessageType
.
SecureMessage
,
MessageType
.
SecureClose
):
security_header
=
SymmetricAlgorithmHeader
.
from_binary
(
data
)
crypto
=
security_policy
.
symmetric_cryptography
elif
header
.
MessageType
==
MessageType
.
SecureOpen
:
security_header
=
AsymmetricAlgorithmHeader
.
from_binary
(
data
)
crypto
=
security_policy
.
asymmetric_cryptography
else
:
raise
Exception
(
"Unsupported message type: {}"
.
format
(
header
.
MessageType
))
obj
=
MessageChunk
(
crypto
)
obj
.
MessageHeader
=
header
obj
.
SecurityHeader
=
security_header
decrypted
=
crypto
.
decrypt
(
data
.
read
(
len
(
data
)))
signature_size
=
crypto
.
vsignature_size
()
if
signature_size
>
0
:
signature
=
decrypted
[
-
signature_size
:]
decrypted
=
decrypted
[:
-
signature_size
]
crypto
.
verify
(
obj
.
MessageHeader
.
to_binary
()
+
obj
.
SecurityHeader
.
to_binary
()
+
decrypted
,
signature
)
data
=
utils
.
Buffer
(
crypto
.
remove_padding
(
decrypted
))
obj
.
SequenceHeader
=
SequenceHeader
.
from_binary
(
data
)
obj
.
Body
=
data
.
read
(
len
(
data
))
return
obj
def
encrypted_size
(
self
,
plain_size
):
size
=
plain_size
+
self
.
_security_policy
.
signature_size
()
pbs
=
self
.
_security_policy
.
plain_block_size
()
assert
(
size
%
pbs
==
0
)
return
size
//
pbs
*
self
.
_security_policy
.
encrypted_block_size
()
def
to_binary
(
self
):
security
=
self
.
SecurityHeader
.
to_binary
()
encrypted_part
=
self
.
SequenceHeader
.
to_binary
()
+
self
.
Body
encrypted_part
+=
self
.
_security_policy
.
padding
(
len
(
encrypted_part
))
self
.
MessageHeader
.
body_size
=
len
(
security
)
+
self
.
encrypted_size
(
len
(
encrypted_part
))
header
=
self
.
MessageHeader
.
to_binary
()
encrypted_part
+=
self
.
_security_policy
.
signature
(
header
+
security
+
encrypted_part
)
return
header
+
security
+
self
.
_security_policy
.
encrypt
(
encrypted_part
)
@
staticmethod
def
max_body_size
(
crypto
,
max_chunk_size
):
max_encrypted_size
=
max_chunk_size
-
Header
.
max_size
()
-
SymmetricAlgorithmHeader
.
max_size
()
max_plain_size
=
(
max_encrypted_size
//
crypto
.
encrypted_block_size
())
*
crypto
.
plain_block_size
()
return
max_plain_size
-
SequenceHeader
.
max_size
()
-
crypto
.
signature_size
()
-
crypto
.
min_padding_size
()
@
staticmethod
def
message_to_chunks
(
security_policy
,
body
,
max_chunk_size
,
message_type
=
MessageType
.
SecureMessage
,
channel_id
=
1
,
request_id
=
1
,
token_id
=
1
):
"""
Pack message body (as binary string) into one or more chunks.
Size of each chunk will not exceed max_chunk_size.
Returns a list of MessageChunks. SequenceNumber is not initialized here,
it must be set by Secure Channel driver.
"""
if
message_type
==
MessageType
.
SecureOpen
:
# SecureOpen message must be in a single chunk (specs, Part 6, 6.7.2)
chunk
=
MessageChunk
(
security_policy
.
asymmetric_cryptography
,
body
,
message_type
,
ChunkType
.
Single
)
chunk
.
SecurityHeader
.
SecurityPolicyURI
=
security_policy
.
URI
if
security_policy
.
client_certificate
:
chunk
.
SecurityHeader
.
SenderCertificate
=
security_policy
.
client_certificate
if
security_policy
.
server_certificate
:
chunk
.
SecurityHeader
.
ReceiverCertificateThumbPrint
=
hashlib
.
sha1
(
security_policy
.
server_certificate
).
digest
()
chunk
.
MessageHeader
.
ChannelId
=
channel_id
chunk
.
SequenceHeader
.
RequestId
=
request_id
return
[
chunk
]
crypto
=
security_policy
.
symmetric_cryptography
max_size
=
MessageChunk
.
max_body_size
(
crypto
,
max_chunk_size
)
chunks
=
[]
for
i
in
range
(
0
,
len
(
body
),
max_size
):
part
=
body
[
i
:
i
+
max_size
]
if
i
+
max_size
>=
len
(
body
):
chunk_type
=
ChunkType
.
Single
else
:
chunk_type
=
ChunkType
.
Intermediate
chunk
=
MessageChunk
(
crypto
,
part
,
message_type
,
chunk_type
)
chunk
.
SecurityHeader
.
TokenId
=
token_id
chunk
.
MessageHeader
.
ChannelId
=
channel_id
chunk
.
SequenceHeader
.
RequestId
=
request_id
chunks
.
append
(
chunk
)
return
chunks
def
__str__
(
self
):
return
"{}({}, {}, {}, {} bytes)"
.
format
(
self
.
__class__
.
__name__
,
self
.
MessageHeader
,
self
.
SequenceHeader
,
self
.
SecurityHeader
,
len
(
self
.
Body
))
__repr__
=
__str__
# FIXES for missing switchfield in NodeAttributes classes
ana
=
auto
.
NodeAttributesMask
...
...
tests/tests.py
View file @
a80a7be6
...
...
@@ -266,6 +266,29 @@ class Unit(unittest.TestCase):
t4
=
ua
.
LocalizedText
.
from_binary
(
ua
.
utils
.
Buffer
(
t1
.
to_binary
()))
self
.
assertEqual
(
t1
,
t4
)
def
test_message_chunk
(
self
):
pol
=
ua
.
SecurityPolicy
()
chunks
=
ua
.
MessageChunk
.
message_to_chunks
(
pol
,
b'123'
,
65536
)
self
.
assertEqual
(
len
(
chunks
),
1
)
seq
=
0
for
chunk
in
chunks
:
seq
+=
1
chunk
.
SequenceHeader
.
SequenceNumber
=
seq
chunk2
=
ua
.
MessageChunk
.
from_binary
(
pol
,
ua
.
utils
.
Buffer
(
chunks
[
0
].
to_binary
()))
self
.
assertEqual
(
chunks
[
0
].
to_binary
(),
chunk2
.
to_binary
())
# for policy None, MessageChunk overhead is 12+4+8 = 24 bytes
# Let's pack 11 bytes into 28-byte chunks. The message must be split as 4+4+3
chunks
=
ua
.
MessageChunk
.
message_to_chunks
(
pol
,
b'12345678901'
,
28
)
self
.
assertEqual
(
len
(
chunks
),
3
)
self
.
assertEqual
(
chunks
[
0
].
Body
,
b'1234'
)
self
.
assertEqual
(
chunks
[
1
].
Body
,
b'5678'
)
self
.
assertEqual
(
chunks
[
2
].
Body
,
b'901'
)
for
chunk
in
chunks
:
seq
+=
1
chunk
.
SequenceHeader
.
SequenceNumber
=
seq
self
.
assertTrue
(
len
(
chunk
.
to_binary
())
<=
28
)
class
CommonTests
(
object
):
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment