websocket-parser-1.0.0/0000755000175000017500000000000012546767615014363 5ustar globusglobuswebsocket-parser-1.0.0/lib/0000755000175000017500000000000012546767615015131 5ustar globusglobuswebsocket-parser-1.0.0/lib/websocket/0000755000175000017500000000000012546767615017117 5ustar globusglobuswebsocket-parser-1.0.0/lib/websocket/parser.rb0000644000175000017500000001475112546767615020750 0ustar globusglobusmodule WebSocket # # This class parses WebSocket messages and frames. # # Each message is divied in frames as described in RFC 6455. # # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 # +-+-+-+-+-------+-+-------------+-------------------------------+ # |F|R|R|R| opcode|M| Payload len | Extended payload length | # |I|S|S|S| (4) |A| (7) | (16/64) | # |N|V|V|V| |S| | (if payload len==126/127) | # | |1|2|3| |K| | | # +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + # | Extended payload length continued, if payload len == 127 | # + - - - - - - - - - - - - - - - +-------------------------------+ # | |Masking-key, if MASK set to 1 | # +-------------------------------+-------------------------------+ # | Masking-key (continued) | Payload Data | # +-------------------------------- - - - - - - - - - - - - - - - + # : Payload Data continued ... : # + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + # | Payload Data continued ... | # +---------------------------------------------------------------+ # # for more info on the frame format see: http://tools.ietf.org/html/rfc6455#section-5 # class Parser attr_writer :on_message, :on_error, :on_close, :on_ping, :on_pong def initialize @data = ''.force_encoding("ASCII-8BIT") @state = :header @current_message = nil end def on_message(&callback) @on_message = callback end def on_error(&callback) @on_error = callback end def on_close(&callback) @on_close = callback end def on_ping(&callback) @on_ping = callback end def on_pong(&callback) @on_pong = callback end # Stores the data in a buffer for later parsing def append(data) @data << data @data.force_encoding("ASCII-8BIT") end # Receive data and parse it return an array of parsed messages def receive(data) append(data) && next_messages end alias_method :<<, :receive # Parse all messages in buffer def next_messages Array.new.tap do |messages| while msg = next_message do messages << msg end end end # Parse next message in buffer def next_message read_header if @state == :header read_payload_length if @state == :payload_length read_mask_key if @state == :mask read_payload if @state == :payload @state == :complete ? process_frame! : nil rescue StandardError => ex if @on_error @on_error.call(ex.message) else raise ex end end private def read_header return unless @data.length >= 2 # Not enough data @first_byte, @second_byte = @data.slice!(0,2).unpack('C2') @state = :payload_length end def read_payload_length @payload_length = if message_size == :small payload_length_field else read_extended_payload_length end return unless @payload_length @state = masked? ? :mask : :payload end def read_extended_payload_length if message_size == :medium && @data.size >= 2 size = unpack_bytes(2,'n') ParserError.new("Wrong payload length. Expected to be greater than 125") unless size > 125 size elsif message_size == :large && @data.size >= 4 size = unpack_bytes(8,'Q>') ParserError.new("Wrong payload length. Expected to be greater than 65535") unless size > 65_535 size end end def read_mask_key return unless @data.size >= 4 @mask_key = unpack_bytes(4,'a4') @state = :payload end def read_payload return unless @data.length >= @payload_length # Not enough data payload_data = unpack_bytes(@payload_length, "a#{@payload_length}") @payload = if masked? WebSocket.unmask(payload_data, @mask_key) else payload_data end @state = :complete if @payload end def unpack_bytes(num, format) @data.slice!(0,num).unpack(format).first end def message_frame? [:text, :binary].include?(opcode) end def control_frame? [:close, :ping, :pong].include?(opcode) end def process_frame! if @current_message @current_message << @payload else @current_message = @payload end completed_message = if fin? && message_frame? @current_message else nil end fin? ? process_message! : opcode # store the opcode reset_frame! completed_message end def process_message! case opcode when :text msg = @current_message.force_encoding("UTF-8") raise ParserError.new('Payload data is not valid UTF-8') unless msg.valid_encoding? @on_message.call(msg) if @on_message when :binary @on_message.call(@current_message) if @on_message when :ping @on_ping.call(@current_message) if @on_ping when :pong @on_pong.call(@current_message) if @on_pong when :close status_code, message = @current_message.unpack('S 'websocket', 'Connection' => 'Upgrade', 'Sec-WebSocket-Accept' => ClientHandshake.accept_token_for(websocket_key_header) } end def websocket_version_header headers['Sec-WebSocket-Version'] || headers['Sec-Websocket-Version'] end def websocket_key_header headers['Sec-Websocket-Key'] || headers['Sec-WebSocket-Key'] end def to_data data = "#{verb.to_s.upcase} #{uri.path} HTTP/#{version}#{CRLF}" @headers.each do |field, value| data << "#{field}: #{value}#{CRLF}" end data << CRLF data end end endwebsocket-parser-1.0.0/lib/websocket_parser.rb0000644000175000017500000000355312546767615021026 0ustar globusglobusrequire "websocket/version" require "websocket/client_handshake" require "websocket/server_handshake" require "websocket/message" require "websocket/parser" module WebSocket extend self class WebSocket::ParserError < StandardError; end PROTOCOL_VERSION = 13 # RFC 6455 GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" CRLF = "\r\n" # see http://tools.ietf.org/html/rfc6455#section-11.8 OPCODES = { 0 => :continuation, 1 => :text, 2 => :binary, 8 => :close, 9 => :ping, 10 => :pong } OPCODE_VALUES = { :continuation => 0, :text => 1, :binary => 2, :close => 8, :ping => 9, :pong => 10 } # See: http://tools.ietf.org/html/rfc6455#section-7.4.1 STATUS_CODES = { 1000 => :normal_closure, 1001 => :peer_going_away, 1002 => :protocol_error, 1003 => :data_error, 1007 => :data_not_consistent, 1008 => :policy_violation, 1009 => :message_too_big, 1010 => :extension_required, 1011 => :unexpected_condition } # Determines how to unpack the frame depending on # the payload length and wether the frame is masked def frame_format(payload_length, masked = false) format = 'CC' if payload_length > 65_535 format += 'Q>' elsif payload_length > 125 format += 'n' end if masked format += 'a4' end if payload_length > 0 format += "a#{payload_length}" end format end def mask(data, mask_key) masked_data = ''.encode!("ASCII-8BIT") mask_bytes = mask_key.bytes.to_a data.bytes.each_with_index do |byte, i| masked_data << (byte ^ mask_bytes[i%4]) end masked_data end # The same algorithm applies regardless of the direction of the translation, # e.g., the same steps are applied to mask the data as to unmask the data. alias_method :unmask, :mask end websocket-parser-1.0.0/.gitignore0000644000175000017500000000023712546767615016355 0ustar globusglobus*.gem *.rbc .bundle .config .yardoc .rbx Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp websocket-parser-1.0.0/Rakefile0000644000175000017500000000006012546767615016024 0ustar globusglobus#!/usr/bin/env rake require "bundler/gem_tasks" websocket-parser-1.0.0/websocket_parser.gemspec0000644000175000017500000000147112546767615021275 0ustar globusglobus# -*- encoding: utf-8 -*- require File.expand_path('../lib/websocket/version', __FILE__) Gem::Specification.new do |gem| gem.authors = ["Alberto Fernandez-Capel"] gem.email = ["afcapel@gmail.com"] gem.description = %q{WebsocketParser is a RFC6455 compliant parser for websocket messages} gem.summary = %q{Parse websockets messages in Ruby} gem.homepage = "http://github.com/afcapel/websocket_parser" gem.files = `git ls-files`.split($\) gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.name = "websocket_parser" gem.require_paths = ["lib"] gem.version = WebSocket::VERSION gem.add_development_dependency 'rake' gem.add_development_dependency 'rspec' end websocket-parser-1.0.0/spec/0000755000175000017500000000000012546767615015315 5ustar globusglobuswebsocket-parser-1.0.0/spec/websocket/0000755000175000017500000000000012546767615017303 5ustar globusglobuswebsocket-parser-1.0.0/spec/websocket/message_spec.rb0000644000175000017500000000525612546767615022276 0ustar globusglobusrequire 'spec_helper' describe WebSocket::Message do it "knows binary representation of messages with more than 65,535 bytes" do text = 1500.times.collect { 'All work and no play makes Jack a dull boy.' }.join("\n\n") message = WebSocket::Message.new(text) data = message.to_data # 2 bytes from header + 8 bytes from extended payload length + payload data.size.should eq(2 + 8 + text.length) first_byte, second_byte, ext_length, payload = data.unpack("CCQ>a#{text.length}") first_byte.should eq(0b10000001) # Text frame with FIN bit set second_byte.should eq(0b01111111) # Unmasked. Payload length 127. ext_length.should eq(text.length) payload.should eq(text) end it "knows binary representation of messages between 126 and 65,535 bytes" do text = '0'*127 data = WebSocket::Message.new(text).to_data # 2 bytes from header + 2 bytes from extended payload length + payload data.size.should eq(2 + 2 + text.length) # extended payload length should respect endianness data[2..3].should eq([0x00, 0x7F].pack('C*')) first_byte, second_byte, ext_length, payload = data.unpack("CCna#{text.length}") first_byte.should eq(0b10000001) # Text frame with FIN bit set second_byte.should eq(0b01111110) # Unmasked. Payload length 126. ext_length.should eq(text.length) payload.should eq(text) end it "knows binary representation of messages with less than 126 bytes" do text = '0'*125 data = WebSocket::Message.new(text).to_data # 2 bytes from header + payload data.size.should eq(2 + text.length) first_byte, second_byte, payload = data.unpack("CCa#{text.length}") first_byte.should eq(0b10000001) # Text frame with FIN bit set second_byte.should eq(0b01111101) # Unmasked. Payload length 125. payload.should eq(text) end it "can be masked" do message = WebSocket::Message.new('The man with the Iron Mask') message.masked?.should be_false message.mask! message.masked?.should be_true end it "allows status codes for control frames" do msg = WebSocket::Message.close(1001, 'Bye') msg.status_code.should eq(1001) msg.payload.should eq([1001, 'Bye'].pack('S "server.example.com", "Upgrade" => "websocket", "Connection" => "Upgrade", "Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==", "Origin" => "http://example.com", "Sec-WebSocket-Protocol" => "chat, superchat", "Sec-WebSocket-Version" => "13" } end let(:client_handshake) { WebSocket::ClientHandshake.new(:get, '/', handshake_headers) } it "can validate handshake format" do client_handshake.valid?.should be_true end it "can generate an accept response for the client" do response = client_handshake.accept_response response.headers['Upgrade'].should eq('websocket') response.headers['Connection'].should eq('Upgrade') response.headers['Sec-WebSocket-Accept'].should eq('s3pPLMBiTxaQ9kYGzzhZRbK+xOo=') end it "can be seariakized to data" do expected_lines = [ "GET / HTTP/1.1", "Host: server.example.com", "Upgrade: websocket", "Connection: Upgrade", "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==", "Origin: http://example.com", "Sec-WebSocket-Protocol: chat, superchat", "Sec-WebSocket-Version: 13", "\r\n" ] client_handshake.to_data.should eq expected_lines.join("\r\n") end end websocket-parser-1.0.0/spec/websocket/parser_spec.rb0000644000175000017500000001263012546767615022140 0ustar globusglobusrequire 'spec_helper' describe WebSocket::Parser do let(:received_messages) { [] } let(:received_errors) { [] } let(:received_closes) { [] } let(:received_pings) { [] } let(:received_pongs) { [] } let(:parser) do parser = WebSocket::Parser.new parser.on_message { |m| received_messages << m } parser.on_error { |m| received_errors << m } parser.on_close { |status, message| received_closes << [status, message] } parser.on_ping { |m| received_pings << m } parser.on_pong { |m| received_pongs << m } parser end it "recognizes a text message" do parser << WebSocket::Message.new('Once upon a time').to_data received_messages.first.should eq('Once upon a time') end it "returns parsed messages on parse" do msg1 = WebSocket::Message.new('Now is the winter of our discontent').to_data msg2 = WebSocket::Message.new('Made glorious summer by this sun of York').to_data messages = parser << msg1.slice!(0,5) messages.should be_empty # We don't have a complete message yet messages = parser << msg1 + msg2 messages[0].should eq('Now is the winter of our discontent') messages[1].should eq('Made glorious summer by this sun of York') end it "does not return control frames" do msg = WebSocket::Message.close(1001, 'Goodbye!').to_data messages = parser << msg messages.should be_empty end it "can receive a message in parts" do data = WebSocket::Message.new('Once upon a time').to_data parser << data.slice!(0, 5) received_messages.should be_empty parser << data received_messages.first.should eq('Once upon a time') end it "can receive succesive messages" do msg1 = WebSocket::Message.new('Now is the winter of our discontent') msg2 = WebSocket::Message.new('Made glorious summer by this sun of York') parser << msg1.to_data parser << msg2.to_data received_messages[0].should eq('Now is the winter of our discontent') received_messages[1].should eq('Made glorious summer by this sun of York') end it "can receive medium size messages" do # Medium size messages has a payload length between 127 and 65_535 bytes text = 4.times.collect { 'All work and no play makes Jack a dull boy.' }.join("\n\n") text.length.should be > 127 text.length.should be < 65_536 parser << WebSocket::Message.new(text).to_data received_messages.first.should eq(text) end it "can receive large size messages" do # Large size messages has a payload length greater than 65_535 bytes text = 1500.times.collect { 'All work and no play makes Jack a dull boy.' }.join("\n\n") text.length.should be > 65_536 parser << WebSocket::Message.new(text).to_data # Check lengths first to avoid gigantic error message received_messages.first.length.should eq(text.length) received_messages.first.should eq(text) end it "recognizes a ping message" do parser << WebSocket::Message.ping.to_data received_pings.size.should eq(1) end it "recognizes a pong message" do parser << WebSocket::Message.pong.to_data received_pongs.size.should eq(1) end it "recognizes a close message with status code and message" do parser << WebSocket::Message.close(1001, 'Browser leaving page').to_data status, message = received_closes.first status.should eq(:peer_going_away) # Status code 1001 message.should eq('Browser leaving page') end it "recognizes a close message without status code" do parser << WebSocket::Message.close.to_data status, message = received_closes.first status.should be_nil message.should be_empty end it "recognizes a masked frame" do msg = WebSocket::Message.new('Once upon a time') msg.mask! parser << msg.to_data received_messages.first.should eq('Once upon a time') end context "examples from the spec" do # These are literal examples from the spec it "recognizes single-frame unmasked text message" do parser << [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f].pack('C*') received_messages.first.should eq('Hello') end it "recognizes single-frame masked text message" do parser << [0x81, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58].pack('C*') received_messages.first.should eq('Hello') end it "recognizes a fragmented unmasked text message" do parser << [0x01, 0x03, 0x48, 0x65, 0x6c].pack('C*') # contains "Hel" received_messages.should be_empty parser << [0x80, 0x02, 0x6c, 0x6f].pack('C*') # contains "lo" received_messages.first.should eq('Hello') end it "recognizes an unnmasked ping request" do parser << [0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f].pack('C*') received_pings.size.should eq(1) end it "recognizes a masked pong response" do parser << [0x8a, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58].pack('C*') received_pongs.size.should eq(1) end it "recognizes 256 bytes binary message in a single unmasked frame" do data = Array.new(256) { rand(256) }.pack('c*') parser << [0x82, 0x7E, 0x0100].pack('CCn') + data received_messages.first.should eq(data) end it "recoginzes 64KiB binary message in a single unmasked frame" do data = Array.new(65536) { rand(256) }.pack('c*') parser << [0x82, 0x7F, 0x0000000000010000].pack('CCQ>') + data received_messages.first.should eq(data) end end end websocket-parser-1.0.0/spec/spec_helper.rb0000644000175000017500000000015512546767615020134 0ustar globusglobusrequire 'bundler/setup' require 'websocket_parser' RSpec.configure do |config| config.warnings = true end websocket-parser-1.0.0/LICENSE0000644000175000017500000000206712546767615015375 0ustar globusglobusCopyright (c) 2012 Alberto Fernandez-Capel MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.websocket-parser-1.0.0/README.md0000644000175000017500000000435412546767615015650 0ustar globusglobus# WebSocketParser [![Build Status](https://secure.travis-ci.org/afcapel/websocket_parser.png)](http://travis-ci.org/afcapel/websocket_parser) [![Code Climate](https://codeclimate.com/github/afcapel/websocket_parser.png)](https://codeclimate.com/github/afcapel/websocket_parser) WebsocketParser is a RFC6455 compliant parser for websocket messages written in Ruby. It is intended to write websockets servers in Ruby, but it only handles the parsing of the WebSocket protocol, leaving I/O to the server. ## Installation Add this line to your application's Gemfile: gem 'websocket_parser' And then execute: $ bundle Or install it yourself as: $ gem install websocket_parser ## Usage. TMTOWTDI. ### Return values The simplest way to use the websocket parser is to create a new one, fetch it with data and query it for new messages. ```ruby require 'websocket_parser' parser = WebSocket::Parser.new parser.append data parser.next_message # return next message or nil parser.next_messages # return an array with all parsed messages # To send a message: socket << WebSocket::Message.new('Hi there!').to_data ``` Only text or binary messages are returned on the parse methods. To intercept control frames use the parser's callbacks. ### Use callbacks In addition to return values, you can register callbacks to get notified when a certain event happens. ```ruby require 'websocket_parser' socket = # Handle I/O with your server/event loop. parser = WebSocket::Parser.new parser.on_message do |m| puts "Received message #{m}" end parser.on_error do |m| puts "Received error #{m}" socket.close! end parser.on_close do |status, message| # According to the spec the server must respond with another # close message before closing the connection socket << WebSocket::Message.close.to_data socket.close! puts "Client closed connection. Status: #{status}. Reason: #{message}" end parser.on_ping do |payload| socket << WebSocket::Message.pong(payload).to_data end parser << socket.read(4096) ``` ## Contributing 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Added some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request websocket-parser-1.0.0/metadata.yml0000644000175000017500000000425312546767615016672 0ustar globusglobus--- !ruby/object:Gem::Specification name: websocket_parser version: !ruby/object:Gem::Version version: 1.0.0 platform: ruby authors: - Alberto Fernandez-Capel autorequire: bindir: bin cert_chain: [] date: 2015-01-25 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' description: WebsocketParser is a RFC6455 compliant parser for websocket messages email: - afcapel@gmail.com executables: [] extensions: [] extra_rdoc_files: [] files: - ".gitignore" - ".travis.yml" - Gemfile - LICENSE - README.md - Rakefile - lib/websocket/client_handshake.rb - lib/websocket/message.rb - lib/websocket/parser.rb - lib/websocket/server_handshake.rb - lib/websocket/version.rb - lib/websocket_parser.rb - spec/spec_helper.rb - spec/websocket/handshake_spec.rb - spec/websocket/message_spec.rb - spec/websocket/parser_spec.rb - websocket_parser.gemspec homepage: http://github.com/afcapel/websocket_parser licenses: [] metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.2.2 signing_key: specification_version: 4 summary: Parse websockets messages in Ruby test_files: - spec/spec_helper.rb - spec/websocket/handshake_spec.rb - spec/websocket/message_spec.rb - spec/websocket/parser_spec.rb websocket-parser-1.0.0/.travis.yml0000644000175000017500000000020712546767615016473 0ustar globusglobuslanguage: ruby rvm: - 1.9.2 - 1.9.3 - jruby-19mode # JRuby in 1.9 mode - rbx - 2.0.0 - 2.1.0 script: bundle exec rspec specwebsocket-parser-1.0.0/Gemfile0000644000175000017500000000033712546767615015661 0ustar globusglobussource 'https://rubygems.org' gem 'jruby-openssl' if defined? JRUBY_VERSION platforms :rbx do gem 'racc' gem 'rubysl', '~> 2.0' gem 'psych' end # Specify your gem's dependencies in websocket_parser.gemspec gemspec