Class: Spyder::WebSocket

Inherits:
Object
  • Object
show all
Defined in:
lib/spyder/web_socket.rb

Defined Under Namespace

Classes: Frame

Constant Summary collapse

WS_CONST =
'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeWebSocket

Returns a new instance of WebSocket.



17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/spyder/web_socket.rb', line 17

def initialize
  @socket = nil
  @on_start = nil
  @on_message = nil
  @on_close = nil

  @streaming_buffer = WebSocketStreamingBuffer.new do |frame, mode, fragmented, last_fragment|
    @on_message.call(frame, mode) unless fragmented
  end
  @streaming_buffer.on_close = proc do
    @socket.close rescue nil
  end
end

Class Method Details

.upgrade_websocket_request(request) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/spyder/web_socket.rb', line 77

def self.upgrade_websocket_request(request)
  ws_key = request.headers.dict['sec-websocket-key']
  return unless ws_key && request.headers.dict['upgrade'] == 'websocket'

  conns = request.headers.dict.fetch('connection', '').split(' ').map(&:strip)
  return unless conns.include?('Upgrade')

  ws_version = request.headers.dict['sec-websocket-version'] # expect: 13
  return unless ws_version

  protocols = request.headers.dict['sec-websocket-protocol']
  protocols = protocols.split(',').map(&:strip) if protocols

  extensions = request.headers.dict['sec-websocket-extensions']
  extensions = extensions.split(';').map(&:strip) if extensions

  decoded_key = Base64.strict_decode64(ws_key) rescue nil
  return unless decoded_key&.length == 16

  response_key = Base64.strict_encode64(
    OpenSSL::Digest::SHA1.digest("#{ws_key}#{WS_CONST}")
  )

  ws = new

  chosen_proto = yield(ws, protocols) if (block_given? && protocols)
  chosen_proto = protocols.first if !chosen_proto && protocols

  resp = Spyder::Response.new(code: 101)
  resp.add_standard_headers
  resp.set_header 'connection', 'Upgrade'
  resp.set_header 'upgrade', 'websocket'
  resp.set_header('sec-websocket-protocol', chosen_proto) if chosen_proto
  resp.set_header 'sec-websocket-accept', response_key

  resp.hijack = proc { |client_socket| ws.hijacked!(client_socket) }

  [resp, ws]
end

Instance Method Details

#hijacked!(socket) ⇒ Object



69
70
71
72
73
74
75
# File 'lib/spyder/web_socket.rb', line 69

def hijacked!(socket)
  @socket = socket
  puts "websocket hijacked! #{socket}"
  @thread = Thread.start do
    self.threaded_start!
  end
end

#on_close(&blk) ⇒ Object



57
58
59
# File 'lib/spyder/web_socket.rb', line 57

def on_close(&blk)
  @on_close = blk
end

#on_message(&blk) ⇒ Object



61
62
63
# File 'lib/spyder/web_socket.rb', line 61

def on_message(&blk)
  @on_message = blk
end

#on_start(&blk) ⇒ Object



65
66
67
# File 'lib/spyder/web_socket.rb', line 65

def on_start(&blk)
  @on_start = blk
end

#send_binary(data) ⇒ Object



35
36
37
# File 'lib/spyder/web_socket.rb', line 35

def send_binary(data)
  send_data(data, :binary)
end

#send_data(data, mode) ⇒ Object



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/spyder/web_socket.rb', line 39

def send_data(data, mode)
  data = data.b

  length = data.length
  buffer = String.new(encoding: 'ascii-8bit', capacity: length + 32)
  buffer += ((1 << 7) | (mode == :binary ? 2 : 1)).chr
  if length < 126
    buffer += length.chr
  elsif length <= 0xFFFF
    buffer += [126, length].pack('CS>')
  else
    buffer += [127, length].pack('CQ>')
  end

  @socket.write(buffer)
  @socket.write(data)
end

#send_text(data) ⇒ Object



31
32
33
# File 'lib/spyder/web_socket.rb', line 31

def send_text(data)
  send_data(data, :text)
end

#threaded_start!Object



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/spyder/web_socket.rb', line 117

def threaded_start!
  @on_start.call if @on_start

  while !@socket.closed? && !@socket.eof?
    data = nil
    begin
      data = @socket.read_nonblock(16 * 1024)
    rescue IO::WaitReadable
      IO.select([@socket], [], [@socket])
    end

    next unless data

    puts "ws: read #{data.length} bytes"
    @streaming_buffer.feed(data)
  end

  @on_close.call unless @on_close
end