Class: WebSocket

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

Direct Known Subclasses

Breakout::Socket

Defined Under Namespace

Classes: Error

Class Attribute Summary collapse

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(arg, params = {}) ⇒ WebSocket

Returns a new instance of WebSocket.



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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
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
# File 'lib/breakout/web_socket.rb', line 25

def initialize(arg, params = {})
  if params[:server] # server

    @server = params[:server]
    @socket = arg
    line = gets().chomp()
    if !(line =~ /\AGET (\S+) HTTP\/1.1\z/n)
      raise(WebSocket::Error, "invalid request: #{line}")
    end
    @path = $1
    read_header()
    if @header["sec-websocket-key1"] && @header["sec-websocket-key2"]
      @key3 = read(8)
    else
      # Old Draft 75 protocol
      @key3 = nil
    end
    if !@server.accepted_origin?(self.origin)
      raise(WebSocket::Error,
        ("Unaccepted origin: %s (server.accepted_domains = %p)\n\n" +
          "To accept this origin, write e.g. \n" +
          "  WebSocketServer.new(..., :accepted_domains => [%p]), or\n" +
          "  WebSocketServer.new(..., :accepted_domains => [\"*\"])\n") %
        [self.origin, @server.accepted_domains, @server.origin_to_domain(self.origin)])
    end
    @handshaked = false

  else # client

    uri = arg.is_a?(String) ? URI.parse(arg) : arg

    if uri.scheme == "ws"
      default_port = 80
    elsif uri.scheme = "wss"
      default_port = 443
    else
      raise(WebSocket::Error, "unsupported scheme: #{uri.scheme}")
    end

    @path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "")
    host = uri.host + (uri.port == default_port ? "" : ":#{uri.port}")
    origin = params[:origin] || "http://#{uri.host}"
    key1 = generate_key()
    key2 = generate_key()
    key3 = generate_key3()

    socket = TCPSocket.new(uri.host, uri.port || default_port)

    if uri.scheme == "ws"
      @socket = socket
    else
      @socket = ssl_handshake(socket)
    end

    write(
      "GET #{@path} HTTP/1.1\r\n" +
      "Upgrade: WebSocket\r\n" +
      "Connection: Upgrade\r\n" +
      "Host: #{host}\r\n" +
      "Origin: #{origin}\r\n" +
      "Sec-WebSocket-Key1: #{key1}\r\n" +
      "Sec-WebSocket-Key2: #{key2}\r\n" +
      "\r\n" +
      "#{key3}")
    flush()

    line = gets().chomp()
    raise(WebSocket::Error, "bad response: #{line}") if !(line =~ /\AHTTP\/1.1 101 /n)
    read_header()
    if (@header["sec-websocket-origin"] || "").downcase() != origin.downcase()
      raise(WebSocket::Error,
        "origin doesn't match: '#{@header["sec-websocket-origin"]}' != '#{origin}'")
    end
    reply_digest = read(16)
    expected_digest = security_digest(key1, key2, key3)
    if reply_digest != expected_digest
      raise(WebSocket::Error,
        "security digest doesn't match: %p != %p" % [reply_digest, expected_digest])
    end
    @handshaked = true

  end
  @received = []
  @buffer = ""
  @closing_started = false
end

Class Attribute Details

.debugObject

Returns the value of attribute debug.



17
18
19
# File 'lib/breakout/web_socket.rb', line 17

def debug
  @debug
end

Instance Attribute Details

#headerObject (readonly)

Returns the value of attribute header.



112
113
114
# File 'lib/breakout/web_socket.rb', line 112

def header
  @header
end

#pathObject (readonly)

Returns the value of attribute path.



112
113
114
# File 'lib/breakout/web_socket.rb', line 112

def path
  @path
end

#serverObject (readonly)

Returns the value of attribute server.



112
113
114
# File 'lib/breakout/web_socket.rb', line 112

def server
  @server
end

Instance Method Details

#closeObject

Does closing handshake.



188
189
190
191
192
193
# File 'lib/breakout/web_socket.rb', line 188

def close()
  return if @closing_started
  write("\xff\x00")
  @socket.close() if !@server
  @closing_started = true
end

#close_socketObject



195
196
197
# File 'lib/breakout/web_socket.rb', line 195

def close_socket()
  @socket.close()
end

#handshake(status = nil, header = {}) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/breakout/web_socket.rb', line 114

def handshake(status = nil, header = {})
  if @handshaked
    raise(WebSocket::Error, "handshake has already been done")
  end
  status ||= "101 Web Socket Protocol Handshake"
  sec_prefix = @key3 ? "Sec-" : ""
  def_header = {
    "#{sec_prefix}WebSocket-Origin" => self.origin,
    "#{sec_prefix}WebSocket-Location" => self.location,
  }
  header = def_header.merge(header)
  header_str = header.map(){ |k, v| "#{k}: #{v}\r\n" }.join("")
  if @key3
    digest = security_digest(
      @header["Sec-WebSocket-Key1"], @header["Sec-WebSocket-Key2"], @key3)
  else
    digest = ""
  end
  # Note that Upgrade and Connection must appear in this order.
  write(
    "HTTP/1.1 #{status}\r\n" +
    "Upgrade: WebSocket\r\n" +
    "Connection: Upgrade\r\n" +
    "#{header_str}\r\n#{digest}")
  flush()
  @handshaked = true
end

#hostObject



175
176
177
# File 'lib/breakout/web_socket.rb', line 175

def host
  return @header["host"]
end

#locationObject



183
184
185
# File 'lib/breakout/web_socket.rb', line 183

def location
  return "ws://#{self.host}#{@path}"
end

#originObject



179
180
181
# File 'lib/breakout/web_socket.rb', line 179

def origin
  return @header["origin"]
end

#receiveObject



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/breakout/web_socket.rb', line 151

def receive()
  if !@handshaked
    raise(WebSocket::Error, "call WebSocket\#handshake first")
  end
  packet = gets("\xff")
  return nil if !packet
  if packet =~ /\A\x00(.*)\xff\z/nm
    return force_encoding($1, "UTF-8")
  elsif packet == "\xff" && read(1) == "\x00" # closing
    if @server
      @socket.close()
    else
      close()
    end
    return nil
  else
    raise(WebSocket::Error, "input must be either '\\x00...\\xff' or '\\xff\\x00'")
  end
end

#send(data) ⇒ Object



142
143
144
145
146
147
148
149
# File 'lib/breakout/web_socket.rb', line 142

def send(data)
  if !@handshaked
    raise(WebSocket::Error, "call WebSocket\#handshake first")
  end
  data = force_encoding(data.dup(), "ASCII-8BIT")
  write("\x00#{data}\xff")
  flush()
end

#tcp_socketObject



171
172
173
# File 'lib/breakout/web_socket.rb', line 171

def tcp_socket
  return @socket
end