polyphony-http 0.24
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/.gitignore +56 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +51 -0
- data/LICENSE +21 -0
- data/README.md +47 -0
- data/Rakefile +20 -0
- data/TODO.md +59 -0
- data/bin/poly +11 -0
- data/docs/README.md +38 -0
- data/docs/summary.md +60 -0
- data/examples/cuba.ru +22 -0
- data/examples/happy_eyeballs.rb +37 -0
- data/examples/http2_raw.rb +135 -0
- data/examples/http_client.rb +28 -0
- data/examples/http_get.rb +33 -0
- data/examples/http_parse_experiment.rb +123 -0
- data/examples/http_proxy.rb +83 -0
- data/examples/http_server.js +24 -0
- data/examples/http_server.rb +21 -0
- data/examples/http_server_forked.rb +29 -0
- data/examples/http_server_graceful.rb +27 -0
- data/examples/http_server_simple.rb +11 -0
- data/examples/http_server_throttled.rb +15 -0
- data/examples/http_server_timeout.rb +35 -0
- data/examples/http_ws_server.rb +37 -0
- data/examples/https_raw_client.rb +12 -0
- data/examples/https_server.rb +22 -0
- data/examples/https_wss_server.rb +39 -0
- data/examples/rack_server.rb +12 -0
- data/examples/rack_server_https.rb +19 -0
- data/examples/rack_server_https_forked.rb +27 -0
- data/examples/websocket_secure_server.rb +27 -0
- data/examples/websocket_server.rb +24 -0
- data/examples/ws_page.html +34 -0
- data/examples/wss_page.html +34 -0
- data/lib/polyphony/http.rb +16 -0
- data/lib/polyphony/http/client/agent.rb +131 -0
- data/lib/polyphony/http/client/http1.rb +129 -0
- data/lib/polyphony/http/client/http2.rb +180 -0
- data/lib/polyphony/http/client/response.rb +32 -0
- data/lib/polyphony/http/client/site_connection_manager.rb +109 -0
- data/lib/polyphony/http/server.rb +49 -0
- data/lib/polyphony/http/server/http1.rb +267 -0
- data/lib/polyphony/http/server/http2.rb +78 -0
- data/lib/polyphony/http/server/http2_stream.rb +135 -0
- data/lib/polyphony/http/server/rack.rb +64 -0
- data/lib/polyphony/http/server/request.rb +118 -0
- data/lib/polyphony/http/version.rb +7 -0
- data/lib/polyphony/websocket.rb +59 -0
- data/polyphony-http.gemspec +34 -0
- data/test/coverage.rb +45 -0
- data/test/eg.rb +27 -0
- data/test/helper.rb +35 -0
- data/test/run.rb +5 -0
- data/test/test_http_server.rb +313 -0
- metadata +245 -0
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :Agent
|
4
|
+
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
ResourcePool = import '../../core/resource_pool'
|
8
|
+
SiteConnectionManager = import './site_connection_manager'
|
9
|
+
|
10
|
+
# Implements an HTTP agent
|
11
|
+
class Agent
|
12
|
+
def self.get(*args, &block)
|
13
|
+
default.get(*args, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.post(*args, &block)
|
17
|
+
default.post(*args, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.default
|
21
|
+
@default ||= new
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@pools = Hash.new do |h, k|
|
26
|
+
h[k] = SiteConnectionManager.new(k)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
OPTS_DEFAULT = {}.freeze
|
31
|
+
|
32
|
+
def get(url, opts = OPTS_DEFAULT, &block)
|
33
|
+
request(url, opts.merge(method: :GET), &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def post(url, opts = OPTS_DEFAULT, &block)
|
37
|
+
request(url, opts.merge(method: :POST), &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def request(url, opts = OPTS_DEFAULT, &block)
|
41
|
+
ctx = request_ctx(url, opts)
|
42
|
+
|
43
|
+
response = do_request(ctx, &block)
|
44
|
+
case response.status_code
|
45
|
+
when 301, 302
|
46
|
+
redirect(response.headers['Location'], ctx, opts, &block)
|
47
|
+
when 200, 204
|
48
|
+
response
|
49
|
+
else
|
50
|
+
raise "Error received from server: #{response.status_code}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def redirect(url, ctx, opts, &block)
|
55
|
+
url = redirect_url(url, ctx)
|
56
|
+
request(url, opts, &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
def redirect_url(url, ctx)
|
60
|
+
case url
|
61
|
+
when /^http(?:s)?\:\/\//
|
62
|
+
url
|
63
|
+
when /^\/\/(.+)$/
|
64
|
+
ctx[:uri].scheme + url
|
65
|
+
when /^\//
|
66
|
+
format_uri(url, ctx)
|
67
|
+
else
|
68
|
+
ctx[:uri] + url
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def format_uri(url, ctx)
|
73
|
+
format(
|
74
|
+
'%<scheme>s://%<host>s%<url>s',
|
75
|
+
scheme: ctx[:uri].scheme,
|
76
|
+
host: ctx[:uri].host,
|
77
|
+
url: url
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def request_ctx(url, opts)
|
82
|
+
{
|
83
|
+
method: opts[:method] || :GET,
|
84
|
+
uri: url_to_uri(url, opts),
|
85
|
+
opts: opts,
|
86
|
+
retry: 0
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
def url_to_uri(url, opts)
|
91
|
+
uri = URI(url)
|
92
|
+
if opts[:query]
|
93
|
+
query = opts[:query].map { |k, v| "#{k}=#{v}" }.join('&')
|
94
|
+
if uri.query
|
95
|
+
v.query = "#{uri.query}&#{query}"
|
96
|
+
else
|
97
|
+
uri.query = query
|
98
|
+
end
|
99
|
+
end
|
100
|
+
uri
|
101
|
+
end
|
102
|
+
|
103
|
+
def do_request(ctx, &block)
|
104
|
+
key = uri_key(ctx[:uri])
|
105
|
+
|
106
|
+
@pools[key].acquire do |adapter|
|
107
|
+
send_request_and_check_response(adapter, ctx, &block)
|
108
|
+
end
|
109
|
+
rescue Exception => e
|
110
|
+
p e
|
111
|
+
puts e.backtrace.join("\n")
|
112
|
+
end
|
113
|
+
|
114
|
+
def send_request_and_check_response(adapter, ctx, &block)
|
115
|
+
response = adapter.request(ctx)
|
116
|
+
case response.status_code
|
117
|
+
when 200, 204
|
118
|
+
if block
|
119
|
+
block.(response)
|
120
|
+
else
|
121
|
+
# read body
|
122
|
+
response.body
|
123
|
+
end
|
124
|
+
end
|
125
|
+
response
|
126
|
+
end
|
127
|
+
|
128
|
+
def uri_key(uri)
|
129
|
+
{ scheme: uri.scheme, host: uri.host, port: uri.port }
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :HTTP1Adapter
|
4
|
+
|
5
|
+
require 'http/parser'
|
6
|
+
|
7
|
+
Response = import './response'
|
8
|
+
|
9
|
+
# HTTP 1 adapter
|
10
|
+
class HTTP1Adapter
|
11
|
+
def initialize(socket)
|
12
|
+
@socket = socket
|
13
|
+
@parser = HTTP::Parser.new(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_headers_complete(headers)
|
17
|
+
@headers = headers
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_body(chunk)
|
21
|
+
if @waiting_for_chunk
|
22
|
+
@buffered_chunks ||= []
|
23
|
+
@buffered_chunks << chunk
|
24
|
+
elsif @buffered_body
|
25
|
+
@buffered_body << chunk
|
26
|
+
else
|
27
|
+
@buffered_body = +chunk
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_message_complete
|
32
|
+
@done = true
|
33
|
+
end
|
34
|
+
|
35
|
+
def request(ctx)
|
36
|
+
# consume previous response if not finished
|
37
|
+
consume_response if @done == false
|
38
|
+
|
39
|
+
@socket << format_http1_request(ctx)
|
40
|
+
|
41
|
+
@buffered_body = nil
|
42
|
+
@done = false
|
43
|
+
|
44
|
+
read_headers
|
45
|
+
Response.new(self, @parser.status_code, @headers)
|
46
|
+
end
|
47
|
+
|
48
|
+
def read_headers
|
49
|
+
@headers = nil
|
50
|
+
while !@headers && (data = @socket.readpartial(8192))
|
51
|
+
@parser << data
|
52
|
+
end
|
53
|
+
|
54
|
+
raise 'Socket closed by host' unless @headers
|
55
|
+
end
|
56
|
+
|
57
|
+
def body
|
58
|
+
@waiting_for_chunk = nil
|
59
|
+
consume_response
|
60
|
+
@buffered_body
|
61
|
+
end
|
62
|
+
|
63
|
+
def each_chunk(&block)
|
64
|
+
if (body = @buffered_body)
|
65
|
+
@buffered_body = nil
|
66
|
+
@waiting_for_chunk = true
|
67
|
+
block.(body)
|
68
|
+
end
|
69
|
+
while !@done && (data = @socket.readpartial(8192))
|
70
|
+
@parser << data
|
71
|
+
end
|
72
|
+
raise 'Socket closed by host' unless @done
|
73
|
+
|
74
|
+
@buffered_chunks.each(&block)
|
75
|
+
end
|
76
|
+
|
77
|
+
def next_body_chunk
|
78
|
+
return nil if @done
|
79
|
+
if @buffered_chunks && !@buffered_chunks.empty?
|
80
|
+
return @buffered_chunks.shift
|
81
|
+
end
|
82
|
+
|
83
|
+
read_next_body_chunk
|
84
|
+
end
|
85
|
+
|
86
|
+
def read_next_body_chunk
|
87
|
+
@waiting_for_chunk = true
|
88
|
+
while !@done && (data = @socket.readpartial(8192))
|
89
|
+
@parser << data
|
90
|
+
break unless @buffered_chunks.empty?
|
91
|
+
end
|
92
|
+
@buffered_chunks.shift
|
93
|
+
end
|
94
|
+
|
95
|
+
def consume_response
|
96
|
+
while !@done && (data = @socket.readpartial(8192))
|
97
|
+
@parser << data
|
98
|
+
end
|
99
|
+
|
100
|
+
raise 'Socket closed by host' unless @done
|
101
|
+
end
|
102
|
+
|
103
|
+
HTTP1_REQUEST = <<~HTTP.gsub("\n", "\r\n")
|
104
|
+
%<method>s %<request>s HTTP/1.1
|
105
|
+
Host: %<host>s
|
106
|
+
%<headers>s
|
107
|
+
|
108
|
+
HTTP
|
109
|
+
|
110
|
+
def format_http1_request(ctx)
|
111
|
+
headers = format_headers(ctx)
|
112
|
+
|
113
|
+
format(
|
114
|
+
HTTP1_REQUEST,
|
115
|
+
method: ctx[:method],
|
116
|
+
request: ctx[:uri].request_uri,
|
117
|
+
host: ctx[:uri].host,
|
118
|
+
headers: headers
|
119
|
+
)
|
120
|
+
end
|
121
|
+
|
122
|
+
def format_headers(headers)
|
123
|
+
headers.map { |k, v| "#{k}: #{v}\r\n" }.join
|
124
|
+
end
|
125
|
+
|
126
|
+
def protocol
|
127
|
+
:http1
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :HTTP2Adapter
|
4
|
+
|
5
|
+
require 'http/2'
|
6
|
+
|
7
|
+
Response = import './response'
|
8
|
+
|
9
|
+
# HTTP 2 adapter
|
10
|
+
class HTTP2Adapter
|
11
|
+
def initialize(socket)
|
12
|
+
@socket = socket
|
13
|
+
@client = HTTP2::Client.new
|
14
|
+
@client.on(:frame) { |bytes| socket << bytes }
|
15
|
+
# @client.on(:frame_received) do |frame|
|
16
|
+
# puts "Received frame: #{frame.inspect}"
|
17
|
+
# end
|
18
|
+
# @client.on(:frame_sent) do |frame|
|
19
|
+
# puts "Sent frame: #{frame.inspect}"
|
20
|
+
# end
|
21
|
+
|
22
|
+
@reader = spin do
|
23
|
+
while (data = socket.readpartial(8192))
|
24
|
+
@client << data
|
25
|
+
snooze
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def allocate_stream_adapter
|
31
|
+
StreamAdapter.new(self)
|
32
|
+
end
|
33
|
+
|
34
|
+
def allocate_stream
|
35
|
+
@client.new_stream
|
36
|
+
end
|
37
|
+
|
38
|
+
def protocol
|
39
|
+
:http2
|
40
|
+
end
|
41
|
+
|
42
|
+
# Virtualizes adapter over HTTP2 stream
|
43
|
+
class StreamAdapter
|
44
|
+
def initialize(connection)
|
45
|
+
@connection = connection
|
46
|
+
end
|
47
|
+
|
48
|
+
def request(ctx)
|
49
|
+
stream = setup_stream # (ctx, stream)
|
50
|
+
send_request(ctx, stream)
|
51
|
+
|
52
|
+
stream.on(:headers, &method(:on_headers))
|
53
|
+
stream.on(:data, &method(:on_data))
|
54
|
+
stream.on(:close, &method(:on_close))
|
55
|
+
|
56
|
+
# stream.on(:active) { puts "* active" }
|
57
|
+
# stream.on(:half_close) { puts "* half_close" }
|
58
|
+
|
59
|
+
wait_for_response(ctx, stream)
|
60
|
+
rescue Exception => e
|
61
|
+
p e
|
62
|
+
puts e.backtrace.join("\n")
|
63
|
+
# ensure
|
64
|
+
# stream.close
|
65
|
+
end
|
66
|
+
|
67
|
+
def send_request(ctx, stream)
|
68
|
+
headers = prepare_headers(ctx)
|
69
|
+
if ctx[:opts][:payload]
|
70
|
+
stream.headers(headers, end_stream: false)
|
71
|
+
stream.data(ctx[:opts][:payload], end_stream: true)
|
72
|
+
else
|
73
|
+
stream.headers(headers, end_stream: true)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def on_headers(headers)
|
78
|
+
if @waiting_headers_fiber
|
79
|
+
@waiting_headers_fiber.schedule headers.to_h
|
80
|
+
else
|
81
|
+
@headers = headers.to_h
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def on_data(chunk)
|
86
|
+
if @waiting_chunk_fiber
|
87
|
+
@waiting_chunk_fiber&.schedule chunk
|
88
|
+
else
|
89
|
+
@buffered_chunks << chunk
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def on_close(_stream)
|
94
|
+
@done = true
|
95
|
+
@waiting_done_fiber&.schedule
|
96
|
+
end
|
97
|
+
|
98
|
+
def setup_stream
|
99
|
+
stream = @connection.allocate_stream
|
100
|
+
|
101
|
+
@headers = nil
|
102
|
+
@done = nil
|
103
|
+
@buffered_chunks = []
|
104
|
+
|
105
|
+
@waiting_headers_fiber = nil
|
106
|
+
@waiting_chunk_fiber = nil
|
107
|
+
@waiting_done_fiber = nil
|
108
|
+
|
109
|
+
stream
|
110
|
+
end
|
111
|
+
|
112
|
+
def wait_for_response(_ctx, _stream)
|
113
|
+
headers = wait_for_headers
|
114
|
+
Response.new(self, headers[':status'].to_i, headers)
|
115
|
+
end
|
116
|
+
|
117
|
+
def wait_for_headers
|
118
|
+
return @headers if @headers
|
119
|
+
|
120
|
+
@waiting_headers_fiber = Fiber.current
|
121
|
+
suspend
|
122
|
+
end
|
123
|
+
|
124
|
+
def protocol
|
125
|
+
:http2
|
126
|
+
end
|
127
|
+
|
128
|
+
def prepare_headers(ctx)
|
129
|
+
headers = {
|
130
|
+
':method' => ctx[:method].to_s,
|
131
|
+
':scheme' => ctx[:uri].scheme,
|
132
|
+
':authority' => [ctx[:uri].host, ctx[:uri].port].join(':'),
|
133
|
+
':path' => ctx[:uri].request_uri,
|
134
|
+
'User-Agent' => 'curl/7.54.0'
|
135
|
+
}
|
136
|
+
headers.merge!(ctx[:opts][:headers]) if ctx[:opts][:headers]
|
137
|
+
headers
|
138
|
+
end
|
139
|
+
|
140
|
+
def body
|
141
|
+
@waiting_done_fiber = Fiber.current
|
142
|
+
suspend
|
143
|
+
@buffered_chunks.join
|
144
|
+
# body = +''
|
145
|
+
# while !@done
|
146
|
+
# p :body_suspend_pre
|
147
|
+
# chunk = suspend
|
148
|
+
# p :body_suspend_post
|
149
|
+
# body << chunk
|
150
|
+
# end
|
151
|
+
# puts ""
|
152
|
+
# body
|
153
|
+
rescue Exception => e
|
154
|
+
p e
|
155
|
+
puts e.backtrace.join("\n")
|
156
|
+
end
|
157
|
+
|
158
|
+
def each_chunk
|
159
|
+
yield @buffered_chunks.shift until @buffered_chunks.empty?
|
160
|
+
|
161
|
+
@waiting_chunk_fiber = Fiber.current
|
162
|
+
until @done
|
163
|
+
chunk = suspend
|
164
|
+
yield chunk
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def next_body_chunk
|
169
|
+
return yield @buffered_chunks.shift unless @buffered_chunks.empty?
|
170
|
+
|
171
|
+
@waiting_chunk_fuber = Fiber.current
|
172
|
+
until @done
|
173
|
+
chunk = suspend
|
174
|
+
return yield chunk
|
175
|
+
end
|
176
|
+
|
177
|
+
nil
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :Response
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
# HTTP response
|
8
|
+
class Response
|
9
|
+
attr_reader :status_code, :headers
|
10
|
+
|
11
|
+
def initialize(adapter, status_code, headers)
|
12
|
+
@adapter = adapter
|
13
|
+
@status_code = status_code
|
14
|
+
@headers = headers
|
15
|
+
end
|
16
|
+
|
17
|
+
def body
|
18
|
+
@body ||= @adapter.body
|
19
|
+
end
|
20
|
+
|
21
|
+
def each_chunk(&block)
|
22
|
+
@adapter.each_chunk(&block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def next_body_chunk
|
26
|
+
@adapter.next_body_chunk
|
27
|
+
end
|
28
|
+
|
29
|
+
def json
|
30
|
+
@json ||= ::JSON.parse(body)
|
31
|
+
end
|
32
|
+
end
|