libwebsocket 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +88 -0
- data/Rakefile +26 -0
- data/examples/eventmachine_server.rb +36 -0
- data/examples/plain_client.rb +59 -0
- data/examples/thin_server.rb +69 -0
- data/lib/libwebsocket.rb +17 -0
- data/lib/libwebsocket/cookie.rb +60 -0
- data/lib/libwebsocket/cookie/request.rb +48 -0
- data/lib/libwebsocket/cookie/response.rb +44 -0
- data/lib/libwebsocket/frame.rb +67 -0
- data/lib/libwebsocket/handshake.rb +28 -0
- data/lib/libwebsocket/handshake/client.rb +129 -0
- data/lib/libwebsocket/handshake/server.rb +114 -0
- data/lib/libwebsocket/message.rb +167 -0
- data/lib/libwebsocket/request.rb +288 -0
- data/lib/libwebsocket/response.rb +215 -0
- data/lib/libwebsocket/stateful.rb +24 -0
- data/lib/libwebsocket/url.rb +67 -0
- data/test/libwebsocket/cookie/request.rb +37 -0
- data/test/libwebsocket/cookie/response.rb +32 -0
- data/test/libwebsocket/handshake/test_client.rb +64 -0
- data/test/libwebsocket/handshake/test_server.rb +39 -0
- data/test/libwebsocket/test_cookie.rb +21 -0
- data/test/libwebsocket/test_frame.rb +65 -0
- data/test/libwebsocket/test_message.rb +16 -0
- data/test/libwebsocket/test_request_75.rb +145 -0
- data/test/libwebsocket/test_request_76.rb +122 -0
- data/test/libwebsocket/test_request_common.rb +26 -0
- data/test/libwebsocket/test_response_75.rb +80 -0
- data/test/libwebsocket/test_response_76.rb +115 -0
- data/test/libwebsocket/test_response_common.rb +17 -0
- data/test/libwebsocket/test_url.rb +49 -0
- data/test/test_helper.rb +4 -0
- metadata +116 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
module LibWebSocket
|
2
|
+
# Construct or parse a WebSocket frame.
|
3
|
+
#
|
4
|
+
# SYNOPSIS
|
5
|
+
#
|
6
|
+
# # Create frame
|
7
|
+
# frame = LibWebSocket::Frame.new('123')
|
8
|
+
# frame.to_s # \x00123\xff
|
9
|
+
#
|
10
|
+
# # Parse frames
|
11
|
+
# frame = LibWebSocket::Frame.new
|
12
|
+
# frame.append("123\x00foo\xff56\x00bar\xff789")
|
13
|
+
# frame.next # => foo
|
14
|
+
# frame.next # => bar
|
15
|
+
class Frame
|
16
|
+
|
17
|
+
def initialize(buffer = nil)
|
18
|
+
@buffer = buffer || ''
|
19
|
+
end
|
20
|
+
|
21
|
+
# Create new frame without modification of current
|
22
|
+
def new(buffer = nil)
|
23
|
+
self.class.new(buffer)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Append a frame chunk.
|
27
|
+
# @example
|
28
|
+
# frame.append("\x00foo")
|
29
|
+
# frame.append("bar\xff")
|
30
|
+
def append(string = nil)
|
31
|
+
return unless string.is_a?(String)
|
32
|
+
|
33
|
+
@buffer += string
|
34
|
+
|
35
|
+
return self
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return the next frame.
|
39
|
+
# @example
|
40
|
+
# frame.append("\x00foo")
|
41
|
+
# frame.append("\xff\x00bar\xff")
|
42
|
+
#
|
43
|
+
# frame.next; # => foo
|
44
|
+
# frame.next; # => bar
|
45
|
+
def next
|
46
|
+
return unless @buffer.slice!(/^[^\x00]*\x00(.*?)\xff/m)
|
47
|
+
|
48
|
+
string = $1
|
49
|
+
string.force_encoding('UTF-8') if string.respond_to?(:force_encoding)
|
50
|
+
|
51
|
+
return string
|
52
|
+
end
|
53
|
+
|
54
|
+
# Construct a WebSocket frame.
|
55
|
+
# @example
|
56
|
+
# frame = LibWebSocket::Frame.new('foo')
|
57
|
+
# frame.to_s # => \x00foo\xff
|
58
|
+
def to_s
|
59
|
+
ary = ["\x00", @buffer.dup, "\xff"]
|
60
|
+
|
61
|
+
ary.collect{ |s| s.force_encoding('UTF-8') if s.respond_to?(:force_encoding) }
|
62
|
+
|
63
|
+
return ary.join
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module LibWebSocket
|
2
|
+
# This is a base class for LibWebSocket::Handshake::Client and LibWebSocket::Handshake::Server.
|
3
|
+
class Handshake
|
4
|
+
|
5
|
+
autoload :Client, "#{File.dirname(__FILE__)}/handshake/client"
|
6
|
+
autoload :Server, "#{File.dirname(__FILE__)}/handshake/server"
|
7
|
+
|
8
|
+
attr_accessor :secure, :error
|
9
|
+
|
10
|
+
# Convert all hash keys to instance variables.
|
11
|
+
def initialize(hash = {})
|
12
|
+
hash.each do |k,v|
|
13
|
+
instance_variable_set("@#{k}",v)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# WebSocket request object.
|
18
|
+
def req
|
19
|
+
@req ||= Request.new
|
20
|
+
end
|
21
|
+
|
22
|
+
# WebSocket response object.
|
23
|
+
def res
|
24
|
+
@res ||= Response.new
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module LibWebSocket
|
2
|
+
class Handshake
|
3
|
+
# Construct or parse a client WebSocket handshake. This module is written for
|
4
|
+
# convenience, since using request and response directly requires the same code
|
5
|
+
# again and again.
|
6
|
+
#
|
7
|
+
# SYNOPSIS
|
8
|
+
#
|
9
|
+
# h = LibWebSocket::Handshake::Client.new(:url => 'ws://example.com')
|
10
|
+
#
|
11
|
+
# # Create request
|
12
|
+
# h.to_s # GET /demo HTTP/1.1
|
13
|
+
# # Upgrade: WebSocket
|
14
|
+
# # Connection: Upgrade
|
15
|
+
# # Host: example.com
|
16
|
+
# # Origin: http://example.com
|
17
|
+
# # Sec-WebSocket-Key1: 18x 6]8vM;54 *(5: { U1]8 z [ 8
|
18
|
+
# # Sec-WebSocket-Key2: 1_ tx7X d < nw 334J702) 7]o}` 0
|
19
|
+
# #
|
20
|
+
# # Tm[K T2u
|
21
|
+
#
|
22
|
+
# # Parse server response
|
23
|
+
# h.parse \<<EOF
|
24
|
+
# HTTP/1.1 101 WebSocket Protocol Handshake
|
25
|
+
# Upgrade: WebSocket
|
26
|
+
# Connection: Upgrade
|
27
|
+
# Sec-WebSocket-Origin: http://example.com
|
28
|
+
# Sec-WebSocket-Location: ws://example.com/demo
|
29
|
+
#
|
30
|
+
# fQJ,fN/4F4!~K~MH
|
31
|
+
# EOF
|
32
|
+
#
|
33
|
+
# h.error # Check if there were any errors
|
34
|
+
# h.done? # Returns true
|
35
|
+
class Client < Handshake
|
36
|
+
|
37
|
+
attr_accessor :url
|
38
|
+
|
39
|
+
def initialize(hash = {})
|
40
|
+
super
|
41
|
+
|
42
|
+
self.set_url(self.url) if self.url
|
43
|
+
end
|
44
|
+
|
45
|
+
# Set or get WebSocket url.
|
46
|
+
# @see LibWebSocket::URL#initialize
|
47
|
+
# @example
|
48
|
+
# handshake.url = 'ws://example.com/demo'
|
49
|
+
def url=(val)
|
50
|
+
self.set_url(val)
|
51
|
+
|
52
|
+
return self
|
53
|
+
end
|
54
|
+
|
55
|
+
# Parse server response
|
56
|
+
# @example
|
57
|
+
# h.parse \<<EOF
|
58
|
+
# HTTP/1.1 101 WebSocket Protocol Handshake
|
59
|
+
# Upgrade: WebSocket
|
60
|
+
# Connection: Upgrade
|
61
|
+
# Sec-WebSocket-Origin: http://example.com
|
62
|
+
# Sec-WebSocket-Location: ws://example.com/demo
|
63
|
+
#
|
64
|
+
# fQJ,fN/4F4!~K~MH
|
65
|
+
# EOF
|
66
|
+
def parse(opts)
|
67
|
+
req = self.req
|
68
|
+
res = self.res
|
69
|
+
|
70
|
+
unless res.done?
|
71
|
+
unless res.parse(opts)
|
72
|
+
self.error = res.error
|
73
|
+
return
|
74
|
+
end
|
75
|
+
|
76
|
+
if res.done?
|
77
|
+
if req.version > 75 && req.checksum != res.checksum
|
78
|
+
self.error = 'Checksum is wrong.'
|
79
|
+
return
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
return true
|
85
|
+
end
|
86
|
+
|
87
|
+
# Check if response is correct
|
88
|
+
def done?
|
89
|
+
res.done?
|
90
|
+
end
|
91
|
+
|
92
|
+
# Create request
|
93
|
+
# @example
|
94
|
+
# h.to_s # GET /demo HTTP/1.1
|
95
|
+
# # Upgrade: WebSocket
|
96
|
+
# # Connection: Upgrade
|
97
|
+
# # Host: example.com
|
98
|
+
# # Origin: http://example.com
|
99
|
+
# # Sec-WebSocket-Key1: 18x 6]8vM;54 *(5: { U1]8 z [ 8
|
100
|
+
# # Sec-WebSocket-Key2: 1_ tx7X d < nw 334J702) 7]o}` 0
|
101
|
+
# #
|
102
|
+
# # Tm[K T2u
|
103
|
+
def to_s
|
104
|
+
req.to_s
|
105
|
+
end
|
106
|
+
|
107
|
+
protected
|
108
|
+
|
109
|
+
def build_url
|
110
|
+
LibWebSocket::URL.new
|
111
|
+
end
|
112
|
+
|
113
|
+
def set_url(url)
|
114
|
+
@url = self.build_url.parse(url) unless url.is_a?(LibWebSocket::URL)
|
115
|
+
|
116
|
+
req = self.req
|
117
|
+
|
118
|
+
host = @url.host
|
119
|
+
host += ':' + @url.port if @url.port
|
120
|
+
req.host = host
|
121
|
+
|
122
|
+
req.resource_name = @url.resource_name
|
123
|
+
|
124
|
+
return self
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module LibWebSocket
|
2
|
+
class Handshake
|
3
|
+
# Construct or parse a server WebSocket handshake. This module is written for
|
4
|
+
# convenience, since using request and response directly requires the same code
|
5
|
+
# again and again.
|
6
|
+
#
|
7
|
+
# SYNOPSIS
|
8
|
+
#
|
9
|
+
# h = LibWebSocket::Handshake::Server.new
|
10
|
+
#
|
11
|
+
# # Parse client request
|
12
|
+
# h.parse \<<EOF
|
13
|
+
# GET /demo HTTP/1.1
|
14
|
+
# Upgrade: WebSocket
|
15
|
+
# Connection: Upgrade
|
16
|
+
# Host: example.com
|
17
|
+
# Origin: http://example.com
|
18
|
+
# Sec-WebSocket-Key1: 18x 6]8vM;54 *(5: { U1]8 z [ 8
|
19
|
+
# Sec-WebSocket-Key2: 1_ tx7X d < nw 334J702) 7]o}` 0
|
20
|
+
#
|
21
|
+
# Tm[K T2u
|
22
|
+
# EOF
|
23
|
+
#
|
24
|
+
# h.error # Check if there were any errors
|
25
|
+
# h.idone? # Returns true
|
26
|
+
#
|
27
|
+
# # Create response
|
28
|
+
# h.to_s # HTTP/1.1 101 WebSocket Protocol Handshake
|
29
|
+
# # Upgrade: WebSocket
|
30
|
+
# # Connection: Upgrade
|
31
|
+
# # Sec-WebSocket-Origin: http://example.com
|
32
|
+
# # Sec-WebSocket-Location: ws://example.com/demo
|
33
|
+
# #
|
34
|
+
# # fQJ,fN/4F4!~K~MH
|
35
|
+
class Server < Handshake
|
36
|
+
|
37
|
+
# Parse client request
|
38
|
+
# @example
|
39
|
+
# h.parse \<<EOF
|
40
|
+
# GET /demo HTTP/1.1
|
41
|
+
# Upgrade: WebSocket
|
42
|
+
# Connection: Upgrade
|
43
|
+
# Host: example.com
|
44
|
+
# Origin: http://example.com
|
45
|
+
# Sec-WebSocket-Key1: 18x 6]8vM;54 *(5: { U1]8 z [ 8
|
46
|
+
# Sec-WebSocket-Key2: 1_ tx7X d < nw 334J702) 7]o}` 0
|
47
|
+
#
|
48
|
+
# Tm[K T2u
|
49
|
+
# EOF
|
50
|
+
def parse(opts)
|
51
|
+
req = self.req
|
52
|
+
res = self.res
|
53
|
+
|
54
|
+
unless req.done?
|
55
|
+
unless req.parse(opts)
|
56
|
+
self.error = req.error
|
57
|
+
return
|
58
|
+
end
|
59
|
+
|
60
|
+
if req.done?
|
61
|
+
res.version = req.version
|
62
|
+
res.host = req.host
|
63
|
+
|
64
|
+
# res.secure = req.secure
|
65
|
+
res.resource_name = req.resource_name
|
66
|
+
res.origin = req.origin
|
67
|
+
|
68
|
+
if req.version > 75
|
69
|
+
res.number1 = req.number1
|
70
|
+
res.number2 = req.number2
|
71
|
+
res.challenge = req.challenge
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
return true
|
77
|
+
end
|
78
|
+
|
79
|
+
# Check if request is correct and done
|
80
|
+
def done?
|
81
|
+
req.done?
|
82
|
+
end
|
83
|
+
|
84
|
+
# Create response in string format
|
85
|
+
# @example
|
86
|
+
# h.to_s # HTTP/1.1 101 WebSocket Protocol Handshake
|
87
|
+
# # Upgrade: WebSocket
|
88
|
+
# # Connection: Upgrade
|
89
|
+
# # Sec-WebSocket-Origin: http://example.com
|
90
|
+
# # Sec-WebSocket-Location: ws://example.com/demo
|
91
|
+
# #
|
92
|
+
# # fQJ,fN/4F4!~K~MH
|
93
|
+
def to_s
|
94
|
+
res.to_s
|
95
|
+
end
|
96
|
+
|
97
|
+
# Create response in rack format
|
98
|
+
# @example
|
99
|
+
# h.to_rack # [ 101,
|
100
|
+
# # {
|
101
|
+
# # 'Upgrade' => 'WebSocket'
|
102
|
+
# # 'Connection' => 'Upgrade'
|
103
|
+
# # 'Sec-WebSocket-Origin' => 'http://example.com'
|
104
|
+
# # 'Sec-WebSocket-Location' => 'ws://example.com/demo'
|
105
|
+
# # 'Content-Length' => 16
|
106
|
+
# # },
|
107
|
+
# # [ 'fQJ,fN/4F4!~K~MH' ] ]
|
108
|
+
def to_rack
|
109
|
+
res.to_rack
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module LibWebSocket
|
4
|
+
# A base class for LibWebSocket::Request and LibWebSocket::Response.
|
5
|
+
class Message
|
6
|
+
include Stateful
|
7
|
+
|
8
|
+
attr_accessor :fields, :error, :subprotocol, :host, :origin, :version, :number1, :number2, :challenge, :checksum
|
9
|
+
|
10
|
+
# A new instance of Message.
|
11
|
+
# Instance variables will be set from hash.
|
12
|
+
# @example
|
13
|
+
# msg = LibMessage.new( :host => 'example.org' )
|
14
|
+
# msg.host # => 'example.org'
|
15
|
+
def initialize(hash = {})
|
16
|
+
hash.each do |k,v|
|
17
|
+
instance_variable_set("@#{k}",v)
|
18
|
+
end
|
19
|
+
|
20
|
+
@version ||= 76
|
21
|
+
@buffer = ''
|
22
|
+
@fields ||= {}
|
23
|
+
@max_message_size ||= 2048
|
24
|
+
@cookies ||= []
|
25
|
+
@state = 'first_line'
|
26
|
+
end
|
27
|
+
|
28
|
+
# Alias for fields.
|
29
|
+
# Use this instead of fields directly to preserve normalization(lowercase etc.)
|
30
|
+
def field(f, val = nil)
|
31
|
+
name = f.downcase
|
32
|
+
|
33
|
+
return self.fields[name] unless val
|
34
|
+
|
35
|
+
self.fields[name] = val
|
36
|
+
|
37
|
+
return self
|
38
|
+
end
|
39
|
+
|
40
|
+
# Set state to 'error' and write error message
|
41
|
+
def error=(val)
|
42
|
+
@error = val
|
43
|
+
self.state = 'error'
|
44
|
+
end
|
45
|
+
|
46
|
+
# Calculate Draft 76 checksum
|
47
|
+
def checksum
|
48
|
+
return @checksum if @checksum
|
49
|
+
|
50
|
+
raise 'number1 is required' unless self.number1
|
51
|
+
raise 'number2 is required' unless self.number2
|
52
|
+
raise 'challenge is required' unless self.challenge
|
53
|
+
|
54
|
+
checksum = ''
|
55
|
+
checksum += [self.number1].pack('N')
|
56
|
+
checksum += [self.number2].pack('N')
|
57
|
+
checksum += self.challenge
|
58
|
+
checksum = Digest::MD5.digest(checksum)
|
59
|
+
|
60
|
+
return @checksum ||= checksum
|
61
|
+
end
|
62
|
+
|
63
|
+
# Parse string
|
64
|
+
# @see LibWebSocket::Request#parse
|
65
|
+
# @see LibWebSocket::Response#parse
|
66
|
+
def parse(string)
|
67
|
+
return unless string.is_a?(String)
|
68
|
+
|
69
|
+
return if self.error
|
70
|
+
|
71
|
+
return unless self.append(string)
|
72
|
+
|
73
|
+
while(!self.state?('body') && line = self.get_line)
|
74
|
+
if self.state?('first_line')
|
75
|
+
return unless self.parse_first_line(line)
|
76
|
+
|
77
|
+
self.state = 'fields'
|
78
|
+
elsif line != ''
|
79
|
+
return unless self.parse_field(line)
|
80
|
+
else
|
81
|
+
self.state = 'body'
|
82
|
+
break
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
return true unless self.state?('body')
|
87
|
+
|
88
|
+
rv = self.parse_body
|
89
|
+
return unless rv
|
90
|
+
|
91
|
+
# Need more data
|
92
|
+
return rv unless rv != true
|
93
|
+
|
94
|
+
return self.done
|
95
|
+
end
|
96
|
+
|
97
|
+
protected
|
98
|
+
|
99
|
+
def number(name, key, value = nil)
|
100
|
+
if value
|
101
|
+
return self.instance_variable_set("@#{name}", value)
|
102
|
+
end
|
103
|
+
|
104
|
+
return self.instance_variable_get("@#{name}") if self.instance_variable_get("@#{name}")
|
105
|
+
|
106
|
+
return self.instance_variable_set("@#{name}", self.extract_number(self.send(key)))
|
107
|
+
end
|
108
|
+
|
109
|
+
def extract_number(key)
|
110
|
+
number = ''
|
111
|
+
while key.slice!(/(\d)/)
|
112
|
+
number += $1
|
113
|
+
end
|
114
|
+
number = number.to_i
|
115
|
+
|
116
|
+
spaces = 0
|
117
|
+
while key.slice!(/ /)
|
118
|
+
spaces += 1
|
119
|
+
end
|
120
|
+
|
121
|
+
return if spaces == 0
|
122
|
+
|
123
|
+
return (number / spaces).to_i
|
124
|
+
end
|
125
|
+
|
126
|
+
def append(data)
|
127
|
+
return if self.error
|
128
|
+
|
129
|
+
@buffer += data
|
130
|
+
|
131
|
+
if @buffer.length > @max_message_size
|
132
|
+
self.error = 'Message is too long'
|
133
|
+
return
|
134
|
+
end
|
135
|
+
|
136
|
+
return self
|
137
|
+
end
|
138
|
+
|
139
|
+
def get_line
|
140
|
+
if @buffer.slice!(/\A(.*?)\x0d?\x0a/)
|
141
|
+
return $1
|
142
|
+
end
|
143
|
+
return
|
144
|
+
end
|
145
|
+
|
146
|
+
def parse_first_line(line)
|
147
|
+
self
|
148
|
+
end
|
149
|
+
|
150
|
+
def parse_field(line)
|
151
|
+
name, value = line.split(': ', 2)
|
152
|
+
unless name && value
|
153
|
+
self.error = 'Invalid field'
|
154
|
+
return
|
155
|
+
end
|
156
|
+
|
157
|
+
self.field(name, value)
|
158
|
+
|
159
|
+
return self
|
160
|
+
end
|
161
|
+
|
162
|
+
def parse_body
|
163
|
+
self
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
end
|