spyder 0.1.1 → 0.2.0
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/response.rb +5 -2
- data/lib/spyder/router.rb +18 -2
- data/lib/spyder/server.rb +57 -43
- data/lib/spyder/version.rb +1 -2
- data/lib/spyder/web/file_server.rb +88 -0
- data/lib/spyder/web_socket.rb +137 -0
- data/lib/spyder/web_socket_streaming_buffer.rb +192 -0
- data/lib/spyder.rb +9 -0
- metadata +23 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b5ee9dba2fca697adc1456d6473a6a1f7e011108ce4d8baf0f7659c9886b85c0
|
4
|
+
data.tar.gz: 719a5fe8d53574d442ddec5cbb666db6018526901be76ebf7ac665451da3c512
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 90c3f303a6d4c12abf9250ad13caba9cdf06632cfc4a68a317c7a8ccac47b2aa4c568c315581a03a353d9d0b15317a4dbe1e245ad9a1e70335a2406cb9f30ba3
|
7
|
+
data.tar.gz: 471ab8a863891a2b27f236bb0cea65028e37e62ac12a387daa81a302d58adf5cf7f81c236b8d4953e9e15815a3b56c86a15bf497f7c207a1ac4c8fecfe2a1447
|
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,11 @@
|
|
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 = []
|
9
10
|
end
|
10
11
|
|
11
12
|
def add_route(verb, matcher, &handler)
|
@@ -14,6 +15,12 @@ module Spyder
|
|
14
15
|
@routes[verb.to_s.upcase] << [matcher, handler]
|
15
16
|
end
|
16
17
|
|
18
|
+
def add_fallback(callable = nil, &blk)
|
19
|
+
raise "Provide either a callable or a block, but not both" if callable && block_given?
|
20
|
+
|
21
|
+
@fallback_stack << (block_given? ? blk : callable)
|
22
|
+
end
|
23
|
+
|
17
24
|
def call(ctx, request)
|
18
25
|
only_path = request.path_info
|
19
26
|
|
@@ -26,7 +33,16 @@ module Spyder
|
|
26
33
|
end
|
27
34
|
end
|
28
35
|
|
29
|
-
match_data ? handler.call(request, match_data) :
|
36
|
+
response = match_data ? handler.call(request, match_data) : nil
|
37
|
+
return response if response.is_a?(Response)
|
38
|
+
|
39
|
+
fallback_stack.each do |blk|
|
40
|
+
response = blk.call(request)
|
41
|
+
|
42
|
+
return response if response.is_a?(Response)
|
43
|
+
end
|
44
|
+
|
45
|
+
Response.make_generic(:not_found)
|
30
46
|
end
|
31
47
|
end
|
32
48
|
|
data/lib/spyder/server.rb
CHANGED
@@ -4,8 +4,9 @@ module Spyder
|
|
4
4
|
class Server
|
5
5
|
attr_accessor :router
|
6
6
|
|
7
|
-
def initialize(bind, port, router: Router.new, max_threads: 4)
|
7
|
+
def initialize(bind, port, router: Router.new, max_threads: 4, tcp_backlog: 10)
|
8
8
|
@server = TCPServer.new(bind, port)
|
9
|
+
@tcp_backlog = tcp_backlog
|
9
10
|
@max_threads = max_threads
|
10
11
|
@middleware = []
|
11
12
|
@threads = []
|
@@ -13,49 +14,54 @@ module Spyder
|
|
13
14
|
@router = router
|
14
15
|
end
|
15
16
|
|
16
|
-
def add_middleware(callable, args)
|
17
|
+
def add_middleware(callable, args=[])
|
17
18
|
@middleware << [callable, args]
|
18
19
|
end
|
19
20
|
|
20
21
|
def start
|
21
|
-
|
22
|
-
|
23
|
-
client = @server.accept
|
22
|
+
busy_threads = 0
|
23
|
+
@server.listen(@tcp_backlog)
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
25
|
+
loop do
|
26
|
+
time_start = Process.clock_gettime(:CLOCK_MONOTONIC, :float_second)
|
27
|
+
loop do
|
28
|
+
current_busy = @tp_sync.synchronize { busy_threads }
|
29
|
+
break if current_busy < @max_threads
|
30
|
+
sleep(0)
|
31
|
+
current_time = Process.clock_gettime(:CLOCK_MONOTONIC, :float_second)
|
32
|
+
if (current_time - time_start) > 1.0
|
33
|
+
# puts "Waiting a long time: #{(current_time - time_start)}"
|
34
|
+
sleep 0.2
|
31
35
|
end
|
36
|
+
end
|
32
37
|
|
33
|
-
|
34
|
-
|
38
|
+
client = @server.accept
|
39
|
+
@tp_sync.synchronize { busy_threads += 1 }
|
35
40
|
|
36
|
-
|
37
|
-
|
38
|
-
|
41
|
+
Thread.new do
|
42
|
+
begin
|
43
|
+
error, response = nil
|
44
|
+
begin
|
45
|
+
response = process_new_client(client)
|
46
|
+
rescue Exception => e
|
47
|
+
error = e
|
48
|
+
end
|
39
49
|
|
40
|
-
|
41
|
-
|
50
|
+
if error
|
51
|
+
puts error.full_message
|
42
52
|
|
43
|
-
|
44
|
-
|
45
|
-
while over_capacity
|
46
|
-
@tp_sync.synchronize do
|
47
|
-
unless added_thread_to_list
|
48
|
-
@threads << app_thread
|
49
|
-
added_thread_to_list = true
|
53
|
+
response = Response.make_generic :internal_server_error
|
54
|
+
dispatch_response(client, response)
|
50
55
|
end
|
51
|
-
over_capacity = (@threads.length >= @max_threads)
|
52
|
-
# puts("#{@threads.length} of #{@max_threads}")
|
53
56
|
|
54
|
-
|
57
|
+
if response&.hijack
|
58
|
+
response.hijack.call(client)
|
59
|
+
else
|
60
|
+
client.close rescue nil
|
61
|
+
end
|
62
|
+
ensure
|
63
|
+
@tp_sync.synchronize { busy_threads -= 1 }
|
55
64
|
end
|
56
|
-
|
57
|
-
# puts("XXX OVER CAPACITY!") if over_capacity
|
58
|
-
sleep 0 if over_capacity
|
59
65
|
end
|
60
66
|
end
|
61
67
|
end
|
@@ -97,22 +103,30 @@ module Spyder
|
|
97
103
|
content_length = response.headers.dict['content-length']
|
98
104
|
if !content_length && response.body && response.body.is_a?(String)
|
99
105
|
content_length = response.body.length
|
106
|
+
response.set_header 'content-length', content_length.to_s
|
100
107
|
end
|
101
108
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
socket.write("\r\n")
|
109
|
-
|
110
|
-
if response.body
|
111
|
-
Array(response.body).each do |part|
|
112
|
-
content = part.respond_to?(:call) ? part.call : part
|
113
|
-
socket.write(content.b)
|
109
|
+
response.set_header('connection', 'close') unless response.headers.dict['connection']
|
110
|
+
|
111
|
+
begin
|
112
|
+
socket.write("HTTP/1.1 #{response.code} #{response.reason_sentence.b}\r\n")
|
113
|
+
response.headers.ordered.each do |name, value|
|
114
|
+
socket.write("#{name.b}: #{value.b}\r\n")
|
114
115
|
end
|
116
|
+
socket.write("\r\n")
|
117
|
+
|
118
|
+
if response.body
|
119
|
+
Array(response.body).each do |part|
|
120
|
+
content = part.respond_to?(:call) ? part.call : part
|
121
|
+
socket.write(content.b)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
rescue Errno::EPIPE
|
125
|
+
# socket closed. So what?
|
126
|
+
socket.close rescue nil
|
115
127
|
end
|
128
|
+
|
129
|
+
response
|
116
130
|
end
|
117
131
|
|
118
132
|
def read_line(socket)
|
data/lib/spyder/version.rb
CHANGED
@@ -0,0 +1,88 @@
|
|
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
|
+
File.expand_path(File.join(Dir.pwd, path))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(request)
|
17
|
+
return unless request.verb == 'GET'
|
18
|
+
|
19
|
+
input_path = request.path
|
20
|
+
input_path = @default_index if request.path == '/' && @default_index
|
21
|
+
|
22
|
+
req_path = safe_request_path(input_path)
|
23
|
+
full_path = nil
|
24
|
+
return unless @base_paths.any? do |base|
|
25
|
+
fp = File.join(base, *req_path)
|
26
|
+
next unless File.file?(fp)
|
27
|
+
full_path = fp
|
28
|
+
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
st = File.readable?(full_path) ? File.stat(full_path) : nil
|
33
|
+
|
34
|
+
return unless st && %w[file link].include?(st.ftype)
|
35
|
+
|
36
|
+
etag = "\"#{"%xT-%x0" % [st.mtime, st.size]}\""
|
37
|
+
|
38
|
+
resp = serve_not_modified_response(request, st, etag)
|
39
|
+
return resp if resp
|
40
|
+
|
41
|
+
resp = Spyder::Response.new
|
42
|
+
resp.add_standard_headers
|
43
|
+
resp.set_header 'last-modified', st.mtime.httpdate
|
44
|
+
resp.set_header 'etag', etag
|
45
|
+
resp.set_header 'cache-control', 'public, must-revalidate, max-age=0'
|
46
|
+
|
47
|
+
File.open full_path do |fp|
|
48
|
+
mime = Marcel::MimeType.for(fp)
|
49
|
+
if mime == 'application/octet-stream' || mime == 'text/plain'
|
50
|
+
mime = Marcel::MimeType.for(name: req_path.last)
|
51
|
+
end
|
52
|
+
|
53
|
+
resp.set_header('content-type', mime) if mime
|
54
|
+
|
55
|
+
fp.rewind
|
56
|
+
resp.body = fp.read
|
57
|
+
end
|
58
|
+
|
59
|
+
resp
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def serve_not_modified_response(request, st, etag)
|
65
|
+
if_etag = request.headers.dict['if-none-match']
|
66
|
+
if_modified_since = request.headers.dict['if-modified-since']
|
67
|
+
|
68
|
+
return unless if_etag || if_modified_since
|
69
|
+
|
70
|
+
resp = Spyder::Response.new
|
71
|
+
resp.add_standard_headers
|
72
|
+
resp.code = 304
|
73
|
+
|
74
|
+
return resp if if_etag && if_etag == etag
|
75
|
+
|
76
|
+
if_ms = if_modified_since ? Time.parse(if_modified_since) : nil
|
77
|
+
|
78
|
+
(if_ms && Time.at(st.mtime.to_i) <= if_ms) ? resp : nil
|
79
|
+
end
|
80
|
+
|
81
|
+
def safe_request_path(path)
|
82
|
+
path.split('/').reject do |component|
|
83
|
+
component == '..' || component == '' || component == '.'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
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
@@ -13,3 +13,12 @@ require 'spyder/request'
|
|
13
13
|
require 'spyder/response'
|
14
14
|
require 'spyder/router'
|
15
15
|
require 'spyder/server'
|
16
|
+
require 'spyder/web_socket_streaming_buffer'
|
17
|
+
require 'spyder/web_socket'
|
18
|
+
|
19
|
+
# spyder-web
|
20
|
+
require 'marcel'
|
21
|
+
require 'base64'
|
22
|
+
require 'openssl'
|
23
|
+
|
24
|
+
require 'spyder/web/file_server'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spyder
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- André D. Piske
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-06-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mustermann
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: marcel
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.0'
|
27
41
|
description: Spyder Web
|
28
42
|
email: andrepiske@gmail.com
|
29
43
|
executables: []
|
@@ -39,11 +53,14 @@ files:
|
|
39
53
|
- lib/spyder/router.rb
|
40
54
|
- lib/spyder/server.rb
|
41
55
|
- lib/spyder/version.rb
|
56
|
+
- lib/spyder/web/file_server.rb
|
57
|
+
- lib/spyder/web_socket.rb
|
58
|
+
- lib/spyder/web_socket_streaming_buffer.rb
|
42
59
|
homepage: https://github.com/andrepiske/spyder
|
43
60
|
licenses:
|
44
61
|
- MIT
|
45
62
|
metadata: {}
|
46
|
-
post_install_message:
|
63
|
+
post_install_message:
|
47
64
|
rdoc_options: []
|
48
65
|
require_paths:
|
49
66
|
- lib
|
@@ -58,8 +75,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
75
|
- !ruby/object:Gem::Version
|
59
76
|
version: '0'
|
60
77
|
requirements: []
|
61
|
-
rubygems_version: 3.
|
62
|
-
signing_key:
|
78
|
+
rubygems_version: 3.0.3.1
|
79
|
+
signing_key:
|
63
80
|
specification_version: 4
|
64
81
|
summary: Spyder
|
65
82
|
test_files: []
|