mt-ruby-tls 2.4.0
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/README.md +102 -0
- data/lib/mt-ruby-tls/ssl.rb +864 -0
- data/lib/mt-ruby-tls/version.rb +5 -0
- data/lib/mt-ruby-tls.rb +6 -0
- data/mt-ruby-tls.gemspec +31 -0
- data/spec/alpn_spec.rb +415 -0
- data/spec/client.crt +31 -0
- data/spec/client.key +51 -0
- data/spec/comms_spec.rb +120 -0
- data/spec/verify_spec.rb +267 -0
- metadata +121 -0
@@ -0,0 +1,864 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ffi'
|
4
|
+
require 'ffi-compiler/loader'
|
5
|
+
require 'thread'
|
6
|
+
require 'concurrent'
|
7
|
+
|
8
|
+
|
9
|
+
module MTRubyTls
|
10
|
+
module SSL
|
11
|
+
extend FFI::Library
|
12
|
+
if FFI::Platform.windows?
|
13
|
+
begin
|
14
|
+
ffi_lib 'libeay32', 'ssleay32'
|
15
|
+
rescue LoadError
|
16
|
+
ffi_lib 'libcrypto-1_1-x64', 'libssl-1_1-x64'
|
17
|
+
end
|
18
|
+
else
|
19
|
+
ffi_lib 'ssl'
|
20
|
+
end
|
21
|
+
|
22
|
+
# Common structures
|
23
|
+
typedef :pointer, :user_data
|
24
|
+
typedef :pointer, :bio
|
25
|
+
typedef :pointer, :evp_key
|
26
|
+
typedef :pointer, :evp_key_pointer
|
27
|
+
typedef :pointer, :x509
|
28
|
+
typedef :pointer, :x509_pointer
|
29
|
+
typedef :pointer, :ssl
|
30
|
+
typedef :pointer, :ssl_ctx
|
31
|
+
typedef :int, :buffer_length
|
32
|
+
typedef :int, :pass_length
|
33
|
+
typedef :int, :read_write_flag
|
34
|
+
|
35
|
+
SSL_ST_OK = 0x03
|
36
|
+
begin
|
37
|
+
attach_function :SSL_library_init, [], :int
|
38
|
+
attach_function :SSL_load_error_strings, [], :void
|
39
|
+
attach_function :ERR_load_crypto_strings, [], :void
|
40
|
+
|
41
|
+
attach_function :SSL_state, [:ssl], :int
|
42
|
+
def self.SSL_is_init_finished(ssl)
|
43
|
+
SSL_state(ssl) == SSL_ST_OK
|
44
|
+
end
|
45
|
+
|
46
|
+
OPENSSL_V1_1 = false
|
47
|
+
rescue FFI::NotFoundError
|
48
|
+
OPENSSL_V1_1 = true
|
49
|
+
OPENSSL_INIT_LOAD_SSL_STRINGS = 0x200000
|
50
|
+
OPENSSL_INIT_NO_LOAD_SSL_STRINGS = 0x100000
|
51
|
+
attach_function :OPENSSL_init_ssl, [:uint64, :pointer], :int
|
52
|
+
|
53
|
+
attach_function :SSL_get_state, [:ssl], :int
|
54
|
+
def self.SSL_is_init_finished(ssl)
|
55
|
+
SSL_get_state(ssl) == SSL_ST_OK
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Multi-threaded support
|
60
|
+
#callback :locking_cb, [:int, :int, :string, :int], :void
|
61
|
+
#callback :thread_id_cb, [], :ulong
|
62
|
+
#attach_function :CRYPTO_num_locks, [], :int
|
63
|
+
#attach_function :CRYPTO_set_locking_callback, [:locking_cb], :void
|
64
|
+
#attach_function :CRYPTO_set_id_callback, [:thread_id_cb], :void
|
65
|
+
|
66
|
+
# InitializeDefaultCredentials
|
67
|
+
attach_function :BIO_new_mem_buf, [:string, :buffer_length], :bio
|
68
|
+
attach_function :EVP_PKEY_free, [:evp_key], :void
|
69
|
+
|
70
|
+
callback :pem_password_cb, [:pointer, :buffer_length, :read_write_flag, :user_data], :pass_length
|
71
|
+
attach_function :PEM_read_bio_PrivateKey, [:bio, :evp_key_pointer, :pem_password_cb, :user_data], :evp_key
|
72
|
+
|
73
|
+
attach_function :X509_free, [:x509], :void
|
74
|
+
attach_function :PEM_read_bio_X509, [:bio, :x509_pointer, :pem_password_cb, :user_data], :x509
|
75
|
+
|
76
|
+
attach_function :BIO_free, [:bio], :int
|
77
|
+
|
78
|
+
# GetPeerCert
|
79
|
+
attach_function :SSL_get_peer_certificate, [:ssl], :x509
|
80
|
+
|
81
|
+
|
82
|
+
# PutPlaintext
|
83
|
+
attach_function :SSL_write, [:ssl, :buffer_in, :buffer_length], :int
|
84
|
+
attach_function :SSL_get_error, [:ssl, :int], :int
|
85
|
+
|
86
|
+
|
87
|
+
# GetCiphertext
|
88
|
+
attach_function :BIO_read, [:bio, :buffer_out, :buffer_length], :int
|
89
|
+
|
90
|
+
# CanGetCiphertext
|
91
|
+
attach_function :BIO_ctrl, [:bio, :int, :long, :pointer], :long
|
92
|
+
BIO_CTRL_PENDING = 10 # opt - is their more data buffered?
|
93
|
+
def self.BIO_pending(bio)
|
94
|
+
BIO_ctrl(bio, BIO_CTRL_PENDING, 0, nil)
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
# GetPlaintext
|
99
|
+
attach_function :SSL_accept, [:ssl], :int
|
100
|
+
attach_function :SSL_read, [:ssl, :buffer_out, :buffer_length], :int
|
101
|
+
attach_function :SSL_pending, [:ssl], :int
|
102
|
+
|
103
|
+
# PutCiphertext
|
104
|
+
attach_function :BIO_write, [:bio, :buffer_in, :buffer_length], :int
|
105
|
+
|
106
|
+
# Deconstructor
|
107
|
+
attach_function :SSL_get_shutdown, [:ssl], :int
|
108
|
+
attach_function :SSL_shutdown, [:ssl], :int
|
109
|
+
attach_function :SSL_clear, [:ssl], :void
|
110
|
+
attach_function :SSL_free, [:ssl], :void
|
111
|
+
|
112
|
+
|
113
|
+
# Constructor
|
114
|
+
attach_function :BIO_s_mem, [], :pointer
|
115
|
+
attach_function :BIO_new, [:pointer], :bio
|
116
|
+
attach_function :SSL_new, [:ssl_ctx], :ssl
|
117
|
+
# r, w
|
118
|
+
attach_function :SSL_set_bio, [:ssl, :bio, :bio], :void
|
119
|
+
|
120
|
+
attach_function :SSL_set_ex_data, [:ssl, :int, :string], :int
|
121
|
+
callback :verify_callback, [:int, :x509], :int
|
122
|
+
attach_function :SSL_set_verify, [:ssl, :int, :verify_callback], :void
|
123
|
+
attach_function :SSL_connect, [:ssl], :int
|
124
|
+
|
125
|
+
# Verify callback
|
126
|
+
attach_function :X509_STORE_CTX_get_current_cert, [:pointer], :x509
|
127
|
+
attach_function :SSL_get_ex_data_X509_STORE_CTX_idx, [], :int
|
128
|
+
attach_function :X509_STORE_CTX_get_ex_data, [:pointer, :int], :ssl
|
129
|
+
attach_function :PEM_write_bio_X509, [:bio, :x509], :int
|
130
|
+
|
131
|
+
# SSL Context Class
|
132
|
+
# OpenSSL before 1.1.0 do not have these methods
|
133
|
+
# https://www.openssl.org/docs/man1.1.0/ssl/TLSv1_2_server_method.html
|
134
|
+
begin
|
135
|
+
attach_function :TLS_server_method, [], :pointer
|
136
|
+
attach_function :TLS_client_method, [], :pointer
|
137
|
+
rescue FFI::NotFoundError
|
138
|
+
attach_function :SSLv23_server_method, [], :pointer
|
139
|
+
attach_function :SSLv23_client_method, [], :pointer
|
140
|
+
|
141
|
+
def self.TLS_server_method; self.SSLv23_server_method; end
|
142
|
+
def self.TLS_client_method; self.SSLv23_client_method; end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Version can be one of:
|
146
|
+
# :SSL3, :TLS1, :TLS1_1, :TLS1_2, :TLS1_3, :TLS_MAX
|
147
|
+
begin
|
148
|
+
attach_function :SSL_CTX_set_min_proto_version, [:ssl_ctx, :int], :int
|
149
|
+
attach_function :SSL_CTX_set_max_proto_version, [:ssl_ctx, :int], :int
|
150
|
+
|
151
|
+
VERSION_SUPPORTED = true
|
152
|
+
|
153
|
+
SSL3_VERSION = 0x0300
|
154
|
+
TLS1_VERSION = 0x0301
|
155
|
+
TLS1_1_VERSION = 0x0302
|
156
|
+
TLS1_2_VERSION = 0x0303
|
157
|
+
TLS1_3_VERSION = 0x0304
|
158
|
+
TLS_MAX_VERSION = TLS1_3_VERSION
|
159
|
+
ANY_VERSION = 0
|
160
|
+
rescue FFI::NotFoundError
|
161
|
+
VERSION_SUPPORTED = false
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
attach_function :SSL_CTX_new, [:pointer], :ssl_ctx
|
166
|
+
|
167
|
+
attach_function :SSL_CTX_ctrl, [:ssl_ctx, :int, :ulong, :pointer], :long
|
168
|
+
SSL_CTRL_OPTIONS = 32
|
169
|
+
def self.SSL_CTX_set_options(ssl_ctx, op)
|
170
|
+
SSL_CTX_ctrl(ssl_ctx, SSL_CTRL_OPTIONS, op, nil)
|
171
|
+
end
|
172
|
+
SSL_CTRL_MODE = 33
|
173
|
+
def self.SSL_CTX_set_mode(ssl_ctx, op)
|
174
|
+
SSL_CTX_ctrl(ssl_ctx, SSL_CTRL_MODE, op, nil)
|
175
|
+
end
|
176
|
+
SSL_CTRL_SET_SESS_CACHE_SIZE = 42
|
177
|
+
def self.SSL_CTX_sess_set_cache_size(ssl_ctx, op)
|
178
|
+
SSL_CTX_ctrl(ssl_ctx, SSL_CTRL_SET_SESS_CACHE_SIZE, op, nil)
|
179
|
+
end
|
180
|
+
|
181
|
+
attach_function :SSL_ctrl, [:ssl, :int, :long, :pointer], :long
|
182
|
+
SSL_CTRL_SET_TLSEXT_HOSTNAME = 55
|
183
|
+
def self.SSL_set_tlsext_host_name(ssl, host_name)
|
184
|
+
name = FFI::MemoryPointer.from_string(host_name)
|
185
|
+
SSL_ctrl(ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, name)
|
186
|
+
end
|
187
|
+
|
188
|
+
# Server Name Indication (SNI) Support
|
189
|
+
# NOTE:: We've hard coded the callback here (SSL defines a NULL callback)
|
190
|
+
callback :ssl_servername_cb, [:ssl, :pointer, :pointer], :int
|
191
|
+
attach_function :SSL_CTX_callback_ctrl, [:ssl_ctx, :int, :ssl_servername_cb], :long
|
192
|
+
SSL_CTRL_SET_TLSEXT_SERVERNAME_CB = 53
|
193
|
+
def self.SSL_CTX_set_tlsext_servername_callback(ctx, callback)
|
194
|
+
SSL_CTX_callback_ctrl(ctx, SSL_CTRL_SET_TLSEXT_SERVERNAME_CB, callback)
|
195
|
+
end
|
196
|
+
|
197
|
+
attach_function :SSL_get_servername, [:ssl, :int], :string
|
198
|
+
TLSEXT_NAMETYPE_host_name = 0
|
199
|
+
|
200
|
+
attach_function :SSL_set_SSL_CTX, [:ssl, :ssl_ctx], :ssl_ctx
|
201
|
+
|
202
|
+
SSL_TLSEXT_ERR_OK = 0
|
203
|
+
SSL_TLSEXT_ERR_ALERT_WARNING = 1
|
204
|
+
SSL_TLSEXT_ERR_ALERT_FATAL = 2
|
205
|
+
SSL_TLSEXT_ERR_NOACK = 3
|
206
|
+
|
207
|
+
attach_function :SSL_CTX_use_PrivateKey_file, [:ssl_ctx, :string, :int], :int, :blocking => true
|
208
|
+
attach_function :SSL_CTX_use_PrivateKey, [:ssl_ctx, :pointer], :int
|
209
|
+
attach_function :ERR_print_errors_fp, [:pointer], :void # Pointer == File Handle
|
210
|
+
attach_function :SSL_CTX_use_certificate_chain_file, [:ssl_ctx, :string], :int, :blocking => true
|
211
|
+
attach_function :SSL_CTX_use_certificate, [:ssl_ctx, :x509], :int
|
212
|
+
attach_function :SSL_CTX_set_cipher_list, [:ssl_ctx, :string], :int
|
213
|
+
attach_function :SSL_CTX_set_session_id_context, [:ssl_ctx, :string, :buffer_length], :int
|
214
|
+
attach_function :SSL_load_client_CA_file, [:string], :pointer
|
215
|
+
attach_function :SSL_CTX_set_client_CA_list, [:ssl_ctx, :pointer], :void
|
216
|
+
attach_function :SSL_CTX_load_verify_locations, [:ssl_ctx, :pointer], :int, :blocking => true
|
217
|
+
|
218
|
+
# OpenSSL before 1.0.2 do not have these methods
|
219
|
+
begin
|
220
|
+
attach_function :SSL_CTX_set_alpn_protos, [:ssl_ctx, :string, :uint], :int
|
221
|
+
|
222
|
+
OPENSSL_NPN_UNSUPPORTED = 0
|
223
|
+
OPENSSL_NPN_NEGOTIATED = 1
|
224
|
+
OPENSSL_NPN_NO_OVERLAP = 2
|
225
|
+
|
226
|
+
attach_function :SSL_select_next_proto, [:pointer, :pointer, :string, :uint, :string, :uint], :int
|
227
|
+
|
228
|
+
# array of str, unit8 out,uint8 in, *arg
|
229
|
+
callback :alpn_select_cb, [:ssl, :pointer, :pointer, :string, :uint, :pointer], :int
|
230
|
+
attach_function :SSL_CTX_set_alpn_select_cb, [:ssl_ctx, :alpn_select_cb, :pointer], :void
|
231
|
+
|
232
|
+
attach_function :SSL_get0_alpn_selected, [:ssl, :pointer, :pointer], :void
|
233
|
+
ALPN_SUPPORTED = true
|
234
|
+
rescue FFI::NotFoundError
|
235
|
+
ALPN_SUPPORTED = false
|
236
|
+
end
|
237
|
+
|
238
|
+
|
239
|
+
# Deconstructor
|
240
|
+
attach_function :SSL_CTX_free, [:ssl_ctx], :void
|
241
|
+
|
242
|
+
|
243
|
+
PrivateMaterials = <<-keystr
|
244
|
+
-----BEGIN RSA PRIVATE KEY-----
|
245
|
+
MIICXAIBAAKBgQDCYYhcw6cGRbhBVShKmbWm7UVsEoBnUf0cCh8AX+MKhMxwVDWV
|
246
|
+
Igdskntn3cSJjRtmgVJHIK0lpb/FYHQB93Ohpd9/Z18pDmovfFF9nDbFF0t39hJ/
|
247
|
+
AqSzFB3GiVPoFFZJEE1vJqh+3jzsSF5K56bZ6azz38VlZgXeSozNW5bXkQIDAQAB
|
248
|
+
AoGALA89gIFcr6BIBo8N5fL3aNHpZXjAICtGav+kTUpuxSiaym9cAeTHuAVv8Xgk
|
249
|
+
H2Wbq11uz+6JMLpkQJH/WZ7EV59DPOicXrp0Imr73F3EXBfR7t2EQDYHPMthOA1D
|
250
|
+
I9EtCzvV608Ze90hiJ7E3guGrGppZfJ+eUWCPgy8CZH1vRECQQDv67rwV/oU1aDo
|
251
|
+
6/+d5nqjeW6mWkGqTnUU96jXap8EIw6B+0cUKskwx6mHJv+tEMM2748ZY7b0yBlg
|
252
|
+
w4KDghbFAkEAz2h8PjSJG55LwqmXih1RONSgdN9hjB12LwXL1CaDh7/lkEhq0PlK
|
253
|
+
PCAUwQSdM17Sl0Xxm2CZiekTSlwmHrtqXQJAF3+8QJwtV2sRJp8u2zVe37IeH1cJ
|
254
|
+
xXeHyjTzqZ2803fnjN2iuZvzNr7noOA1/Kp+pFvUZUU5/0G2Ep8zolPUjQJAFA7k
|
255
|
+
xRdLkzIx3XeNQjwnmLlncyYPRv+qaE3FMpUu7zftuZBnVCJnvXzUxP3vPgKTlzGa
|
256
|
+
dg5XivDRfsV+okY5uQJBAMV4FesUuLQVEKb6lMs7rzZwpeGQhFDRfywJzfom2TLn
|
257
|
+
2RdJQQ3dcgnhdVDgt5o1qkmsqQh8uJrJ9SdyLIaZQIc=
|
258
|
+
-----END RSA PRIVATE KEY-----
|
259
|
+
-----BEGIN CERTIFICATE-----
|
260
|
+
MIID6TCCA1KgAwIBAgIJANm4W/Tzs+s+MA0GCSqGSIb3DQEBBQUAMIGqMQswCQYD
|
261
|
+
VQQGEwJVUzERMA8GA1UECBMITmV3IFlvcmsxETAPBgNVBAcTCE5ldyBZb3JrMRYw
|
262
|
+
FAYDVQQKEw1TdGVhbWhlYXQubmV0MRQwEgYDVQQLEwtFbmdpbmVlcmluZzEdMBsG
|
263
|
+
A1UEAxMUb3BlbmNhLnN0ZWFtaGVhdC5uZXQxKDAmBgkqhkiG9w0BCQEWGWVuZ2lu
|
264
|
+
ZWVyaW5nQHN0ZWFtaGVhdC5uZXQwHhcNMDYwNTA1MTcwNjAzWhcNMjQwMjIwMTcw
|
265
|
+
NjAzWjCBqjELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE5ldyBZb3JrMREwDwYDVQQH
|
266
|
+
EwhOZXcgWW9yazEWMBQGA1UEChMNU3RlYW1oZWF0Lm5ldDEUMBIGA1UECxMLRW5n
|
267
|
+
aW5lZXJpbmcxHTAbBgNVBAMTFG9wZW5jYS5zdGVhbWhlYXQubmV0MSgwJgYJKoZI
|
268
|
+
hvcNAQkBFhllbmdpbmVlcmluZ0BzdGVhbWhlYXQubmV0MIGfMA0GCSqGSIb3DQEB
|
269
|
+
AQUAA4GNADCBiQKBgQDCYYhcw6cGRbhBVShKmbWm7UVsEoBnUf0cCh8AX+MKhMxw
|
270
|
+
VDWVIgdskntn3cSJjRtmgVJHIK0lpb/FYHQB93Ohpd9/Z18pDmovfFF9nDbFF0t3
|
271
|
+
9hJ/AqSzFB3GiVPoFFZJEE1vJqh+3jzsSF5K56bZ6azz38VlZgXeSozNW5bXkQID
|
272
|
+
AQABo4IBEzCCAQ8wHQYDVR0OBBYEFPJvPd1Fcmd8o/Tm88r+NjYPICCkMIHfBgNV
|
273
|
+
HSMEgdcwgdSAFPJvPd1Fcmd8o/Tm88r+NjYPICCkoYGwpIGtMIGqMQswCQYDVQQG
|
274
|
+
EwJVUzERMA8GA1UECBMITmV3IFlvcmsxETAPBgNVBAcTCE5ldyBZb3JrMRYwFAYD
|
275
|
+
VQQKEw1TdGVhbWhlYXQubmV0MRQwEgYDVQQLEwtFbmdpbmVlcmluZzEdMBsGA1UE
|
276
|
+
AxMUb3BlbmNhLnN0ZWFtaGVhdC5uZXQxKDAmBgkqhkiG9w0BCQEWGWVuZ2luZWVy
|
277
|
+
aW5nQHN0ZWFtaGVhdC5uZXSCCQDZuFv087PrPjAMBgNVHRMEBTADAQH/MA0GCSqG
|
278
|
+
SIb3DQEBBQUAA4GBAC1CXey/4UoLgJiwcEMDxOvW74plks23090iziFIlGgcIhk0
|
279
|
+
Df6hTAs7H3MWww62ddvR8l07AWfSzSP5L6mDsbvq7EmQsmPODwb6C+i2aF3EDL8j
|
280
|
+
uw73m4YIGI0Zw2XdBpiOGkx2H56Kya6mJJe/5XORZedh1wpI7zki01tHYbcy
|
281
|
+
-----END CERTIFICATE-----
|
282
|
+
keystr
|
283
|
+
|
284
|
+
|
285
|
+
BuiltinPasswdCB = FFI::Function.new(:int, [:pointer, :int, :int, :pointer]) do |buffer, len, flag, data|
|
286
|
+
buffer.write_string('kittycat')
|
287
|
+
8
|
288
|
+
end
|
289
|
+
|
290
|
+
# Locking isn't provided as long as all writes are done on the same thread.
|
291
|
+
# This is my main use case. Happy to enable it if someone requires it and can
|
292
|
+
# get it to work on MRI Ruby (Currently only works on JRuby and Rubinius)
|
293
|
+
# as MRI callbacks occur on a thread pool?
|
294
|
+
|
295
|
+
#CRYPTO_LOCK = 0x1
|
296
|
+
#LockingCB = FFI::Function.new(:void, [:int, :int, :string, :int]) do |mode, type, file, line|
|
297
|
+
# if (mode & CRYPTO_LOCK) != 0
|
298
|
+
# SSL_LOCKS[type].lock
|
299
|
+
# else
|
300
|
+
# Unlock a lock
|
301
|
+
# SSL_LOCKS[type].unlock
|
302
|
+
# end
|
303
|
+
#end
|
304
|
+
#ThreadIdCB = FFI::Function.new(:ulong, []) do
|
305
|
+
# Thread.current.object_id
|
306
|
+
#end
|
307
|
+
|
308
|
+
|
309
|
+
# INIT CODE
|
310
|
+
@init_required ||= false
|
311
|
+
unless @init_required
|
312
|
+
if OPENSSL_V1_1
|
313
|
+
self.OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS, ::FFI::Pointer::NULL)
|
314
|
+
else
|
315
|
+
self.SSL_load_error_strings
|
316
|
+
self.SSL_library_init
|
317
|
+
self.ERR_load_crypto_strings
|
318
|
+
end
|
319
|
+
|
320
|
+
# Setup multi-threaded support
|
321
|
+
#SSL_LOCKS = []
|
322
|
+
#num_locks = self.CRYPTO_num_locks
|
323
|
+
#num_locks.times { SSL_LOCKS << Mutex.new }
|
324
|
+
|
325
|
+
#self.CRYPTO_set_locking_callback(LockingCB)
|
326
|
+
#self.CRYPTO_set_id_callback(ThreadIdCB)
|
327
|
+
|
328
|
+
|
329
|
+
bio = self.BIO_new_mem_buf(PrivateMaterials, PrivateMaterials.bytesize)
|
330
|
+
|
331
|
+
# Get the private key structure
|
332
|
+
pointer = FFI::MemoryPointer.new(:pointer)
|
333
|
+
self.PEM_read_bio_PrivateKey(bio, pointer, BuiltinPasswdCB, nil)
|
334
|
+
DEFAULT_PRIVATE = pointer.get_pointer(0)
|
335
|
+
|
336
|
+
# Get the certificate structure
|
337
|
+
pointer = FFI::MemoryPointer.new(:pointer)
|
338
|
+
self.PEM_read_bio_X509(bio, pointer, nil, nil)
|
339
|
+
DEFAULT_CERT = pointer.get_pointer(0)
|
340
|
+
|
341
|
+
self.BIO_free(bio)
|
342
|
+
|
343
|
+
@init_required = true
|
344
|
+
end
|
345
|
+
|
346
|
+
|
347
|
+
|
348
|
+
|
349
|
+
# Save RAM by releasing read and write buffers when they're empty
|
350
|
+
SSL_MODE_RELEASE_BUFFERS = 0x00000010
|
351
|
+
SSL_OP_ALL = 0x80000BFF
|
352
|
+
SSL_FILETYPE_PEM = 1
|
353
|
+
|
354
|
+
class Context
|
355
|
+
# Based on information from https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
|
356
|
+
CIPHERS = 'EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4'
|
357
|
+
SESSION = 'ruby-tls'
|
358
|
+
|
359
|
+
|
360
|
+
ALPN_LOOKUP = ::Concurrent::Map.new
|
361
|
+
ALPN_Select_CB = FFI::Function.new(:int, [
|
362
|
+
# array of str, unit8 out,uint8 in, *arg
|
363
|
+
:pointer, :pointer, :pointer, :string, :uint, :pointer
|
364
|
+
]) do |ssl_p, out, outlen, inp, inlen, arg|
|
365
|
+
ssl = Box::InstanceLookup[ssl_p.address]
|
366
|
+
return SSL::SSL_TLSEXT_ERR_ALERT_FATAL unless ssl
|
367
|
+
|
368
|
+
protos = ssl.context.alpn_str
|
369
|
+
status = SSL.SSL_select_next_proto(out, outlen, protos, protos.length, inp, inlen)
|
370
|
+
ssl.negotiated
|
371
|
+
|
372
|
+
case status
|
373
|
+
when SSL::OPENSSL_NPN_UNSUPPORTED
|
374
|
+
SSL::SSL_TLSEXT_ERR_ALERT_FATAL
|
375
|
+
when SSL::OPENSSL_NPN_NEGOTIATED
|
376
|
+
SSL::SSL_TLSEXT_ERR_OK
|
377
|
+
when SSL::OPENSSL_NPN_NO_OVERLAP
|
378
|
+
SSL::SSL_TLSEXT_ERR_ALERT_WARNING
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
def initialize(server, options = {})
|
383
|
+
@is_server = server
|
384
|
+
|
385
|
+
if @is_server
|
386
|
+
@ssl_ctx = SSL.SSL_CTX_new(SSL.TLS_server_method)
|
387
|
+
set_private_key(options[:private_key] || SSL::DEFAULT_PRIVATE)
|
388
|
+
set_certificate(options[:cert_chain] || SSL::DEFAULT_CERT)
|
389
|
+
set_client_ca(options[:client_ca])
|
390
|
+
else
|
391
|
+
@ssl_ctx = SSL.SSL_CTX_new(SSL.TLS_client_method)
|
392
|
+
end
|
393
|
+
|
394
|
+
SSL.SSL_CTX_set_options(@ssl_ctx, SSL::SSL_OP_ALL)
|
395
|
+
SSL.SSL_CTX_set_mode(@ssl_ctx, SSL::SSL_MODE_RELEASE_BUFFERS)
|
396
|
+
|
397
|
+
SSL.SSL_CTX_set_cipher_list(@ssl_ctx, options[:ciphers] || CIPHERS)
|
398
|
+
@alpn_set = false
|
399
|
+
|
400
|
+
version = options[:version]
|
401
|
+
if version
|
402
|
+
vresult = set_min_proto_version(version)
|
403
|
+
raise "#{version} is unsupported" unless vresult
|
404
|
+
end
|
405
|
+
|
406
|
+
if @is_server
|
407
|
+
SSL.SSL_CTX_sess_set_cache_size(@ssl_ctx, 128)
|
408
|
+
SSL.SSL_CTX_set_session_id_context(@ssl_ctx, SESSION, 8)
|
409
|
+
|
410
|
+
if SSL::ALPN_SUPPORTED && options[:protocols]
|
411
|
+
@alpn_str = Context.build_alpn_string(options[:protocols])
|
412
|
+
SSL.SSL_CTX_set_alpn_select_cb(@ssl_ctx, ALPN_Select_CB, nil)
|
413
|
+
@alpn_set = true
|
414
|
+
end
|
415
|
+
else
|
416
|
+
set_private_key(options[:private_key])
|
417
|
+
set_certificate(options[:cert_chain])
|
418
|
+
|
419
|
+
# Check for ALPN support
|
420
|
+
if SSL::ALPN_SUPPORTED && options[:protocols]
|
421
|
+
protocols = Context.build_alpn_string(options[:protocols])
|
422
|
+
@alpn_set = SSL.SSL_CTX_set_alpn_protos(@ssl_ctx, protocols, protocols.length) == 0
|
423
|
+
end
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
# Version can be one of:
|
428
|
+
# :SSL3, :TLS1, :TLS1_1, :TLS1_2, :TLS1_3, :TLS_MAX
|
429
|
+
if SSL::VERSION_SUPPORTED
|
430
|
+
def set_min_proto_version(version)
|
431
|
+
num = SSL.const_get("#{version}_VERSION")
|
432
|
+
SSL.SSL_CTX_set_min_proto_version(@ssl_ctx, num) == 1
|
433
|
+
rescue NameError
|
434
|
+
false
|
435
|
+
end
|
436
|
+
|
437
|
+
def set_max_proto_version(version)
|
438
|
+
num = SSL.const_get("#{version}_VERSION")
|
439
|
+
SSL.SSL_CTX_set_max_proto_version(@ssl_ctx, num) == 1
|
440
|
+
rescue NameError
|
441
|
+
false
|
442
|
+
end
|
443
|
+
else
|
444
|
+
def set_min_proto_version(version); false; end
|
445
|
+
def set_max_proto_version(version); false; end
|
446
|
+
end
|
447
|
+
|
448
|
+
def cleanup
|
449
|
+
if @ssl_ctx
|
450
|
+
SSL.SSL_CTX_free(@ssl_ctx)
|
451
|
+
@ssl_ctx = nil
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
attr_reader :is_server
|
456
|
+
attr_reader :ssl_ctx
|
457
|
+
attr_reader :alpn_set
|
458
|
+
attr_reader :alpn_str
|
459
|
+
|
460
|
+
def add_server_name_indication
|
461
|
+
raise 'only valid for server mode context' unless @is_server
|
462
|
+
SSL.SSL_CTX_set_tlsext_servername_callback(@ssl_ctx, ServerNameCB)
|
463
|
+
end
|
464
|
+
|
465
|
+
ServerNameCB = FFI::Function.new(:int, [:pointer, :pointer, :pointer]) do |ssl, _, _|
|
466
|
+
ruby_ssl = Box::InstanceLookup[ssl.address]
|
467
|
+
return SSL::SSL_TLSEXT_ERR_NOACK unless ruby_ssl
|
468
|
+
|
469
|
+
ctx = ruby_ssl.hosts[SSL.SSL_get_servername(ssl, SSL::TLSEXT_NAMETYPE_host_name)]
|
470
|
+
if ctx
|
471
|
+
SSL.SSL_set_SSL_CTX(ssl, ctx.ssl_ctx)
|
472
|
+
SSL::SSL_TLSEXT_ERR_OK
|
473
|
+
else
|
474
|
+
SSL::SSL_TLSEXT_ERR_ALERT_FATAL
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
|
479
|
+
private
|
480
|
+
|
481
|
+
|
482
|
+
def self.build_alpn_string(protos)
|
483
|
+
protocols = String.new.force_encoding('ASCII-8BIT')
|
484
|
+
protos.each do |prot|
|
485
|
+
protocol = prot.to_s
|
486
|
+
protocols << protocol.length
|
487
|
+
protocols << protocol
|
488
|
+
end
|
489
|
+
protocols
|
490
|
+
end
|
491
|
+
|
492
|
+
def set_private_key(key)
|
493
|
+
err = if key.is_a? FFI::Pointer
|
494
|
+
SSL.SSL_CTX_use_PrivateKey(@ssl_ctx, key)
|
495
|
+
elsif key && File.file?(key)
|
496
|
+
SSL.SSL_CTX_use_PrivateKey_file(@ssl_ctx, key, SSL_FILETYPE_PEM)
|
497
|
+
else
|
498
|
+
1
|
499
|
+
end
|
500
|
+
|
501
|
+
# Check for errors
|
502
|
+
if err <= 0
|
503
|
+
# TODO:: ERR_print_errors_fp or ERR_print_errors
|
504
|
+
# So we can properly log the issue
|
505
|
+
cleanup
|
506
|
+
raise 'invalid private key or file not found'
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
def set_certificate(cert)
|
511
|
+
err = if cert.is_a? FFI::Pointer
|
512
|
+
SSL.SSL_CTX_use_certificate(@ssl_ctx, cert)
|
513
|
+
elsif cert && File.file?(cert)
|
514
|
+
SSL.SSL_CTX_use_certificate_chain_file(@ssl_ctx, cert)
|
515
|
+
else
|
516
|
+
1
|
517
|
+
end
|
518
|
+
|
519
|
+
if err <= 0
|
520
|
+
cleanup
|
521
|
+
raise 'invalid certificate or file not found'
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
def set_client_ca(ca)
|
526
|
+
return unless ca
|
527
|
+
|
528
|
+
if File.file?(ca) && (ca_ptr = SSL.SSL_load_client_CA_file(ca))
|
529
|
+
# there is no error checking provided by SSL_CTX_set_client_CA_list
|
530
|
+
SSL.SSL_CTX_set_client_CA_list(@ssl_ctx, ca_ptr)
|
531
|
+
else
|
532
|
+
cleanup
|
533
|
+
raise 'invalid ca certificate or file not found'
|
534
|
+
end
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
|
539
|
+
|
540
|
+
|
541
|
+
class Box
|
542
|
+
InstanceLookup = ::Concurrent::Map.new
|
543
|
+
|
544
|
+
READ_BUFFER = 2048
|
545
|
+
|
546
|
+
SSL_VERIFY_PEER = 0x01
|
547
|
+
SSL_VERIFY_CLIENT_ONCE = 0x04
|
548
|
+
def initialize(server, transport, options = {})
|
549
|
+
@ready = true
|
550
|
+
|
551
|
+
@handshake_completed = false
|
552
|
+
@handshake_signaled = false
|
553
|
+
@negotiated = false
|
554
|
+
@transport = transport
|
555
|
+
|
556
|
+
@read_buffer = FFI::MemoryPointer.new(:char, READ_BUFFER, false)
|
557
|
+
|
558
|
+
@is_server = server
|
559
|
+
@context = Context.new(server, options)
|
560
|
+
@bioRead = SSL.BIO_new(SSL.BIO_s_mem)
|
561
|
+
@bioWrite = SSL.BIO_new(SSL.BIO_s_mem)
|
562
|
+
@ssl = SSL.SSL_new(@context.ssl_ctx)
|
563
|
+
SSL.SSL_set_bio(@ssl, @bioRead, @bioWrite)
|
564
|
+
|
565
|
+
@write_queue = []
|
566
|
+
|
567
|
+
InstanceLookup[@ssl.address] = self
|
568
|
+
|
569
|
+
@alpn_fallback = options[:fallback]
|
570
|
+
if options[:verify_peer]
|
571
|
+
SSL.SSL_set_verify(@ssl, SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, VerifyCB)
|
572
|
+
end
|
573
|
+
|
574
|
+
# Add Server Name Indication (SNI) for client connections
|
575
|
+
if options[:host_name]
|
576
|
+
if server
|
577
|
+
@hosts = ::Concurrent::Map.new
|
578
|
+
@hosts[options[:host_name].to_s] = @context
|
579
|
+
@context.add_server_name_indication
|
580
|
+
else
|
581
|
+
SSL.SSL_set_tlsext_host_name(@ssl, options[:host_name])
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
SSL.SSL_connect(@ssl) unless server
|
586
|
+
end
|
587
|
+
|
588
|
+
|
589
|
+
def add_host(host_name:, **options)
|
590
|
+
raise 'Server Name Indication (SNI) not configured for default host' unless @hosts
|
591
|
+
raise 'only valid for server mode context' unless @is_server
|
592
|
+
context = Context.new(true, options)
|
593
|
+
@hosts[host_name.to_s] = context
|
594
|
+
context.add_server_name_indication
|
595
|
+
nil
|
596
|
+
end
|
597
|
+
|
598
|
+
# Careful with this.
|
599
|
+
# If you remove all the hosts you'll end up with a segfault
|
600
|
+
def remove_host(host_name)
|
601
|
+
raise 'Server Name Indication (SNI) not configured for default host' unless @hosts
|
602
|
+
raise 'only valid for server mode context' unless @is_server
|
603
|
+
context = @hosts[host_name.to_s]
|
604
|
+
if context
|
605
|
+
@hosts.delete(host_name.to_s)
|
606
|
+
context.cleanup
|
607
|
+
end
|
608
|
+
nil
|
609
|
+
end
|
610
|
+
|
611
|
+
|
612
|
+
attr_reader :is_server, :context
|
613
|
+
attr_reader :handshake_completed
|
614
|
+
attr_reader :hosts
|
615
|
+
|
616
|
+
|
617
|
+
def get_peer_cert
|
618
|
+
return '' unless @ready
|
619
|
+
SSL.SSL_get_peer_certificate(@ssl)
|
620
|
+
end
|
621
|
+
|
622
|
+
def negotiated_protocol
|
623
|
+
return nil unless @context.alpn_set
|
624
|
+
|
625
|
+
proto = FFI::MemoryPointer.new(:pointer, 1, true)
|
626
|
+
len = FFI::MemoryPointer.new(:uint, 1, true)
|
627
|
+
SSL.SSL_get0_alpn_selected(@ssl, proto, len)
|
628
|
+
|
629
|
+
resp = proto.get_pointer(0)
|
630
|
+
if resp.address == 0
|
631
|
+
:failed
|
632
|
+
else
|
633
|
+
length = len.get_uint(0)
|
634
|
+
resp.read_string(length).to_sym
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
638
|
+
def start
|
639
|
+
return unless @ready
|
640
|
+
|
641
|
+
dispatch_cipher_text
|
642
|
+
end
|
643
|
+
|
644
|
+
def encrypt(data)
|
645
|
+
return unless @ready
|
646
|
+
|
647
|
+
wrote = put_plain_text data
|
648
|
+
if wrote < 0
|
649
|
+
@transport.close_cb
|
650
|
+
else
|
651
|
+
dispatch_cipher_text
|
652
|
+
end
|
653
|
+
end
|
654
|
+
|
655
|
+
SSL_ERROR_WANT_READ = 2
|
656
|
+
SSL_ERROR_SSL = 1
|
657
|
+
def decrypt(data)
|
658
|
+
return unless @ready
|
659
|
+
|
660
|
+
put_cipher_text data
|
661
|
+
|
662
|
+
if not SSL.SSL_is_init_finished(@ssl)
|
663
|
+
resp = @is_server ? SSL.SSL_accept(@ssl) : SSL.SSL_connect(@ssl)
|
664
|
+
|
665
|
+
if resp < 0
|
666
|
+
err_code = SSL.SSL_get_error(@ssl, resp)
|
667
|
+
if err_code != SSL_ERROR_WANT_READ
|
668
|
+
@transport.close_cb if err_code == SSL_ERROR_SSL
|
669
|
+
return
|
670
|
+
end
|
671
|
+
end
|
672
|
+
|
673
|
+
@handshake_completed = true
|
674
|
+
signal_handshake unless @handshake_signaled
|
675
|
+
end
|
676
|
+
|
677
|
+
while true do
|
678
|
+
size = get_plain_text(@read_buffer, READ_BUFFER)
|
679
|
+
if size > 0
|
680
|
+
@transport.dispatch_cb @read_buffer.read_string(size)
|
681
|
+
else
|
682
|
+
break
|
683
|
+
end
|
684
|
+
end
|
685
|
+
|
686
|
+
dispatch_cipher_text
|
687
|
+
end
|
688
|
+
|
689
|
+
def signal_handshake
|
690
|
+
@handshake_signaled = true
|
691
|
+
|
692
|
+
# Check protocol support here
|
693
|
+
if @context.alpn_set
|
694
|
+
proto = negotiated_protocol
|
695
|
+
|
696
|
+
if proto == :failed
|
697
|
+
if @negotiated
|
698
|
+
# We should shutdown if this is the case
|
699
|
+
@transport.close_cb
|
700
|
+
return
|
701
|
+
elsif @alpn_fallback
|
702
|
+
# Client or Server with a client that doesn't support ALPN
|
703
|
+
proto = @alpn_fallback.to_sym
|
704
|
+
end
|
705
|
+
end
|
706
|
+
else
|
707
|
+
proto = nil
|
708
|
+
end
|
709
|
+
|
710
|
+
@transport.handshake_cb(proto)
|
711
|
+
end
|
712
|
+
|
713
|
+
def negotiated
|
714
|
+
@negotiated = true
|
715
|
+
end
|
716
|
+
|
717
|
+
SSL_RECEIVED_SHUTDOWN = 2
|
718
|
+
def cleanup
|
719
|
+
return unless @ready
|
720
|
+
@ready = false
|
721
|
+
|
722
|
+
InstanceLookup.delete @ssl.address
|
723
|
+
|
724
|
+
if (SSL.SSL_get_shutdown(@ssl) & SSL_RECEIVED_SHUTDOWN) != 0
|
725
|
+
SSL.SSL_shutdown @ssl
|
726
|
+
else
|
727
|
+
SSL.SSL_clear @ssl
|
728
|
+
end
|
729
|
+
|
730
|
+
SSL.SSL_free @ssl
|
731
|
+
|
732
|
+
if @hosts
|
733
|
+
@hosts.each_value do |context|
|
734
|
+
context.cleanup
|
735
|
+
end
|
736
|
+
@hosts = nil
|
737
|
+
else
|
738
|
+
@context.cleanup
|
739
|
+
end
|
740
|
+
end
|
741
|
+
|
742
|
+
# Called from class level callback function
|
743
|
+
def verify(cert)
|
744
|
+
@transport.verify_cb(cert) == true ? 1 : 0
|
745
|
+
end
|
746
|
+
|
747
|
+
|
748
|
+
private
|
749
|
+
|
750
|
+
|
751
|
+
def get_plain_text(buffer, ready)
|
752
|
+
# Read the buffered clear text
|
753
|
+
size = SSL.SSL_read(@ssl, buffer, ready)
|
754
|
+
if size >= 0
|
755
|
+
size
|
756
|
+
else
|
757
|
+
SSL.SSL_get_error(@ssl, size) == SSL_ERROR_WANT_READ ? 0 : -1
|
758
|
+
end
|
759
|
+
end
|
760
|
+
|
761
|
+
VerifyCB = FFI::Function.new(:int, [:int, :pointer]) do |preverify_ok, x509_store|
|
762
|
+
x509 = SSL.X509_STORE_CTX_get_current_cert(x509_store)
|
763
|
+
ssl = SSL.X509_STORE_CTX_get_ex_data(x509_store, SSL.SSL_get_ex_data_X509_STORE_CTX_idx)
|
764
|
+
|
765
|
+
bio_out = SSL.BIO_new(SSL.BIO_s_mem)
|
766
|
+
SSL.PEM_write_bio_X509(bio_out, x509)
|
767
|
+
|
768
|
+
len = SSL.BIO_pending(bio_out)
|
769
|
+
buffer = FFI::MemoryPointer.new(:char, len, false)
|
770
|
+
size = SSL.BIO_read(bio_out, buffer, len)
|
771
|
+
|
772
|
+
# THis is the callback into the ruby class
|
773
|
+
result = InstanceLookup[ssl.address].verify(buffer.read_string(size))
|
774
|
+
|
775
|
+
SSL.BIO_free(bio_out)
|
776
|
+
result
|
777
|
+
end
|
778
|
+
|
779
|
+
|
780
|
+
def pending_data(bio)
|
781
|
+
SSL.BIO_pending(bio)
|
782
|
+
end
|
783
|
+
|
784
|
+
def get_cipher_text(buffer, length)
|
785
|
+
SSL.BIO_read(@bioWrite, buffer, length)
|
786
|
+
end
|
787
|
+
|
788
|
+
def put_cipher_text(data)
|
789
|
+
len = data.bytesize
|
790
|
+
wrote = SSL.BIO_write(@bioRead, data, len)
|
791
|
+
wrote == len
|
792
|
+
end
|
793
|
+
|
794
|
+
|
795
|
+
SSL_ERROR_WANT_WRITE = 3
|
796
|
+
def put_plain_text(data)
|
797
|
+
@write_queue.push(data) if data
|
798
|
+
return 0 unless SSL.SSL_is_init_finished(@ssl)
|
799
|
+
|
800
|
+
fatal = false
|
801
|
+
did_work = false
|
802
|
+
|
803
|
+
while !@write_queue.empty? do
|
804
|
+
data = @write_queue.pop
|
805
|
+
len = data.bytesize
|
806
|
+
|
807
|
+
wrote = SSL.SSL_write(@ssl, data, len)
|
808
|
+
|
809
|
+
if wrote > 0
|
810
|
+
did_work = true;
|
811
|
+
else
|
812
|
+
err_code = SSL.SSL_get_error(@ssl, wrote)
|
813
|
+
if (err_code != SSL_ERROR_WANT_READ) && (err_code != SSL_ERROR_WANT_WRITE)
|
814
|
+
fatal = true
|
815
|
+
else
|
816
|
+
# Not fatal - add back to the queue
|
817
|
+
@write_queue.unshift data
|
818
|
+
end
|
819
|
+
|
820
|
+
break
|
821
|
+
end
|
822
|
+
end
|
823
|
+
|
824
|
+
if did_work
|
825
|
+
1
|
826
|
+
elsif fatal
|
827
|
+
-1
|
828
|
+
else
|
829
|
+
0
|
830
|
+
end
|
831
|
+
end
|
832
|
+
|
833
|
+
|
834
|
+
CIPHER_DISPATCH_FAILED = 'Cipher text dispatch failed'
|
835
|
+
def dispatch_cipher_text
|
836
|
+
begin
|
837
|
+
did_work = false
|
838
|
+
|
839
|
+
# Get all the encrypted data and transmit it
|
840
|
+
pending = pending_data(@bioWrite)
|
841
|
+
if pending > 0
|
842
|
+
buffer = FFI::MemoryPointer.new(:char, pending, false)
|
843
|
+
|
844
|
+
resp = get_cipher_text(buffer, pending)
|
845
|
+
raise CIPHER_DISPATCH_FAILED unless resp > 0
|
846
|
+
|
847
|
+
@transport.transmit_cb(buffer.read_string(resp))
|
848
|
+
did_work = true
|
849
|
+
end
|
850
|
+
|
851
|
+
# Send any queued out going data
|
852
|
+
unless @write_queue.empty?
|
853
|
+
resp = put_plain_text nil
|
854
|
+
if resp > 0
|
855
|
+
did_work = true
|
856
|
+
elsif resp < 0
|
857
|
+
@transport.close_cb
|
858
|
+
end
|
859
|
+
end
|
860
|
+
end while did_work
|
861
|
+
end
|
862
|
+
end
|
863
|
+
end
|
864
|
+
end
|