raptor 0.3.0 → 0.4.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/CHANGELOG.md +13 -0
- data/README.md +13 -13
- data/ext/raptor_http2/raptor_http2.c +1 -0
- data/lib/rackup/handler/raptor.rb +3 -2
- data/lib/raptor/cli.rb +20 -4
- data/lib/raptor/cluster.rb +16 -10
- data/lib/raptor/http2.rb +321 -40
- data/lib/raptor/reactor.rb +67 -27
- data/lib/raptor/request.rb +100 -49
- data/lib/raptor/server.rb +112 -36
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/cli.rbs +14 -0
- data/sig/generated/raptor/cluster.rbs +2 -2
- data/sig/generated/raptor/http2.rbs +125 -6
- data/sig/generated/raptor/reactor.rbs +22 -0
- data/sig/generated/raptor/request.rbs +34 -13
- data/sig/generated/raptor/server.rbs +51 -12
- metadata +1 -1
data/lib/raptor/request.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "socket"
|
|
|
5
5
|
require "stringio"
|
|
6
6
|
require "tempfile"
|
|
7
7
|
|
|
8
|
+
require "atomic-ruby/atomic_boolean"
|
|
8
9
|
require "rack"
|
|
9
10
|
|
|
10
11
|
require_relative "raptor_http"
|
|
@@ -39,6 +40,7 @@ module Raptor
|
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
STATUS_WITH_NO_ENTITY_BODY = Set.new([204, 304, *100..199]).freeze
|
|
43
|
+
BAD_REQUEST_RESPONSE = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
42
44
|
INTERNAL_SERVER_ERROR_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
43
45
|
CONTENT_TOO_LARGE_RESPONSE = "HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
44
46
|
|
|
@@ -92,11 +94,39 @@ module Raptor
|
|
|
92
94
|
[decoded, :incomplete]
|
|
93
95
|
end
|
|
94
96
|
|
|
97
|
+
# Writes a string to the socket, retrying on partial writes and flow control blocks.
|
|
98
|
+
#
|
|
99
|
+
# Uses write_nonblock with `WRITE_TIMEOUT` to avoid blocking the thread
|
|
100
|
+
# indefinitely on slow clients.
|
|
101
|
+
#
|
|
102
|
+
# @param socket [TCPSocket] the socket to write to
|
|
103
|
+
# @param string [String] the data to write
|
|
104
|
+
# @return [void]
|
|
105
|
+
# @raise [WriteError] if the socket is not writable within the timeout or raises IOError
|
|
106
|
+
#
|
|
107
|
+
# @rbs (TCPSocket socket, String string) -> void
|
|
108
|
+
def self.socket_write(socket, string)
|
|
109
|
+
bytes = 0
|
|
110
|
+
byte_size = string.bytesize
|
|
111
|
+
|
|
112
|
+
while bytes < byte_size
|
|
113
|
+
begin
|
|
114
|
+
bytes += socket.write_nonblock(bytes.zero? ? string : string.byteslice(bytes..-1))
|
|
115
|
+
rescue IO::WaitWritable
|
|
116
|
+
raise WriteError unless socket.wait_writable(WRITE_TIMEOUT)
|
|
117
|
+
retry
|
|
118
|
+
rescue IOError
|
|
119
|
+
raise WriteError
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
95
124
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
96
125
|
# @rbs @server_port: Integer
|
|
97
126
|
# @rbs @max_body_size: Integer?
|
|
98
127
|
# @rbs @body_spool_threshold: Integer?
|
|
99
128
|
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
129
|
+
# @rbs @running: AtomicBoolean
|
|
100
130
|
|
|
101
131
|
# Creates a new Request handler.
|
|
102
132
|
#
|
|
@@ -115,6 +145,17 @@ module Raptor
|
|
|
115
145
|
@max_body_size = client_options[:max_body_size]
|
|
116
146
|
@body_spool_threshold = client_options[:body_spool_threshold]
|
|
117
147
|
@on_error = on_error
|
|
148
|
+
@running = AtomicBoolean.new(true)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Signals eager keep-alive loops to stop processing further requests on
|
|
152
|
+
# their connections. In-flight requests complete normally.
|
|
153
|
+
#
|
|
154
|
+
# @return [void]
|
|
155
|
+
#
|
|
156
|
+
# @rbs () -> void
|
|
157
|
+
def shutdown
|
|
158
|
+
@running.make_false
|
|
118
159
|
end
|
|
119
160
|
|
|
120
161
|
# Eagerly reads and parses the first request on a freshly accepted
|
|
@@ -155,7 +196,12 @@ module Raptor
|
|
|
155
196
|
|
|
156
197
|
parser = HttpParser.new
|
|
157
198
|
env = {}
|
|
158
|
-
nread =
|
|
199
|
+
nread = begin
|
|
200
|
+
parser.execute(env, buffer, 0)
|
|
201
|
+
rescue HttpParserError
|
|
202
|
+
reject_malformed(socket)
|
|
203
|
+
return
|
|
204
|
+
end
|
|
159
205
|
parse_data = { parse_count: 1, content_length: parser.content_length }
|
|
160
206
|
|
|
161
207
|
body = nil
|
|
@@ -210,7 +256,11 @@ module Raptor
|
|
|
210
256
|
|
|
211
257
|
parser = Raptor::HttpParser.new
|
|
212
258
|
env = {}
|
|
213
|
-
nread =
|
|
259
|
+
nread = begin
|
|
260
|
+
parser.execute(env, data[:buffer], 0)
|
|
261
|
+
rescue Raptor::HttpParserError
|
|
262
|
+
next Ractor.make_shareable(data.merge(complete: true, malformed: true))
|
|
263
|
+
end
|
|
214
264
|
parse_data = if data[:parse_data]
|
|
215
265
|
data[:parse_data].dup
|
|
216
266
|
else
|
|
@@ -269,6 +319,12 @@ module Raptor
|
|
|
269
319
|
return
|
|
270
320
|
end
|
|
271
321
|
|
|
322
|
+
if parsed_request[:malformed]
|
|
323
|
+
socket = reactor.remove(parsed_request[:id])
|
|
324
|
+
reject_malformed(socket) if socket
|
|
325
|
+
return
|
|
326
|
+
end
|
|
327
|
+
|
|
272
328
|
unless parsed_request[:complete]
|
|
273
329
|
reactor.update_state(parsed_request)
|
|
274
330
|
else
|
|
@@ -394,6 +450,11 @@ module Raptor
|
|
|
394
450
|
# @rbs (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, Integer request_count, String remote_addr, String url_scheme) -> void
|
|
395
451
|
def eager_keepalive(socket, id, reactor, thread_pool, request_count, remote_addr, url_scheme)
|
|
396
452
|
loop do
|
|
453
|
+
unless @running.true?
|
|
454
|
+
socket.close rescue nil
|
|
455
|
+
return
|
|
456
|
+
end
|
|
457
|
+
|
|
397
458
|
unless socket.wait_readable(KEEPALIVE_READ_TIMEOUT)
|
|
398
459
|
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
399
460
|
return
|
|
@@ -418,7 +479,12 @@ module Raptor
|
|
|
418
479
|
|
|
419
480
|
parser = HttpParser.new
|
|
420
481
|
env = {}
|
|
421
|
-
nread =
|
|
482
|
+
nread = begin
|
|
483
|
+
parser.execute(env, buffer, 0)
|
|
484
|
+
rescue HttpParserError
|
|
485
|
+
reject_malformed(socket)
|
|
486
|
+
return
|
|
487
|
+
end
|
|
422
488
|
parse_data = { parse_count: 1, content_length: parser.content_length }
|
|
423
489
|
|
|
424
490
|
body = nil
|
|
@@ -515,6 +581,18 @@ module Raptor
|
|
|
515
581
|
socket.close rescue nil
|
|
516
582
|
end
|
|
517
583
|
|
|
584
|
+
# Writes a 400 response and closes the socket. Used when the HTTP parser
|
|
585
|
+
# rejects the request line or headers.
|
|
586
|
+
#
|
|
587
|
+
# @param socket [TCPSocket] the client socket
|
|
588
|
+
# @return [void]
|
|
589
|
+
#
|
|
590
|
+
# @rbs (TCPSocket socket) -> void
|
|
591
|
+
def reject_malformed(socket)
|
|
592
|
+
socket.write(BAD_REQUEST_RESPONSE) rescue nil
|
|
593
|
+
socket.close rescue nil
|
|
594
|
+
end
|
|
595
|
+
|
|
518
596
|
# Builds a Rack environment hash from parsed HTTP request data.
|
|
519
597
|
#
|
|
520
598
|
# Populates all required Rack env keys including rack.* keys, REMOTE_ADDR,
|
|
@@ -644,7 +722,7 @@ module Raptor
|
|
|
644
722
|
end
|
|
645
723
|
response << "\r\n"
|
|
646
724
|
|
|
647
|
-
socket_write(socket, response)
|
|
725
|
+
Request.socket_write(socket, response)
|
|
648
726
|
end
|
|
649
727
|
|
|
650
728
|
# Writes a complete HTTP response to the socket.
|
|
@@ -777,7 +855,7 @@ module Raptor
|
|
|
777
855
|
def write_hijacked_response(socket, response, headers, response_hijack)
|
|
778
856
|
response << format_headers(headers)
|
|
779
857
|
response << "\r\n"
|
|
780
|
-
socket_write(socket, response)
|
|
858
|
+
Request.socket_write(socket, response)
|
|
781
859
|
uncork_socket(socket)
|
|
782
860
|
response_hijack.call(socket)
|
|
783
861
|
end
|
|
@@ -802,7 +880,7 @@ module Raptor
|
|
|
802
880
|
|
|
803
881
|
response << format_headers(headers)
|
|
804
882
|
response << "\r\n"
|
|
805
|
-
socket_write(socket, response)
|
|
883
|
+
Request.socket_write(socket, response)
|
|
806
884
|
end
|
|
807
885
|
|
|
808
886
|
# Writes a complete response with a body.
|
|
@@ -824,7 +902,7 @@ module Raptor
|
|
|
824
902
|
if body.respond_to?(:call)
|
|
825
903
|
response << format_headers(headers)
|
|
826
904
|
response << "\r\n"
|
|
827
|
-
socket_write(socket, response)
|
|
905
|
+
Request.socket_write(socket, response)
|
|
828
906
|
uncork_socket(socket)
|
|
829
907
|
body.call(socket)
|
|
830
908
|
return
|
|
@@ -861,7 +939,7 @@ module Raptor
|
|
|
861
939
|
raise TypeError, "body must respond to each, to_ary, or to_path"
|
|
862
940
|
end
|
|
863
941
|
|
|
864
|
-
socket_write(socket, "0\r\n\r\n") if use_chunked
|
|
942
|
+
Request.socket_write(socket, "0\r\n\r\n") if use_chunked
|
|
865
943
|
end
|
|
866
944
|
|
|
867
945
|
# Calculates content length from an array or file body without consuming it.
|
|
@@ -901,15 +979,15 @@ module Raptor
|
|
|
901
979
|
def write_file_body(socket, response, path, content_length, use_chunked)
|
|
902
980
|
File.open(path, "rb") do |file|
|
|
903
981
|
if use_chunked
|
|
904
|
-
socket_write(socket, response)
|
|
982
|
+
Request.socket_write(socket, response)
|
|
905
983
|
while (chunk = file.read(FILE_CHUNK_SIZE))
|
|
906
|
-
socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
984
|
+
Request.socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
907
985
|
end
|
|
908
986
|
elsif content_length && content_length < BODY_BUFFER_THRESHOLD
|
|
909
987
|
response << file.read(content_length)
|
|
910
|
-
socket_write(socket, response)
|
|
988
|
+
Request.socket_write(socket, response)
|
|
911
989
|
else
|
|
912
|
-
socket_write(socket, response)
|
|
990
|
+
Request.socket_write(socket, response)
|
|
913
991
|
IO.copy_stream(file, socket)
|
|
914
992
|
end
|
|
915
993
|
end
|
|
@@ -952,12 +1030,12 @@ module Raptor
|
|
|
952
1030
|
|
|
953
1031
|
if use_chunked
|
|
954
1032
|
response << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
|
955
|
-
socket_write(socket, response)
|
|
1033
|
+
Request.socket_write(socket, response)
|
|
956
1034
|
elsif chunk.bytesize < BODY_BUFFER_THRESHOLD
|
|
957
|
-
socket_write(socket, response << chunk)
|
|
1035
|
+
Request.socket_write(socket, response << chunk)
|
|
958
1036
|
else
|
|
959
|
-
socket_write(socket, response)
|
|
960
|
-
socket_write(socket, chunk)
|
|
1037
|
+
Request.socket_write(socket, response)
|
|
1038
|
+
Request.socket_write(socket, chunk)
|
|
961
1039
|
end
|
|
962
1040
|
end
|
|
963
1041
|
|
|
@@ -973,13 +1051,13 @@ module Raptor
|
|
|
973
1051
|
# @rbs (TCPSocket socket, String response, Array[String] body_array, bool use_chunked) -> void
|
|
974
1052
|
def write_multiple_chunks(socket, response, body_array, use_chunked)
|
|
975
1053
|
if use_chunked
|
|
976
|
-
socket_write(socket, response)
|
|
1054
|
+
Request.socket_write(socket, response)
|
|
977
1055
|
body_array.each do |chunk|
|
|
978
1056
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
979
1057
|
|
|
980
1058
|
next if chunk.empty?
|
|
981
1059
|
|
|
982
|
-
socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
1060
|
+
Request.socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
983
1061
|
end
|
|
984
1062
|
else
|
|
985
1063
|
body_array.each do |chunk|
|
|
@@ -987,7 +1065,7 @@ module Raptor
|
|
|
987
1065
|
|
|
988
1066
|
response << chunk
|
|
989
1067
|
end
|
|
990
|
-
socket_write(socket, response)
|
|
1068
|
+
Request.socket_write(socket, response)
|
|
991
1069
|
end
|
|
992
1070
|
end
|
|
993
1071
|
|
|
@@ -1003,13 +1081,13 @@ module Raptor
|
|
|
1003
1081
|
# @rbs (TCPSocket socket, String response, untyped body, bool use_chunked) -> void
|
|
1004
1082
|
def write_enumerable_body(socket, response, body, use_chunked)
|
|
1005
1083
|
if use_chunked
|
|
1006
|
-
socket_write(socket, response)
|
|
1084
|
+
Request.socket_write(socket, response)
|
|
1007
1085
|
body.each do |chunk|
|
|
1008
1086
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
1009
1087
|
|
|
1010
1088
|
next if chunk.empty?
|
|
1011
1089
|
|
|
1012
|
-
socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
1090
|
+
Request.socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
1013
1091
|
end
|
|
1014
1092
|
else
|
|
1015
1093
|
body.each do |chunk|
|
|
@@ -1017,7 +1095,7 @@ module Raptor
|
|
|
1017
1095
|
|
|
1018
1096
|
response << chunk
|
|
1019
1097
|
end
|
|
1020
|
-
socket_write(socket, response)
|
|
1098
|
+
Request.socket_write(socket, response)
|
|
1021
1099
|
end
|
|
1022
1100
|
end
|
|
1023
1101
|
|
|
@@ -1090,33 +1168,6 @@ module Raptor
|
|
|
1090
1168
|
end
|
|
1091
1169
|
end
|
|
1092
1170
|
|
|
1093
|
-
# Writes a string to the socket, retrying on partial writes and flow control blocks.
|
|
1094
|
-
#
|
|
1095
|
-
# Uses write_nonblock with a 5-second writable timeout to avoid blocking the
|
|
1096
|
-
# thread indefinitely on slow clients.
|
|
1097
|
-
#
|
|
1098
|
-
# @param socket [TCPSocket] the socket to write to
|
|
1099
|
-
# @param string [String] the data to write
|
|
1100
|
-
# @return [void]
|
|
1101
|
-
# @raise [WriteError] if the socket is not writable within the timeout or raises IOError
|
|
1102
|
-
#
|
|
1103
|
-
# @rbs (TCPSocket socket, String string) -> void
|
|
1104
|
-
def socket_write(socket, string)
|
|
1105
|
-
bytes = 0
|
|
1106
|
-
byte_size = string.bytesize
|
|
1107
|
-
|
|
1108
|
-
while bytes < byte_size
|
|
1109
|
-
begin
|
|
1110
|
-
bytes += socket.write_nonblock(bytes.zero? ? string : string.byteslice(bytes..-1))
|
|
1111
|
-
rescue IO::WaitWritable
|
|
1112
|
-
raise WriteError unless socket.wait_writable(WRITE_TIMEOUT)
|
|
1113
|
-
retry
|
|
1114
|
-
rescue IOError
|
|
1115
|
-
raise WriteError
|
|
1116
|
-
end
|
|
1117
|
-
end
|
|
1118
|
-
end
|
|
1119
|
-
|
|
1120
1171
|
if Socket.const_defined?(:TCP_CORK)
|
|
1121
1172
|
# Enables TCP_CORK on the socket to batch outgoing packets into fewer segments.
|
|
1122
1173
|
#
|
data/lib/raptor/server.rb
CHANGED
|
@@ -14,8 +14,8 @@ module Raptor
|
|
|
14
14
|
# providing natural backpressure based on system capacity.
|
|
15
15
|
#
|
|
16
16
|
# Supports TCP, Unix domain, and SSL listeners transparently. TCP_NODELAY is
|
|
17
|
-
# applied only to TCP sockets, and SSL handshakes are
|
|
18
|
-
#
|
|
17
|
+
# applied only to TCP sockets, and SSL handshakes are offloaded to the thread
|
|
18
|
+
# pool so a slow client cannot block the server thread.
|
|
19
19
|
#
|
|
20
20
|
# For HTTP/1.1 connections the first request is parsed inline on the server
|
|
21
21
|
# thread and dispatched directly to the thread pool, falling back to the
|
|
@@ -27,7 +27,7 @@ module Raptor
|
|
|
27
27
|
# binder = Binder.new(["tcp://0.0.0.0:3000"])
|
|
28
28
|
# reactor = Reactor.new(thread_pool, ractor_pool, client_options: {})
|
|
29
29
|
# request = Request.new(app, 3000)
|
|
30
|
-
# server = Server.new(binder, reactor, thread_pool, request)
|
|
30
|
+
# server = Server.new(binder, reactor, thread_pool, request, client_options: { first_data_timeout: 30 })
|
|
31
31
|
# server.run
|
|
32
32
|
# # ... later
|
|
33
33
|
# server.shutdown
|
|
@@ -41,6 +41,7 @@ module Raptor
|
|
|
41
41
|
# @rbs @reactor: Reactor
|
|
42
42
|
# @rbs @thread_pool: AtomicThreadPool
|
|
43
43
|
# @rbs @request: Request
|
|
44
|
+
# @rbs @client_options: Hash[Symbol, untyped]
|
|
44
45
|
# @rbs @running: AtomicBoolean
|
|
45
46
|
|
|
46
47
|
# Creates a new Server instance.
|
|
@@ -49,14 +50,16 @@ module Raptor
|
|
|
49
50
|
# @param reactor [Reactor] the reactor for handling client connections
|
|
50
51
|
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
51
52
|
# @param request [Request] the HTTP/1.1 request handler
|
|
53
|
+
# @param client_options [Hash] client timeout configuration, used to bound TLS handshakes
|
|
52
54
|
# @return [void]
|
|
53
55
|
#
|
|
54
|
-
# @rbs (Binder binder, Reactor reactor, AtomicThreadPool thread_pool, Request request) -> void
|
|
55
|
-
def initialize(binder, reactor, thread_pool, request)
|
|
56
|
+
# @rbs (Binder binder, Reactor reactor, AtomicThreadPool thread_pool, Request request, client_options: Hash[Symbol, untyped]) -> void
|
|
57
|
+
def initialize(binder, reactor, thread_pool, request, client_options:)
|
|
56
58
|
@binder = binder
|
|
57
59
|
@reactor = reactor
|
|
58
60
|
@thread_pool = thread_pool
|
|
59
61
|
@request = request
|
|
62
|
+
@client_options = client_options
|
|
60
63
|
@running = AtomicBoolean.new(true)
|
|
61
64
|
end
|
|
62
65
|
|
|
@@ -108,9 +111,11 @@ module Raptor
|
|
|
108
111
|
|
|
109
112
|
# Accepts a connection from the given listener and dispatches it.
|
|
110
113
|
#
|
|
111
|
-
# For SSL
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
+
# For SSL listeners the TLS handshake is offloaded to the thread pool so
|
|
115
|
+
# a slow client cannot block the server thread. For SSL connections with
|
|
116
|
+
# h2 negotiated via ALPN, the server sends initial SETTINGS and adds the
|
|
117
|
+
# connection to the reactor as an HTTP/2 connection. All other connections
|
|
118
|
+
# follow the HTTP/1.1 path.
|
|
114
119
|
#
|
|
115
120
|
# @param listener [TCPServer, UNIXServer, Binder::SslListener] the ready listener
|
|
116
121
|
# @param reactor [Reactor] the reactor to dispatch connections to
|
|
@@ -131,46 +136,117 @@ module Raptor
|
|
|
131
136
|
remote_addr = "127.0.0.1"
|
|
132
137
|
end
|
|
133
138
|
|
|
134
|
-
url_scheme = HTTP_SCHEME
|
|
135
|
-
client = tcp_client
|
|
136
|
-
|
|
137
139
|
if listener.is_a?(Binder::SslListener)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_client, listener.ssl_context)
|
|
141
|
-
ssl_socket.sync_close = true
|
|
142
|
-
ssl_socket.accept
|
|
143
|
-
client = ssl_socket
|
|
144
|
-
rescue OpenSSL::SSL::SSLError => error
|
|
145
|
-
warn "SSL handshake failed: #{error.message}"
|
|
146
|
-
tcp_client.close rescue nil
|
|
147
|
-
return
|
|
140
|
+
@thread_pool << proc do
|
|
141
|
+
dispatch_ssl_connection(listener, tcp_client, remote_addr, reactor)
|
|
148
142
|
end
|
|
143
|
+
return
|
|
144
|
+
end
|
|
149
145
|
|
|
150
|
-
|
|
151
|
-
|
|
146
|
+
@request.eager_accept(
|
|
147
|
+
tcp_client,
|
|
148
|
+
tcp_client.object_id,
|
|
149
|
+
reactor,
|
|
150
|
+
@thread_pool,
|
|
151
|
+
remote_addr,
|
|
152
|
+
HTTP_SCHEME
|
|
153
|
+
)
|
|
154
|
+
end
|
|
152
155
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
# Performs the TLS handshake for an accepted SSL connection and dispatches
|
|
157
|
+
# it through the HTTP/2 or HTTP/1.1 path. The handshake is bounded by
|
|
158
|
+
# `:first_data_timeout` so a slow client cannot pin a worker thread.
|
|
159
|
+
#
|
|
160
|
+
# @param listener [Binder::SslListener] the SSL listener that accepted the connection
|
|
161
|
+
# @param tcp_client [TCPSocket] the accepted TCP socket
|
|
162
|
+
# @param remote_addr [String] the client's IP address
|
|
163
|
+
# @param reactor [Reactor] the reactor to dispatch the connection to
|
|
164
|
+
# @return [void]
|
|
165
|
+
#
|
|
166
|
+
# @rbs (Binder::SslListener listener, TCPSocket tcp_client, String remote_addr, Reactor reactor) -> void
|
|
167
|
+
def dispatch_ssl_connection(listener, tcp_client, remote_addr, reactor)
|
|
168
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_client, listener.ssl_context)
|
|
169
|
+
ssl_socket.sync_close = true
|
|
170
|
+
return unless perform_ssl_handshake(ssl_socket)
|
|
171
|
+
|
|
172
|
+
if ssl_socket.alpn_protocol == H2_PROTOCOL
|
|
173
|
+
ssl_socket.write(Http2.build_server_settings_frame) rescue nil
|
|
174
|
+
|
|
175
|
+
reactor.add(
|
|
176
|
+
id: ssl_socket.object_id,
|
|
177
|
+
socket: ssl_socket,
|
|
178
|
+
remote_addr: remote_addr,
|
|
179
|
+
url_scheme: HTTPS_SCHEME,
|
|
180
|
+
protocol: :http2,
|
|
181
|
+
writer: Http2::Writer.new,
|
|
182
|
+
flow_control: Http2::FlowControl.new
|
|
183
|
+
)
|
|
161
184
|
|
|
162
|
-
|
|
163
|
-
end
|
|
185
|
+
return
|
|
164
186
|
end
|
|
165
187
|
|
|
166
188
|
@request.eager_accept(
|
|
167
|
-
|
|
168
|
-
|
|
189
|
+
ssl_socket,
|
|
190
|
+
ssl_socket.object_id,
|
|
169
191
|
reactor,
|
|
170
192
|
@thread_pool,
|
|
171
193
|
remote_addr,
|
|
172
|
-
|
|
194
|
+
HTTPS_SCHEME
|
|
173
195
|
)
|
|
174
196
|
end
|
|
197
|
+
|
|
198
|
+
# Drives a non-blocking SSL handshake to completion, bounded by the
|
|
199
|
+
# configured first-data timeout. Returns true on success, false on
|
|
200
|
+
# timeout or SSL error.
|
|
201
|
+
#
|
|
202
|
+
# @param ssl_socket [OpenSSL::SSL::SSLSocket] the SSL socket to hand-shake
|
|
203
|
+
# @return [Boolean] true if the handshake completed
|
|
204
|
+
#
|
|
205
|
+
# @rbs (OpenSSL::SSL::SSLSocket ssl_socket) -> bool
|
|
206
|
+
def perform_ssl_handshake(ssl_socket)
|
|
207
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @client_options[:first_data_timeout]
|
|
208
|
+
|
|
209
|
+
begin
|
|
210
|
+
ssl_socket.accept_nonblock
|
|
211
|
+
true
|
|
212
|
+
rescue IO::WaitReadable
|
|
213
|
+
return false unless wait_for_handshake(ssl_socket, deadline, :read)
|
|
214
|
+
|
|
215
|
+
retry
|
|
216
|
+
rescue IO::WaitWritable
|
|
217
|
+
return false unless wait_for_handshake(ssl_socket, deadline, :write)
|
|
218
|
+
|
|
219
|
+
retry
|
|
220
|
+
rescue OpenSSL::SSL::SSLError => error
|
|
221
|
+
warn "SSL handshake failed: #{error.message}"
|
|
222
|
+
ssl_socket.close rescue nil
|
|
223
|
+
false
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Waits up to `deadline` for the socket to become ready for the next step
|
|
228
|
+
# of the SSL handshake. Closes the socket and returns false on timeout.
|
|
229
|
+
#
|
|
230
|
+
# @param ssl_socket [OpenSSL::SSL::SSLSocket] the SSL socket
|
|
231
|
+
# @param deadline [Float] absolute monotonic deadline
|
|
232
|
+
# @param direction [Symbol] either `:read` or `:write`
|
|
233
|
+
# @return [Boolean] true if the socket became ready before the deadline
|
|
234
|
+
#
|
|
235
|
+
# @rbs (OpenSSL::SSL::SSLSocket ssl_socket, Float deadline, Symbol direction) -> bool
|
|
236
|
+
def wait_for_handshake(ssl_socket, deadline, direction)
|
|
237
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
238
|
+
ready = if remaining <= 0
|
|
239
|
+
false
|
|
240
|
+
elsif direction == :read
|
|
241
|
+
ssl_socket.wait_readable(remaining)
|
|
242
|
+
else
|
|
243
|
+
ssl_socket.wait_writable(remaining)
|
|
244
|
+
end
|
|
245
|
+
return true if ready
|
|
246
|
+
|
|
247
|
+
warn "SSL handshake timed out"
|
|
248
|
+
ssl_socket.close rescue nil
|
|
249
|
+
false
|
|
250
|
+
end
|
|
175
251
|
end
|
|
176
252
|
end
|
data/lib/raptor/version.rb
CHANGED
|
@@ -20,6 +20,8 @@ module Raptor
|
|
|
20
20
|
|
|
21
21
|
DEFAULT_OPTIONS: untyped
|
|
22
22
|
|
|
23
|
+
DEFAULT_CONFIG_PATHS: untyped
|
|
24
|
+
|
|
23
25
|
# Loads a configuration file and returns the hash it evaluates to.
|
|
24
26
|
#
|
|
25
27
|
# The file is evaluated at the top level so constants like `Raptor::*` resolve
|
|
@@ -33,6 +35,18 @@ module Raptor
|
|
|
33
35
|
# @rbs (String path) -> Hash[Symbol, untyped]
|
|
34
36
|
def self.load_config_file: (String path) -> Hash[Symbol, untyped]
|
|
35
37
|
|
|
38
|
+
# Returns the first existing path in {DEFAULT_CONFIG_PATHS} resolved
|
|
39
|
+
# against `root`, or nil if none exist.
|
|
40
|
+
#
|
|
41
|
+
# Used to pick up a project-local config file when no `-c`/`--config`
|
|
42
|
+
# flag was supplied.
|
|
43
|
+
#
|
|
44
|
+
# @param root [String] directory to resolve the default paths against
|
|
45
|
+
# @return [String, nil] the config path, or nil if no default file exists
|
|
46
|
+
#
|
|
47
|
+
# @rbs (?String root) -> String?
|
|
48
|
+
def self.default_config_path: (?String root) -> String?
|
|
49
|
+
|
|
36
50
|
@command: Symbol
|
|
37
51
|
|
|
38
52
|
@options: Hash[Symbol, untyped]
|
|
@@ -53,7 +53,7 @@ module Raptor
|
|
|
53
53
|
|
|
54
54
|
@binder: Binder
|
|
55
55
|
|
|
56
|
-
@
|
|
56
|
+
@pid_file: String?
|
|
57
57
|
|
|
58
58
|
@stats_file: String?
|
|
59
59
|
|
|
@@ -83,7 +83,7 @@ module Raptor
|
|
|
83
83
|
# @option options [Hash] :client client configuration
|
|
84
84
|
# @option options [#call] :on_error callback invoked with (env, exception) when the Rack app raises
|
|
85
85
|
# @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
|
|
86
|
-
# @option options [String, nil] :
|
|
86
|
+
# @option options [String, nil] :pid_file path to write the master PID to, or nil to disable
|
|
87
87
|
# @return [void]
|
|
88
88
|
#
|
|
89
89
|
# @rbs (Hash[Symbol, untyped] options) -> void
|