websocket-driver 0.7.1
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +136 -0
- data/LICENSE.md +12 -0
- data/README.md +380 -0
- data/ext/websocket-driver/WebsocketMaskService.java +57 -0
- data/ext/websocket-driver/extconf.rb +4 -0
- data/ext/websocket-driver/websocket_mask.c +32 -0
- data/lib/websocket/driver.rb +233 -0
- data/lib/websocket/driver/client.rb +140 -0
- data/lib/websocket/driver/draft75.rb +102 -0
- data/lib/websocket/driver/draft76.rb +98 -0
- data/lib/websocket/driver/event_emitter.rb +54 -0
- data/lib/websocket/driver/headers.rb +45 -0
- data/lib/websocket/driver/hybi.rb +414 -0
- data/lib/websocket/driver/hybi/frame.rb +20 -0
- data/lib/websocket/driver/hybi/message.rb +31 -0
- data/lib/websocket/driver/proxy.rb +68 -0
- data/lib/websocket/driver/server.rb +80 -0
- data/lib/websocket/driver/stream_reader.rb +55 -0
- data/lib/websocket/http.rb +15 -0
- data/lib/websocket/http/headers.rb +112 -0
- data/lib/websocket/http/request.rb +45 -0
- data/lib/websocket/http/response.rb +29 -0
- data/lib/websocket/mask.rb +14 -0
- data/lib/websocket/websocket_mask.rb +2 -0
- metadata +142 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
module WebSocket
|
2
|
+
class Driver
|
3
|
+
class Hybi
|
4
|
+
|
5
|
+
class Message
|
6
|
+
attr_accessor :rsv1,
|
7
|
+
:rsv2,
|
8
|
+
:rsv3,
|
9
|
+
:opcode,
|
10
|
+
:data
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@rsv1 = false
|
14
|
+
@rsv2 = false
|
15
|
+
@rsv3 = false
|
16
|
+
@opcode = nil
|
17
|
+
@data = String.new('').force_encoding(BINARY)
|
18
|
+
end
|
19
|
+
|
20
|
+
def <<(frame)
|
21
|
+
@rsv1 ||= frame.rsv1
|
22
|
+
@rsv2 ||= frame.rsv2
|
23
|
+
@rsv3 ||= frame.rsv3
|
24
|
+
@opcode ||= frame.opcode
|
25
|
+
@data << frame.payload
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module WebSocket
|
2
|
+
class Driver
|
3
|
+
|
4
|
+
class Proxy
|
5
|
+
include EventEmitter
|
6
|
+
|
7
|
+
PORTS = {'ws' => 80, 'wss' => 443}
|
8
|
+
|
9
|
+
attr_reader :status, :headers
|
10
|
+
|
11
|
+
def initialize(client, origin, options)
|
12
|
+
super()
|
13
|
+
|
14
|
+
@client = client
|
15
|
+
@http = HTTP::Response.new
|
16
|
+
@socket = client.instance_variable_get(:@socket)
|
17
|
+
@origin = URI.parse(@socket.url)
|
18
|
+
@url = URI.parse(origin)
|
19
|
+
@options = options
|
20
|
+
@state = 0
|
21
|
+
|
22
|
+
@headers = Headers.new
|
23
|
+
@headers['Host'] = @origin.host + (@origin.port ? ":#{@origin.port}" : '')
|
24
|
+
@headers['Connection'] = 'keep-alive'
|
25
|
+
@headers['Proxy-Connection'] = 'keep-alive'
|
26
|
+
|
27
|
+
if @url.user
|
28
|
+
auth = Base64.strict_encode64([@url.user, @url.password] * ':')
|
29
|
+
@headers['Proxy-Authorization'] = 'Basic ' + auth
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def set_header(name, value)
|
34
|
+
return false unless @state == 0
|
35
|
+
@headers[name] = value
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def start
|
40
|
+
return false unless @state == 0
|
41
|
+
@state = 1
|
42
|
+
|
43
|
+
port = @origin.port || PORTS[@origin.scheme]
|
44
|
+
start = "CONNECT #{@origin.host}:#{port} HTTP/1.1"
|
45
|
+
headers = [start, @headers.to_s, '']
|
46
|
+
|
47
|
+
@socket.write(headers.join("\r\n"))
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse(chunk)
|
52
|
+
@http.parse(chunk)
|
53
|
+
return unless @http.complete?
|
54
|
+
|
55
|
+
@status = @http.code
|
56
|
+
@headers = Headers.new(@http.headers)
|
57
|
+
|
58
|
+
if @status == 200
|
59
|
+
emit(:connect, ConnectEvent.new)
|
60
|
+
else
|
61
|
+
message = "Can't establish a connection to the server at #{@socket.url}"
|
62
|
+
emit(:error, ProtocolError.new(message))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module WebSocket
|
2
|
+
class Driver
|
3
|
+
|
4
|
+
class Server < Driver
|
5
|
+
EVENTS = %w[open message error close]
|
6
|
+
|
7
|
+
def initialize(socket, options = {})
|
8
|
+
super
|
9
|
+
@http = HTTP::Request.new
|
10
|
+
@delegate = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def env
|
14
|
+
@http.complete? ? @http.env : nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def url
|
18
|
+
return nil unless e = env
|
19
|
+
|
20
|
+
url = "ws://#{e['HTTP_HOST']}"
|
21
|
+
url << e['PATH_INFO']
|
22
|
+
url << "?#{e['QUERY_STRING']}" unless e['QUERY_STRING'] == ''
|
23
|
+
url
|
24
|
+
end
|
25
|
+
|
26
|
+
%w[add_extension set_header start frame text binary ping close].each do |method|
|
27
|
+
define_method(method) do |*args, &block|
|
28
|
+
if @delegate
|
29
|
+
@delegate.__send__(method, *args, &block)
|
30
|
+
else
|
31
|
+
@queue << [method, args, block]
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
%w[protocol version].each do |method|
|
38
|
+
define_method(method) do
|
39
|
+
@delegate && @delegate.__send__(method)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse(chunk)
|
44
|
+
return @delegate.parse(chunk) if @delegate
|
45
|
+
|
46
|
+
@http.parse(chunk)
|
47
|
+
return fail_request('Invalid HTTP request') if @http.error?
|
48
|
+
return unless @http.complete?
|
49
|
+
|
50
|
+
@delegate = Driver.rack(self, @options)
|
51
|
+
open
|
52
|
+
|
53
|
+
EVENTS.each do |event|
|
54
|
+
@delegate.on(event) { |e| emit(event, e) }
|
55
|
+
end
|
56
|
+
|
57
|
+
emit(:connect, ConnectEvent.new)
|
58
|
+
end
|
59
|
+
|
60
|
+
def write(buffer)
|
61
|
+
@socket.write(buffer)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def fail_request(message)
|
67
|
+
emit(:error, ProtocolError.new(message))
|
68
|
+
emit(:close, CloseEvent.new(Hybi::ERRORS[:protocol_error], message))
|
69
|
+
end
|
70
|
+
|
71
|
+
def open
|
72
|
+
@queue.each do |method, args, block|
|
73
|
+
@delegate.__send__(method, *args, &block)
|
74
|
+
end
|
75
|
+
@queue = []
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module WebSocket
|
2
|
+
class Driver
|
3
|
+
|
4
|
+
class StreamReader
|
5
|
+
# Try to minimise the number of reallocations done:
|
6
|
+
MINIMUM_AUTOMATIC_PRUNE_OFFSET = 128
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@buffer = String.new('').force_encoding(BINARY)
|
10
|
+
@offset = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def put(chunk)
|
14
|
+
return unless chunk and chunk.bytesize > 0
|
15
|
+
@buffer << chunk.force_encoding(BINARY)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Read bytes from the data:
|
19
|
+
def read(length)
|
20
|
+
return nil if (@offset + length) > @buffer.bytesize
|
21
|
+
|
22
|
+
chunk = @buffer.byteslice(@offset, length)
|
23
|
+
@offset += chunk.bytesize
|
24
|
+
|
25
|
+
prune if @offset > MINIMUM_AUTOMATIC_PRUNE_OFFSET
|
26
|
+
|
27
|
+
return chunk
|
28
|
+
end
|
29
|
+
|
30
|
+
def each_byte
|
31
|
+
prune
|
32
|
+
|
33
|
+
@buffer.each_byte do |octet|
|
34
|
+
@offset += 1
|
35
|
+
yield octet
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def prune
|
42
|
+
buffer_size = @buffer.bytesize
|
43
|
+
|
44
|
+
if @offset > buffer_size
|
45
|
+
@buffer = String.new('').force_encoding(BINARY)
|
46
|
+
else
|
47
|
+
@buffer = @buffer.byteslice(@offset, buffer_size - @offset)
|
48
|
+
end
|
49
|
+
|
50
|
+
@offset = 0
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module WebSocket
|
2
|
+
module HTTP
|
3
|
+
|
4
|
+
root = File.expand_path('../http', __FILE__)
|
5
|
+
|
6
|
+
autoload :Headers, root + '/headers'
|
7
|
+
autoload :Request, root + '/request'
|
8
|
+
autoload :Response, root + '/response'
|
9
|
+
|
10
|
+
def self.normalize_header(name)
|
11
|
+
name.to_s.strip.downcase.gsub(/^http_/, '').gsub(/_/, '-')
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module WebSocket
|
2
|
+
module HTTP
|
3
|
+
|
4
|
+
module Headers
|
5
|
+
MAX_LINE_LENGTH = 4096
|
6
|
+
CR = 0x0D
|
7
|
+
LF = 0x0A
|
8
|
+
|
9
|
+
# RFC 2616 grammar rules:
|
10
|
+
#
|
11
|
+
# CHAR = <any US-ASCII character (octets 0 - 127)>
|
12
|
+
#
|
13
|
+
# CTL = <any US-ASCII control character
|
14
|
+
# (octets 0 - 31) and DEL (127)>
|
15
|
+
#
|
16
|
+
# SP = <US-ASCII SP, space (32)>
|
17
|
+
#
|
18
|
+
# HT = <US-ASCII HT, horizontal-tab (9)>
|
19
|
+
#
|
20
|
+
# token = 1*<any CHAR except CTLs or separators>
|
21
|
+
#
|
22
|
+
# separators = "(" | ")" | "<" | ">" | "@"
|
23
|
+
# | "," | ";" | ":" | "\" | <">
|
24
|
+
# | "/" | "[" | "]" | "?" | "="
|
25
|
+
# | "{" | "}" | SP | HT
|
26
|
+
#
|
27
|
+
# Or, as redefined in RFC 7230:
|
28
|
+
#
|
29
|
+
# token = 1*tchar
|
30
|
+
#
|
31
|
+
# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
|
32
|
+
# / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
|
33
|
+
# / DIGIT / ALPHA
|
34
|
+
# ; any VCHAR, except delimiters
|
35
|
+
|
36
|
+
HEADER_LINE = /^([!#\$%&'\*\+\-\.\^_`\|~0-9a-z]+):\s*([\x20-\x7e]*?)\s*$/i
|
37
|
+
|
38
|
+
attr_reader :headers
|
39
|
+
|
40
|
+
def initialize
|
41
|
+
@buffer = []
|
42
|
+
@env = {}
|
43
|
+
@headers = {}
|
44
|
+
@stage = 0
|
45
|
+
end
|
46
|
+
|
47
|
+
def complete?
|
48
|
+
@stage == 2
|
49
|
+
end
|
50
|
+
|
51
|
+
def error?
|
52
|
+
@stage == -1
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse(chunk)
|
56
|
+
chunk.each_byte do |octet|
|
57
|
+
if octet == LF and @stage < 2
|
58
|
+
@buffer.pop if @buffer.last == CR
|
59
|
+
if @buffer.empty?
|
60
|
+
complete if @stage == 1
|
61
|
+
else
|
62
|
+
result = case @stage
|
63
|
+
when 0 then start_line(string_buffer)
|
64
|
+
when 1 then header_line(string_buffer)
|
65
|
+
end
|
66
|
+
|
67
|
+
if result
|
68
|
+
@stage = 1
|
69
|
+
else
|
70
|
+
error
|
71
|
+
end
|
72
|
+
end
|
73
|
+
@buffer = []
|
74
|
+
else
|
75
|
+
@buffer << octet if @stage >= 0
|
76
|
+
error if @stage < 2 and @buffer.size > MAX_LINE_LENGTH
|
77
|
+
end
|
78
|
+
end
|
79
|
+
@env['rack.input'] = StringIO.new(string_buffer)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def complete
|
85
|
+
@stage = 2
|
86
|
+
end
|
87
|
+
|
88
|
+
def error
|
89
|
+
@stage = -1
|
90
|
+
end
|
91
|
+
|
92
|
+
def header_line(line)
|
93
|
+
return false unless parsed = line.scan(HEADER_LINE).first
|
94
|
+
|
95
|
+
key = HTTP.normalize_header(parsed[0])
|
96
|
+
value = parsed[1].strip
|
97
|
+
|
98
|
+
if @headers.has_key?(key)
|
99
|
+
@headers[key] << ', ' << value
|
100
|
+
else
|
101
|
+
@headers[key] = value
|
102
|
+
end
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
def string_buffer
|
107
|
+
@buffer.pack('C*')
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module WebSocket
|
2
|
+
module HTTP
|
3
|
+
|
4
|
+
class Request
|
5
|
+
include Headers
|
6
|
+
|
7
|
+
REQUEST_LINE = /^(OPTIONS|GET|HEAD|POST|PUT|DELETE|TRACE|CONNECT) ([\x21-\x7e]+) (HTTP\/[0-9]+\.[0-9]+)$/
|
8
|
+
REQUEST_TARGET = /^(.*?)(\?(.*))?$/
|
9
|
+
RESERVED_HEADERS = %w[content-length content-type]
|
10
|
+
|
11
|
+
attr_reader :env
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def start_line(line)
|
16
|
+
return false unless parsed = line.scan(REQUEST_LINE).first
|
17
|
+
|
18
|
+
target = parsed[1].scan(REQUEST_TARGET).first
|
19
|
+
|
20
|
+
@env = {
|
21
|
+
'REQUEST_METHOD' => parsed[0],
|
22
|
+
'SCRIPT_NAME' => '',
|
23
|
+
'PATH_INFO' => target[0],
|
24
|
+
'QUERY_STRING' => target[2] || ''
|
25
|
+
}
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
def complete
|
30
|
+
super
|
31
|
+
@headers.each do |name, value|
|
32
|
+
rack_name = name.upcase.gsub(/-/, '_')
|
33
|
+
rack_name = "HTTP_#{rack_name}" unless RESERVED_HEADERS.include?(name)
|
34
|
+
@env[rack_name] = value
|
35
|
+
end
|
36
|
+
if host = @env['HTTP_HOST']
|
37
|
+
uri = URI.parse("http://#{host}")
|
38
|
+
@env['SERVER_NAME'] = uri.host
|
39
|
+
@env['SERVER_PORT'] = uri.port.to_s
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module WebSocket
|
2
|
+
module HTTP
|
3
|
+
|
4
|
+
class Response
|
5
|
+
include Headers
|
6
|
+
|
7
|
+
STATUS_LINE = /^(HTTP\/[0-9]+\.[0-9]+) ([0-9]{3}) ([\x20-\x7e]+)$/
|
8
|
+
|
9
|
+
attr_reader :code
|
10
|
+
|
11
|
+
def [](name)
|
12
|
+
@headers[HTTP.normalize_header(name)]
|
13
|
+
end
|
14
|
+
|
15
|
+
def body
|
16
|
+
@buffer.pack('C*')
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def start_line(line)
|
22
|
+
return false unless parsed = line.scan(STATUS_LINE).first
|
23
|
+
@code = parsed[1].to_i
|
24
|
+
true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|