# -*- 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 . """ 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[^:]+)[:](?P.*)') # 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