Class: OverSIP::WebSocket::WsServer

Inherits:
Connection
  • Object
show all
Defined in:
lib/oversip/websocket/listeners/ws_server.rb

Direct Known Subclasses

IPv4WsServer, IPv6WsServer, WssServer, WssTunnelServer

Constant Summary collapse

HEADERS_MAX_SIZE =

(avoid DoS attacks).

2048
WS_MAGIC_GUID_04 =
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11".freeze
WS_VERSIONS =
{ 7=>true, 8=>true, 13=>true }
HDR_SUPPORTED_WEBSOCKET_VERSIONS =
[ "X-Supported-WebSocket-Versions: #{WS_VERSIONS.keys.join(", ")}" ]

Constants included from SIP::MessageProcessor

SIP::MessageProcessor::MSG_TYPE

Instance Attribute Summary collapse

Attributes inherited from Connection

#cvars

Instance Method Summary collapse

Methods inherited from Connection

#close, #initialize, #open?, outbound_listener?, reliable_transport_listener?

Methods included from Logger

close, fg_system_msg2str, init_logger_mq, load_methods, #log_id, syslog_system_msg2str, syslog_user_msg2str

Constructor Details

This class inherits a constructor from OverSIP::WebSocket::Connection

Instance Attribute Details

#client_closed=(value) ⇒ Object (writeonly)

Sets the attribute client_closed

Parameters:

  • value

    the value to set the attribute client_closed to.



15
16
17
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 15

def client_closed=(value)
  @client_closed = value
end

#outbound_flow_tokenObject (readonly)

Returns the value of attribute outbound_flow_token.



14
15
16
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 14

def outbound_flow_token
  @outbound_flow_token
end

#ws_established=(value) ⇒ Object (writeonly)

Sets the attribute ws_established

Parameters:

  • value

    the value to set the attribute ws_established to.



15
16
17
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 15

def ws_established=(value)
  @ws_established = value
end

Instance Method Details

#accept_ws_handshakeObject



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 273

def accept_ws_handshake
  sec_websocket_accept = Digest::SHA1::base64digest @http_request.hdr_sec_websocket_key + WS_MAGIC_GUID_04

  extra_headers = [
    "Upgrade: websocket",
    "Connection: Upgrade",
    "Sec-WebSocket-Accept: #{sec_websocket_accept}"
  ]

  if @websocket_protocol_negotiated
    extra_headers << "Sec-WebSocket-Protocol: #{WS_SIP_PROTOCOL}"
  end

  if @websocket_extensions
    extra_headers << "Sec-WebSocket-Extensions: #{@websocket_extensions.to_s}"
  end

  @http_request.reply 101, nil, extra_headers

  # Set the WS framing layer and WS application layer.
  @ws_framing = ::OverSIP::WebSocket::WsFraming.new self, @buffer
  ws_sip_app = ::OverSIP::WebSocket::WsSipApp.new self, @ws_framing
  @ws_framing.ws_app = ws_sip_app

  @state = :websocket
  true
end

#check_http_requestObject



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 178

def check_http_request
  # Check OverSIP status.
  unless ::OverSIP.status == :running
    case ::OverSIP.status
    when :loading
      http_reject 500, "Server Still Loading", [ "Retry-After: 5" ]
    when :terminating
      http_reject 500, "Server is Being Stopped"
    end
    return false
  end

  # HTTP method must be GET.
  if @http_request.http_method != :GET
    log_system_notice "rejecting HTTP #{@http_request.http_method} request => 405"
    http_reject 405
    return false
  end

  # "Sec-WebSocket-Version" must be 8.
  unless WS_VERSIONS[@http_request.hdr_sec_websocket_version]
    if @http_request.hdr_sec_websocket_version
      log_system_notice "WebSocket version #{@http_request.hdr_sec_websocket_version} not implemented => 426"
    else
      log_system_notice "WebSocket version header not present => 426"
    end
    http_reject 426, nil, HDR_SUPPORTED_WEBSOCKET_VERSIONS
    return false
  end

  # Connection header must include "upgrade".
  unless @http_request.hdr_connection and @http_request.hdr_connection.include? "upgrade"
    log_system_notice "Connection header must include \"upgrade\" => 400"
    http_reject 400, "Connection header must include \"upgrade\""
    return false
  end

  # "Upgrade: websocket" is required.
  unless @http_request.hdr_upgrade == "websocket"
    log_system_notice "Upgrade header must be \"websocket\" => 400"
    http_reject 400, "Upgrade header must be \"websocket\""
    return false
  end

  # Sec-WebSocket-Key is required.
  unless @http_request.hdr_sec_websocket_key
    log_system_notice "Sec-WebSocket-Key header not present => 400"
    http_reject 400, "Sec-WebSocket-Key header not present"
    return false
  end

  # Check Sec-WebSocket-Protocol.
  if @http_request.hdr_sec_websocket_protocol
    if @http_request.hdr_sec_websocket_protocol.include? WS_SIP_PROTOCOL
      @websocket_protocol_negotiated = true
    else
      log_system_notice "Sec-WebSocket-Protocol does not contain a supported protocol but #{@http_request.hdr_sec_websocket_protocol} => 501"
      http_reject 501, "No Suitable WebSocket Protocol"
      return false
    end
  end

  @state = :on_connection_callback
  true
end

#do_on_connection_callbackObject



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 245

def do_on_connection_callback
  # Set the state to :waiting_for_on_connection so data received before
  # user callback validation is just stored.
  @state = :waiting_for_on_connection

  # Run OverSIP::WebSocketEvents.on_connection.
  ::Fiber.new do
    begin
      log_system_debug "running OverSIP::WebSocketEvents.on_connection()..."  if $oversip_debug
      ::OverSIP::WebSocketEvents.on_connection self, @http_request
      # If the user of the peer has not closed the connection then continue.
      unless @local_closed or error?
        @state = :accept_ws_handshake
        # Call process_received_data() to process possible data received in the meanwhile.
        process_received_data
      else
        log_system_debug "connection closed during OverSIP::WebSocketEvents.on_connection(), aborting"  if $oversip_debug
      end

    rescue ::Exception => e
      log_system_error "error calling OverSIP::WebSocketEvents.on_connection() => 500:"
      log_system_error e
      http_reject 500
    end
  end.resume
end

#http_reject(status_code = 403, reason_phrase = nil, extra_headers = nil) ⇒ Object



302
303
304
305
306
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 302

def http_reject status_code=403, reason_phrase=nil, extra_headers=nil
  @http_request.reply(status_code, reason_phrase, extra_headers)
  close_connection_after_writing
  @state = :ignore
end

#parse_http_headersObject



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 147

def parse_http_headers
  return false if @buffer.empty?

  # Parse the currently buffered data. If parsing fails @http_parser_nbytes gets nil value.
  unless @http_parser_nbytes = @http_parser.execute(@http_request, @buffer.to_str, @http_parser_nbytes)
    log_system_warn "parsing error: \"#{@http_parser.error}\""
    close_connection_after_writing
    @state = :ignore
    return false
  end

  # Avoid flood attacks in TCP (very long headers).
  if @http_parser_nbytes > HEADERS_MAX_SIZE
    log_system_warn "DoS attack detected: headers size exceedes #{HEADERS_MAX_SIZE} bytes, closing connection with #{remote_desc}"
    close_connection
    @state = :ignore
    return false
  end

  return false  unless @http_parser.finished?

  # Clear parsed data from the buffer.
  @buffer.read(@http_parser_nbytes)

  @http_request.connection = self

  @state = :check_http_request
  true
end

#post_connectionObject



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 34

def post_connection
  begin
    @remote_port, @remote_ip = ::Socket.unpack_sockaddr_in(get_peername)
  rescue => e
    log_system_error "error obtaining remote IP/port (#{e.class}: #{e.message}), closing connection"
    close_connection
    @state = :ignore
    return
  end

  @connection_id = ::OverSIP::SIP::TransportManager.add_connection self, self.class, self.class.ip_type, @remote_ip, @remote_port

  # Create an Outbound (RFC 5626) flow token for this connection.
  @outbound_flow_token = ::OverSIP::SIP::TransportManager.add_outbound_connection self

  log_system_debug("connection opened from " << remote_desc)  if $oversip_debug
end

#process_received_dataObject



110
111
112
113
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
141
142
143
144
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 110

def process_received_data
  @state == :ignore and return

  while (case @state
    when :init
      @http_parser = ::OverSIP::WebSocket::HttpRequestParser.new
      @http_request = ::OverSIP::WebSocket::HttpRequest.new
      @http_parser_nbytes = 0
      @bytes_remaining = 0
      @state = :http_headers

    when :http_headers
      parse_http_headers

    when :check_http_request
      check_http_request

    when :on_connection_callback
      do_on_connection_callback
      false

    when :accept_ws_handshake
      accept_ws_handshake

    when :websocket
      @ws_established = true
      @ws_framing.receive_data
      false

    when :ignore
      false
    end)
  end  # while

end

#receive_data(data) ⇒ Object



101
102
103
104
105
106
107
108
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 101

def receive_data data
  @state == :ignore and return
  @buffer << data
  @state == :waiting_for_on_client_tls_handshake and return
  @state == :waiting_for_on_connection and return

  process_received_data
end

#remote_desc(force = nil) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 53

def remote_desc force=nil
  if force
    @remote_desc = case @remote_ip_type
      when :ipv4  ; "#{@remote_ip}:#{@remote_port.to_s}"
      when :ipv6  ; "[#{@remote_ip}]:#{@remote_port.to_s}"
      end
  else
    @remote_desc ||= case self.class.ip_type
      when :ipv4  ; "#{@remote_ip}:#{@remote_port.to_s}"
      when :ipv6  ; "[#{@remote_ip}]:#{@remote_port.to_s}"
      end
  end
end

#remote_ipObject



22
23
24
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 22

def remote_ip
  @remote_ip
end

#remote_ip_typeObject



18
19
20
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 18

def remote_ip_type
  @remote_ip_type || self.class.ip_type
end

#remote_portObject



26
27
28
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 26

def remote_port
  @remote_port
end

#send_sip_msg(msg, ip = nil, port = nil) ⇒ Object

Parameters ip and port are just included because they are needed in UDP, so the API remains equal.



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 310

def send_sip_msg msg, ip=nil, port=nil
  if self.error?
    log_system_notice "SIP message could not be sent, connection is closed"
    return false
  end

  # If the SIP message is fully valid UTF-8 send a WS text frame.
  if msg.force_encoding(::Encoding::UTF_8).valid_encoding?
    @ws_framing.send_text_frame msg

  # If not, send a WS binary frame.
  else
    @ws_framing.send_binary_frame msg
  end

  true
end

#transportObject



30
31
32
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 30

def transport
  self.class.transport
end

#unbind(cause = nil) ⇒ Object



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
# File 'lib/oversip/websocket/listeners/ws_server.rb', line 68

def unbind cause=nil
  @state = :ignore

  # Remove the connection.
  self.class.connections.delete @connection_id

  # Remove the Outbound token flow.
  ::OverSIP::SIP::TransportManager.delete_outbound_connection @outbound_flow_token

  @local_closed = true  if cause == ::Errno::ETIMEDOUT
  @local_closed = false  if @client_closed

  if $oversip_debug
    log_msg = "connection from #{remote_desc} "
    log_msg << ( @local_closed ? "locally closed" : "remotely closed" )
    log_msg << " (cause: #{cause.inspect})"  if cause
    log_system_debug log_msg
  end unless $!

  if @ws_established
    # Run OverSIP::WebSocketEvents.on_disconnection
    ::Fiber.new do
      begin
        ::OverSIP::WebSocketEvents.on_disconnection self, !@local_closed
      rescue ::Exception => e
        log_system_error "error calling OverSIP::WebSocketEvents.on_disconnection():"
        log_system_error e
      end
    end.resume
  end unless $!
end