polyphony-http 0.26 → 0.27
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/.github/workflows/test.yml +18 -0
- data/CHANGELOG.md +5 -1
- data/Gemfile.lock +6 -7
- data/lib/polyphony/http/server.rb +51 -46
- data/lib/polyphony/http/server/http1.rb +264 -261
- data/lib/polyphony/http/server/http2.rb +64 -61
- data/lib/polyphony/http/server/http2_stream.rb +132 -129
- data/lib/polyphony/http/server/request.rb +116 -112
- data/lib/polyphony/http/version.rb +1 -1
- data/polyphony-http.gemspec +2 -2
- data/test/test_http_server.rb +10 -10
- metadata +19 -19
- data/lib/polyphony/http.rb +0 -16
@@ -1,78 +1,81 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
export_default :Protocol
|
4
|
-
|
5
3
|
require 'http/2'
|
4
|
+
require_relative './http2_stream'
|
6
5
|
|
7
|
-
|
6
|
+
module Polyphony
|
7
|
+
module HTTP
|
8
|
+
module Server
|
9
|
+
# HTTP2 server adapter
|
10
|
+
class HTTP2Adapter
|
11
|
+
def self.upgrade_each(socket, opts, headers, &block)
|
12
|
+
adapter = new(socket, opts, headers)
|
13
|
+
adapter.each(&block)
|
14
|
+
end
|
8
15
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
adapter.each(&block)
|
14
|
-
end
|
16
|
+
def initialize(conn, opts, upgrade_headers = nil)
|
17
|
+
@conn = conn
|
18
|
+
@opts = opts
|
19
|
+
@upgrade_headers = upgrade_headers
|
15
20
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
21
|
+
@interface = ::HTTP2::Server.new
|
22
|
+
@connection_fiber = Fiber.current
|
23
|
+
@interface.on(:frame, &method(:send_frame))
|
24
|
+
@streams = {}
|
25
|
+
end
|
20
26
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
27
|
+
def send_frame(data)
|
28
|
+
@conn << data
|
29
|
+
rescue Exception => e
|
30
|
+
@connection_fiber.transfer e
|
31
|
+
end
|
26
32
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
end
|
33
|
+
UPGRADE_MESSAGE = <<~HTTP.gsub("\n", "\r\n")
|
34
|
+
HTTP/1.1 101 Switching Protocols
|
35
|
+
Connection: Upgrade
|
36
|
+
Upgrade: h2c
|
32
37
|
|
33
|
-
|
34
|
-
HTTP/1.1 101 Switching Protocols
|
35
|
-
Connection: Upgrade
|
36
|
-
Upgrade: h2c
|
38
|
+
HTTP
|
37
39
|
|
38
|
-
|
40
|
+
def upgrade
|
41
|
+
@conn << UPGRADE_MESSAGE
|
42
|
+
settings = @upgrade_headers['HTTP2-Settings']
|
43
|
+
Fiber.current.schedule(nil)
|
44
|
+
@interface.upgrade(settings, @upgrade_headers, '')
|
45
|
+
ensure
|
46
|
+
@upgrade_headers = nil
|
47
|
+
end
|
39
48
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
@interface.upgrade(settings, @upgrade_headers, '')
|
45
|
-
ensure
|
46
|
-
@upgrade_headers = nil
|
47
|
-
end
|
49
|
+
# Iterates over incoming requests
|
50
|
+
def each(&block)
|
51
|
+
@interface.on(:stream) { |stream| start_stream(stream, &block) }
|
52
|
+
upgrade if @upgrade_headers
|
48
53
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
54
|
+
while (data = @conn.readpartial(8192))
|
55
|
+
@interface << data
|
56
|
+
snooze
|
57
|
+
end
|
58
|
+
rescue SystemCallError, IOError
|
59
|
+
# ignore
|
60
|
+
ensure
|
61
|
+
finalize_client_loop
|
62
|
+
end
|
53
63
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
rescue SystemCallError, IOError
|
59
|
-
# ignore
|
60
|
-
ensure
|
61
|
-
finalize_client_loop
|
62
|
-
end
|
64
|
+
def start_stream(stream, &block)
|
65
|
+
stream = Stream.new(stream, &block)
|
66
|
+
@streams[stream] = true
|
67
|
+
end
|
63
68
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
69
|
+
def finalize_client_loop
|
70
|
+
@interface = nil
|
71
|
+
@streams.each_key(&:stop)
|
72
|
+
@conn.close
|
73
|
+
end
|
68
74
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
def close
|
76
|
-
@conn.close
|
75
|
+
def close
|
76
|
+
@conn.close
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
77
80
|
end
|
78
81
|
end
|
@@ -1,135 +1,138 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
export_default :StreamHandler
|
4
|
-
|
5
3
|
require 'http/2'
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
4
|
+
require_relative './request'
|
5
|
+
|
6
|
+
module Polyphony
|
7
|
+
module HTTP
|
8
|
+
module Server
|
9
|
+
# Manages an HTTP 2 stream
|
10
|
+
class HTTP2StreamHandler
|
11
|
+
attr_accessor :__next__
|
12
|
+
|
13
|
+
def initialize(stream, &block)
|
14
|
+
@stream = stream
|
15
|
+
@calling_fiber = Fiber.current
|
16
|
+
@stream_fiber = Fiber.new { |req| handle_request(req, &block) }
|
17
|
+
|
18
|
+
# Stream callbacks occur on the connection fiber (see HTTP2::Protocol#each).
|
19
|
+
# The request handler is run on a separate fiber for each stream, allowing
|
20
|
+
# concurrent handling of incoming requests on the same HTTP/2 connection.
|
21
|
+
#
|
22
|
+
# The different stream adapter APIs suspend the stream fiber, waiting for
|
23
|
+
# stream callbacks to be called. The callbacks, in turn, transfer control to
|
24
|
+
# the stream fiber, effectively causing the return of the adapter API calls.
|
25
|
+
#
|
26
|
+
# Note: the request handler is run once headers are received. Reading the
|
27
|
+
# request body, if present, is at the discretion of the request handler.
|
28
|
+
# This mirrors the behaviour of the HTTP/1 adapter.
|
29
|
+
stream.on(:headers, &method(:on_headers))
|
30
|
+
stream.on(:data, &method(:on_data))
|
31
|
+
stream.on(:half_close, &method(:on_half_close))
|
32
|
+
end
|
33
|
+
|
34
|
+
def handle_request(request, &block)
|
35
|
+
error = nil
|
36
|
+
block.(request)
|
37
|
+
@calling_fiber.transfer
|
38
|
+
rescue Polyphony::MoveOn
|
39
|
+
# ignore
|
40
|
+
rescue Exception => e
|
41
|
+
error = e
|
42
|
+
ensure
|
43
|
+
@done = true
|
44
|
+
@calling_fiber.transfer error
|
45
|
+
end
|
46
|
+
|
47
|
+
def on_headers(headers)
|
48
|
+
@request = Request.new(headers.to_h, self)
|
49
|
+
@stream_fiber.transfer(@request)
|
50
|
+
end
|
51
|
+
|
52
|
+
def on_data(data)
|
53
|
+
if @waiting_for_body_chunk
|
54
|
+
@waiting_for_body_chunk = nil
|
55
|
+
@stream_fiber.transfer(data)
|
56
|
+
else
|
57
|
+
@request.buffer_body_chunk(data)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def on_half_close
|
62
|
+
if @waiting_for_body_chunk
|
63
|
+
@waiting_for_body_chunk = nil
|
64
|
+
@stream_fiber.transfer(nil)
|
65
|
+
elsif @waiting_for_half_close
|
66
|
+
@waiting_for_half_close = nil
|
67
|
+
@stream_fiber.transfer(nil)
|
68
|
+
else
|
69
|
+
@request.complete!
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def protocol
|
74
|
+
'h2'
|
75
|
+
end
|
76
|
+
|
77
|
+
def get_body_chunk
|
78
|
+
# called in the context of the stream fiber
|
79
|
+
return nil if @request.complete?
|
80
|
+
|
81
|
+
@waiting_for_body_chunk = true
|
82
|
+
# the chunk (or an exception) will be returned once the stream fiber is
|
83
|
+
# resumed
|
84
|
+
suspend
|
85
|
+
ensure
|
86
|
+
@waiting_for_body_chunk = nil
|
87
|
+
end
|
88
|
+
|
89
|
+
# Wait for request to finish
|
90
|
+
def consume_request
|
91
|
+
return if @request.complete?
|
92
|
+
|
93
|
+
@waiting_for_half_close = true
|
94
|
+
suspend
|
95
|
+
ensure
|
96
|
+
@waiting_for_half_close = nil
|
97
|
+
end
|
98
|
+
|
99
|
+
# response API
|
100
|
+
def respond(chunk, headers)
|
101
|
+
headers[':status'] ||= '200'
|
102
|
+
@stream.headers(headers, end_stream: false)
|
103
|
+
@stream.data(chunk, end_stream: true)
|
104
|
+
@headers_sent = true
|
105
|
+
end
|
106
|
+
|
107
|
+
def send_headers(headers, empty_response = false)
|
108
|
+
return if @headers_sent
|
109
|
+
|
110
|
+
headers[':status'] ||= (empty_response ? 204 : 200).to_s
|
111
|
+
@stream.headers(headers, end_stream: false)
|
112
|
+
@headers_sent = true
|
113
|
+
end
|
114
|
+
|
115
|
+
def send_chunk(chunk, done: false)
|
116
|
+
send_headers({}, false) unless @headers_sent
|
117
|
+
@stream.data(chunk, end_stream: done)
|
118
|
+
end
|
119
|
+
|
120
|
+
def finish
|
121
|
+
if @headers_sent
|
122
|
+
@stream.close
|
123
|
+
else
|
124
|
+
headers[':status'] ||= '204'
|
125
|
+
@stream.headers(headers, end_stream: true)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def stop
|
130
|
+
return if @done
|
131
|
+
|
132
|
+
@stream.close
|
133
|
+
@stream_fiber.schedule(Polyphony::MoveOn.new)
|
134
|
+
end
|
135
|
+
end
|
126
136
|
end
|
127
137
|
end
|
128
|
-
|
129
|
-
def stop
|
130
|
-
return if @done
|
131
|
-
|
132
|
-
@stream.close
|
133
|
-
@stream_fiber.schedule(Polyphony::MoveOn.new)
|
134
|
-
end
|
135
138
|
end
|
@@ -1,118 +1,122 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
export_default :Request
|
4
|
-
|
5
3
|
require 'uri'
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
5
|
+
module Polyphony
|
6
|
+
module HTTP
|
7
|
+
module Server
|
8
|
+
# HTTP request
|
9
|
+
class Request
|
10
|
+
attr_reader :headers, :adapter
|
11
|
+
attr_accessor :__next__
|
12
|
+
|
13
|
+
def initialize(headers, adapter)
|
14
|
+
@headers = headers
|
15
|
+
@adapter = adapter
|
16
|
+
end
|
17
|
+
|
18
|
+
def protocol
|
19
|
+
@protocol = @adapter.protocol
|
20
|
+
end
|
21
|
+
|
22
|
+
def method
|
23
|
+
@method ||= @headers[':method']
|
24
|
+
end
|
25
|
+
|
26
|
+
def scheme
|
27
|
+
@scheme ||= @headers[':scheme']
|
28
|
+
end
|
29
|
+
|
30
|
+
def uri
|
31
|
+
@uri ||= URI.parse(@headers[':path'] || '')
|
32
|
+
end
|
33
|
+
|
34
|
+
def path
|
35
|
+
@path ||= uri.path
|
36
|
+
end
|
37
|
+
|
38
|
+
def query_string
|
39
|
+
@query_string ||= uri.query
|
40
|
+
end
|
41
|
+
|
42
|
+
def query
|
43
|
+
return @query if @query
|
44
|
+
|
45
|
+
@query = (q = uri.query) ? split_query_string(q) : {}
|
46
|
+
end
|
47
|
+
|
48
|
+
def split_query_string(query)
|
49
|
+
query.split('&').each_with_object({}) do |kv, h|
|
50
|
+
k, v = kv.split('=')
|
51
|
+
h[k.to_sym] = URI.decode_www_form_component(v)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def buffer_body_chunk(chunk)
|
56
|
+
@buffered_body_chunks ||= []
|
57
|
+
@buffered_body_chunks << chunk
|
58
|
+
end
|
59
|
+
|
60
|
+
def each_chunk(&block)
|
61
|
+
if @buffered_body_chunks
|
62
|
+
@buffered_body_chunks.each(&block)
|
63
|
+
@buffered_body_chunks = nil
|
64
|
+
end
|
65
|
+
while !@message_complete && (chunk = @adapter.get_body_chunk)
|
66
|
+
yield chunk
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def complete!(keep_alive = nil)
|
71
|
+
@message_complete = true
|
72
|
+
@keep_alive = keep_alive
|
73
|
+
end
|
74
|
+
|
75
|
+
def complete?
|
76
|
+
@message_complete
|
77
|
+
end
|
78
|
+
|
79
|
+
def consume
|
80
|
+
@adapter.consume_request
|
81
|
+
end
|
82
|
+
|
83
|
+
def keep_alive?
|
84
|
+
@keep_alive
|
85
|
+
end
|
86
|
+
|
87
|
+
def read
|
88
|
+
buf = @buffered_body_chunks ? @buffered_body_chunks.join : +''
|
89
|
+
while (chunk = @adapter.get_body_chunk)
|
90
|
+
buf << chunk
|
91
|
+
end
|
92
|
+
buf
|
93
|
+
end
|
94
|
+
|
95
|
+
def respond(body, headers = {})
|
96
|
+
@adapter.respond(body, headers)
|
97
|
+
@headers_sent = true
|
98
|
+
end
|
99
|
+
|
100
|
+
def send_headers(headers = {}, empty_response = false)
|
101
|
+
return if @headers_sent
|
102
|
+
|
103
|
+
@headers_sent = true
|
104
|
+
@adapter.send_headers(headers, empty_response: empty_response)
|
105
|
+
end
|
106
|
+
|
107
|
+
def send_chunk(body, done: false)
|
108
|
+
send_headers({}) unless @headers_sent
|
109
|
+
|
110
|
+
@adapter.send_chunk(body, done: done)
|
111
|
+
end
|
112
|
+
alias_method :<<, :send_chunk
|
113
|
+
|
114
|
+
def finish
|
115
|
+
send_headers({}) unless @headers_sent
|
116
|
+
|
117
|
+
@adapter.finish
|
118
|
+
end
|
119
|
+
end
|
63
120
|
end
|
64
|
-
while !@message_complete && (chunk = @adapter.get_body_chunk)
|
65
|
-
yield chunk
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def complete!(keep_alive = nil)
|
70
|
-
@message_complete = true
|
71
|
-
@keep_alive = keep_alive
|
72
|
-
end
|
73
|
-
|
74
|
-
def complete?
|
75
|
-
@message_complete
|
76
|
-
end
|
77
|
-
|
78
|
-
def consume
|
79
|
-
@adapter.consume_request
|
80
|
-
end
|
81
|
-
|
82
|
-
def keep_alive?
|
83
|
-
@keep_alive
|
84
|
-
end
|
85
|
-
|
86
|
-
def read
|
87
|
-
buf = @buffered_body_chunks ? @buffered_body_chunks.join : +''
|
88
|
-
while (chunk = @adapter.get_body_chunk)
|
89
|
-
buf << chunk
|
90
|
-
end
|
91
|
-
buf
|
92
|
-
end
|
93
|
-
|
94
|
-
def respond(body, headers = {})
|
95
|
-
@adapter.respond(body, headers)
|
96
|
-
@headers_sent = true
|
97
|
-
end
|
98
|
-
|
99
|
-
def send_headers(headers = {}, empty_response = false)
|
100
|
-
return if @headers_sent
|
101
|
-
|
102
|
-
@headers_sent = true
|
103
|
-
@adapter.send_headers(headers, empty_response: empty_response)
|
104
|
-
end
|
105
|
-
|
106
|
-
def send_chunk(body, done: false)
|
107
|
-
send_headers({}) unless @headers_sent
|
108
|
-
|
109
|
-
@adapter.send_chunk(body, done: done)
|
110
|
-
end
|
111
|
-
alias_method :<<, :send_chunk
|
112
|
-
|
113
|
-
def finish
|
114
|
-
send_headers({}) unless @headers_sent
|
115
|
-
|
116
|
-
@adapter.finish
|
117
121
|
end
|
118
|
-
end
|
122
|
+
end
|