libwebsocket 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|