You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gns3-server/gns3server/stomp/frame.py

162 lines
4.4 KiB

# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
STOMP frame representation, decoding and encoding
http://stomp.github.io/stomp-specification-1.2.html
Adapted from Jason R. Briggs's code
https://github.com/jasonrbriggs/stomp.py
"""
import re
from .utils import encode
class Frame(object):
"""
A STOMP frame. Comprises a command, the headers and the body content.
"""
# Used to parse STOMP header lines in the format "key:value",
HEADER_LINE_RE = re.compile('(?P<key>[^:]+)[:](?P<value>.*)')
# As of STOMP 1.2, lines can end with either line feed, or carriage return plus line feed.
PREAMBLE_END_RE = re.compile('\n\n|\r\n\r\n')
# As of STOMP 1.2, lines can end with either line feed, or carriage return plus line feed.
LINE_END_RE = re.compile('\n|\r\n')
# NULL value
NULL = b'\x00'
def __init__(self, cmd=None, headers={}, body=None):
self._cmd = cmd
self._headers = headers
self._body = body
@property
def cmd(self):
return(self._cmd)
@cmd.setter
def cmd(self, cmd):
self._cmd = cmd
@property
def headers(self):
return(self._headers)
@headers.setter
def headers(self, headers):
self._headers = headers
@property
def body(self):
return(self._body)
@body.setter
def body(self, body):
self._body = body
def encode(self):
"""
Encodes this frame to be send on the wire
"""
lines = []
if self._cmd:
lines.append(self._cmd)
lines.append("\n")
for key, vals in sorted(self._headers.items()):
if type(vals) != tuple:
vals = (vals,)
for val in vals:
lines.append("%s:%s\n" % (key, val))
lines.append("\n")
if self._body:
lines.append(self._body)
if self._cmd:
lines.append(self.NULL)
encoded_lines = (encode(line) for line in lines)
return b''.join(encoded_lines)
@classmethod
def parse_headers(cls, lines, offset=0):
"""
Parses frame headers
:param lines: Frame preamble lines
:param offset: To start parsing at the given offset
:returns: Headers in dict header:value
"""
headers = {}
for header_line in lines[offset:]:
header_match = cls.HEADER_LINE_RE.match(header_line)
if header_match:
key = header_match.group('key')
if key not in headers:
headers[key] = header_match.group('value')
return headers
@classmethod
def parse_frame(cls, frame):
"""
Parses a frame
:params frame: The frame data to be parsed
:returns: STOMP Frame object
"""
f = Frame()
# End-of-line (EOL) indicates an heart beat frame
if frame == '\x0a':
f.cmd = 'heartbeat' # This will have the frame ignored
return f
mat = cls.PREAMBLE_END_RE.search(frame)
preamble_end = -1
if mat:
preamble_end = mat.start()
if preamble_end == -1:
preamble_end = len(frame)
preamble = frame[0:preamble_end]
preamble_lines = cls.LINE_END_RE.split(preamble)
f.body = frame[preamble_end + 2:]
if f.body[-1] == '\x00':
f.body = f.body[:-1]
# Skip any leading newlines
first_line = 0
while first_line < len(preamble_lines) and len(preamble_lines[first_line]) == 0:
first_line += 1
# Extract frame type/command
f.cmd = preamble_lines[first_line]
# Put headers into a key/value map
f.headers = cls.parse_headers(preamble_lines, first_line + 1)
return f