/usr/lib/python3/dist-packages/Pyro4/message.py is in python3-pyro4 4.23-2.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | """
The pyro wire protocol message.
Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net).
"""
from __future__ import with_statement
import hashlib
import hmac
import struct
import logging
import sys
import Pyro4
from Pyro4 import constants, errors
__all__ = ["Message"]
log = logging.getLogger("Pyro4.message")
MSG_CONNECT = 1
MSG_CONNECTOK = 2
MSG_CONNECTFAIL = 3
MSG_INVOKE = 4
MSG_RESULT = 5
MSG_PING = 6
FLAGS_EXCEPTION = 1<<0
FLAGS_COMPRESSED = 1<<1
FLAGS_ONEWAY = 1<<2
FLAGS_BATCH = 1<<3
SERIALIZER_SERPENT = 1
SERIALIZER_JSON = 2
SERIALIZER_MARSHAL = 3
SERIALIZER_PICKLE = 4
class Message(object):
"""
Pyro write protocol message.
Wire messages contains of a fixed size header, an optional set of annotation chunks,
and then the payload data. This class doesn't deal with the payload data:
(de)serialization and handling of that data is done elsewhere.
Annotation chunks are only parsed, except the 'HMAC' chunk: that is created
and validated because it is used as a message digest.
The header format is::
4 id ('PYRO')
2 protocol version
2 message type
2 message flags
2 sequence number
4 data length
2 data serialization format (serializer id)
2 annotations length (total of all chunks, 0 if no annotation chunks present)
2 (reserved)
2 checksum
After the header, zero or more annotation chunks may follow, of the format::
4 id (ASCII)
2 chunk length
x annotation chunk databytes
After that, the actual payload data bytes follow.
The sequencenumber is used to check if response messages correspond to the
actual request message. This prevents the situation where Pyro would perhaps return
the response data from another remote call (which would not result in an error otherwise!)
This could happen for instance if the socket data stream gets out of sync, perhaps due To
some form of signal that interrupts I/O.
The header checksum is a simple sum of the header fields to make reasonably sure
that we are dealing with an actual correct PYRO protocol header and not some random
data that happens to start with the 'PYRO' protocol identifier.
An 'HMAC' annotation chunk contains the hmac digest of the message data bytes and
all of the annotation chunk data bytes (except those of the HMAC chunk itself).
"""
__slots__ = ["type", "flags", "seq", "data", "data_size", "serializer_id", "annotations", "annotations_size"]
header_format = '!4sHHHHiHHHH'
header_size = struct.calcsize(header_format)
checksum_magic = 0x34E9
def __init__(self, msgType, databytes, serializer_id, flags, seq, annotations=None):
self.type = msgType
self.flags = flags
self.seq = seq
self.data = databytes
self.data_size = len(self.data)
self.serializer_id = serializer_id
self.annotations = annotations or {}
if Pyro4.config.HMAC_KEY:
self.annotations["HMAC"] = self.hmac()
self.annotations_size = sum([6+len(v) for v in self.annotations.values()])
if 0 < Pyro4.config.MAX_MESSAGE_SIZE < (self.data_size + self.annotations_size):
raise errors.ProtocolError("max message size exceeded (%d where max=%d)" % (self.data_size+self.annotations_size, Pyro4.config.MAX_MESSAGE_SIZE))
def __repr__(self):
return "<%s.%s at %x, type=%d flags=%d seq=%d datasize=%d #ann=%d>" % (self.__module__, self.__class__.__name__, id(self), self.type, self.flags, self.seq, self.data_size, len(self.annotations))
def to_bytes(self):
"""creates a byte stream containing the header followed by annotations (if any) followed by the data"""
return self.__header_bytes() + self.__annotations_bytes() + self.data
def __header_bytes(self):
checksum = (self.type+constants.PROTOCOL_VERSION+self.data_size+self.annotations_size+self.serializer_id+self.flags+self.seq+self.checksum_magic)&0xffff
return struct.pack(self.header_format, b"PYRO", constants.PROTOCOL_VERSION, self.type, self.flags, self.seq, self.data_size, self.serializer_id, self.annotations_size, 0, checksum)
def __annotations_bytes(self):
if self.annotations:
a = []
for k, v in self.annotations.items():
if len(k)!=4:
raise errors.ProtocolError("annotation key must be of length 4")
if sys.version_info>=(3,0):
k = k.encode("ASCII")
a.append(struct.pack("!4sH", k, len(v)))
a.append(v)
if sys.platform=="cli":
return "".join(a)
return b"".join(a)
return b""
# Note: this 'chunked' way of sending is not used because it triggers Nagle's algorithm
# on some systems (linux). This causes massive delays, unless you change the socket option
# TCP_NODELAY to disable the algorithm. What also works, is sending all the message bytes
# in one go: connection.send(message.to_bytes())
# def send(self, connection):
# """send the message as bytes over the connection"""
# connection.send(self.__header_bytes())
# if self.annotations:
# connection.send(self.__annotations_bytes())
# connection.send(self.data)
@classmethod
def from_header(cls, headerData):
"""Parses a message header. Does not yet process the annotations chunks and message data."""
if not headerData or len(headerData)!=cls.header_size:
raise errors.ProtocolError("header data size mismatch")
tag, ver, msg_type, flags, seq, data_size, serializer_id, annotations_size, _, checksum = struct.unpack(cls.header_format, headerData)
if tag!=b"PYRO" or ver!=constants.PROTOCOL_VERSION:
raise errors.ProtocolError("invalid data or unsupported protocol version")
if checksum!=(msg_type+ver+data_size+annotations_size+flags+serializer_id+seq+cls.checksum_magic)&0xffff:
raise errors.ProtocolError("header checksum mismatch")
msg = Message(msg_type, b"", serializer_id, flags, seq)
msg.data_size = data_size
msg.annotations_size = annotations_size
return msg
@classmethod
def recv(cls, connection, requiredMsgTypes=None):
"""
Receives a pyro message from a given connection.
Accepts the given message types (None=any, or pass a sequence).
Also reads annotation chunks and the actual payload data.
Validates a HMAC chunk if present.
"""
msg = cls.from_header(connection.recv(cls.header_size))
if 0 < Pyro4.config.MAX_MESSAGE_SIZE < (msg.data_size+msg.annotations_size):
errorMsg = "max message size exceeded (%d where max=%d)" % (msg.data_size+msg.annotations_size, Pyro4.config.MAX_MESSAGE_SIZE)
log.error("connection "+str(connection)+": "+errorMsg)
connection.close() # close the socket because at this point we can't return the correct sequence number for returning an error message
raise errors.ProtocolError(errorMsg)
if requiredMsgTypes and msg.type not in requiredMsgTypes:
err = "invalid msg type %d received" % msg.type
log.error(err)
raise errors.ProtocolError(err)
if msg.annotations_size:
# read annotation chunks
annotations_data = connection.recv(msg.annotations_size)
msg.annotations = {}
i = 0
while i < msg.annotations_size:
anno, length = struct.unpack("!4sH", annotations_data[i:i+6])
if sys.version_info>=(3,0):
anno = anno.decode("ASCII")
msg.annotations[anno] = annotations_data[i+6:i+6+length]
i += 6+length
# read data
msg.data = connection.recv(msg.data_size)
if "HMAC" in msg.annotations and Pyro4.config.HMAC_KEY:
if msg.annotations["HMAC"] != msg.hmac():
raise errors.SecurityError("message hmac mismatch")
elif ("HMAC" in msg.annotations) != bool(Pyro4.config.HMAC_KEY):
# Message contains hmac and local HMAC_KEY not set, or vice versa. This is not allowed.
err = "hmac key config not symmetric"
log.warning(err)
raise errors.SecurityError(err)
return msg
def hmac(self):
"""returns the hmac of the data and the annotation chunk values (except HMAC chunk itself)"""
mac = hmac.new(Pyro4.config.HMAC_KEY, self.data, digestmod=hashlib.sha1)
for k, v in self.annotations.items():
if k != "HMAC":
mac.update(v)
return mac.digest()
|