1
2
3
4
5
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
|
# -*- frozen-string-literal: true -*-
module Plum
class Client
DEFAULT_CONFIG = {
https: true
}.freeze
attr_reader :host, :port, :config
attr_reader :socket
def self.start(host, port, config = {}, &block)
client = self.new(host, port, config)
client.start(&block)
end
def initialize(host, port, config = {})
@host = host
@port = port
@config = DEFAULT_CONFIG.merge(config)
@response_handlers = {}
@responses = {}
end
def start
raise IOError, "Session already started" if @started
_start
if block_given?
begin
ret = yield(self)
wait
return ret
ensure
close
end
end
self
end
def wait
while !@responses.empty?
_succ
end
end
def close
begin
@plum.close if @plum
ensure
@socket.close if @socket
end
end
def request_async(headers, body = nil, &block)
stream = @plum.open_stream
response = Response.new
@responses[stream] = response
if body
stream.send_headers(headers, end_stream: false)
stream.send_data(body, end_stream: true)
else
stream.send_headers(headers, end_stream: true)
end
if block_given?
@response_handlers[stream] = block
end
response
end
def request(headers, body = nil)
response = request_async(headers, body)
_succ while !response.finished?
response
end
%w(GET POST HEAD PUT DELETE).each { |method|
define_method(method.downcase.to_sym) do |headers = {}|
request(headers)
end
define_method(:"#{method.downcase}_async") do |headers = {}, &block|
request_async(headers, &block)
end
}
def https?
!!@config[:https]
end
private
def _start
@started = true
sock = TCPSocket.open(host, port)
if https?
ctx = @config[:ssl_context] || new_ssl_ctx
sock = OpenSSL::SSL::SSLSocket.new(sock, ctx)
sock.sync_close = true
sock.connect
if ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
sock.post_connection_check(@config[:hostname] || @host)
end
end
@socket = sock
@plum = setup_plum(sock)
end
def setup_plum(sock)
local_settings = {
enable_push: 0,
initial_window_size: (1 << 30) - 1,
}
plum = ClientConnection.new(sock.method(:write), local_settings)
plum.on(:protocol_error) { |ex| raise ex }
plum.on(:stream_error) { |stream, ex| raise ex }
plum.on(:headers) { |stream, headers|
response = @responses[stream]
response._headers(headers)
}
plum.on(:data) { |stream, chunk|
response = @responses[stream]
response._chunk(chunk)
}
plum.on(:end_stream) { |stream|
response = @responses.delete(stream)
response._finish
if handler = @response_handlers.delete(stream)
handler.call(response)
end
}
plum
end
def _succ
@plum << @socket.readpartial(1024)
end
def new_ssl_ctx
ctx = OpenSSL::SSL::SSLContext.new
ctx.ssl_version = :TLSv1_2
if ctx.respond_to?(:hostname=)
ctx.hostname = @config[:hostname] || @host
end
if ctx.respond_to?(:alpn_protocols)
ctx.alpn_protocols = ["h2", "http/1.1"]
end
if ctx.respond_to?(:npn_select_cb)
ctx.alpn_select_cb = -> protocols {
protocols.include?("h2") ? "h2" : protocols.first
}
end
ctx
end
end
end
|