spyder 0.1.2 → 0.3.0.pre.beta1
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 +4 -4
- data/lib/spyder/basic_server.rb +29 -0
- data/lib/spyder/rack_adapter.rb +38 -0
- data/lib/spyder/request.rb +11 -1
- data/lib/spyder/response.rb +5 -2
- data/lib/spyder/router.rb +26 -2
- data/lib/spyder/server.rb +70 -51
- data/lib/spyder/version.rb +1 -1
- data/lib/spyder/web/file_server.rb +92 -0
- data/lib/spyder/web_socket.rb +137 -0
- data/lib/spyder/web_socket_streaming_buffer.rb +192 -0
- data/lib/spyder.rb +10 -0
- metadata +36 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e1ade7f9c9ee1b99448fc6a0fb8addcbbca80a2696581b1bb4fa05c27ea534a
|
|
4
|
+
data.tar.gz: 075d91b41bdd5a7264bfadd3e014ef67b7c311545417b1503c9a245935f12860
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0cc009cdbf9d1ab3bf184be0214c6d1e0366b6d0823dc6df8c2b548db4e2b631d972f8e7a2dd2ec9fabfdcb2b6135e4ba1c79d3276d6bc5864005df73f32750
|
|
7
|
+
data.tar.gz: ba21a6413f6cf9ffe290806cd7ee7b96c877b592243d3549caaa374dd455a40037e2f240f41743c83fcc753f0cf97ad682892f2c0cfe4f96d3f99ad50f77bf5c
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spyder
|
|
4
|
+
class BasicServer
|
|
5
|
+
attr_accessor :router
|
|
6
|
+
|
|
7
|
+
def initialize(router)
|
|
8
|
+
@router = router
|
|
9
|
+
@middleware = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def process_request(request)
|
|
13
|
+
mids = @middleware + [[RouterApp, @router]]
|
|
14
|
+
app = nil
|
|
15
|
+
|
|
16
|
+
loop do
|
|
17
|
+
klass, args = mids.pop
|
|
18
|
+
break unless klass
|
|
19
|
+
app = klass.new(args, app)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
app.call({}, request)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add_middleware(callable, args=[])
|
|
26
|
+
@middleware << [callable, args]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'spyder'
|
|
3
|
+
|
|
4
|
+
module Spyder
|
|
5
|
+
class RackServer < BasicServer
|
|
6
|
+
attr_accessor :router
|
|
7
|
+
|
|
8
|
+
def initialize(router: Router.new)
|
|
9
|
+
super(router)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(env)
|
|
13
|
+
qs = env[Rack::QUERY_STRING]
|
|
14
|
+
|
|
15
|
+
request = Request.new
|
|
16
|
+
request.path = (!qs || qs.empty? ? env[Rack::PATH_INFO] : "#{env[Rack::PATH_INFO]}?#{qs}")
|
|
17
|
+
request.verb = env[Rack::REQUEST_METHOD]
|
|
18
|
+
request.http_version = env['HTTP_VERSION']
|
|
19
|
+
request.io = env['rack.input']
|
|
20
|
+
|
|
21
|
+
env.each do |key, value|
|
|
22
|
+
next unless key.start_with?('HTTP_') && key != 'HTTP_VERSION'
|
|
23
|
+
|
|
24
|
+
header_name = key[5..].downcase.tr('_', '-')
|
|
25
|
+
request.add_header(header_name, value)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
response = process_request(request)
|
|
29
|
+
|
|
30
|
+
[
|
|
31
|
+
response.code,
|
|
32
|
+
# response.headers.ordered.map { |k, v| ["HTTP_#{k.tr('-', '_').upcase}", v] },
|
|
33
|
+
response.headers.ordered,
|
|
34
|
+
[response.body]
|
|
35
|
+
]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/spyder/request.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Spyder
|
|
|
7
7
|
attr_accessor :verb
|
|
8
8
|
attr_accessor :path
|
|
9
9
|
attr_accessor :headers
|
|
10
|
-
attr_accessor :
|
|
10
|
+
attr_accessor :http_version, :remote_address
|
|
11
11
|
attr_accessor :io
|
|
12
12
|
|
|
13
13
|
def_delegators :@headers, :ordered, :dict, :add_header
|
|
@@ -73,6 +73,16 @@ module Spyder
|
|
|
73
73
|
len ? io.read(Integer(len)) : io.read # FIXME:
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
def debug_dump(include_body: false)
|
|
77
|
+
lines = ["verb=#{@verb}\npath=#{@path}\nhttp_version=#{@http_version}"]
|
|
78
|
+
lines << "has #{@headers.dict.length} headers"
|
|
79
|
+
@headers.dict.each do |name, value|
|
|
80
|
+
lines << "\t#{name} = #{value}"
|
|
81
|
+
end
|
|
82
|
+
lines << "has body: #{has_body? ? 'yes' : 'no'}"
|
|
83
|
+
lines.join("\n")
|
|
84
|
+
end
|
|
85
|
+
|
|
76
86
|
def has_body?
|
|
77
87
|
dict['transfer-encoding'] != nil || dict['content-length'] != nil
|
|
78
88
|
end
|
data/lib/spyder/response.rb
CHANGED
|
@@ -4,6 +4,7 @@ module Spyder
|
|
|
4
4
|
class Response
|
|
5
5
|
attr_reader :code
|
|
6
6
|
attr_accessor :body
|
|
7
|
+
attr_accessor :hijack
|
|
7
8
|
attr_reader :headers
|
|
8
9
|
attr_writer :reason_sentence
|
|
9
10
|
|
|
@@ -60,8 +61,10 @@ module Spyder
|
|
|
60
61
|
]
|
|
61
62
|
end.freeze
|
|
62
63
|
|
|
63
|
-
def initialize
|
|
64
|
-
|
|
64
|
+
def initialize(code: :ok)
|
|
65
|
+
@body = nil
|
|
66
|
+
@hijack = false
|
|
67
|
+
self.code = code
|
|
65
68
|
@headers = HeaderStore.new(:response)
|
|
66
69
|
end
|
|
67
70
|
|
data/lib/spyder/router.rb
CHANGED
|
@@ -2,10 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module Spyder
|
|
4
4
|
class Router
|
|
5
|
-
attr_accessor :
|
|
5
|
+
attr_accessor :fallback_stack
|
|
6
6
|
|
|
7
7
|
def initialize
|
|
8
8
|
@routes = Hash.new { |k, v| k[v] = [] }
|
|
9
|
+
@fallback_stack = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def add_route_method(verb, matcher, obj, method_name)
|
|
13
|
+
h = proc do |req, rq|
|
|
14
|
+
obj.public_send(method_name, req, rq)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_route(verb, matcher, &h)
|
|
9
18
|
end
|
|
10
19
|
|
|
11
20
|
def add_route(verb, matcher, &handler)
|
|
@@ -14,6 +23,12 @@ module Spyder
|
|
|
14
23
|
@routes[verb.to_s.upcase] << [matcher, handler]
|
|
15
24
|
end
|
|
16
25
|
|
|
26
|
+
def add_fallback(callable = nil, &blk)
|
|
27
|
+
raise "Provide either a callable or a block, but not both" if callable && block_given?
|
|
28
|
+
|
|
29
|
+
@fallback_stack << (block_given? ? blk : callable)
|
|
30
|
+
end
|
|
31
|
+
|
|
17
32
|
def call(ctx, request)
|
|
18
33
|
only_path = request.path_info
|
|
19
34
|
|
|
@@ -26,7 +41,16 @@ module Spyder
|
|
|
26
41
|
end
|
|
27
42
|
end
|
|
28
43
|
|
|
29
|
-
match_data ? handler.call(request, match_data) :
|
|
44
|
+
response = match_data ? handler.call(request, match_data) : nil
|
|
45
|
+
return response if response.is_a?(Response)
|
|
46
|
+
|
|
47
|
+
fallback_stack.each do |blk|
|
|
48
|
+
response = blk.call(request)
|
|
49
|
+
|
|
50
|
+
return response if response.is_a?(Response)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
Response.make_generic(:not_found)
|
|
30
54
|
end
|
|
31
55
|
end
|
|
32
56
|
|
data/lib/spyder/server.rb
CHANGED
|
@@ -1,84 +1,100 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Spyder
|
|
4
|
-
class Server
|
|
4
|
+
class Server < BasicServer
|
|
5
5
|
attr_accessor :router
|
|
6
6
|
|
|
7
7
|
def initialize(bind, port, router: Router.new, max_threads: 4, tcp_backlog: 10)
|
|
8
|
+
super(router)
|
|
9
|
+
|
|
8
10
|
@server = TCPServer.new(bind, port)
|
|
9
11
|
@tcp_backlog = tcp_backlog
|
|
10
12
|
@max_threads = max_threads
|
|
11
|
-
@
|
|
12
|
-
@
|
|
13
|
+
@max_queue_size = max_threads * tcp_backlog
|
|
14
|
+
@tp_read, @tp_write = IO.pipe
|
|
13
15
|
@tp_sync = Mutex.new
|
|
14
|
-
@
|
|
16
|
+
@waiting_clients = []
|
|
17
|
+
@threads = max_threads.times.map { create_request_handler_thread }
|
|
15
18
|
end
|
|
16
19
|
|
|
17
|
-
def
|
|
18
|
-
|
|
20
|
+
def create_request_handler_thread
|
|
21
|
+
Thread.new do
|
|
22
|
+
loop do
|
|
23
|
+
@tp_read.read(1)
|
|
24
|
+
client = nil
|
|
25
|
+
|
|
26
|
+
@tp_sync.synchronize do
|
|
27
|
+
client = @waiting_clients.shift
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
handle_new_client(client)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def handle_new_client(client)
|
|
36
|
+
error, response = nil
|
|
37
|
+
|
|
38
|
+
request = read_client_request(client)
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
response = process_request(request)
|
|
42
|
+
rescue Exception => e
|
|
43
|
+
error = e
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if error
|
|
47
|
+
puts "Request errored in spyder. Error dump:"
|
|
48
|
+
puts error.full_message
|
|
49
|
+
puts "Request dump:\n#{request.debug_dump}"
|
|
50
|
+
|
|
51
|
+
response = Response.make_generic :internal_server_error
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
dispatch_response(client, response)
|
|
55
|
+
|
|
56
|
+
if !error && response&.hijack
|
|
57
|
+
response.hijack.call(client)
|
|
58
|
+
else
|
|
59
|
+
client.close rescue nil
|
|
60
|
+
end
|
|
19
61
|
end
|
|
20
62
|
|
|
21
63
|
def start
|
|
22
|
-
busy_threads = 0
|
|
23
64
|
@server.listen(@tcp_backlog)
|
|
24
65
|
|
|
25
66
|
loop do
|
|
26
67
|
time_start = Process.clock_gettime(:CLOCK_MONOTONIC, :float_second)
|
|
27
68
|
loop do
|
|
28
|
-
|
|
29
|
-
|
|
69
|
+
queue_size = nil
|
|
70
|
+
@tp_sync.synchronize do
|
|
71
|
+
queue_size = @waiting_clients.length
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
break if queue_size < @max_queue_size # current_busy < @max_threads
|
|
30
75
|
sleep(0)
|
|
31
76
|
current_time = Process.clock_gettime(:CLOCK_MONOTONIC, :float_second)
|
|
32
77
|
if (current_time - time_start) > 1.0
|
|
33
|
-
|
|
78
|
+
puts "Waiting a long time: #{(current_time - time_start)}"
|
|
34
79
|
sleep 0.2
|
|
35
80
|
end
|
|
36
81
|
end
|
|
37
82
|
|
|
38
|
-
|
|
39
|
-
@tp_sync.synchronize
|
|
40
|
-
|
|
41
|
-
Thread.new do
|
|
42
|
-
begin
|
|
43
|
-
error = nil
|
|
44
|
-
begin
|
|
45
|
-
process_new_client(client)
|
|
46
|
-
rescue Exception => e
|
|
47
|
-
error = e
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
if error
|
|
51
|
-
puts error.full_message
|
|
52
|
-
|
|
53
|
-
response = Response.make_generic :internal_server_error
|
|
54
|
-
dispatch_response(client, response)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
client.close rescue nil
|
|
58
|
-
ensure
|
|
59
|
-
@tp_sync.synchronize { busy_threads -= 1 }
|
|
60
|
-
end
|
|
83
|
+
client_socket = @server.accept
|
|
84
|
+
@tp_sync.synchronize do
|
|
85
|
+
@waiting_clients << client_socket
|
|
61
86
|
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
87
|
|
|
65
|
-
|
|
66
|
-
mids = @middleware + [[RouterApp, @router]]
|
|
67
|
-
app = nil
|
|
68
|
-
loop do
|
|
69
|
-
klass, args = mids.pop
|
|
70
|
-
break unless klass
|
|
71
|
-
app = klass.new(args, app)
|
|
88
|
+
@tp_write.write(1)
|
|
72
89
|
end
|
|
73
|
-
|
|
74
|
-
app.call({}, request)
|
|
75
90
|
end
|
|
76
91
|
|
|
77
|
-
def
|
|
78
|
-
verb, path,
|
|
92
|
+
def read_client_request(socket)
|
|
93
|
+
verb, path, http_version = read_line(socket).split(' ', 3)
|
|
79
94
|
request = Request.new
|
|
80
95
|
request.path = path
|
|
81
96
|
request.verb = verb
|
|
97
|
+
request.http_version = http_version
|
|
82
98
|
request.io = socket
|
|
83
99
|
|
|
84
100
|
loop do
|
|
@@ -90,24 +106,25 @@ module Spyder
|
|
|
90
106
|
request.add_header(name, value)
|
|
91
107
|
end
|
|
92
108
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
dispatch_response(socket, response)
|
|
109
|
+
request
|
|
96
110
|
end
|
|
97
111
|
|
|
98
112
|
def dispatch_response(socket, response)
|
|
99
113
|
content_length = response.headers.dict['content-length']
|
|
100
114
|
if !content_length && response.body && response.body.is_a?(String)
|
|
101
115
|
content_length = response.body.length
|
|
116
|
+
response.set_header 'content-length', content_length.to_s
|
|
117
|
+
elsif !content_length && (200..203) === response.code
|
|
118
|
+
response.set_header 'content-length', '0'
|
|
102
119
|
end
|
|
103
120
|
|
|
121
|
+
response.set_header('connection', 'close') unless response.headers.dict['connection']
|
|
122
|
+
|
|
104
123
|
begin
|
|
105
124
|
socket.write("HTTP/1.1 #{response.code} #{response.reason_sentence.b}\r\n")
|
|
106
125
|
response.headers.ordered.each do |name, value|
|
|
107
126
|
socket.write("#{name.b}: #{value.b}\r\n")
|
|
108
127
|
end
|
|
109
|
-
socket.write("connection: close\r\n") # FIXME:
|
|
110
|
-
socket.write("content-length: #{content_length}\r\n") if content_length
|
|
111
128
|
socket.write("\r\n")
|
|
112
129
|
|
|
113
130
|
if response.body
|
|
@@ -120,6 +137,8 @@ module Spyder
|
|
|
120
137
|
# socket closed. So what?
|
|
121
138
|
socket.close rescue nil
|
|
122
139
|
end
|
|
140
|
+
|
|
141
|
+
response
|
|
123
142
|
end
|
|
124
143
|
|
|
125
144
|
def read_line(socket)
|
data/lib/spyder/version.rb
CHANGED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spyder
|
|
4
|
+
module Web
|
|
5
|
+
class FileServer
|
|
6
|
+
attr_reader :base_paths
|
|
7
|
+
attr_reader :default_index
|
|
8
|
+
|
|
9
|
+
def initialize(paths, index: nil)
|
|
10
|
+
@default_index = index
|
|
11
|
+
@base_paths = Array(paths).map do |path|
|
|
12
|
+
if path[0] == '/'
|
|
13
|
+
path
|
|
14
|
+
else
|
|
15
|
+
File.expand_path(File.join(Dir.pwd, path))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(request)
|
|
21
|
+
return unless request.verb == 'GET'
|
|
22
|
+
|
|
23
|
+
input_path = request.path_info
|
|
24
|
+
input_path = @default_index if request.path == '/' && @default_index
|
|
25
|
+
|
|
26
|
+
req_path = safe_request_path(input_path)
|
|
27
|
+
full_path = nil
|
|
28
|
+
return unless @base_paths.any? do |base|
|
|
29
|
+
fp = File.join(base, *req_path)
|
|
30
|
+
next unless File.file?(fp)
|
|
31
|
+
full_path = fp
|
|
32
|
+
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
st = File.readable?(full_path) ? File.stat(full_path) : nil
|
|
37
|
+
|
|
38
|
+
return unless st && %w[file link].include?(st.ftype)
|
|
39
|
+
|
|
40
|
+
etag = "\"#{"%xT-%x0" % [st.mtime, st.size]}\""
|
|
41
|
+
|
|
42
|
+
resp = serve_not_modified_response(request, st, etag)
|
|
43
|
+
return resp if resp
|
|
44
|
+
|
|
45
|
+
resp = Spyder::Response.new
|
|
46
|
+
resp.add_standard_headers
|
|
47
|
+
resp.set_header 'last-modified', st.mtime.httpdate
|
|
48
|
+
resp.set_header 'etag', etag
|
|
49
|
+
resp.set_header 'cache-control', 'public, must-revalidate, max-age=0'
|
|
50
|
+
|
|
51
|
+
File.open full_path do |fp|
|
|
52
|
+
mime = Marcel::MimeType.for(fp)
|
|
53
|
+
if mime == 'application/octet-stream' || mime == 'text/plain'
|
|
54
|
+
mime = Marcel::MimeType.for(name: req_path.last)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
resp.set_header('content-type', mime) if mime
|
|
58
|
+
|
|
59
|
+
fp.rewind
|
|
60
|
+
resp.body = fp.read
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
resp
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def serve_not_modified_response(request, st, etag)
|
|
69
|
+
if_etag = request.headers.dict['if-none-match']
|
|
70
|
+
if_modified_since = request.headers.dict['if-modified-since']
|
|
71
|
+
|
|
72
|
+
return unless if_etag || if_modified_since
|
|
73
|
+
|
|
74
|
+
resp = Spyder::Response.new
|
|
75
|
+
resp.add_standard_headers
|
|
76
|
+
resp.code = 304
|
|
77
|
+
|
|
78
|
+
return resp if if_etag && if_etag == etag
|
|
79
|
+
|
|
80
|
+
if_ms = if_modified_since ? Time.parse(if_modified_since) : nil
|
|
81
|
+
|
|
82
|
+
(if_ms && Time.at(st.mtime.to_i) <= if_ms) ? resp : nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def safe_request_path(path)
|
|
86
|
+
path.split('/').reject do |component|
|
|
87
|
+
component == '..' || component == '' || component == '.'
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spyder
|
|
4
|
+
class WebSocket
|
|
5
|
+
WS_CONST = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
|
6
|
+
|
|
7
|
+
class Frame
|
|
8
|
+
def initialize
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def decode(raw_data)
|
|
12
|
+
fin = raw_data[0] & 1
|
|
13
|
+
rsv = (raw_data[0] & 0b0111) >> 1
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@socket = nil
|
|
19
|
+
@on_start = nil
|
|
20
|
+
@on_message = nil
|
|
21
|
+
@on_close = nil
|
|
22
|
+
|
|
23
|
+
@streaming_buffer = WebSocketStreamingBuffer.new do |frame, mode, fragmented, last_fragment|
|
|
24
|
+
@on_message.call(frame, mode) unless fragmented
|
|
25
|
+
end
|
|
26
|
+
@streaming_buffer.on_close = proc do
|
|
27
|
+
@socket.close rescue nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def send_text(data)
|
|
32
|
+
send_data(data, :text)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def send_binary(data)
|
|
36
|
+
send_data(data, :binary)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def send_data(data, mode)
|
|
40
|
+
data = data.b
|
|
41
|
+
|
|
42
|
+
length = data.length
|
|
43
|
+
buffer = String.new(encoding: 'ascii-8bit', capacity: length + 32)
|
|
44
|
+
buffer += ((1 << 7) | (mode == :binary ? 2 : 1)).chr
|
|
45
|
+
if length < 126
|
|
46
|
+
buffer += length.chr
|
|
47
|
+
elsif length <= 0xFFFF
|
|
48
|
+
buffer += [126, length].pack('CS>')
|
|
49
|
+
else
|
|
50
|
+
buffer += [127, length].pack('CQ>')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@socket.write(buffer)
|
|
54
|
+
@socket.write(data)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def on_close(&blk)
|
|
58
|
+
@on_close = blk
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def on_message(&blk)
|
|
62
|
+
@on_message = blk
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def on_start(&blk)
|
|
66
|
+
@on_start = blk
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def hijacked!(socket)
|
|
70
|
+
@socket = socket
|
|
71
|
+
puts "websocket hijacked! #{socket}"
|
|
72
|
+
@thread = Thread.start do
|
|
73
|
+
self.threaded_start!
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.upgrade_websocket_request(request)
|
|
78
|
+
ws_key = request.headers.dict['sec-websocket-key']
|
|
79
|
+
return unless ws_key && request.headers.dict['upgrade'] == 'websocket'
|
|
80
|
+
|
|
81
|
+
conns = request.headers.dict.fetch('connection', '').split(' ').map(&:strip)
|
|
82
|
+
return unless conns.include?('Upgrade')
|
|
83
|
+
|
|
84
|
+
ws_version = request.headers.dict['sec-websocket-version'] # expect: 13
|
|
85
|
+
return unless ws_version
|
|
86
|
+
|
|
87
|
+
protocols = request.headers.dict['sec-websocket-protocol']
|
|
88
|
+
protocols = protocols.split(',').map(&:strip) if protocols
|
|
89
|
+
|
|
90
|
+
extensions = request.headers.dict['sec-websocket-extensions']
|
|
91
|
+
extensions = extensions.split(';').map(&:strip) if extensions
|
|
92
|
+
|
|
93
|
+
decoded_key = Base64.strict_decode64(ws_key) rescue nil
|
|
94
|
+
return unless decoded_key&.length == 16
|
|
95
|
+
|
|
96
|
+
response_key = Base64.strict_encode64(
|
|
97
|
+
OpenSSL::Digest::SHA1.digest("#{ws_key}#{WS_CONST}")
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
ws = new
|
|
101
|
+
|
|
102
|
+
chosen_proto = yield(ws, protocols) if (block_given? && protocols)
|
|
103
|
+
chosen_proto = protocols.first if !chosen_proto && protocols
|
|
104
|
+
|
|
105
|
+
resp = Spyder::Response.new(code: 101)
|
|
106
|
+
resp.add_standard_headers
|
|
107
|
+
resp.set_header 'connection', 'Upgrade'
|
|
108
|
+
resp.set_header 'upgrade', 'websocket'
|
|
109
|
+
resp.set_header('sec-websocket-protocol', chosen_proto) if chosen_proto
|
|
110
|
+
resp.set_header 'sec-websocket-accept', response_key
|
|
111
|
+
|
|
112
|
+
resp.hijack = proc { |client_socket| ws.hijacked!(client_socket) }
|
|
113
|
+
|
|
114
|
+
[resp, ws]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def threaded_start!
|
|
118
|
+
@on_start.call if @on_start
|
|
119
|
+
|
|
120
|
+
while !@socket.closed? && !@socket.eof?
|
|
121
|
+
data = nil
|
|
122
|
+
begin
|
|
123
|
+
data = @socket.read_nonblock(16 * 1024)
|
|
124
|
+
rescue IO::WaitReadable
|
|
125
|
+
IO.select([@socket], [], [@socket])
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
next unless data
|
|
129
|
+
|
|
130
|
+
puts "ws: read #{data.length} bytes"
|
|
131
|
+
@streaming_buffer.feed(data)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
@on_close.call unless @on_close
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spyder
|
|
4
|
+
class WebSocketStreamingBuffer
|
|
5
|
+
attr_reader :buffer
|
|
6
|
+
attr_accessor :on_close
|
|
7
|
+
|
|
8
|
+
def initialize(&frame_callback)
|
|
9
|
+
@frame_callback = frame_callback
|
|
10
|
+
@on_close = nil
|
|
11
|
+
@buffer = []
|
|
12
|
+
_reset
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def feed(buf)
|
|
16
|
+
@buffer += buf.bytes
|
|
17
|
+
|
|
18
|
+
# printable = buf.bytes.map{ |x| x.to_s(16).rjust(2, '0').upcase }.join(' ')
|
|
19
|
+
# puts "Got #{buf.bytes.length} more bytes: [#{printable}]"
|
|
20
|
+
|
|
21
|
+
iterations = 0
|
|
22
|
+
loop do
|
|
23
|
+
_flush
|
|
24
|
+
|
|
25
|
+
iterations += 1
|
|
26
|
+
if iterations > 1_000
|
|
27
|
+
raise "Watchdog error: got stuck on a loop of #{iterations} iterations."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
break if (@buffer.length == 0 && !@need_buffer_length) ||
|
|
31
|
+
(@need_buffer_length && @buffer.length < @need_buffer_length)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def _reset
|
|
38
|
+
@state = :initial
|
|
39
|
+
@mask = nil
|
|
40
|
+
@opcode = nil
|
|
41
|
+
@data_mode = nil
|
|
42
|
+
@payload_length = nil
|
|
43
|
+
@need_buffer_length = nil
|
|
44
|
+
@flag_fin = nil
|
|
45
|
+
@fragmented = false
|
|
46
|
+
@fragmented_total_size = nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def _flush
|
|
50
|
+
send(:"_flush_#{@state}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def _flush_data
|
|
54
|
+
return unless @buffer.length >= @payload_length
|
|
55
|
+
|
|
56
|
+
data = if @mask
|
|
57
|
+
(0...@payload_length).map do |i|
|
|
58
|
+
@buffer[i] ^ @mask[i & 3]
|
|
59
|
+
end
|
|
60
|
+
else
|
|
61
|
+
@buffer[0...@payload_length]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
last_fragment = @fragmented && @flag_fin
|
|
65
|
+
@frame_callback.call(data, @data_mode, @fragmented, last_fragment)
|
|
66
|
+
|
|
67
|
+
@buffer.shift(@payload_length)
|
|
68
|
+
|
|
69
|
+
if @fragmented && !last_fragment
|
|
70
|
+
@need_buffer_length = 2
|
|
71
|
+
@opcode = nil
|
|
72
|
+
@payload_length = nil
|
|
73
|
+
@state = "initial"
|
|
74
|
+
else
|
|
75
|
+
_reset
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def close_with_error(msg)
|
|
80
|
+
@socket.close rescue nil
|
|
81
|
+
|
|
82
|
+
puts "Closing websocket: #{msg}"
|
|
83
|
+
|
|
84
|
+
@on_close.call if @on_close
|
|
85
|
+
|
|
86
|
+
_reset
|
|
87
|
+
|
|
88
|
+
@state = :closed
|
|
89
|
+
@socket = nil
|
|
90
|
+
@buffer = nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def _flush_readhdr
|
|
94
|
+
return unless @buffer.length >= @need_buffer_length
|
|
95
|
+
|
|
96
|
+
@flag_fin = ((@buffer[0] & 0x80) != 0)
|
|
97
|
+
@opcode = (@buffer[0] & 0xF)
|
|
98
|
+
data_len = (@buffer[1] & 0x7F)
|
|
99
|
+
mask_flag = ((@buffer[1] & 0x80) != 0)
|
|
100
|
+
|
|
101
|
+
is_control_frame = ((@opcode & 8) != 0)
|
|
102
|
+
if is_control_frame && (data_len > 125 || !@flag_fin)
|
|
103
|
+
# Control frames must not be fragmented and must be <= 125 bytes in size
|
|
104
|
+
return close_with_error("Control frame invalid state")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
if @fragmented && !is_control_frame && @opcode != 0
|
|
108
|
+
# We're in the middle of a fragmented frame and received a non-control
|
|
109
|
+
# frame. That's an error.
|
|
110
|
+
return close_with_error("Got non-control frame during fragmented frame.")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if !@fragmented && !@flag_fin && !is_control_frame
|
|
114
|
+
@fragmented = true
|
|
115
|
+
@fragmented_total_size = 0
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
shift_length = 2
|
|
119
|
+
|
|
120
|
+
@payload_length = if data_len == 127
|
|
121
|
+
shift_length += 8
|
|
122
|
+
@buffer[2...10].pack('C*').unpack('Q>').first
|
|
123
|
+
elsif data_len == 126
|
|
124
|
+
shift_length += 2
|
|
125
|
+
@buffer[2...4].pack('C*').unpack('S>').first
|
|
126
|
+
else
|
|
127
|
+
shift_length += 0
|
|
128
|
+
data_len
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if mask_flag
|
|
132
|
+
@mask = @buffer[shift_length...(shift_length + 4)].pack('C*').bytes
|
|
133
|
+
shift_length += 4
|
|
134
|
+
else
|
|
135
|
+
@mask = nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
@fragmented_total_size += @payload_length if @fragmented
|
|
139
|
+
|
|
140
|
+
fgr = nil
|
|
141
|
+
fgr = ", fgr=#{@fragmented_total_size}" if @fragmented
|
|
142
|
+
puts "frame(code=#{@opcode}, len=#{@payload_length}, fin=#{@flag_fin}#{fgr})"
|
|
143
|
+
|
|
144
|
+
@buffer.shift(shift_length)
|
|
145
|
+
@need_buffer_length = ((0..2) === @opcode ? @payload_length : nil)
|
|
146
|
+
|
|
147
|
+
case @opcode
|
|
148
|
+
when 0x0 # continuation
|
|
149
|
+
@state = "data"
|
|
150
|
+
when 0x1 # text frame
|
|
151
|
+
@state = "data"
|
|
152
|
+
@data_mode = :text
|
|
153
|
+
when 0x2 # binary frame
|
|
154
|
+
@state = "data"
|
|
155
|
+
@data_mode = :binary
|
|
156
|
+
when (0x3..0x7) # reserved
|
|
157
|
+
unexpected_opcode(@opcode)
|
|
158
|
+
when 0x8 # connection close
|
|
159
|
+
@on_close.call if @on_close
|
|
160
|
+
|
|
161
|
+
_reset
|
|
162
|
+
when 0x9 # ping
|
|
163
|
+
# TODO: send pong?
|
|
164
|
+
when 0xA # pong
|
|
165
|
+
puts "WS: got pong!"
|
|
166
|
+
|
|
167
|
+
_reset
|
|
168
|
+
else
|
|
169
|
+
unexpected_opcode(@opcode)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def _flush_initial
|
|
174
|
+
return unless @buffer.length >= 2
|
|
175
|
+
|
|
176
|
+
data_len = (@buffer[1] & 0x7F)
|
|
177
|
+
mask_flag = (@buffer[1] & 0x80)
|
|
178
|
+
|
|
179
|
+
@need_buffer_length = 2 + (mask_flag ? 4 : 0) + (
|
|
180
|
+
data_len == 127 ? 8 : (data_len == 126 ? 2 : 0)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
@state = "readhdr"
|
|
184
|
+
_flush
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def unexpected_opcode opcode
|
|
188
|
+
puts "WS: unexpected opcode #{opcode}"
|
|
189
|
+
exit 1
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
data/lib/spyder.rb
CHANGED
|
@@ -12,4 +12,14 @@ require 'spyder/header_store'
|
|
|
12
12
|
require 'spyder/request'
|
|
13
13
|
require 'spyder/response'
|
|
14
14
|
require 'spyder/router'
|
|
15
|
+
require 'spyder/basic_server'
|
|
15
16
|
require 'spyder/server'
|
|
17
|
+
require 'spyder/web_socket_streaming_buffer'
|
|
18
|
+
require 'spyder/web_socket'
|
|
19
|
+
|
|
20
|
+
# spyder-web
|
|
21
|
+
require 'marcel'
|
|
22
|
+
require 'base64'
|
|
23
|
+
require 'openssl'
|
|
24
|
+
|
|
25
|
+
require 'spyder/web/file_server'
|
metadata
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spyder
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0.pre.beta1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- André D. Piske
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.3'
|
|
13
26
|
- !ruby/object:Gem::Dependency
|
|
14
27
|
name: mustermann
|
|
15
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -24,6 +37,20 @@ dependencies:
|
|
|
24
37
|
- - "~>"
|
|
25
38
|
- !ruby/object:Gem::Version
|
|
26
39
|
version: '3.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: marcel
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.0'
|
|
27
54
|
description: Spyder Web
|
|
28
55
|
email: andrepiske@gmail.com
|
|
29
56
|
executables: []
|
|
@@ -32,18 +59,22 @@ extra_rdoc_files: []
|
|
|
32
59
|
files:
|
|
33
60
|
- lib/examples/simplest_server/server.rb
|
|
34
61
|
- lib/spyder.rb
|
|
62
|
+
- lib/spyder/basic_server.rb
|
|
35
63
|
- lib/spyder/error.rb
|
|
36
64
|
- lib/spyder/header_store.rb
|
|
65
|
+
- lib/spyder/rack_adapter.rb
|
|
37
66
|
- lib/spyder/request.rb
|
|
38
67
|
- lib/spyder/response.rb
|
|
39
68
|
- lib/spyder/router.rb
|
|
40
69
|
- lib/spyder/server.rb
|
|
41
70
|
- lib/spyder/version.rb
|
|
71
|
+
- lib/spyder/web/file_server.rb
|
|
72
|
+
- lib/spyder/web_socket.rb
|
|
73
|
+
- lib/spyder/web_socket_streaming_buffer.rb
|
|
42
74
|
homepage: https://github.com/andrepiske/spyder
|
|
43
75
|
licenses:
|
|
44
76
|
- MIT
|
|
45
77
|
metadata: {}
|
|
46
|
-
post_install_message:
|
|
47
78
|
rdoc_options: []
|
|
48
79
|
require_paths:
|
|
49
80
|
- lib
|
|
@@ -58,8 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
58
89
|
- !ruby/object:Gem::Version
|
|
59
90
|
version: '0'
|
|
60
91
|
requirements: []
|
|
61
|
-
rubygems_version:
|
|
62
|
-
signing_key:
|
|
92
|
+
rubygems_version: 4.0.23
|
|
63
93
|
specification_version: 4
|
|
64
94
|
summary: Spyder
|
|
65
95
|
test_files: []
|