# -*- encoding: utf-8 -*- # This class serializes Stomp frames to IO streams. Submodule mixins within # this class are used to adjust the serialization behavior depending upon # the Stomp Protocol version being used. # @see Stomper::Support::Ruby1_8::FrameSerializer Implementation for Ruby 1.8.7 # @see Stomper::Support::Ruby1_9::FrameSerializer Implementation for Ruby 1.9 class Stomper::FrameSerializer # The character that must be present at the end of every Stomp frame. FRAME_TERMINATOR = "\000" # Creates a new frame serializer that will read {Stomper::Frame frames} from # and write {Stomper::Frame frames} to the supplied IO object (typically # a TCP or SSL socket.) # @param [IO] io IO stream to read from and write to def initialize(io) @io = io @write_mutex = ::Mutex.new @read_mutex = ::Mutex.new end # Extends the serializer based on the version of the protocol being used. # @param [String] version protocol version being used # @return [self] def extend_for_protocol(version) if EXTEND_BY_VERSION[version] EXTEND_BY_VERSION[version].each { |m| extend m } end self end # Serializes and writes a {Stomper::Frame} to the underlying IO stream. This # includes setting the appropriate values for 'content-length' and # 'content-type' headers, if applicable. # @param [Stomper::Frame] frame # @return [Stomper::Frame] the frame that was passed to the method def write_frame(frame) @write_mutex.synchronize { __write_frame__(frame) } end # Deserializes and returns a {Stomper::Frame} read from the underlying IO # stream. If the IO stream produces data that violates the Stomp protocol # specification, an instance of {Stomper::Errors::FatalProtocolError}, or # one of its subclasses, will be raised. # @return [Stomper::Frame] # @raise [Stomper::Errors::MalformedFrameError] if the frame is not properly terminated # @raise [Stomper::Errors::MalformedHeaderError] if a header is malformed (eg: contains no ':' separator) def read_frame @read_mutex.synchronize { __read_frame__ } end # Converts the headers of the supplied frame into a single "\n" delimited # string that's suitable for writing to io. # @param [Stomper::Frame] frame the frame whose headers should be serialized # @return [String] def serialize_headers(frame) serialized = frame.headers.inject('') do |head_str, (k, v)| k = escape_header_name(k) next head_str if k.empty? || ['content-type', 'content-length'].include?(k) head_str << "#{k}:#{escape_header_value(v)}\n" end if frame.body if ct = determine_content_type(frame) serialized << "content-type:#{ct}\n" end if clen = determine_content_length(frame) serialized << "content-length:#{clen}\n" end end serialized end # Escape a header name to comply with Stomp Protocol 1.0 specifications. # All LF ("\n") and ":" characters are replaced with empty strings. # @note If the connection is using the 1.1 protocol, this method will # be overridden by {FrameSerializer::V1_1#escape_header_name} # @param [String] str # @return [String] escaped header name def escape_header_name(str) str.gsub(/[\n:]/, '') end # Escape a header value to comply with Stomp Protocol 1.0 specifications. # All LF ("\n") characters are replaced with empty strings. # @note If the connection is using the 1.1 protocol, this method will # be overridden by {FrameSerializer::V1_1#escape_header_value} # @param [String] str # @return [String] escaped header value def escape_header_value(str) str.gsub(/\n/, '') end # Return the header name as it was passed. Stomp 1.0 does not provide # any means for escaping special characters such as ":" and "\n" # @note If the connection is using the 1.1 protocol, this method will # be overridden by {FrameSerializer::V1_1#unescape_header_name} # @param [String] str # @return [String] def unescape_header_name(str); str; end alias :unescape_header_value :unescape_header_name private # These are the un-synchronized methods that do the real reading/writing # of frames. This approach facilitates easier testing of Thread safety def __write_frame__(frame) if frame.command @io.write [frame.command, "\n", serialize_headers(frame), "\n", frame.body, FRAME_TERMINATOR].compact.join else @io.write "\n" end frame end def __read_frame__ command = @io.gets return nil if command.nil? command.chomp! frame = Stomper::Frame.new unless command.nil? || command.empty? frame.command = command while (header_line = get_header_line.chomp).length > 0 raise ::Stomper::Errors::MalformedHeaderError, "unterminated header: '#{header_line}'" unless header_line.include? ':' k, v = header_line.split(':', 2) frame.headers.append(unescape_header_name(k), unescape_header_value(v)) end body = nil if frame[:'content-length'] && (len = frame[:'content-length'].to_i) > 0 body = @io.read len raise ::Stomper::Errors::MalformedFrameError, "frame was not properly terminated" if get_body_byte else while (c = get_body_byte) body ||= "" body << c end end frame.body = body && encode_body(body, frame[:'content-type']) end frame end # Stomp Protocol 1.1 specific frame writing / reading methods. module V1_1 # Mapping of characters to their appropriate escape sequences. This # is used when escaping headers for frames being written to the stream. CHARACTER_ESCAPES = { ':' => "\\c", "\n" => "\\n", "\\" => "\\\\" } # Mapping of escape sequences to their appropriate characters. This # is used when unescaping headers being read from the stream. ESCAPE_SEQUENCES = { 'c' => ':', '\\' => "\\", 'n' => "\n" } # Escape a header name to comply with Stomp Protocol 1.1 specifications. # All special characters (the keys of {CHARACTER_ESCAPES}) are replaced # by their corresponding escape sequences. # @param [String] str # @return [String] escaped header name def escape_header_name(hdr) hdr.each_char.inject('') do |esc, ch| esc << (CHARACTER_ESCAPES[ch] || ch) end end alias :escape_header_value :escape_header_name # Return the header name after known escape sequences have been # translated to their respective values. The keys of {ESCAPE_SEQUENCES}, # prefixed with a '\' character, denote the allowed escape sequences. # If an unknown escape sequence is encountered, an error is raised. # @param [String] str header string to unescape # @return [String] unescaped header string # @raise [Stomper::Errors::InvalidHeaderEscapeSequenceError] if an # unknown escape sequence is encountered within the string or if # an escape sequence is not properly completed (the last character of # +str+ is '\') def unescape_header_name(str) state = :read str.each_char.inject('') do |unesc, ch| case state when :read if ch == '\\' state = :unescape else unesc << ch end when :unescape state = :read if ESCAPE_SEQUENCES[ch] unesc << ESCAPE_SEQUENCES[ch] else raise ::Stomper::Errors::InvalidHeaderEscapeSequenceError, "invalid header escape sequence encountered '\\#{ch}'" end end unesc end.tap do raise ::Stomper::Errors::InvalidHeaderEscapeSequenceError, "incomplete escape sequence encountered in '#{str}'" if state != :read end end alias :unescape_header_value :unescape_header_name end # The modules to mix-in to {FrameSerializer} instances depending upon which # protocol version is being used. EXTEND_BY_VERSION = { '1.1' => [ ::Stomper::FrameSerializer::V1_1 ] } end