Class: Iodine::Http::Websockets

Inherits:
Protocol
  • Object
show all
Defined in:
lib/iodine/http/websockets.rb

Instance Attribute Summary

Attributes inherited from Protocol

#io, #locker, #options

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Protocol

#call, #close, #closed?, each, #id, #initialize, #read, #set_timeout, #ssl?, #timeout?, #write

Constructor Details

This class inherits a constructor from Iodine::Protocol

Class Method Details

.broadcast(data, ignore_io = nil) ⇒ Object

Broadcasts data to ALL the websocket connections EXCEPT the once specified (if specified).

Data broadcasted will be recived by the websocket handler it’s #on_broadcast(ws) method (if exists).

Accepts:

data

One object of data. Usually a Hash, Array, String or a JSON formatted object.

ignore_io (optional)

The IO to be ignored by the broadcast. Usually the broadcaster’s IO.



102
103
104
105
106
107
108
109
110
# File 'lib/iodine/http/websockets.rb', line 102

def self.broadcast data, ignore_io = nil
  if ignore_io
    ig_id = ignore_io.object_id
    each {|io| Iodine.run io, data, &broadcast_proc unless io.object_id == ig_id}
  else
    each {|io| Iodine.run io, data, &broadcast_proc }
  end
  true
end

.default_timeoutObject

Gets the new connection timeout in seconds. Whenever this timeout is reached, a ping will be sent. Defaults to 40 (seconds).



168
169
170
# File 'lib/iodine/http/websockets.rb', line 168

def self.default_timeout
  @default_timeout
end

.default_timeout=(val) ⇒ Object

Sets the new connection timeout in seconds. Whenever this timeout is reached, a ping will be sent. Defaults to 40 (seconds).



172
173
174
# File 'lib/iodine/http/websockets.rb', line 172

def self.default_timeout= val
  @default_timeout = val
end

.handshake(request, response, handler) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/iodine/http/websockets.rb', line 135

def self.handshake request, response, handler
  # review handshake (version, extentions)
  # should consider adopting the websocket gem for handshake and framing:
  # https://github.com/imanel/websocket-ruby
  # http://www.rubydoc.info/github/imanel/websocket-ruby
  return refuse response unless handler || handler == true
  io = request[:io]
  response.keep_alive = true
  response.status = 101
  response['upgrade'.freeze] = 'websocket'.freeze
  response['content-length'.freeze] = '0'.freeze
  response['connection'.freeze] = 'Upgrade'.freeze
  response['sec-websocket-version'.freeze] = '13'.freeze
  # Note that the client is only offering to use any advertised extensions
  # and MUST NOT use them unless the server indicates that it wishes to use the extension.
  ws_extentions = []
  ext = []
  request['sec-websocket-extensions'.freeze].to_s.split(/[\s]*[,][\s]*/.freeze).each {|ex| ex = ex.split(/[\s]*;[\s]*/.freeze); ( ( tmp = SUPPORTED_EXTENTIONS[ ex[0] ].call(ex[1..-1]) ) && (ws_extentions << tmp) && (ext << tmp.name) ) if SUPPORTED_EXTENTIONS[ ex[0] ] }
  ext.compact!
  if ext.any?
    response['sec-websocket-extensions'.freeze] = ext.join(', '.freeze)
  else
    ws_extentions = nil
  end
  response['sec-websocket-accept'.freeze] = Digest::SHA1.base64digest(request['sec-websocket-key'.freeze] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'.freeze)
  response.session
  # Iodine.log "#{@request[:client_ip]} [#{Time.now.utc}] - #{@connection.object_id} Upgraded HTTP to WebSockets.\n"
  response.finish
  self.new(io.io, handler: handler, request: request, ext: ws_extentions)
  return true
end

.message_size_limitObject

Gets the message byte size limit for a Websocket message. Defaults to 0 (no limit)



185
186
187
# File 'lib/iodine/http/websockets.rb', line 185

def self.message_size_limit
  @message_size_limit ||= 0
end

.message_size_limit=(val) ⇒ Object

Sets the message byte size limit for a Websocket message. Defaults to 0 (no limit)

Although memory will be allocated for the latest TCP/IP frame, this allows the websocket to disconnect if the incoming expected message size exceeds the allowed maximum size.

If the message size limit is exceeded, the disconnection will be immidiate as an attack will be assumed. The protocol’s normal disconnect sequesnce will be discarded.



181
182
183
# File 'lib/iodine/http/websockets.rb', line 181

def self.message_size_limit=val
  @message_size_limit = val
end

.unicast(id, data) ⇒ true, false

Unicast data to a specific websocket connection (ONLY the connection specified).

Data broadcasted will be recived by the websocket handler it’s #on_broadcast(ws) method (if exists). Accepts:

uuid

the UUID of the websocket connection recipient.

data

the data to be sent.

Returns:

  • (true, false)

    Returns true if the object was found and the unicast was sent (the task will be executed asynchronously once the unicast was sent).



125
126
127
128
129
# File 'lib/iodine/http/websockets.rb', line 125

def self.unicast id, data
  return false unless id && data
  each {|io| next unless io.id == id; Iodine.run io, data, &broadcast_proc; return true}
  false
end

Instance Method Details

#broadcast(data) ⇒ Object

Broadcasts the data to all the listening websockets, except self. See broadcast



113
114
115
# File 'lib/iodine/http/websockets.rb', line 113

def broadcast data
  self.class.broadcast data, self
end

#go_awayObject

a politer disconnection.



31
32
33
34
# File 'lib/iodine/http/websockets.rb', line 31

def go_away
  write CLOSE_FRAME
  close
end

#on_broadcast(data) ⇒ Object

handle broadcasts.



18
19
20
# File 'lib/iodine/http/websockets.rb', line 18

def on_broadcast data
  @locker.synchronize { @handler.on_broadcast(data) } if @handler.respond_to? :on_broadcast
end

#on_closeObject

cleanup after closing.



22
23
24
25
26
27
28
# File 'lib/iodine/http/websockets.rb', line 22

def on_close
  @handler.on_close if @handler.respond_to? :on_close
  if @ws_extentions
    @ws_extentions.each { |ex| ex.close }
    @ws_extentions.clear
  end
end

#on_message(data) ⇒ Object

parse and handle messages.



14
15
16
# File 'lib/iodine/http/websockets.rb', line 14

def on_message data
  extract_message StringIO.new(data)
end

#on_openObject

continue to initialize the websocket protocol.



5
6
7
8
9
10
11
12
# File 'lib/iodine/http/websockets.rb', line 5

def on_open
  @handler = @options[:handler]
  @ws_extentions = @options[:ext]
  @options[:request][:io] = self
  @parser = {body: String.new, stage: 0, step: 0, mask_key: [], len_bytes: []}
  set_timeout self.class.default_timeout
  @handler.on_open if @handler.respond_to? :on_open
end

#on_shutdownObject

a politer disconnection during shutdown.



37
38
39
40
# File 'lib/iodine/http/websockets.rb', line 37

def on_shutdown
  @handler.on_shutdown if @handler.respond_to?(:on_shutdown)
  go_away
end

#pingObject

Sends a ping and calles the :on_ping callback (if exists).



85
86
87
# File 'lib/iodine/http/websockets.rb', line 85

def ping
  write(PING_FRAME) && ( (@handler.respond_to?(:on_ping) && @handler.on_ping) || true)
end

#pongObject

Sends an empty pong.



89
90
91
# File 'lib/iodine/http/websockets.rb', line 89

def pong
  write PONG_FRAME
end

#send_data(data, op_code = nil, fin = true, ext = 0) ⇒ Object Also known as: <<

Sends the data as one (or more) Websocket frames.

Use THIS method to send data using the Websocket protocol. Using Protocol#write will bypass the Websocket data framing and send the raw data, breaking the connection.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/iodine/http/websockets.rb', line 53

def send_data data, op_code = nil, fin = true, ext = 0
  return false if !data || data.empty?
  return false if @io.closed?
  data = data.dup # needed?
  unless op_code # apply extenetions to the message as a whole
    op_code = (data.encoding == ::Encoding::UTF_8 ? 1 : 2) 
    @ws_extentions.each { |ex| ext |= ex.edit_message data } if @ws_extentions
  end
  byte_size = data.bytesize
  if byte_size > (FRAME_SIZE_LIMIT+2)
    # sections = byte_size/FRAME_SIZE_LIMIT + (byte_size%FRAME_SIZE_LIMIT ? 1 : 0)
    send_data( data.slice!( 0...FRAME_SIZE_LIMIT ), op_code, data.empty?, ext) && (ext = op_code = 0) until data.empty?
    return true # avoid sending an empty frame.
  end
  @ws_extentions.each { |ex| ext |= ex.edit_frame data } if @ws_extentions
  header = ( (fin ? 0b10000000 : 0) | (op_code & 0b00001111) | ext).chr.force_encoding(::Encoding::ASCII_8BIT)

  if byte_size < 125
    header << byte_size.chr
  elsif byte_size.bit_length <= 16         
    header << 126.chr
    header << [byte_size].pack('S>'.freeze)
  else
    header << 127.chr
    header << [byte_size].pack('Q>'.freeze)
  end
  write header
  write(data) && true
end

#send_response(response, finish = false) ⇒ Object Also known as: stream_response

allow Http responses to be used for sending Websocket data.



43
44
45
46
# File 'lib/iodine/http/websockets.rb', line 43

def send_response response, finish = false
  body = response.extract_body
  send_data body.read
end

#unicast(id, data) ⇒ true, false

Returns Unicasts the data to the requested connection. returns ‘true` if the requested connection id was found on this server. See unicast.

Returns:

  • (true, false)

    Unicasts the data to the requested connection. returns ‘true` if the requested connection id was found on this server. See unicast



131
132
133
# File 'lib/iodine/http/websockets.rb', line 131

def unicast id, data
  self.class.unicast id, data
end