spyder 0.1.2 → 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 +13 -6
- data/lib/spyder/version.rb +1 -1
- 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
@@ -14,7 +14,7 @@ module Spyder
|
|
14
14
|
@router = router
|
15
15
|
end
|
16
16
|
|
17
|
-
def add_middleware(callable, args)
|
17
|
+
def add_middleware(callable, args=[])
|
18
18
|
@middleware << [callable, args]
|
19
19
|
end
|
20
20
|
|
@@ -40,9 +40,9 @@ module Spyder
|
|
40
40
|
|
41
41
|
Thread.new do
|
42
42
|
begin
|
43
|
-
error = nil
|
43
|
+
error, response = nil
|
44
44
|
begin
|
45
|
-
process_new_client(client)
|
45
|
+
response = process_new_client(client)
|
46
46
|
rescue Exception => e
|
47
47
|
error = e
|
48
48
|
end
|
@@ -54,7 +54,11 @@ module Spyder
|
|
54
54
|
dispatch_response(client, response)
|
55
55
|
end
|
56
56
|
|
57
|
-
|
57
|
+
if response&.hijack
|
58
|
+
response.hijack.call(client)
|
59
|
+
else
|
60
|
+
client.close rescue nil
|
61
|
+
end
|
58
62
|
ensure
|
59
63
|
@tp_sync.synchronize { busy_threads -= 1 }
|
60
64
|
end
|
@@ -99,15 +103,16 @@ module Spyder
|
|
99
103
|
content_length = response.headers.dict['content-length']
|
100
104
|
if !content_length && response.body && response.body.is_a?(String)
|
101
105
|
content_length = response.body.length
|
106
|
+
response.set_header 'content-length', content_length.to_s
|
102
107
|
end
|
103
108
|
|
109
|
+
response.set_header('connection', 'close') unless response.headers.dict['connection']
|
110
|
+
|
104
111
|
begin
|
105
112
|
socket.write("HTTP/1.1 #{response.code} #{response.reason_sentence.b}\r\n")
|
106
113
|
response.headers.ordered.each do |name, value|
|
107
114
|
socket.write("#{name.b}: #{value.b}\r\n")
|
108
115
|
end
|
109
|
-
socket.write("connection: close\r\n") # FIXME:
|
110
|
-
socket.write("content-length: #{content_length}\r\n") if content_length
|
111
116
|
socket.write("\r\n")
|
112
117
|
|
113
118
|
if response.body
|
@@ -120,6 +125,8 @@ module Spyder
|
|
120
125
|
# socket closed. So what?
|
121
126
|
socket.close rescue nil
|
122
127
|
end
|
128
|
+
|
129
|
+
response
|
123
130
|
end
|
124
131
|
|
125
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: []
|