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,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony'
|
5
|
+
require 'http/2'
|
6
|
+
|
7
|
+
# Response = import '../../lib/polyphony/http/client/response'
|
8
|
+
|
9
|
+
url = 'https://realiteq.net/?q=time'
|
10
|
+
uri = URI(url)
|
11
|
+
uri_key = { scheme: uri.scheme, host: uri.host, port: uri.port }
|
12
|
+
|
13
|
+
ctx = {
|
14
|
+
method: :GET,
|
15
|
+
uri: uri,
|
16
|
+
opts: {},
|
17
|
+
retry: 0
|
18
|
+
}
|
19
|
+
|
20
|
+
SECURE_OPTS = { secure: true, alpn_protocols: ['h2', 'http/1.1'] }.freeze
|
21
|
+
socket = Polyphony::Net.tcp_connect(uri_key[:host], uri_key[:port], SECURE_OPTS)
|
22
|
+
|
23
|
+
puts 'connected'
|
24
|
+
|
25
|
+
$client = HTTP2::Client.new
|
26
|
+
$client.on(:frame) { |bytes| socket << bytes }
|
27
|
+
$client.on(:frame_received) do |frame|
|
28
|
+
puts "Received frame: #{frame.inspect}"
|
29
|
+
end
|
30
|
+
# $client.on(:frame_sent) do |frame|
|
31
|
+
# puts "Sent frame: #{frame.inspect}"
|
32
|
+
# end
|
33
|
+
|
34
|
+
reader = spin do
|
35
|
+
while (data = socket.readpartial(8192))
|
36
|
+
$client << data
|
37
|
+
snooze
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
stream = $client.new_stream
|
42
|
+
|
43
|
+
$headers = nil
|
44
|
+
$done = nil
|
45
|
+
@buffered_chunks = []
|
46
|
+
|
47
|
+
@waiting_headers_fiber = nil
|
48
|
+
@waiting_chunk_fiber = nil
|
49
|
+
@waiting_done_fiber = nil
|
50
|
+
|
51
|
+
# send request
|
52
|
+
headers = {
|
53
|
+
':method' => ctx[:method].to_s,
|
54
|
+
':scheme' => ctx[:uri].scheme,
|
55
|
+
':authority' => [ctx[:uri].host, ctx[:uri].port].join(':'),
|
56
|
+
':path' => ctx[:uri].request_uri,
|
57
|
+
'User-Agent' => 'curl/7.54.0'
|
58
|
+
}
|
59
|
+
headers.merge!(ctx[:opts][:headers]) if ctx[:opts][:headers]
|
60
|
+
|
61
|
+
if ctx[:opts][:payload]
|
62
|
+
stream.headers(headers, end_stream: false)
|
63
|
+
stream.data(ctx[:opts][:payload], end_stream: true)
|
64
|
+
else
|
65
|
+
stream.headers(headers, end_stream: true)
|
66
|
+
end
|
67
|
+
|
68
|
+
stream.on(:headers) { |headers|
|
69
|
+
puts "got headers"
|
70
|
+
# if @waiting_headers_fiber
|
71
|
+
# @waiting_headers_fiber.transfer headers.to_h
|
72
|
+
# else
|
73
|
+
$headers = headers.to_h
|
74
|
+
# end
|
75
|
+
}
|
76
|
+
stream.on(:data) { |chunk|
|
77
|
+
puts "got data"
|
78
|
+
# if @waiting_chunk_fiber
|
79
|
+
# @waiting_chunk_fiber&.transfer c
|
80
|
+
# else
|
81
|
+
@buffered_chunks << chunk
|
82
|
+
# end
|
83
|
+
}
|
84
|
+
|
85
|
+
def close
|
86
|
+
puts "got close"
|
87
|
+
$done = true
|
88
|
+
end
|
89
|
+
|
90
|
+
stream.on(:close) { close }
|
91
|
+
# @waiting_done_fiber&.transfer
|
92
|
+
# }
|
93
|
+
|
94
|
+
stream.on(:active) { puts "* active" }
|
95
|
+
stream.on(:half_close) { puts "* half_close" }
|
96
|
+
|
97
|
+
|
98
|
+
|
99
|
+
# wait for response
|
100
|
+
# unless $headers
|
101
|
+
# @waiting_headers_fiber = Fiber.current
|
102
|
+
# $headers = suspend
|
103
|
+
# end
|
104
|
+
# p $headers
|
105
|
+
# response = Response.new(self, $headers[':status'].to_i, $headers)
|
106
|
+
# p response
|
107
|
+
|
108
|
+
puts "waiting for response"
|
109
|
+
while !$done
|
110
|
+
puts "waiting..."
|
111
|
+
sleep 1
|
112
|
+
end
|
113
|
+
puts "done"
|
114
|
+
|
115
|
+
# def body
|
116
|
+
# @waiting_chunk_fiber = Fiber.current
|
117
|
+
# body = +''
|
118
|
+
# while !$done
|
119
|
+
# chunk = suspend
|
120
|
+
# body << chunk
|
121
|
+
# end
|
122
|
+
# body
|
123
|
+
# rescue => e
|
124
|
+
# p e
|
125
|
+
# puts e.backtrace.join("\n")
|
126
|
+
# end
|
127
|
+
# end
|
128
|
+
|
129
|
+
# adapter = StreamAdapter.new
|
130
|
+
# resp = adapter.request(ctx)
|
131
|
+
# puts "*" * 40
|
132
|
+
# p resp
|
133
|
+
|
134
|
+
# body = resp.body
|
135
|
+
# p body
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony/http'
|
5
|
+
|
6
|
+
Exception.__disable_sanitized_backtrace__ = true
|
7
|
+
|
8
|
+
TIME_URI = 'https://ui.realiteq.net/'
|
9
|
+
|
10
|
+
def get_server_time
|
11
|
+
json = Polyphony::HTTP::Agent.get(TIME_URI, query: { q: :time }).json
|
12
|
+
puts "*" * 40
|
13
|
+
p json
|
14
|
+
end
|
15
|
+
|
16
|
+
X = 1
|
17
|
+
puts "Making #{X} requests..."
|
18
|
+
t0 = Time.now
|
19
|
+
supervise do |s|
|
20
|
+
X.times {
|
21
|
+
s.spin {
|
22
|
+
get_server_time
|
23
|
+
}
|
24
|
+
}
|
25
|
+
end
|
26
|
+
# get_server_time
|
27
|
+
elapsed = Time.now - t0
|
28
|
+
puts "count: #{X} elapsed: #{elapsed} rate: #{X / elapsed} reqs/s"
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony/http'
|
5
|
+
require 'polyphony'
|
6
|
+
|
7
|
+
Exception.__disable_sanitized_backtrace__ = true
|
8
|
+
|
9
|
+
resp = Polyphony::HTTP::Agent.get('https://realiteq.net/?q=time')
|
10
|
+
puts "*" * 40
|
11
|
+
puts resp.body
|
12
|
+
|
13
|
+
__END__
|
14
|
+
|
15
|
+
X = 1
|
16
|
+
Y = 1
|
17
|
+
t0 = Time.now
|
18
|
+
supervise { |s|
|
19
|
+
X.times {
|
20
|
+
s.spin {
|
21
|
+
Y.times {
|
22
|
+
resp = Polyphony::HTTP::Agent.get('http://about.gitlab.com/')
|
23
|
+
puts "*" * 40
|
24
|
+
p resp.headers
|
25
|
+
puts "*" * 40
|
26
|
+
puts resp.body
|
27
|
+
# puts "body size: #{resp.body.bytesize}"
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}
|
31
|
+
}
|
32
|
+
elapsed = Time.now - t0
|
33
|
+
puts "\nelapsed: #{elapsed} rate: #{(X * Y) / elapsed}"
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony'
|
5
|
+
|
6
|
+
require 'http/parser'
|
7
|
+
require 'fiber'
|
8
|
+
|
9
|
+
i, o = IO.pipe
|
10
|
+
|
11
|
+
class ParseLoop
|
12
|
+
def initialize(conn)
|
13
|
+
@parser = HTTP::Parser.new(self)
|
14
|
+
@conn = conn
|
15
|
+
@parse_fiber = Fiber.new do
|
16
|
+
while (data = conn.readpartial(8192))
|
17
|
+
@parser << data
|
18
|
+
snooze
|
19
|
+
end
|
20
|
+
rescue StandardError => e
|
21
|
+
conn.close
|
22
|
+
e
|
23
|
+
ensure
|
24
|
+
@message_in_train = nil
|
25
|
+
end
|
26
|
+
@state = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def on_headers_complete(headers)
|
30
|
+
@calling_fiber.transfer(headers)
|
31
|
+
end
|
32
|
+
|
33
|
+
def on_body(chunk)
|
34
|
+
@calling_fiber.transfer(chunk) if @read_body
|
35
|
+
end
|
36
|
+
|
37
|
+
def on_message_begin
|
38
|
+
@message_in_train = true
|
39
|
+
end
|
40
|
+
|
41
|
+
def on_message_complete
|
42
|
+
@message_in_train = nil
|
43
|
+
@calling_fiber.transfer nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def parse_headers
|
47
|
+
@calling_fiber = Fiber.current
|
48
|
+
@parse_fiber.safe_transfer
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_body_chunk
|
52
|
+
@calling_fiber = Fiber.current
|
53
|
+
@read_body = true
|
54
|
+
@parse_fiber.safe_transfer
|
55
|
+
end
|
56
|
+
|
57
|
+
def consume_request
|
58
|
+
return unless @message_in_train
|
59
|
+
|
60
|
+
@calling_fiber = Fiber.current
|
61
|
+
@read_body = false
|
62
|
+
@parse_fiber.safe_transfer while @message_in_train
|
63
|
+
end
|
64
|
+
|
65
|
+
def alive?
|
66
|
+
@parse_fiber.alive?
|
67
|
+
end
|
68
|
+
|
69
|
+
def busy?
|
70
|
+
@message_in_train
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def handle(parser)
|
75
|
+
headers = parser.parse_headers
|
76
|
+
return unless headers
|
77
|
+
|
78
|
+
puts "headers: #{headers.inspect}"
|
79
|
+
headers['Content-Length']
|
80
|
+
# if content_length && (content_length.to_i < 1000)
|
81
|
+
while (chunk = parser.parse_body_chunk)
|
82
|
+
puts "chunk: #{chunk.inspect}"
|
83
|
+
end
|
84
|
+
# else
|
85
|
+
# parser.consume_request
|
86
|
+
# end
|
87
|
+
puts 'end of request'
|
88
|
+
rescue StandardError => e
|
89
|
+
puts "error: #{e.inspect}"
|
90
|
+
raise e
|
91
|
+
end
|
92
|
+
|
93
|
+
writer = spin do
|
94
|
+
o << "POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 6\r\n\r\n"
|
95
|
+
o << 'Hello!'
|
96
|
+
|
97
|
+
o << "POST / HTTP/1.1\r\nHost: example.com\r\n\r\n"
|
98
|
+
|
99
|
+
# o << "POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 8192\r\n\r\n"
|
100
|
+
# o << ("Bye!" * 2048)
|
101
|
+
|
102
|
+
o << "POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 4\r\n\r\n"
|
103
|
+
o << 'Bye!'
|
104
|
+
|
105
|
+
begin
|
106
|
+
(o << ('BLAH' * 100_000))
|
107
|
+
rescue StandardError
|
108
|
+
nil
|
109
|
+
end
|
110
|
+
o.close
|
111
|
+
end
|
112
|
+
|
113
|
+
begin
|
114
|
+
parse_loop = ParseLoop.new(i)
|
115
|
+
while parse_loop.alive?
|
116
|
+
puts '*' * 40
|
117
|
+
handle(parse_loop)
|
118
|
+
end
|
119
|
+
rescue StandardError => e
|
120
|
+
writer.stop
|
121
|
+
puts "#{e.class}: #{e.message}"
|
122
|
+
puts e.backtrace
|
123
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony/http'
|
5
|
+
require 'localhost/authority'
|
6
|
+
|
7
|
+
# p Polyphony::HTTP::Agent.get('https://ui.realiteq.net/', q: :time)
|
8
|
+
|
9
|
+
BASE_URL = 'http://realiteq.net'
|
10
|
+
|
11
|
+
CACHE = {}.freeze
|
12
|
+
|
13
|
+
def proxy(uri, opts)
|
14
|
+
now = Time.now
|
15
|
+
uri = BASE_URL + uri
|
16
|
+
entry = CACHE[uri]
|
17
|
+
return entry[:response] if entry && entry[:expires] >= now
|
18
|
+
|
19
|
+
puts "proxy => #{uri} (#{opts.inspect})"
|
20
|
+
response = Polyphony::HTTP::Agent.get(uri, opts)
|
21
|
+
# CACHE[uri] = {
|
22
|
+
# expires: now + 60,
|
23
|
+
# response: response
|
24
|
+
# }
|
25
|
+
response
|
26
|
+
end
|
27
|
+
|
28
|
+
HEADERS_BLACK_LIST = %w[
|
29
|
+
Transfer-Encoding Date Server Connection Content-Length Cache-Control
|
30
|
+
:method :authority :scheme :path
|
31
|
+
].freeze
|
32
|
+
|
33
|
+
def sanitize_headers(headers)
|
34
|
+
headers.reject { |k, _v| HEADERS_BLACK_LIST.include?(k) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def sanitize_html(html)
|
38
|
+
# html.gsub('http://martigny-le-comte.fr/', '/')
|
39
|
+
end
|
40
|
+
|
41
|
+
# authority = Localhost::Authority.fetch
|
42
|
+
|
43
|
+
rsa_cert = OpenSSL::X509::Certificate.new(
|
44
|
+
IO.read('../reality/aws/config/ssl/full_chain.pem')
|
45
|
+
)
|
46
|
+
rsa_pkey = OpenSSL::PKey.read(
|
47
|
+
IO.read('../reality/aws/config/ssl/private_key.pem')
|
48
|
+
)
|
49
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
50
|
+
ctx.add_certificate(rsa_cert, rsa_pkey)
|
51
|
+
|
52
|
+
opts = {
|
53
|
+
reuse_addr: true,
|
54
|
+
dont_linger: true,
|
55
|
+
secure_context: ctx # authority.server_context
|
56
|
+
}
|
57
|
+
|
58
|
+
spin do
|
59
|
+
Polyphony::HTTP::Server.serve('0.0.0.0', 1234, opts) do |req|
|
60
|
+
puts "#{req.method} #{req.uri}"
|
61
|
+
puts "headers <: #{req.headers.inspect}"
|
62
|
+
# h = {
|
63
|
+
# uri: req.uri.to_s,
|
64
|
+
# protocol: req.protocol,
|
65
|
+
# headers: req.headers,
|
66
|
+
# body: req.body,
|
67
|
+
# }
|
68
|
+
response = proxy(req.uri.to_s, headers: sanitize_headers(req.headers))
|
69
|
+
headers = sanitize_headers(response[:headers])
|
70
|
+
body = response[:body]
|
71
|
+
# puts "body class: #{body.class}"
|
72
|
+
puts "headers >: #{response[:headers].inspect}"
|
73
|
+
# body = sanitize_html(body) if headers['Content-Type'] =~ /text\/html/
|
74
|
+
req.respond(body, headers)
|
75
|
+
rescue StandardError => e
|
76
|
+
puts 'error'
|
77
|
+
p e
|
78
|
+
puts e.backtrace.join("\n")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
puts "pid: #{Process.pid}"
|
83
|
+
puts 'Listening on port 1234...'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
// For the sake of comparing performance, here's a node.js-based HTTP server
|
2
|
+
// doing roughly the same thing as http_server. Preliminary benchmarking shows
|
3
|
+
// the ruby version has a throughput (req/s) of about 2/3 of the JS version.
|
4
|
+
|
5
|
+
const http = require('http');
|
6
|
+
|
7
|
+
const MSG = 'Hello World';
|
8
|
+
|
9
|
+
const server = http.createServer((req, res) => {
|
10
|
+
// let requestCopy = {
|
11
|
+
// method: req.method,
|
12
|
+
// request_url: req.url,
|
13
|
+
// headers: req.headers
|
14
|
+
// };
|
15
|
+
|
16
|
+
// res.writeHead(200, { 'Content-Type': 'application/json' });
|
17
|
+
// res.end(JSON.stringify(requestCopy));
|
18
|
+
|
19
|
+
res.writeHead(200);
|
20
|
+
res.end(MSG)
|
21
|
+
});
|
22
|
+
|
23
|
+
server.listen(1235);
|
24
|
+
console.log('Listening on port 1235');
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony/http'
|
5
|
+
|
6
|
+
opts = {
|
7
|
+
reuse_addr: true,
|
8
|
+
dont_linger: true
|
9
|
+
}
|
10
|
+
|
11
|
+
spin do
|
12
|
+
Polyphony::HTTP::Server.serve('0.0.0.0', 1234, opts) do |req|
|
13
|
+
req.respond("Hello world!\n")
|
14
|
+
rescue Exception => e
|
15
|
+
p e
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
puts "pid: #{Process.pid}"
|
20
|
+
puts 'Listening on port 1234...'
|
21
|
+
suspend
|