spinel-spinner 0.1.0.pre.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 +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +164 -0
- data/lib/spinner/body_storage.rb +106 -0
- data/lib/spinner/chunk_decoder.rb +72 -0
- data/lib/spinner/host_authority.rb +86 -0
- data/lib/spinner/input.rb +58 -0
- data/lib/spinner/proxy_metadata.rb +93 -0
- data/lib/spinner/reactor.rb +575 -0
- data/lib/spinner/request_parser.rb +183 -0
- data/lib/spinner/response_writer.rb +123 -0
- data/lib/spinner/server.rb +384 -0
- data/lib/spinner/task_pool.rb +62 -0
- data/lib/spinner/thread_pool.rb +229 -0
- data/lib/spinner/tls.rb +116 -0
- data/lib/spinner/transport_socket.rb +121 -0
- data/lib/spinner/upgrade.rb +20 -0
- data/lib/spinner/version.rb +5 -0
- data/lib/spinner/websocket.rb +179 -0
- data/lib/spinner.rb +21 -0
- data/sig/spinner.rbs +4 -0
- metadata +63 -0
data/lib/spinner/tls.rb
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
require "ffi"
|
|
2
|
+
|
|
3
|
+
module Spinner
|
|
4
|
+
module TLSNative
|
|
5
|
+
extend FFI::Library
|
|
6
|
+
|
|
7
|
+
def self.load!
|
|
8
|
+
return if @loaded
|
|
9
|
+
# macOS's system libssl can be incompatible with the libcrypto used by
|
|
10
|
+
# Spinel's bundled OpenSSL package. Allow an explicit, matching library.
|
|
11
|
+
library = ENV["SPINNER_SSL_LIBRARY"]
|
|
12
|
+
if library.nil? || library.empty?
|
|
13
|
+
library = ["/opt/homebrew/opt/openssl@3/lib/libssl.dylib",
|
|
14
|
+
"/usr/local/opt/openssl@3/lib/libssl.dylib"].find { |path| File.file?(path) }
|
|
15
|
+
library ||= "libssl.so.3"
|
|
16
|
+
end
|
|
17
|
+
ffi_lib library
|
|
18
|
+
|
|
19
|
+
attach_function :TLS_server_method, [], :pointer
|
|
20
|
+
callback :alpn_select, [:pointer, :pointer, :pointer, :pointer, :uint, :pointer], :int
|
|
21
|
+
attach_function :SSL_CTX_set_alpn_select_cb, [:pointer, :alpn_select, :pointer], :void
|
|
22
|
+
attach_function :SSL_CTX_new, [:pointer], :pointer
|
|
23
|
+
attach_function :SSL_CTX_free, [:pointer], :void
|
|
24
|
+
attach_function :SSL_CTX_ctrl, [:pointer, :int, :long, :pointer], :long
|
|
25
|
+
attach_function :SSL_CTX_use_certificate_chain_file, [:pointer, :string], :int
|
|
26
|
+
attach_function :SSL_CTX_use_PrivateKey_file, [:pointer, :string, :int], :int
|
|
27
|
+
attach_function :SSL_CTX_check_private_key, [:pointer], :int
|
|
28
|
+
attach_function :SSL_new, [:pointer], :pointer
|
|
29
|
+
attach_function :SSL_free, [:pointer], :void
|
|
30
|
+
attach_function :SSL_set_fd, [:pointer, :int], :int
|
|
31
|
+
attach_function :SSL_accept, [:pointer], :int
|
|
32
|
+
attach_function :SSL_get_error, [:pointer, :int], :int
|
|
33
|
+
attach_function :SSL_read, [:pointer, :pointer, :int], :int
|
|
34
|
+
attach_function :SSL_write, [:pointer, :pointer, :int], :int
|
|
35
|
+
attach_function :SSL_pending, [:pointer], :int
|
|
36
|
+
attach_function :SSL_shutdown, [:pointer], :int
|
|
37
|
+
attach_function :ERR_get_error, [], :ulong
|
|
38
|
+
attach_function :ERR_error_string_n, [:ulong, :pointer, :size_t], :void
|
|
39
|
+
|
|
40
|
+
@loaded = true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.error(prefix)
|
|
44
|
+
code = self.ERR_get_error
|
|
45
|
+
return prefix if code == 0
|
|
46
|
+
buffer = FFI::MemoryPointer.new(:char, 256)
|
|
47
|
+
self.ERR_error_string_n(code, buffer, 256)
|
|
48
|
+
"#{prefix}: #{buffer.read_string}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
module TLSFileControl
|
|
53
|
+
extend FFI::Library
|
|
54
|
+
ffi_lib FFI::Library::LIBC
|
|
55
|
+
attach_function :fcntl, [:int, :int, :int], :int
|
|
56
|
+
|
|
57
|
+
def self.nonblock!(fd)
|
|
58
|
+
flags = fcntl(fd, 3, 0) # F_GETFL
|
|
59
|
+
raise IOError, "fcntl(F_GETFL) failed" if flags < 0
|
|
60
|
+
bit = RUBY_PLATFORM.include?("darwin") ? 4 : 2048
|
|
61
|
+
raise IOError, "fcntl(F_SETFL) failed" if fcntl(fd, 4, flags | bit) < 0
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
module TLS
|
|
66
|
+
class Context
|
|
67
|
+
attr_reader :pointer
|
|
68
|
+
|
|
69
|
+
def initialize(cert_file, key_file)
|
|
70
|
+
TLSNative.load!
|
|
71
|
+
@pointer = TLSNative.SSL_CTX_new(TLSNative.TLS_server_method)
|
|
72
|
+
raise IOError, TLSNative.error("SSL_CTX_new") if @pointer.null?
|
|
73
|
+
begin
|
|
74
|
+
# SSL_CTRL_SET_MIN_PROTO_VERSION, TLS1_2_VERSION.
|
|
75
|
+
raise IOError, "TLS 1.2 minimum unavailable" unless TLSNative.SSL_CTX_ctrl(@pointer, 123, 0x0303, nil) == 1
|
|
76
|
+
raise IOError, "TLS 1.3 maximum unavailable" unless TLSNative.SSL_CTX_ctrl(@pointer, 124, 0x0304, nil) == 1
|
|
77
|
+
@alpn_callback = proc do |_ssl, output, output_length, offered, length, _arg|
|
|
78
|
+
protocols = offered.read_string(length)
|
|
79
|
+
offset = 0
|
|
80
|
+
selected = nil
|
|
81
|
+
while offset < length
|
|
82
|
+
size = protocols.getbyte(offset)
|
|
83
|
+
break if size.nil? || size == 0 || offset + size + 1 > length
|
|
84
|
+
if protocols.byteslice(offset + 1, size) == "http/1.1"
|
|
85
|
+
selected = offset + 1
|
|
86
|
+
break
|
|
87
|
+
end
|
|
88
|
+
offset += size + 1
|
|
89
|
+
end
|
|
90
|
+
if selected.nil?
|
|
91
|
+
2 # fatal alert: never select HTTP/2
|
|
92
|
+
else
|
|
93
|
+
output.write_pointer(offered + selected)
|
|
94
|
+
output_length.write_uint8(8)
|
|
95
|
+
0
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
TLSNative.SSL_CTX_set_alpn_select_cb(@pointer, @alpn_callback, nil)
|
|
99
|
+
raise IOError, TLSNative.error("certificate chain") unless TLSNative.SSL_CTX_use_certificate_chain_file(@pointer, cert_file) == 1
|
|
100
|
+
raise IOError, TLSNative.error("private key") unless TLSNative.SSL_CTX_use_PrivateKey_file(@pointer, key_file, 1) == 1
|
|
101
|
+
raise IOError, TLSNative.error("certificate/key mismatch") unless TLSNative.SSL_CTX_check_private_key(@pointer) == 1
|
|
102
|
+
rescue StandardError
|
|
103
|
+
close
|
|
104
|
+
raise
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def close
|
|
109
|
+
return if @pointer.nil? || @pointer.null?
|
|
110
|
+
TLSNative.SSL_CTX_free(@pointer)
|
|
111
|
+
@pointer = nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
module Spinner
|
|
2
|
+
module TLS
|
|
3
|
+
class Socket
|
|
4
|
+
def initialize(socket, context)
|
|
5
|
+
@socket = socket
|
|
6
|
+
@io_lock = Mutex.new
|
|
7
|
+
@ssl = nil
|
|
8
|
+
@handshake_complete = false
|
|
9
|
+
@pending_write = nil
|
|
10
|
+
@pending_bytes = nil
|
|
11
|
+
return if context.nil?
|
|
12
|
+
TLSFileControl.nonblock!(socket.fileno)
|
|
13
|
+
@ssl = TLSNative.SSL_new(context.pointer)
|
|
14
|
+
raise IOError, TLSNative.error("SSL_new") if @ssl.null?
|
|
15
|
+
unless TLSNative.SSL_set_fd(@ssl, socket.fileno) == 1
|
|
16
|
+
close
|
|
17
|
+
raise IOError, TLSNative.error("SSL_set_fd")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_io
|
|
22
|
+
@socket
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def closed?
|
|
26
|
+
@socket.closed?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def pending
|
|
30
|
+
@io_lock.synchronize { pending_unlocked }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def pending_unlocked
|
|
34
|
+
@ssl.nil? ? 0 : TLSNative.SSL_pending(@ssl)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def accept_nonblock
|
|
38
|
+
@io_lock.synchronize { accept_nonblock_unlocked }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def accept_nonblock_unlocked
|
|
42
|
+
return self if @ssl.nil?
|
|
43
|
+
result = TLSNative.SSL_accept(@ssl)
|
|
44
|
+
if result == 1
|
|
45
|
+
@handshake_complete = true
|
|
46
|
+
return self
|
|
47
|
+
end
|
|
48
|
+
wait_or_raise(result, "SSL_accept")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def read_nonblock(max_bytes)
|
|
52
|
+
@io_lock.synchronize { read_nonblock_unlocked(max_bytes) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def read_nonblock_unlocked(max_bytes)
|
|
56
|
+
return @socket.read_nonblock(max_bytes) if @ssl.nil?
|
|
57
|
+
buffer = FFI::MemoryPointer.new(:char, max_bytes)
|
|
58
|
+
result = TLSNative.SSL_read(@ssl, buffer, max_bytes)
|
|
59
|
+
return buffer.read_string(result).b if result > 0
|
|
60
|
+
wait_or_raise(result, "SSL_read")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def write_nonblock(bytes)
|
|
64
|
+
@io_lock.synchronize { write_nonblock_unlocked(bytes) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Keep the binary conversion at the native socket call for framed writes.
|
|
68
|
+
def write_stream_nonblock(bytes)
|
|
69
|
+
@io_lock.synchronize do
|
|
70
|
+
if @ssl.nil?
|
|
71
|
+
@socket.write_nonblock(bytes.b)
|
|
72
|
+
else
|
|
73
|
+
write_nonblock_unlocked(bytes)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def write_nonblock_unlocked(bytes)
|
|
79
|
+
return @socket.write_nonblock(bytes.b) if @ssl.nil?
|
|
80
|
+
if @pending_write.nil?
|
|
81
|
+
@pending_write = FFI::MemoryPointer.new(:char, bytes.bytesize)
|
|
82
|
+
@pending_write.put_bytes(0, bytes)
|
|
83
|
+
@pending_bytes = bytes
|
|
84
|
+
elsif @pending_bytes != bytes
|
|
85
|
+
raise IOError, "TLS write changed during retry"
|
|
86
|
+
end
|
|
87
|
+
result = TLSNative.SSL_write(@ssl, @pending_write, bytes.bytesize)
|
|
88
|
+
if result > 0
|
|
89
|
+
@pending_write = nil
|
|
90
|
+
@pending_bytes = nil
|
|
91
|
+
return result
|
|
92
|
+
end
|
|
93
|
+
wait_or_raise(result, "SSL_write")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def close
|
|
97
|
+
@io_lock.synchronize { close_unlocked }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def close_unlocked
|
|
101
|
+
unless @ssl.nil? || @ssl.null?
|
|
102
|
+
TLSNative.SSL_shutdown(@ssl) if @handshake_complete && !@socket.closed?
|
|
103
|
+
TLSNative.SSL_free(@ssl)
|
|
104
|
+
@ssl = nil
|
|
105
|
+
end
|
|
106
|
+
@socket.close unless @socket.closed?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def wait_or_raise(result, operation)
|
|
112
|
+
case TLSNative.SSL_get_error(@ssl, result)
|
|
113
|
+
when 2 then raise IO::EAGAINWaitReadable
|
|
114
|
+
when 3 then raise IO::EAGAINWaitWritable
|
|
115
|
+
when 6 then raise EOFError
|
|
116
|
+
else raise IOError, TLSNative.error(operation)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Spinner
|
|
2
|
+
# The callback owns the transport for its lifetime. Returning closes it.
|
|
3
|
+
class Upgrade
|
|
4
|
+
def initialize(close_handler = nil, &handler)
|
|
5
|
+
@handler = handler
|
|
6
|
+
@close_handler = close_handler
|
|
7
|
+
@closed = false
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(transport, buffered_bytes)
|
|
11
|
+
@handler.call(transport, buffered_bytes)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def close
|
|
15
|
+
return if @closed
|
|
16
|
+
@closed = true
|
|
17
|
+
@close_handler.call unless @close_handler.nil?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
require "base64"
|
|
2
|
+
require "digest/sha1"
|
|
3
|
+
require_relative "../spinner"
|
|
4
|
+
|
|
5
|
+
module Spinner
|
|
6
|
+
module WebSocket
|
|
7
|
+
class Adapter
|
|
8
|
+
def initialize(application, max_message_bytes: 1048576, idle_timeout: 30.0)
|
|
9
|
+
raise ArgumentError, "invalid WebSocket limits" unless max_message_bytes.is_a?(Integer) && max_message_bytes > 0 && idle_timeout.is_a?(Numeric) && idle_timeout > 0 && idle_timeout.finite?
|
|
10
|
+
@application = application
|
|
11
|
+
@maximum = max_message_bytes
|
|
12
|
+
@timeout = idle_timeout
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(env)
|
|
16
|
+
upgrades = env["HTTP_UPGRADE"].to_s.downcase.split(",").map(&:strip)
|
|
17
|
+
return @application.call(env) unless upgrades.include?("websocket")
|
|
18
|
+
return [400, {}, ["Invalid WebSocket upgrade"]] unless upgrades == ["websocket"]
|
|
19
|
+
key = env["HTTP_SEC_WEBSOCKET_KEY"].to_s
|
|
20
|
+
valid_key = key.match?(/\A[A-Za-z0-9+\/]{22}==\z/)
|
|
21
|
+
valid_key &&= Base64.strict_decode64(key).bytesize == 16
|
|
22
|
+
valid_key &&= Base64.strict_encode64(Base64.strict_decode64(key)) == key
|
|
23
|
+
connection = env["HTTP_CONNECTION"].to_s.downcase.split(",").map(&:strip)
|
|
24
|
+
unless valid_key && connection.include?("upgrade") && env["REQUEST_METHOD"] == "GET" &&
|
|
25
|
+
env["SERVER_PROTOCOL"] == "HTTP/1.1" && env["HTTP_SEC_WEBSOCKET_VERSION"] == "13" &&
|
|
26
|
+
env["CONTENT_LENGTH"].to_s.to_i == 0 && env["HTTP_TRANSFER_ENCODING"].nil?
|
|
27
|
+
return [400, {}, ["Invalid WebSocket request"]]
|
|
28
|
+
end
|
|
29
|
+
response = @application.call(env)
|
|
30
|
+
return response unless response.is_a?(Array) && response.length == 3 && response[0] == 101
|
|
31
|
+
handler = response[2]
|
|
32
|
+
raise ArgumentError, "expected WebSocket handler" unless handler.respond_to?(:call)
|
|
33
|
+
accept = Base64.strict_encode64(Digest::SHA1.digest(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
|
|
34
|
+
headers = response[1].merge("upgrade" => "websocket", "sec-websocket-accept" => accept)
|
|
35
|
+
upgrade = Upgrade.new(proc { handler.close if handler.respond_to?(:close) }) do |transport, buffered|
|
|
36
|
+
session = Session.new(transport, buffered, @maximum, @timeout)
|
|
37
|
+
begin
|
|
38
|
+
handler.call(session)
|
|
39
|
+
ensure
|
|
40
|
+
session.close
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
[101, headers, upgrade]
|
|
44
|
+
rescue ArgumentError
|
|
45
|
+
[400, {}, ["Invalid WebSocket request"]]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class Session
|
|
50
|
+
def initialize(transport, buffered, maximum, timeout)
|
|
51
|
+
@transport = transport
|
|
52
|
+
@buffer = buffered.b
|
|
53
|
+
@maximum = maximum
|
|
54
|
+
@timeout = timeout
|
|
55
|
+
@closed = false
|
|
56
|
+
@received_close = false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def exact(length, deadline)
|
|
60
|
+
while @buffer.bytesize < length
|
|
61
|
+
@buffer << @transport.read_chunk(deadline, [4096, length - @buffer.bytesize].min, "websocket")
|
|
62
|
+
end
|
|
63
|
+
@buffer.slice!(0, length).b
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def read_message
|
|
67
|
+
data = "".b
|
|
68
|
+
message_opcode = nil
|
|
69
|
+
deadline = Transport.now + @timeout
|
|
70
|
+
loop do
|
|
71
|
+
head = exact(2, deadline)
|
|
72
|
+
first = head.getbyte(0)
|
|
73
|
+
second = head.getbyte(1)
|
|
74
|
+
fin = (first & 128) != 0
|
|
75
|
+
opcode = first & 15
|
|
76
|
+
return protocol_error(1002) if (first & 112) != 0 || (second & 128) == 0
|
|
77
|
+
length = second & 127
|
|
78
|
+
if length == 126 || length == 127
|
|
79
|
+
count = length == 126 ? 2 : 8
|
|
80
|
+
encoded = exact(count, deadline)
|
|
81
|
+
length = 0
|
|
82
|
+
return protocol_error(1002) if count == 8 && (encoded.getbyte(0) & 128) != 0
|
|
83
|
+
encoded.bytes.each do |byte|
|
|
84
|
+
return protocol_error(1009) if length > (@maximum >> 8)
|
|
85
|
+
length = (length << 8) | byte
|
|
86
|
+
end
|
|
87
|
+
return protocol_error(1002) if (count == 2 && length < 126) || (count == 8 && length < 65536)
|
|
88
|
+
end
|
|
89
|
+
return protocol_error(1009) if opcode < 8 && (length > @maximum || data.bytesize + length > @maximum)
|
|
90
|
+
return protocol_error(1002) if opcode >= 8 && (!fin || length > 125)
|
|
91
|
+
mask = exact(4, deadline)
|
|
92
|
+
payload = exact(length, deadline)
|
|
93
|
+
decoded = "".b
|
|
94
|
+
payload.bytes.each_with_index { |byte, index| decoded << (byte ^ mask.getbyte(index % 4)).chr.b }
|
|
95
|
+
if opcode == 8
|
|
96
|
+
return protocol_error(1002) if decoded.bytesize == 1
|
|
97
|
+
if decoded.bytesize >= 2
|
|
98
|
+
code = (decoded.getbyte(0) << 8) | decoded.getbyte(1)
|
|
99
|
+
return protocol_error(1002) unless [1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014].include?(code) || (code >= 3000 && code <= 4999)
|
|
100
|
+
return protocol_error(1007) unless decoded.byteslice(2, decoded.bytesize - 2).dup.force_encoding("UTF-8").valid_encoding?
|
|
101
|
+
end
|
|
102
|
+
write_frame(8, decoded) unless @closed
|
|
103
|
+
@received_close = true
|
|
104
|
+
@closed = true
|
|
105
|
+
return nil
|
|
106
|
+
elsif opcode == 9
|
|
107
|
+
write_frame(10, decoded)
|
|
108
|
+
next
|
|
109
|
+
elsif opcode == 10
|
|
110
|
+
next
|
|
111
|
+
elsif opcode == 1 || opcode == 2
|
|
112
|
+
return protocol_error(1002) unless message_opcode.nil?
|
|
113
|
+
message_opcode = opcode
|
|
114
|
+
elsif opcode == 0
|
|
115
|
+
return protocol_error(1002) if message_opcode.nil?
|
|
116
|
+
else
|
|
117
|
+
return protocol_error(1002)
|
|
118
|
+
end
|
|
119
|
+
data << decoded
|
|
120
|
+
if fin
|
|
121
|
+
return protocol_error(1007) if message_opcode == 1 && !data.dup.force_encoding("UTF-8").valid_encoding?
|
|
122
|
+
return [message_opcode, data]
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def write_message(opcode, bytes)
|
|
128
|
+
raise IOError, "closed WebSocket" if @closed
|
|
129
|
+
raise ArgumentError, "invalid message" unless [1, 2].include?(opcode) && bytes.is_a?(String) && bytes.bytesize <= @maximum
|
|
130
|
+
raise ArgumentError, "invalid text message" if opcode == 1 && !bytes.dup.force_encoding("UTF-8").valid_encoding?
|
|
131
|
+
write_frame(opcode, bytes)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def write_frame(opcode, bytes)
|
|
135
|
+
length = bytes.bytesize
|
|
136
|
+
header = [128 | opcode]
|
|
137
|
+
if length < 126
|
|
138
|
+
header << length
|
|
139
|
+
else
|
|
140
|
+
count = length <= 65535 ? 2 : 8
|
|
141
|
+
header << (count == 2 ? 126 : 127)
|
|
142
|
+
(count - 1).downto(0) { |index| header << ((length >> (index * 8)) & 255) }
|
|
143
|
+
end
|
|
144
|
+
deadline = Transport.now + @timeout
|
|
145
|
+
write_binary_frame_bytes(header.pack("C*").b, deadline)
|
|
146
|
+
# Pack dynamic payloads too: Spinel's native socket otherwise stops
|
|
147
|
+
# a decoded string at an embedded NUL during an echo.
|
|
148
|
+
write_binary_frame_bytes(bytes.bytes.pack("C*").b, deadline)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def write_binary_frame_bytes(bytes, deadline)
|
|
152
|
+
offset = 0
|
|
153
|
+
while offset < bytes.bytesize
|
|
154
|
+
remaining = bytes.byteslice(offset, bytes.bytesize - offset).b
|
|
155
|
+
count = @transport.write_stream_chunk(remaining, deadline)
|
|
156
|
+
raise IOError, "invalid WebSocket write progress" if count <= 0 || count > remaining.bytesize
|
|
157
|
+
offset += count
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def protocol_error(code)
|
|
162
|
+
close(code, false)
|
|
163
|
+
raise IOError, "invalid WebSocket frame"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def close(code = 1000, wait = true)
|
|
167
|
+
return if @closed
|
|
168
|
+
@closed = true
|
|
169
|
+
write_frame(8, (code >> 8).chr.b + (code & 255).chr.b)
|
|
170
|
+
deadline = Transport.now + @timeout
|
|
171
|
+
while wait && !@received_close && Transport.now < deadline
|
|
172
|
+
read_message
|
|
173
|
+
end
|
|
174
|
+
rescue IOError, SystemCallError, RequestError
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
data/lib/spinner.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "spinner/version"
|
|
4
|
+
require "socket"
|
|
5
|
+
require_relative "spinner/transport_socket"
|
|
6
|
+
require_relative "spinner/input"
|
|
7
|
+
require_relative "spinner/body_storage"
|
|
8
|
+
require_relative "spinner/task_pool"
|
|
9
|
+
require_relative "spinner/proxy_metadata"
|
|
10
|
+
require_relative "spinner/request_parser"
|
|
11
|
+
require_relative "spinner/chunk_decoder"
|
|
12
|
+
require_relative "spinner/host_authority"
|
|
13
|
+
require_relative "spinner/upgrade"
|
|
14
|
+
require_relative "spinner/response_writer"
|
|
15
|
+
require_relative "spinner/reactor"
|
|
16
|
+
require_relative "spinner/server"
|
|
17
|
+
require_relative "spinner/thread_pool"
|
|
18
|
+
|
|
19
|
+
module Spinner
|
|
20
|
+
class Error < StandardError; end
|
|
21
|
+
end
|
data/sig/spinner.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: spinel-spinner
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0.pre.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Dillon Wyatt
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Spinner compiles with a Spinel application to serve HTTP/1.1 traffic.
|
|
13
|
+
email:
|
|
14
|
+
- dostaglou@gmail.com
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- CHANGELOG.md
|
|
20
|
+
- LICENSE.txt
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/spinner.rb
|
|
23
|
+
- lib/spinner/body_storage.rb
|
|
24
|
+
- lib/spinner/chunk_decoder.rb
|
|
25
|
+
- lib/spinner/host_authority.rb
|
|
26
|
+
- lib/spinner/input.rb
|
|
27
|
+
- lib/spinner/proxy_metadata.rb
|
|
28
|
+
- lib/spinner/reactor.rb
|
|
29
|
+
- lib/spinner/request_parser.rb
|
|
30
|
+
- lib/spinner/response_writer.rb
|
|
31
|
+
- lib/spinner/server.rb
|
|
32
|
+
- lib/spinner/task_pool.rb
|
|
33
|
+
- lib/spinner/thread_pool.rb
|
|
34
|
+
- lib/spinner/tls.rb
|
|
35
|
+
- lib/spinner/transport_socket.rb
|
|
36
|
+
- lib/spinner/upgrade.rb
|
|
37
|
+
- lib/spinner/version.rb
|
|
38
|
+
- lib/spinner/websocket.rb
|
|
39
|
+
- sig/spinner.rbs
|
|
40
|
+
homepage: https://github.com/dostaglou/spinner
|
|
41
|
+
licenses:
|
|
42
|
+
- MIT
|
|
43
|
+
metadata:
|
|
44
|
+
source_code_uri: https://github.com/dostaglou/spinner
|
|
45
|
+
changelog_uri: https://github.com/dostaglou/spinner/blob/main/CHANGELOG.md
|
|
46
|
+
rdoc_options: []
|
|
47
|
+
require_paths:
|
|
48
|
+
- lib
|
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 3.0.0
|
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
requirements: []
|
|
60
|
+
rubygems_version: 3.6.9
|
|
61
|
+
specification_version: 4
|
|
62
|
+
summary: A Spinel-compatible HTTP application server
|
|
63
|
+
test_files: []
|