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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +56 -0
  3. data/CHANGELOG.md +6 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +51 -0
  6. data/LICENSE +21 -0
  7. data/README.md +47 -0
  8. data/Rakefile +20 -0
  9. data/TODO.md +59 -0
  10. data/bin/poly +11 -0
  11. data/docs/README.md +38 -0
  12. data/docs/summary.md +60 -0
  13. data/examples/cuba.ru +22 -0
  14. data/examples/happy_eyeballs.rb +37 -0
  15. data/examples/http2_raw.rb +135 -0
  16. data/examples/http_client.rb +28 -0
  17. data/examples/http_get.rb +33 -0
  18. data/examples/http_parse_experiment.rb +123 -0
  19. data/examples/http_proxy.rb +83 -0
  20. data/examples/http_server.js +24 -0
  21. data/examples/http_server.rb +21 -0
  22. data/examples/http_server_forked.rb +29 -0
  23. data/examples/http_server_graceful.rb +27 -0
  24. data/examples/http_server_simple.rb +11 -0
  25. data/examples/http_server_throttled.rb +15 -0
  26. data/examples/http_server_timeout.rb +35 -0
  27. data/examples/http_ws_server.rb +37 -0
  28. data/examples/https_raw_client.rb +12 -0
  29. data/examples/https_server.rb +22 -0
  30. data/examples/https_wss_server.rb +39 -0
  31. data/examples/rack_server.rb +12 -0
  32. data/examples/rack_server_https.rb +19 -0
  33. data/examples/rack_server_https_forked.rb +27 -0
  34. data/examples/websocket_secure_server.rb +27 -0
  35. data/examples/websocket_server.rb +24 -0
  36. data/examples/ws_page.html +34 -0
  37. data/examples/wss_page.html +34 -0
  38. data/lib/polyphony/http.rb +16 -0
  39. data/lib/polyphony/http/client/agent.rb +131 -0
  40. data/lib/polyphony/http/client/http1.rb +129 -0
  41. data/lib/polyphony/http/client/http2.rb +180 -0
  42. data/lib/polyphony/http/client/response.rb +32 -0
  43. data/lib/polyphony/http/client/site_connection_manager.rb +109 -0
  44. data/lib/polyphony/http/server.rb +49 -0
  45. data/lib/polyphony/http/server/http1.rb +267 -0
  46. data/lib/polyphony/http/server/http2.rb +78 -0
  47. data/lib/polyphony/http/server/http2_stream.rb +135 -0
  48. data/lib/polyphony/http/server/rack.rb +64 -0
  49. data/lib/polyphony/http/server/request.rb +118 -0
  50. data/lib/polyphony/http/version.rb +7 -0
  51. data/lib/polyphony/websocket.rb +59 -0
  52. data/polyphony-http.gemspec +34 -0
  53. data/test/coverage.rb +45 -0
  54. data/test/eg.rb +27 -0
  55. data/test/helper.rb +35 -0
  56. data/test/run.rb +5 -0
  57. data/test/test_http_server.rb +313 -0
  58. metadata +245 -0
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ export_default :SiteConnectionManager
4
+
5
+ ResourcePool = import '../../core/resource_pool'
6
+ HTTP1Adapter = import './http1'
7
+ HTTP2Adapter = import './http2'
8
+
9
+ # HTTP site connection pool
10
+ class SiteConnectionManager < ResourcePool
11
+ def initialize(uri_key)
12
+ @uri_key = uri_key
13
+ super(limit: 4)
14
+ end
15
+
16
+ # def method_missing(sym, *args)
17
+ # raise "Invalid method #{sym}"
18
+ # end
19
+
20
+ def acquire
21
+ Gyro.ref
22
+ prepare_first_connection if @size.zero?
23
+ super
24
+ ensure
25
+ Gyro.unref
26
+ # The size goes back to 0 only in case existing connections get into an
27
+ # error state and then get discarded
28
+ @state = nil if @size == 0
29
+ end
30
+
31
+ def prepare_first_connection
32
+ case @state
33
+ when nil
34
+ @state = :first_connection
35
+ create_first_connection
36
+ when :first_connection
37
+ @first_connection_queue << Fiber.current
38
+ suspend
39
+ end
40
+ end
41
+
42
+ def create_first_connection
43
+ @first_connection_queue = []
44
+ # @first_connection_queue << Fiber.current
45
+
46
+ adapter = connect
47
+ @state = adapter.protocol
48
+ send(:"setup_#{@state}_allocator", adapter)
49
+ dequeue_first_connection_waiters
50
+ end
51
+
52
+ def setup_http1_allocator(adapter)
53
+ @size += 1
54
+ adapter.extend ResourceExtensions
55
+ @stock << adapter
56
+ @allocator = proc { connect }
57
+ end
58
+
59
+ def setup_http2_allocator(adapter)
60
+ @adapter = adapter
61
+ @limit = 20
62
+ @size += 1
63
+ stream_adapter = adapter.allocate_stream_adapter
64
+ stream_adapter.extend ResourceExtensions
65
+ @stock << stream_adapter
66
+ @allocator = proc { adapter.allocate_stream_adapter }
67
+ end
68
+
69
+ def dequeue_first_connection_waiters
70
+ return unless @first_connection_queue
71
+
72
+ @first_connection_queue.each(&:schedule)
73
+ @first_connection_queue = nil
74
+ end
75
+
76
+ def connect
77
+ socket = create_socket
78
+ protocol = socket_protocol(socket)
79
+ case protocol
80
+ when :http1
81
+ HTTP1Adapter.new(socket)
82
+ when :http2
83
+ HTTP2Adapter.new(socket)
84
+ else
85
+ raise "Unknown protocol #{protocol.inspect}"
86
+ end
87
+ end
88
+
89
+ def socket_protocol(socket)
90
+ if socket.is_a?(OpenSSL::SSL::SSLSocket) && socket.alpn_protocol == 'h2'
91
+ :http2
92
+ else
93
+ :http1
94
+ end
95
+ end
96
+
97
+ SECURE_OPTS = { secure: true, alpn_protocols: ['h2', 'http/1.1'] }.freeze
98
+
99
+ def create_socket
100
+ case @uri_key[:scheme]
101
+ when 'http'
102
+ Polyphony::Net.tcp_connect(@uri_key[:host], @uri_key[:port])
103
+ when 'https'
104
+ Polyphony::Net.tcp_connect(@uri_key[:host], @uri_key[:port], SECURE_OPTS)
105
+ else
106
+ raise "Invalid scheme #{@uri_key[:scheme].inspect}"
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ export :serve, :listen, :accept_loop, :client_loop
4
+
5
+ Net = Polyphony::Net
6
+ HTTP1 = import('./server/http1')
7
+ HTTP2 = import('./server/http2')
8
+
9
+ ALPN_PROTOCOLS = %w[h2 http/1.1].freeze
10
+ H2_PROTOCOL = 'h2'
11
+
12
+ def serve(host, port, opts = {}, &handler)
13
+ opts[:alpn_protocols] = ALPN_PROTOCOLS
14
+ server = Net.tcp_listen(host, port, opts)
15
+ accept_loop(server, opts, &handler)
16
+ end
17
+
18
+ def listen(host, port, opts = {})
19
+ opts[:alpn_protocols] = ALPN_PROTOCOLS
20
+ Net.tcp_listen(host, port, opts).tap do |socket|
21
+ socket.define_singleton_method(:each) do |&block|
22
+ MODULE.accept_loop(socket, opts, &block)
23
+ end
24
+ end
25
+ end
26
+
27
+ def accept_loop(server, opts, &handler)
28
+ loop do
29
+ client = server.accept
30
+ spin { client_loop(client, opts, &handler) }
31
+ rescue OpenSSL::SSL::SSLError
32
+ # disregard
33
+ end
34
+ end
35
+
36
+ def client_loop(client, opts, &handler)
37
+ client.no_delay if client.respond_to?(:no_delay)
38
+ adapter = protocol_adapter(client, opts)
39
+ adapter.each(&handler)
40
+ ensure
41
+ client.close
42
+ end
43
+
44
+ def protocol_adapter(socket, opts)
45
+ use_http2 = socket.respond_to?(:alpn_protocol) &&
46
+ socket.alpn_protocol == H2_PROTOCOL
47
+ klass = use_http2 ? HTTP2 : HTTP1
48
+ klass.new(socket, opts)
49
+ end
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ export_default :HTTP1Adapter
4
+
5
+ require 'http/parser'
6
+
7
+ Request = import('./request')
8
+ HTTP2 = import('./http2')
9
+
10
+ # HTTP1 protocol implementation
11
+ class HTTP1Adapter
12
+ # Initializes a protocol adapter instance
13
+ def initialize(conn, opts)
14
+ @conn = conn
15
+ @opts = opts
16
+ @parser = HTTP::Parser.new(self)
17
+ end
18
+
19
+ def each(&block)
20
+ while (data = @conn.readpartial(8192))
21
+ return if handle_incoming_data(data, &block)
22
+ end
23
+ rescue SystemCallError, IOError
24
+ # ignore
25
+ ensure
26
+ finalize_client_loop
27
+ end
28
+
29
+ # return [Boolean] true if client loop should stop
30
+ def handle_incoming_data(data, &block)
31
+ @parser << data
32
+ snooze
33
+ while (request = @requests_head)
34
+ return true if upgrade_connection(request.headers, &block)
35
+
36
+ @requests_head = request.__next__
37
+ block.call(request)
38
+ return true unless request.keep_alive?
39
+ end
40
+ nil
41
+ end
42
+
43
+ def finalize_client_loop
44
+ # release references to various objects
45
+ @requests_head = @requests_tail = nil
46
+ @parser = nil
47
+ @conn.close
48
+ end
49
+
50
+ # Reads a body chunk for the current request. Transfers control to the parse
51
+ # loop, and resumes once the parse_loop has fired the on_body callback
52
+ def get_body_chunk
53
+ @waiting_for_body_chunk = true
54
+ @next_chunk = nil
55
+ while !@requests_tail.complete? && (data = @conn.readpartial(8192))
56
+ @parser << data
57
+ return @next_chunk if @next_chunk
58
+
59
+ snooze
60
+ end
61
+ nil
62
+ ensure
63
+ @waiting_for_body_chunk = nil
64
+ end
65
+
66
+ # Waits for the current request to complete. Transfers control to the parse
67
+ # loop, and resumes once the parse_loop has fired the on_message_complete
68
+ # callback
69
+ def consume_request
70
+ request = @requests_head
71
+ while (data = @conn.readpartial(8192))
72
+ @parser << data
73
+ return if request.complete?
74
+
75
+ snooze
76
+ end
77
+ end
78
+
79
+ def protocol
80
+ version = @parser.http_version
81
+ "HTTP #{version.join('.')}"
82
+ end
83
+
84
+ def on_headers_complete(headers)
85
+ headers[':path'] = @parser.request_url
86
+ headers[':method'] = @parser.http_method
87
+ queue_request(Request.new(headers, self))
88
+ end
89
+
90
+ def queue_request(request)
91
+ if @requests_head
92
+ @requests_tail.__next__ = request
93
+ @requests_tail = request
94
+ else
95
+ @requests_head = @requests_tail = request
96
+ end
97
+ end
98
+
99
+ def on_body(chunk)
100
+ if @waiting_for_body_chunk
101
+ @next_chunk = chunk
102
+ @waiting_for_body_chunk = nil
103
+ else
104
+ @requests_tail.buffer_body_chunk(chunk)
105
+ end
106
+ end
107
+
108
+ def on_message_complete
109
+ @waiting_for_body_chunk = nil
110
+ @requests_tail.complete!(@parser.keep_alive?)
111
+ end
112
+
113
+ # Upgrades the connection to a different protocol, if the 'Upgrade' header is
114
+ # given. By default the only supported upgrade protocol is HTTP2. Additional
115
+ # protocols, notably WebSocket, can be specified by passing a hash to the
116
+ # :upgrade option when starting a server:
117
+ #
118
+ # opts = {
119
+ # upgrade: {
120
+ # websocket: Polyphony::Websocket.handler(&method(:ws_handler))
121
+ # }
122
+ # }
123
+ # Polyphony::HTTP::Server.serve('0.0.0.0', 1234, opts) { |req| ... }
124
+ #
125
+ # @param headers [Hash] request headers
126
+ # @return [boolean] truthy if the connection has been upgraded
127
+ def upgrade_connection(headers, &block)
128
+ upgrade_protocol = headers['Upgrade']
129
+ return nil unless upgrade_protocol
130
+
131
+ upgrade_protocol = upgrade_protocol.downcase.to_sym
132
+ upgrade_handler = @opts[:upgrade] && @opts[:upgrade][upgrade_protocol]
133
+ return upgrade_with_handler(upgrade_handler, headers) if upgrade_handler
134
+ return upgrade_to_http2(headers, &block) if upgrade_protocol == :h2c
135
+
136
+ nil
137
+ end
138
+
139
+ def upgrade_with_handler(handler, headers)
140
+ @parser = @requests_head = @requests_tail = nil
141
+ handler.(@conn, headers)
142
+ true
143
+ end
144
+
145
+ def upgrade_to_http2(headers, &block)
146
+ @parser = @requests_head = @requests_tail = nil
147
+ HTTP2.upgrade_each(@conn, @opts, http2_upgraded_headers(headers), &block)
148
+ true
149
+ end
150
+
151
+ # Returns headers for HTTP2 upgrade
152
+ # @param headers [Hash] request headers
153
+ # @return [Hash] headers for HTTP2 upgrade
154
+ def http2_upgraded_headers(headers)
155
+ headers.merge(
156
+ ':scheme' => 'http',
157
+ ':authority' => headers['Host']
158
+ )
159
+ end
160
+
161
+ # response API
162
+
163
+ # Sends response including headers and body. Waits for the request to complete
164
+ # if not yet completed. The body is sent using chunked transfer encoding.
165
+ # @param body [String] response body
166
+ # @param headers
167
+ def respond(body, headers)
168
+ consume_request if @parsing
169
+ data = format_headers(headers, body)
170
+ if body
171
+ data << if @parser.http_minor == 0
172
+ body
173
+ else
174
+ "#{body.bytesize.to_s(16)}\r\n#{body}\r\n0\r\n\r\n"
175
+ end
176
+ end
177
+ @conn << data
178
+ end
179
+
180
+ DEFAULT_HEADERS_OPTS = {
181
+ empty_response: false,
182
+ consume_request: true
183
+ }.freeze
184
+
185
+ # Sends response headers. If empty_response is truthy, the response status
186
+ # code will default to 204, otherwise to 200.
187
+ # @param headers [Hash] response headers
188
+ # @param empty_response [boolean] whether a response body will be sent
189
+ # @return [void]
190
+ def send_headers(headers, opts = DEFAULT_HEADERS_OPTS)
191
+ @conn << format_headers(headers, !opts[:empty_response])
192
+ end
193
+
194
+ # Sends a response body chunk. If no headers were sent, default headers are
195
+ # sent using #send_headers. if the done option is true(thy), an empty chunk
196
+ # will be sent to signal response completion to the client.
197
+ # @param chunk [String] response body chunk
198
+ # @param done [boolean] whether the response is completed
199
+ # @return [void]
200
+ def send_chunk(chunk, done: false)
201
+ data = +"#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
202
+ data << "0\r\n\r\n" if done
203
+ @conn << data
204
+ end
205
+
206
+ # Finishes the response to the current request. If no headers were sent,
207
+ # default headers are sent using #send_headers.
208
+ # @return [void]
209
+ def finish
210
+ @conn << "0\r\n\r\n"
211
+ end
212
+
213
+ def close
214
+ @conn.close
215
+ end
216
+
217
+ private
218
+
219
+ # Formats response headers. If empty_response is true(thy), the response
220
+ # status code will default to 204, otherwise to 200.
221
+ # @param headers [Hash] response headers
222
+ # @param empty_response [boolean] whether a response body will be sent
223
+ # @return [String] formatted response headers
224
+ def format_headers(headers, body)
225
+ status = headers[':status'] || (body ? 200 : 204)
226
+ data = format_status_line(body, status)
227
+
228
+ headers.each do |k, v|
229
+ next if k =~ /^:/
230
+
231
+ format_header_lines(k, v)
232
+ end
233
+ data << "\r\n"
234
+ end
235
+
236
+ def format_header_lines(key, value)
237
+ if value.is_a?(Array)
238
+ value.each { |item| data << "#{key}: #{item}\r\n" }
239
+ else
240
+ data << "#{key}: #{value}\r\n"
241
+ end
242
+ end
243
+
244
+ def format_status_line(body, status)
245
+ if !body
246
+ empty_status_line(status)
247
+ else
248
+ with_body_status_line(status, body)
249
+ end
250
+ end
251
+
252
+ def empty_status_line(status)
253
+ if status == 204
254
+ +"HTTP/1.1 #{status}\r\n"
255
+ else
256
+ +"HTTP/1.1 #{status}\r\nContent-Length: 0\r\n"
257
+ end
258
+ end
259
+
260
+ def with_body_status_line(status, body)
261
+ if @parser.http_minor == 0
262
+ +"HTTP/1.0 #{status}\r\nContent-Length: #{body.bytesize}\r\n"
263
+ else
264
+ +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ export_default :Protocol
4
+
5
+ require 'http/2'
6
+
7
+ Stream = import './http2_stream'
8
+
9
+ # HTTP2 API
10
+ class Protocol
11
+ def self.upgrade_each(socket, opts, headers, &block)
12
+ adapter = new(socket, opts, headers)
13
+ adapter.each(&block)
14
+ end
15
+
16
+ def initialize(conn, opts, upgrade_headers = nil)
17
+ @conn = conn
18
+ @opts = opts
19
+ @upgrade_headers = upgrade_headers
20
+
21
+ @interface = ::HTTP2::Server.new
22
+ @connection_fiber = Fiber.current
23
+ @interface.on(:frame, &method(:send_frame))
24
+ @streams = {}
25
+ end
26
+
27
+ def send_frame(data)
28
+ @conn << data
29
+ rescue Exception => e
30
+ @connection_fiber.transfer e
31
+ end
32
+
33
+ UPGRADE_MESSAGE = <<~HTTP.gsub("\n", "\r\n")
34
+ HTTP/1.1 101 Switching Protocols
35
+ Connection: Upgrade
36
+ Upgrade: h2c
37
+
38
+ HTTP
39
+
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
48
+
49
+ # Iterates over incoming requests
50
+ def each(&block)
51
+ @interface.on(:stream) { |stream| start_stream(stream, &block) }
52
+ upgrade if @upgrade_headers
53
+
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
63
+
64
+ def start_stream(stream, &block)
65
+ stream = Stream.new(stream, &block)
66
+ @streams[stream] = true
67
+ end
68
+
69
+ def finalize_client_loop
70
+ @interface = nil
71
+ @streams.each_key(&:stop)
72
+ @conn.close
73
+ end
74
+
75
+ def close
76
+ @conn.close
77
+ end
78
+ end