dialed 0.0.1
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/lib/dialed/http/client.rb +90 -0
- data/lib/dialed/http/connection.rb +109 -0
- data/lib/dialed/http/connection_builder.rb +176 -0
- data/lib/dialed/http/dialer.rb +96 -0
- data/lib/dialed/http/direct_connection.rb +25 -0
- data/lib/dialed/http/explicit_client.rb +18 -0
- data/lib/dialed/http/nil_connection.rb +53 -0
- data/lib/dialed/http/operator.rb +62 -0
- data/lib/dialed/http/proxy_uri.rb +27 -0
- data/lib/dialed/http/request.rb +39 -0
- data/lib/dialed/http/response/body.rb +60 -0
- data/lib/dialed/http/response/every_body.rb +8 -0
- data/lib/dialed/http/response/json_body.rb +28 -0
- data/lib/dialed/http/response.rb +62 -0
- data/lib/dialed/http/tunneled_connection.rb +47 -0
- data/lib/dialed/refinements/presence.rb +59 -0
- data/lib/dialed/version.rb +5 -0
- data/lib/dialed.rb +49 -0
- metadata +132 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6d782b0da7b47eb1f23159a03c8ff84b96f619641586f4d4e63c344912d84d06
|
4
|
+
data.tar.gz: c83dc4502bb902e2d7dc0103d27c0dea56f32c2ec324b0eeb993bbe4744b28b9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cc6952bb3d6f5119b9e1c9288c7ad86b5f63db88e4a634749091c1473c33b9a762b9c8a7db5ed5b5b692f0acfcfbaf757362de9fc89837afc6c9141f79d39cb2
|
7
|
+
data.tar.gz: e90af846eabeebcdda0d820691faa64bf47fd6cfa4f35c83f275fa151f45855b50007a4f469777f811e1d5138f59d29956aa77d64ac0dd8be2f154cf25231e51
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dialed
|
4
|
+
module HTTP
|
5
|
+
class Client
|
6
|
+
def self.build(&block)
|
7
|
+
ExplicitClient.new create_connection_builder(&block)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.create_connection_builder(&block)
|
11
|
+
if block_given?
|
12
|
+
connection_builder = ConnectionBuilder.new
|
13
|
+
block.call(connection_builder)
|
14
|
+
return connection_builder
|
15
|
+
end
|
16
|
+
ConnectionBuilder.apply_defaults
|
17
|
+
end
|
18
|
+
|
19
|
+
def close
|
20
|
+
return if @closed
|
21
|
+
|
22
|
+
@closed = true
|
23
|
+
with_dialer(&:hangup!)
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_accessor :waiter
|
27
|
+
|
28
|
+
def initialize(connection_builder = ConnectionBuilder.apply_defaults)
|
29
|
+
@connection_builder = connection_builder
|
30
|
+
@async_task_count = 0
|
31
|
+
@closed = false
|
32
|
+
end
|
33
|
+
|
34
|
+
def get(location, query: {}, **kwargs)
|
35
|
+
with_dialer do |dialer|
|
36
|
+
response = dialer.call('GET', location, **kwargs)
|
37
|
+
response
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def async(&block)
|
42
|
+
Async do |task|
|
43
|
+
waiter = Async::Waiter.new(parent: task)
|
44
|
+
waiting_client = dup
|
45
|
+
waiting_client.waiter = waiter
|
46
|
+
arr = []
|
47
|
+
implicit = block.call(waiting_client, arr)
|
48
|
+
if arr.empty?
|
49
|
+
waiter.wait(waiter.instance_variable_get(:@done).count)
|
50
|
+
implicit
|
51
|
+
else
|
52
|
+
enum = Enumerator::Lazy.new(arr) do |yielder, *values|
|
53
|
+
if values.size == 1
|
54
|
+
value = values.first
|
55
|
+
value.wait
|
56
|
+
yielder << value.result
|
57
|
+
else
|
58
|
+
values.each(&:wait)
|
59
|
+
yielder << values.map(&:result)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
enum
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
attr_reader :connection_builder
|
68
|
+
|
69
|
+
def with_dialer(&block)
|
70
|
+
if waiter
|
71
|
+
waiter.async do
|
72
|
+
fetch_dialer(&block)
|
73
|
+
end
|
74
|
+
elsif Async::Task.current?
|
75
|
+
fetch_dialer do |dialer|
|
76
|
+
block.call(dialer)
|
77
|
+
end
|
78
|
+
else
|
79
|
+
Sync do
|
80
|
+
fetch_dialer do |dialer|
|
81
|
+
dialer.start_session do |session|
|
82
|
+
block.call(session)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dialed
|
4
|
+
module HTTP
|
5
|
+
class Connection
|
6
|
+
attr_reader :configuration
|
7
|
+
|
8
|
+
delegate :ssl_context, to: :configuration
|
9
|
+
delegate :uri, :version, to: :configuration, prefix: :remote
|
10
|
+
delegate :host, :port, to: :remote_host
|
11
|
+
delegate :authority, :scheme, to: :remote_uri
|
12
|
+
|
13
|
+
delegate :version, :http2?, :http1?, to: :internal_connection
|
14
|
+
delegate :call, to: :internal_connection
|
15
|
+
|
16
|
+
alias remote_host remote_uri
|
17
|
+
|
18
|
+
def initialize(configuration)
|
19
|
+
@semaphore = Async::Semaphore.new
|
20
|
+
# @semaphore2 = Async::Semaphore.new
|
21
|
+
@configuration = configuration
|
22
|
+
end
|
23
|
+
|
24
|
+
def ping
|
25
|
+
internal_connection.send_ping(SecureRandom.bytes(8))
|
26
|
+
end
|
27
|
+
|
28
|
+
def address
|
29
|
+
"#{host}:#{port}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def closed?
|
33
|
+
internal_connection.closed?
|
34
|
+
end
|
35
|
+
|
36
|
+
def open?
|
37
|
+
!closed?
|
38
|
+
end
|
39
|
+
|
40
|
+
# def call(...)
|
41
|
+
# Sync do
|
42
|
+
# @semaphore2.acquire do
|
43
|
+
# Sync do
|
44
|
+
# reconnect! if closed?
|
45
|
+
# end
|
46
|
+
# internal_connection.call(...)
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
|
51
|
+
def connect
|
52
|
+
open?
|
53
|
+
# !!internal_connection
|
54
|
+
rescue StandardError => e
|
55
|
+
@internal_connection = NilConnection.new
|
56
|
+
raise e
|
57
|
+
end
|
58
|
+
|
59
|
+
def nil_connection?
|
60
|
+
false
|
61
|
+
end
|
62
|
+
|
63
|
+
def reconnect!
|
64
|
+
@semaphore.acquire do
|
65
|
+
@internal_connection = create_internal_connection
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def close
|
70
|
+
raise NotImplementedError, 'Subclasses must implement close'
|
71
|
+
end
|
72
|
+
|
73
|
+
protected
|
74
|
+
|
75
|
+
def create_internal_connection
|
76
|
+
raise NotImplementedError, 'Subclasses must implement create_internal_connection'
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# A semaphore is used for now to prevent a stampede of opening new connections if multiple
|
82
|
+
# requests are being made concurrently and the connection does not yet exist. Unclear if this is needed
|
83
|
+
# after the connection has been created and/or if async-http handles its own isolation
|
84
|
+
def internal_connection
|
85
|
+
@semaphore.acquire do
|
86
|
+
__fetch_internal_connection
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def __fetch_internal_connection
|
91
|
+
@internal_connection = create_internal_connection if needs_new_connection?
|
92
|
+
@internal_connection
|
93
|
+
end
|
94
|
+
|
95
|
+
# Instead, use the instance variable directly:
|
96
|
+
def needs_new_connection?
|
97
|
+
@internal_connection.nil? || @internal_connection.closed? # Correct - Checks raw state
|
98
|
+
end
|
99
|
+
|
100
|
+
def async_http_protocol
|
101
|
+
case remote_version
|
102
|
+
in :h2 then Async::HTTP::Protocol::HTTP2
|
103
|
+
in :http11 | :http10 | :http1 then Async::HTTP::Protocol::HTTP1
|
104
|
+
else raise "Unsupported protocol: #{remote_version}. Must be either :h2 or :http11 or :http1 or :http10"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dialed
|
4
|
+
module HTTP
|
5
|
+
class ConnectionBuilder
|
6
|
+
DirectConnectionConfiguration = Data.define(:uri, :version, :ssl_context)
|
7
|
+
TunneledConnectionConfiguration = Data.define(:uri, :proxy_uri, :version, :ssl_context)
|
8
|
+
using Dialed::Refinements::Presence
|
9
|
+
|
10
|
+
attr_accessor :ssl_context
|
11
|
+
attr_reader :version, :scheme, :uri, :proxy_uri
|
12
|
+
|
13
|
+
delegate :host, :port, :scheme, to: :uri
|
14
|
+
delegate :host=, :port=, to: :uri
|
15
|
+
|
16
|
+
def self.apply_defaults
|
17
|
+
new.tap(&:apply_defaults!)
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@version = :h2
|
22
|
+
@proxy_uri = nil
|
23
|
+
@ssl_context = build_ssl_context
|
24
|
+
@uri = Addressable::URI.new
|
25
|
+
@uri_defaults = { scheme: 'https', port: 443 }
|
26
|
+
end
|
27
|
+
|
28
|
+
def build_ssl_context
|
29
|
+
OpenSSL::SSL::SSLContext.new.tap do |ssl_context|
|
30
|
+
ssl_context.alpn_protocols = %w[h2 http/1.1]
|
31
|
+
ssl_context.verify_hostname = true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def scheme=(scheme)
|
36
|
+
uri.scheme = scheme
|
37
|
+
case scheme
|
38
|
+
in 'http'
|
39
|
+
uri.port = 80
|
40
|
+
in 'https'
|
41
|
+
uri.port = 443
|
42
|
+
else
|
43
|
+
raise ArgumentError, "Unsupported scheme: #{scheme.inspect}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def uri_valid?
|
48
|
+
uri.send(:validate).nil?
|
49
|
+
end
|
50
|
+
|
51
|
+
def valid?
|
52
|
+
uri_valid? && !ssl_context.nil? && version.present?
|
53
|
+
end
|
54
|
+
|
55
|
+
def build
|
56
|
+
apply_defaults!
|
57
|
+
raise Dialed::Error, 'Cannot build. Invalid' unless valid?
|
58
|
+
|
59
|
+
if proxy_uri
|
60
|
+
configuration = TunneledConnectionConfiguration.new(
|
61
|
+
uri: uri,
|
62
|
+
version: version,
|
63
|
+
ssl_context: ssl_context,
|
64
|
+
proxy_uri: proxy_uri
|
65
|
+
)
|
66
|
+
TunneledConnection.new(configuration)
|
67
|
+
else
|
68
|
+
configuration = DirectConnectionConfiguration.new(
|
69
|
+
uri: uri,
|
70
|
+
version: version,
|
71
|
+
ssl_context: ssl_context
|
72
|
+
)
|
73
|
+
DirectConnection.new(configuration)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def cert_store
|
78
|
+
ssl_context.cert_store ||= OpenSSL::X509::Store.new
|
79
|
+
end
|
80
|
+
|
81
|
+
def alpn_protocols=(protocols)
|
82
|
+
ssl_context.alpn_protocols = protocols
|
83
|
+
end
|
84
|
+
|
85
|
+
def alpn_protocols
|
86
|
+
ssl_context.alpn_protocols
|
87
|
+
end
|
88
|
+
|
89
|
+
def cert_store=(cert_store)
|
90
|
+
ssl_context.cert_store = cert_store
|
91
|
+
end
|
92
|
+
|
93
|
+
def add_certificate(path)
|
94
|
+
pathname = Pathname(path)
|
95
|
+
pathname = Pathname(File.expand_path(path)) if pathname.relative?
|
96
|
+
|
97
|
+
certificate = OpenSSL::X509::Certificate.new(pathname.read)
|
98
|
+
cert_store.add_cert(certificate)
|
99
|
+
self
|
100
|
+
end
|
101
|
+
|
102
|
+
def version=(version)
|
103
|
+
version = version.to_s
|
104
|
+
|
105
|
+
case version
|
106
|
+
in '1.0' | '10' | 1 | 'http/1.0' | 'HTTP/1.0'
|
107
|
+
@version = :http10
|
108
|
+
self.alpn_protocols = ['http/1.0']
|
109
|
+
in '1.1' | '11' | 'http/1.1' | 'HTTP/1.1'
|
110
|
+
@version = :http11
|
111
|
+
self.alpn_protocols = ['http/1.1']
|
112
|
+
in '2.0' | '20' | 2 | 'http/2' | 'HTTP/2'
|
113
|
+
@version = :h2
|
114
|
+
self.alpn_protocols = ['h2']
|
115
|
+
else
|
116
|
+
raise ArgumentError, "Unsupported HTTP version: #{version.inspect}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def verify_peer=(verify_peer)
|
121
|
+
ssl_context.verify_mode = (OpenSSL::SSL::VERIFY_PEER if verify_peer)
|
122
|
+
end
|
123
|
+
|
124
|
+
def verify_none=(verify_none)
|
125
|
+
ssl_context.verify_mode = (OpenSSL::SSL::VERIFY_NONE if verify_none)
|
126
|
+
end
|
127
|
+
|
128
|
+
alias insecure= verify_none=
|
129
|
+
|
130
|
+
def uri=(uri)
|
131
|
+
input = Addressable::URI.parse(uri)
|
132
|
+
input_no_path = input.dup
|
133
|
+
input_no_path.path = nil
|
134
|
+
input_no_path.query = nil
|
135
|
+
input_no_path.fragment = nil
|
136
|
+
@uri = input_no_path
|
137
|
+
end
|
138
|
+
|
139
|
+
def proxy(&block)
|
140
|
+
if block_given?
|
141
|
+
uri = ProxyUri.new.tap(&block)
|
142
|
+
uri.infer_scheme_if_missing!
|
143
|
+
raise ArgumentError, "Invalid proxy URI: #{uri.inspect}" unless uri.valid?
|
144
|
+
|
145
|
+
@proxy_uri = uri
|
146
|
+
|
147
|
+
self
|
148
|
+
else
|
149
|
+
@proxy_uri
|
150
|
+
end
|
151
|
+
self
|
152
|
+
end
|
153
|
+
|
154
|
+
def proxy=(proxy_uri)
|
155
|
+
parsed = Addressable::URI.parse(proxy_uri)
|
156
|
+
proxy do |self_proxy|
|
157
|
+
self_proxy.merge!(parsed)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
alias proxy_uri= proxy=
|
162
|
+
|
163
|
+
def apply_defaults!
|
164
|
+
defaults_to_apply = @uri_defaults.select do |key, _value|
|
165
|
+
uri_value = uri.send(key)
|
166
|
+
next if uri_value.present?
|
167
|
+
|
168
|
+
true
|
169
|
+
end
|
170
|
+
|
171
|
+
uri.merge!(defaults_to_apply)
|
172
|
+
self
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dialed
|
4
|
+
module HTTP
|
5
|
+
class Dialer
|
6
|
+
attr_reader :connection
|
7
|
+
|
8
|
+
def initialize(builder = ConnectionBuilder.apply_defaults, lazy: true, &block)
|
9
|
+
@builder = builder
|
10
|
+
@connection = NilConnection.new
|
11
|
+
@lazy = lazy
|
12
|
+
start_session(&block) if block_given?
|
13
|
+
end
|
14
|
+
|
15
|
+
def connected?
|
16
|
+
connection.open?
|
17
|
+
end
|
18
|
+
|
19
|
+
def lazy?
|
20
|
+
@lazy
|
21
|
+
end
|
22
|
+
|
23
|
+
def disconnected?
|
24
|
+
connection.closed?
|
25
|
+
end
|
26
|
+
|
27
|
+
def current_host
|
28
|
+
return nil unless on_a_call?
|
29
|
+
|
30
|
+
connection.remote_host
|
31
|
+
end
|
32
|
+
|
33
|
+
def start_session(&block)
|
34
|
+
attempt_connection!
|
35
|
+
block.call(self)
|
36
|
+
ensure
|
37
|
+
hangup!
|
38
|
+
end
|
39
|
+
|
40
|
+
def call(verb, location, *args, proxy_uri: nil, **kwargs)
|
41
|
+
location_uri = Addressable::URI.parse(location)
|
42
|
+
request = Request.new(verb, location_uri.path, *args, **kwargs)
|
43
|
+
response = (
|
44
|
+
if connection.open?
|
45
|
+
response = request.call(connection)
|
46
|
+
Response.new(response)
|
47
|
+
elsif lazy?
|
48
|
+
@builder.uri = location_uri
|
49
|
+
@builder.proxy_uri = proxy_uri if proxy_uri
|
50
|
+
success = attempt_connection!
|
51
|
+
raise Dialed::Error, "Failed to connect to #{location}. connection status: #{connection.open?}" unless success
|
52
|
+
|
53
|
+
Response.new(request.call(connection))
|
54
|
+
else
|
55
|
+
success = attempt_connection!
|
56
|
+
raise Dialed::Error, "Failed to connect to #{location}. connection status: #{connection.open?}" unless success
|
57
|
+
|
58
|
+
Response.new(request.call(connection))
|
59
|
+
end
|
60
|
+
|
61
|
+
)
|
62
|
+
|
63
|
+
return response unless block_given?
|
64
|
+
|
65
|
+
begin
|
66
|
+
yield response
|
67
|
+
ensure
|
68
|
+
response.close
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def hangup!
|
73
|
+
connection.close if connection.open?
|
74
|
+
@connection = NilConnection.new
|
75
|
+
end
|
76
|
+
|
77
|
+
def ready?
|
78
|
+
raise 'Expected connection not to be actually nil' if connection.nil?
|
79
|
+
return false if connection.nil_connection?
|
80
|
+
return false if connection.open?
|
81
|
+
return false unless @builder.valid?
|
82
|
+
|
83
|
+
true
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def attempt_connection!
|
89
|
+
return true if ready?
|
90
|
+
|
91
|
+
@connection = @builder.build
|
92
|
+
connection.connect
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dialed
|
4
|
+
module HTTP
|
5
|
+
class DirectConnection < Connection
|
6
|
+
def close
|
7
|
+
internal_connection.close
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def create_internal_connection
|
13
|
+
remote_endpoint = Async::HTTP::Endpoint.parse(
|
14
|
+
remote_uri.to_s,
|
15
|
+
protocol: async_http_protocol,
|
16
|
+
ssl_context: ssl_context,
|
17
|
+
alpn_protocols: ssl_context.alpn_protocols
|
18
|
+
)
|
19
|
+
|
20
|
+
remote_sock = remote_endpoint.connect
|
21
|
+
async_http_protocol.client(remote_sock)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dialed
|
4
|
+
module HTTP
|
5
|
+
class ExplicitClient < Client
|
6
|
+
def initialize(connection_builder)
|
7
|
+
super
|
8
|
+
@dialer = Dialer.new(connection_builder, lazy: false)
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def fetch_dialer(&block)
|
14
|
+
block.call(@dialer)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dialed
|
4
|
+
module HTTP
|
5
|
+
class NilConnection < Connection
|
6
|
+
def initialize
|
7
|
+
super(uri: nil, version: nil, ssl_context: nil)
|
8
|
+
end
|
9
|
+
|
10
|
+
def remote_host
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def remote_uri
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def ssl_context
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def http2?
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
def http1?
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
def open?
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
34
|
+
def nil_connection?
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
def closed?
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def internal_connection
|
45
|
+
Class.new do
|
46
|
+
def call(...)
|
47
|
+
raise Dialed::Net::HTTP::ConnectionError, 'Tried to call a nil connection'
|
48
|
+
end
|
49
|
+
end.new
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dialed
|
4
|
+
module HTTP
|
5
|
+
class Operator
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@dialers = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def request_call(&block)
|
13
|
+
connection_builder = block.call(ConnectionBuilder.new) if block_given?
|
14
|
+
connection_builder ||= ConnectionBuilder.apply_defaults
|
15
|
+
end
|
16
|
+
|
17
|
+
def singleplex_dialers
|
18
|
+
@dialers
|
19
|
+
.reject { |_, dialer| dialer.disconnected? }
|
20
|
+
.select { |_, dialer| dialer.singleplex? }
|
21
|
+
end
|
22
|
+
|
23
|
+
def multiplex_dialers
|
24
|
+
@dialers.reject { |_, dialer| dialer.disconnected? }
|
25
|
+
.select { |_, dialer| dialer.multiplex? }
|
26
|
+
end
|
27
|
+
|
28
|
+
def checkout_dialer(connection_builder, &block)
|
29
|
+
full_connection_uri = connection_builder.full_connection_uri
|
30
|
+
plex_type = connection_builder.plex_type
|
31
|
+
case [full_connection_uri, plex_type, registry.keys]
|
32
|
+
in [URI => uri, :h2, [*, ^uri, *]]
|
33
|
+
block.call multiplex_dialers.fetch(uri)
|
34
|
+
in [URI => uri, :h1 | :h11, [*, ^uri, *]]
|
35
|
+
# not thread safe. Use async gem as it is not implemented with threads
|
36
|
+
dialer = remove_dialer(uri)
|
37
|
+
raise Dialed::Error, 'Dialer not found when it was expected to be. Is it possible you are using multiple threads?' unless dialer
|
38
|
+
|
39
|
+
block.call dialer
|
40
|
+
register_dialer dialer
|
41
|
+
in [URI => uri, Symbol, Array]
|
42
|
+
register_dialer dialer
|
43
|
+
block.call dialer
|
44
|
+
else
|
45
|
+
raise Dialed::Error, "Unknown Dialer type: #{full_connection_uri}, #{plex_type}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def fetch_dialer(_uri, &)
|
50
|
+
Dialer.new(connection_builder, &)
|
51
|
+
end
|
52
|
+
|
53
|
+
def remove_dialer(dialer)
|
54
|
+
@dialers.delete(dialer.full_connection_uri)
|
55
|
+
end
|
56
|
+
|
57
|
+
def register_dialer(dialer)
|
58
|
+
@dialers[dialer.full_connection_uri] = dialer
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dialed
|
4
|
+
module HTTP
|
5
|
+
class ProxyUri < SimpleDelegator
|
6
|
+
using Dialed::Refinements::Presence
|
7
|
+
|
8
|
+
def self.parse(string)
|
9
|
+
new(Addressable::URI.parse(string))
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(uri = Addressable::URI.parse('http://invalid.invalid'))
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def infer_scheme_if_missing!
|
17
|
+
self.scheme ||= 'http'
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid?
|
21
|
+
host.present? &&
|
22
|
+
port.present? &&
|
23
|
+
scheme.present?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Dialed
|
2
|
+
module HTTP
|
3
|
+
class Request
|
4
|
+
attr_reader :verb, :path, :args, :options
|
5
|
+
|
6
|
+
def initialize(verb, path, *args, **options)
|
7
|
+
@verb = verb
|
8
|
+
@path = path
|
9
|
+
@args = args
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(connection)
|
14
|
+
# protocol_request = Protocol::HTTP::Request[
|
15
|
+
# verb,
|
16
|
+
# path,
|
17
|
+
# *args,
|
18
|
+
# version: connection.version,
|
19
|
+
# headers: options[:headers],
|
20
|
+
# method: verb.upcase,
|
21
|
+
# authority: connection.authority,
|
22
|
+
# scheme: connection.scheme,
|
23
|
+
# ]
|
24
|
+
protocol_request = Protocol::HTTP::Request.new.tap do |r|
|
25
|
+
r.path = path
|
26
|
+
r.method = verb.upcase
|
27
|
+
r.headers = options[:headers] if options[:headers]
|
28
|
+
r.version = connection.version
|
29
|
+
r.authority = connection.authority
|
30
|
+
r.scheme = connection.scheme
|
31
|
+
r.body = options[:body]
|
32
|
+
r.protocol = options[:protocol] if options[:protocol]
|
33
|
+
end
|
34
|
+
|
35
|
+
protocol_request.call(connection)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dialed
|
4
|
+
module HTTP
|
5
|
+
class Response::Body
|
6
|
+
def initialize(internal_body)
|
7
|
+
@internal_body = internal_body
|
8
|
+
@file = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def read
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_io
|
16
|
+
@to_io ||= begin
|
17
|
+
file = nil
|
18
|
+
begin
|
19
|
+
file = ::Tempfile.create(anonymous: true)
|
20
|
+
internal_body.each do |chunk|
|
21
|
+
file.write(chunk)
|
22
|
+
end
|
23
|
+
file.rewind
|
24
|
+
file
|
25
|
+
rescue => e
|
26
|
+
file.close
|
27
|
+
raise e
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def http2?; end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def buffered_internal_body
|
37
|
+
@buffered_internal_body ||= begin
|
38
|
+
if @file
|
39
|
+
return to_io.tap(&:rewind)
|
40
|
+
.read
|
41
|
+
end
|
42
|
+
internal_body.read
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def internal_body
|
47
|
+
if @internal_body.respond_to?(:rewindable?) && @internal_body.rewindable?
|
48
|
+
@internal_body
|
49
|
+
.rewind
|
50
|
+
# elsif @internal_body.is_a?(Async::HTTP::Protocol::HTTP2::Input)
|
51
|
+
elsif @internal_body.is_a?(Protocol::HTTP::Body::Buffered)
|
52
|
+
@internal_body.rewind
|
53
|
+
# buffered_body = @internal_body.finish
|
54
|
+
# buffered_body.rewind
|
55
|
+
end
|
56
|
+
@internal_body
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Dialed
|
2
|
+
module HTTP
|
3
|
+
class Response::JsonBody < Response::Body
|
4
|
+
alias to_json to_s
|
5
|
+
|
6
|
+
def read
|
7
|
+
# already memoized
|
8
|
+
buffered_internal_body
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
read.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_h
|
16
|
+
@__to_h ||= JSON.parse(read, symbolize_names: true)
|
17
|
+
end
|
18
|
+
|
19
|
+
def as_json
|
20
|
+
@__as_json ||= JSON.parse(read, symbolize_names: false)
|
21
|
+
end
|
22
|
+
|
23
|
+
def deconstruct_keys(keys)
|
24
|
+
keys ? to_h.slice(*keys) : to_h
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'async/http'
|
4
|
+
::Async::HTTP::Body::Reader.module_eval do
|
5
|
+
def buffered!
|
6
|
+
if @body
|
7
|
+
@body = @body.finish
|
8
|
+
end
|
9
|
+
|
10
|
+
return self
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Dialed
|
15
|
+
module HTTP
|
16
|
+
class Response
|
17
|
+
delegate :to_io, :read, :to_h, :to_s, to: :body
|
18
|
+
|
19
|
+
def initialize(internal_response)
|
20
|
+
@internal_response = internal_response
|
21
|
+
@notifier = Async::Notification.new
|
22
|
+
buffer!
|
23
|
+
end
|
24
|
+
|
25
|
+
def body
|
26
|
+
@body ||= body_klass.new(internal_response.body)
|
27
|
+
end
|
28
|
+
|
29
|
+
def body_klass
|
30
|
+
case headers
|
31
|
+
in { 'content-type': 'application/json' }
|
32
|
+
JsonBody
|
33
|
+
else
|
34
|
+
EveryBody
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def buffer!
|
39
|
+
@internal_response.buffered!
|
40
|
+
end
|
41
|
+
|
42
|
+
def http2?
|
43
|
+
internal_response.version == 'HTTP/2'
|
44
|
+
end
|
45
|
+
|
46
|
+
def http11?
|
47
|
+
internal_response.version == 'HTTP/1.1'
|
48
|
+
end
|
49
|
+
|
50
|
+
def headers
|
51
|
+
@headers ||= internal_response
|
52
|
+
&.headers
|
53
|
+
&.to_h
|
54
|
+
&.transform_keys(&:to_sym)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_reader :internal_response
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dialed
|
4
|
+
module HTTP
|
5
|
+
class TunneledConnection < Connection
|
6
|
+
ProxyConnectError = Class.new(StandardError)
|
7
|
+
|
8
|
+
delegate :proxy_uri, to: :configuration
|
9
|
+
|
10
|
+
def close
|
11
|
+
internal_connection.close
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def create_internal_connection
|
17
|
+
proxy_connection = create_proxy_connection
|
18
|
+
remote_endpoint = Async::HTTP::Endpoint.parse(
|
19
|
+
remote_uri.to_s,
|
20
|
+
protocol: async_http_protocol,
|
21
|
+
ssl_context: ssl_context,
|
22
|
+
alpn_protocols: ssl_context.alpn_protocols
|
23
|
+
)
|
24
|
+
|
25
|
+
proxy = Async::HTTP::Proxy.new(proxy_connection, address)
|
26
|
+
proxied_endpoint = proxy.wrap_endpoint(remote_endpoint)
|
27
|
+
|
28
|
+
proxied_sock = (
|
29
|
+
begin
|
30
|
+
proxied_endpoint.connect
|
31
|
+
rescue Errno::ECONNRESET => e
|
32
|
+
proxy_connection.close
|
33
|
+
raise ProxyConnectError, e
|
34
|
+
end
|
35
|
+
)
|
36
|
+
async_http_protocol.client(proxied_sock)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def create_proxy_connection
|
42
|
+
proxy_endpoint = Async::HTTP::Endpoint.parse(proxy_uri.to_s)
|
43
|
+
Async::HTTP::Client.new(proxy_endpoint)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Dialed
|
2
|
+
module Refinements
|
3
|
+
module PresenceMethods
|
4
|
+
def present?
|
5
|
+
!nil? && !empty?
|
6
|
+
end
|
7
|
+
|
8
|
+
def presence
|
9
|
+
self if present?
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Presence
|
14
|
+
refine NilClass do
|
15
|
+
def present?
|
16
|
+
false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
refine String do
|
21
|
+
def present?
|
22
|
+
!nil? && !empty?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
refine Integer do
|
27
|
+
def present?
|
28
|
+
!nil?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
refine Array do
|
33
|
+
def present?
|
34
|
+
!nil? && !empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
def presence
|
38
|
+
self if present?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
refine Hash do
|
43
|
+
def present?
|
44
|
+
!nil? && !empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
def presence
|
48
|
+
self if present?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
refine Symbol do
|
53
|
+
def present?
|
54
|
+
!nil? && !empty?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/dialed.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'backports/3.2.0/data'
|
5
|
+
require 'zeitwerk'
|
6
|
+
|
7
|
+
|
8
|
+
autoload :Pathname, 'pathname'
|
9
|
+
autoload :Async, 'async'
|
10
|
+
autoload :Tempfile, 'tempfile'
|
11
|
+
autoload :Open3, 'open3'
|
12
|
+
autoload :OpenSSL, 'openssl'
|
13
|
+
autoload :Benchmark, 'benchmark'
|
14
|
+
autoload :Base64, 'base64'
|
15
|
+
autoload :SimpleDelegator, 'delegate'
|
16
|
+
|
17
|
+
require 'active_support/core_ext/module/delegation'
|
18
|
+
|
19
|
+
module Addressable
|
20
|
+
autoload :URI, 'addressable/uri'
|
21
|
+
end
|
22
|
+
|
23
|
+
module Async
|
24
|
+
autoload :Barrier, 'async/barrier'
|
25
|
+
autoload :Semaphore, 'async/semaphore'
|
26
|
+
autoload :HTTP, 'async/http'
|
27
|
+
autoload :Waiter, 'async/waiter'
|
28
|
+
|
29
|
+
module HTTP
|
30
|
+
autoload :Client, 'async/http/client.rb'
|
31
|
+
autoload :Proxy, 'async/http/proxy.rb'
|
32
|
+
autoload :Endpoint, 'async/http/endpoint.rb'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
loader = Zeitwerk::Loader.for_gem
|
37
|
+
loader.inflector.inflect(
|
38
|
+
'io' => 'IO',
|
39
|
+
'http' => 'HTTP'
|
40
|
+
)
|
41
|
+
loader.ignore('test/**/*')
|
42
|
+
loader.ignore('bin/**/*')
|
43
|
+
loader.setup
|
44
|
+
|
45
|
+
module Dialed
|
46
|
+
class Error < StandardError; end
|
47
|
+
|
48
|
+
Client = HTTP::Client
|
49
|
+
end
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dialed
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- " David Gillis"
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-02-28 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: activesupport
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: addressable
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: async-http
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: bundler
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: zeitwerk
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
type: :runtime
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
description: Supports HTTP/2, HTTP/1.X, HTTP proxying, connection pooling, concurrent
|
83
|
+
requests, and lots more
|
84
|
+
email:
|
85
|
+
- david@flipmine.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- lib/dialed.rb
|
91
|
+
- lib/dialed/http/client.rb
|
92
|
+
- lib/dialed/http/connection.rb
|
93
|
+
- lib/dialed/http/connection_builder.rb
|
94
|
+
- lib/dialed/http/dialer.rb
|
95
|
+
- lib/dialed/http/direct_connection.rb
|
96
|
+
- lib/dialed/http/explicit_client.rb
|
97
|
+
- lib/dialed/http/nil_connection.rb
|
98
|
+
- lib/dialed/http/operator.rb
|
99
|
+
- lib/dialed/http/proxy_uri.rb
|
100
|
+
- lib/dialed/http/request.rb
|
101
|
+
- lib/dialed/http/response.rb
|
102
|
+
- lib/dialed/http/response/body.rb
|
103
|
+
- lib/dialed/http/response/every_body.rb
|
104
|
+
- lib/dialed/http/response/json_body.rb
|
105
|
+
- lib/dialed/http/tunneled_connection.rb
|
106
|
+
- lib/dialed/refinements/presence.rb
|
107
|
+
- lib/dialed/version.rb
|
108
|
+
homepage: https://github.com/gillisd/dialed
|
109
|
+
licenses:
|
110
|
+
- MIT
|
111
|
+
metadata:
|
112
|
+
homepage_uri: https://github.com/gillisd/dialed
|
113
|
+
source_code_uri: https://github.com/gillisd/dialed
|
114
|
+
rubygems_mfa_required: 'true'
|
115
|
+
rdoc_options: []
|
116
|
+
require_paths:
|
117
|
+
- lib
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: 2.7.5
|
123
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
requirements: []
|
129
|
+
rubygems_version: 3.6.5
|
130
|
+
specification_version: 4
|
131
|
+
summary: A modern, ergonomic HTTP client built on top of async-http
|
132
|
+
test_files: []
|