Class: Mongrel2::WebSocket::Frame

Inherits:
Object
  • Object
show all
Extended by:
Loggability
Includes:
Constants
Defined in:
lib/mongrel2/websocket.rb

Overview

WebSocket frame class; this is used for both requests and responses in WebSocket services.

Constant Summary collapse

DEFAULT_FLAGS =

The default frame header flags: FIN + CLOSE

DEFAULT_FRAGMENT_SIZE =

The default size of the payload of fragment frames

4096

Constants included from Constants

Constants::CLOSE_ABNORMAL_STATUS, Constants::CLOSE_BAD_DATA, Constants::CLOSE_BAD_DATA_TYPE, Constants::CLOSE_EXCEPTION, Constants::CLOSE_GOING_AWAY, Constants::CLOSE_MESSAGE_TOO_LARGE, Constants::CLOSE_MISSING_EXTENSION, Constants::CLOSE_MISSING_STATUS, Constants::CLOSE_NORMAL, Constants::CLOSE_POLICY_VIOLATION, Constants::CLOSE_PROTOCOL_ERROR, Constants::CLOSE_RESERVED, Constants::CLOSE_TLS_ERROR, Constants::CLOSING_STATUS_DESC, Constants::DEFAULT_CHUNKSIZE, Constants::FIN_FLAG, Constants::OPCODE, Constants::OPCODE_BITMASK, Constants::OPCODE_CONTROL_MASK, Constants::OPCODE_NAME, Constants::RSV1_FLAG, Constants::RSV2_FLAG, Constants::RSV3_FLAG, Constants::RSV_FLAG_MASK

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(payload = '', *flags) ⇒ Frame

Create a new websocket frame that will be the body of a request or response.



495
496
497
498
499
500
501
502
# File 'lib/mongrel2/websocket.rb', line 495

def initialize( payload='', *flags )
	@payload   = StringIO.new( payload.dup )
	@flags     = DEFAULT_FLAGS
	@errors    = []
	@chunksize = DEFAULT_CHUNKSIZE

	self.set_flags( *flags ) unless flags.empty?
end

Instance Attribute Details

#chunksizeObject

The number of bytes to write to Mongrel in a single “chunk”



522
523
524
# File 'lib/mongrel2/websocket.rb', line 522

def chunksize
  @chunksize
end

#errorsObject (readonly)

The Array of validation errors



519
520
521
# File 'lib/mongrel2/websocket.rb', line 519

def errors
  @errors
end

#flagsObject

The frame’s header flags as an Integer



516
517
518
# File 'lib/mongrel2/websocket.rb', line 516

def flags
  @flags
end

#payloadObject Also known as: body

The payload data



510
511
512
# File 'lib/mongrel2/websocket.rb', line 510

def payload
  @payload
end

Class Method Details

.attr_flag(name, bitmask) ⇒ Object

Define accessors for the flag of the specified name and bit.



476
477
478
479
480
481
482
483
484
485
486
487
# File 'lib/mongrel2/websocket.rb', line 476

def self::attr_flag( name, bitmask )
	define_method( "#{name}?" ) do
		(self.flags & bitmask).nonzero?
	end
	define_method( "#{name}=" ) do |newvalue|
		if newvalue
			self.flags |= bitmask
		else
			self.flags ^= ( self.flags & bitmask )
		end
	end
end

.each_fragment(io, opcode, size: DEFAULT_FRAGMENT_SIZE, &block) ⇒ Object

Create one or more fragmented frames for the data read from io and yield each to the specified block. If no block is given, return a iterator that will yield the frames instead. The io can be any object that responds to #readpartial, and the blocking semantics follow those of that method when iterating.

Raises:

  • (ArgumentError)


439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
# File 'lib/mongrel2/websocket.rb', line 439

def self::each_fragment( io, opcode, size: DEFAULT_FRAGMENT_SIZE, &block )
	raise ArgumentError, "Invalid opcode %p" % [opcode] unless OPCODE.key?( opcode )

	iter = Enumerator.new do |yielder|
		count = 0

		until io.eof?
			self.log.debug "Reading frame %d" % [ count ]
			data = io.readpartial( size )
			frame = if count.zero?
					new( data, opcode )
				else
					new( data, :continuation )
				end
			frame.fin = io.eof?

			yielder.yield( frame )

			count += 1
		end
	end

	return iter.each( &block ) if block
	return iter
end

Instance Method Details

#<<(object) ⇒ Object

Append the given object to the payload. Returns the Frame for chaining.



615
616
617
618
# File 'lib/mongrel2/websocket.rb', line 615

def <<( object )
	self.payload << object
	return self
end

#control?Boolean

Returns true if the request is a WebSocket control frame.

Returns:

  • (Boolean)


608
609
610
# File 'lib/mongrel2/websocket.rb', line 608

def control?
	return ( self.flags & OPCODE_CONTROL_MASK ).nonzero?
end

#each_byte(&block) ⇒ Object Also known as: bytes

Return an Enumerator for the bytes of the raw frame as it appears on the wire.



694
695
696
697
698
699
700
701
702
703
704
705
706
# File 'lib/mongrel2/websocket.rb', line 694

def each_byte( &block )
	self.log.debug "Making a bytes iterator for a %s payload" %
		[ self.payload.external_encoding.name ]

	payload_copy = self.payload.clone
	payload_copy.set_encoding( 'binary' )
	payload_copy.rewind

	iter = self.make_header.each_byte + payload_copy.each_byte

	return iter unless block
	return iter.each( &block )
end

#each_chunk(&block) ⇒ Object

Mongrel2::Connection API – Yield the response in chunks if called with a block, else return an Enumerator that will do the same.



672
673
674
675
676
677
678
679
680
681
682
683
# File 'lib/mongrel2/websocket.rb', line 672

def each_chunk( &block )
	self.validate

	iter = Enumerator.new do |yielder|
		self.bytes.each_slice( self.chunksize ) do |bytes|
			yielder.yield( bytes.pack('C*') )
		end
	end

	return iter unless block
	return iter.each( &block )
end

#has_rsv_flags?Boolean

Returns true if one or more of the RSV1-3 bits is set.

Returns:

  • (Boolean)


572
573
574
# File 'lib/mongrel2/websocket.rb', line 572

def has_rsv_flags?
	return ( self.flags & RSV_FLAG_MASK ).nonzero?
end

#inspectObject

Return the frame as a human-readable string suitable for debugging.



711
712
713
714
715
716
717
# File 'lib/mongrel2/websocket.rb', line 711

def inspect
	return "#<%p:%#0x %s>" % [
		self.class,
		self.object_id * 2,
		self.inspect_details,
	]
end

#make_close_frame(statuscode = Mongrel2::WebSocket::CLOSE_NORMAL) ⇒ Object

Set the :close opcode on this frame and set its status to statuscode.



628
629
630
631
# File 'lib/mongrel2/websocket.rb', line 628

def make_close_frame( statuscode=Mongrel2::WebSocket::CLOSE_NORMAL )
	self.opcode = :close
	self.set_status( statuscode )
end

#numeric_opcodeObject

Return the numeric opcode of the frame.



585
586
587
# File 'lib/mongrel2/websocket.rb', line 585

def numeric_opcode
	return self.flags & OPCODE_BITMASK
end

#opcodeObject

Returns the name of the frame’s opcode as a Symbol. The #numeric_opcode method returns the numeric one.



579
580
581
# File 'lib/mongrel2/websocket.rb', line 579

def opcode
	return OPCODE_NAME[ self.numeric_opcode ]
end

#opcode=(code) ⇒ Object

Set the frame’s opcode to code, which should be either a numeric opcode or its equivalent name (i.e., :continuation, :text, :binary, :close, :ping, :pong)



592
593
594
595
596
597
598
599
600
601
602
603
604
# File 'lib/mongrel2/websocket.rb', line 592

def opcode=( code )
	opcode = nil

	if code.is_a?( Numeric )
		opcode = Integer( code )
	else
		opcode = OPCODE[ code.to_sym ] or
			raise ArgumentError, "unknown opcode %p" % [ code ]
	end

	self.flags ^= ( self.flags & OPCODE_BITMASK )
	self.flags |= opcode
end

#puts(*objects) ⇒ Object

Write the given objects to the payload, calling #to_s on each one.



622
623
624
# File 'lib/mongrel2/websocket.rb', line 622

def puts( *objects )
	self.payload.puts( *objects )
end

#set_flags(*flag_symbols) ⇒ Object

Apply flag bits and opcodes: (:fin, :rsv1, :rsv2, :rsv3, :continuation, :text, :binary, :close, :ping, :pong) to the frame.

# Transform the frame into a CLOSE frame and set its FIN flag
frame.set_flags( :fin, :close )


545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
# File 'lib/mongrel2/websocket.rb', line 545

def set_flags( *flag_symbols )
	flag_symbols.flatten!
	flag_symbols.compact!

	self.log.debug "Setting flags for symbols: %p" % [ flag_symbols ]

	flag_symbols.each do |flag|
		case flag
		when :fin, :rsv1, :rsv2, :rsv3
			self.__send__( "#{flag}=", true )
		when :continuation, :text, :binary, :close, :ping, :pong
			self.opcode = flag
		when Integer
			self.log.debug "  setting Integer flags directly: %#08b" % [ flag ]
			self.flags |= flag
		when /\A0x\h{2}\z/
			val = Integer( flag )
			self.log.debug "  setting (stringified) Integer flags directly: %#08b" % [ val ]
			self.flags = val
		else
			raise ArgumentError, "Don't know what the %p flag is." % [ flag ]
		end
	end
end

#set_status(statuscode) ⇒ Object

Overwrite the frame’s payload with a status message based on statuscode.



636
637
638
639
640
641
642
# File 'lib/mongrel2/websocket.rb', line 636

def set_status( statuscode )
	self.log.warn "Unknown status code %d" unless CLOSING_STATUS_DESC.key?( statuscode )
	status_msg = "%d %s" % [ statuscode, CLOSING_STATUS_DESC[statuscode] ]

	self.payload.truncate( 0 )
	self.payload.puts( status_msg )
end

#to_sObject

Stringify into a response suitable for sending to the client.



687
688
689
# File 'lib/mongrel2/websocket.rb', line 687

def to_s
	return self.each_byte.to_a.pack( 'C*' )
end

#valid?Boolean

Sanity-checks the frame and returns false if any problems are found. Error messages will be in #errors.

Returns:

  • (Boolean)


658
659
660
661
662
663
664
665
666
667
# File 'lib/mongrel2/websocket.rb', line 658

def valid?
	self.errors.clear

	self.validate_payload_encoding
	self.validate_control_frame
	self.validate_opcode
	self.validate_reserved_flags

	return self.errors.empty?
end

#validateObject

Validate the frame, raising a Mongrel2::WebSocket::FrameError if there are validation problems.



647
648
649
650
651
652
653
# File 'lib/mongrel2/websocket.rb', line 647

def validate
	unless self.valid?
		self.log.error "Validation failed."
		raise Mongrel2::WebSocket::FrameError, "invalid frame: %s" %
			[ self.errors.join(', ') ]
	end
end