mysql-pr 2.9.11 → 3.0.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/.rspec +3 -0
- data/.rubocop.yml +68 -0
- data/CHANGELOG.md +48 -0
- data/LICENSE +83 -0
- data/README.md +199 -0
- data/Rakefile +10 -0
- data/docker-compose.yml +25 -0
- data/lib/mysql-pr/charset.rb +91 -120
- data/lib/mysql-pr/constants.rb +4 -0
- data/lib/mysql-pr/error.rb +2 -0
- data/lib/mysql-pr/packet.rb +7 -4
- data/lib/mysql-pr/protocol.rb +237 -21
- data/lib/mysql-pr/version.rb +5 -0
- data/lib/mysql-pr.rb +339 -287
- data/mysql-pr.gemspec +36 -0
- metadata +102 -28
- data/README.rdoc +0 -60
- data/spec/mysql/packet_spec.rb +0 -118
- data/spec/mysql_spec.rb +0 -1701
data/lib/mysql-pr/charset.rb
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# Copyright (C) 2008-2012 TOMITA Masahiro
|
|
2
4
|
# mailto:tommy@tmtm.org
|
|
3
5
|
|
|
4
|
-
#
|
|
5
6
|
class MysqlPR
|
|
7
|
+
# MySQL character set and collation handling
|
|
6
8
|
# @!attribute [r] number
|
|
7
|
-
# @
|
|
9
|
+
# @return [Integer] charset number
|
|
8
10
|
# @!attribute [r] name
|
|
9
11
|
# @return [String] charset name
|
|
10
12
|
# @!attribute [r] csname
|
|
11
13
|
# @return [String] collation name
|
|
12
14
|
class Charset
|
|
13
|
-
# @private
|
|
14
15
|
# @param [Integer] number
|
|
15
16
|
# @param [String] name
|
|
16
17
|
# @param [String] csname
|
|
17
18
|
def initialize(number, name, csname)
|
|
18
|
-
@number
|
|
19
|
+
@number = number
|
|
20
|
+
@name = name
|
|
21
|
+
@csname = csname
|
|
19
22
|
@unsafe = false
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
attr_reader :number, :name, :csname
|
|
23
|
-
|
|
24
|
-
# @private
|
|
25
26
|
attr_accessor :unsafe
|
|
26
27
|
|
|
27
28
|
# [[charset_number, charset_name, collation_name, default], ...]
|
|
28
|
-
# @private
|
|
29
29
|
CHARSETS = [
|
|
30
30
|
[ 1, "big5", "big5_chinese_ci", true ],
|
|
31
31
|
[ 2, "latin2", "latin2_czech_cs", false],
|
|
@@ -179,146 +179,117 @@ class MysqlPR
|
|
|
179
179
|
[242, "utf8mb4", "utf8mb4_hungarian_ci", false],
|
|
180
180
|
[243, "utf8mb4", "utf8mb4_sinhala_ci", false],
|
|
181
181
|
[254, "utf8", "utf8_general_cs", false],
|
|
182
|
-
]
|
|
182
|
+
[255, "utf8mb4", "utf8mb4_0900_ai_ci", false],
|
|
183
|
+
].freeze
|
|
183
184
|
|
|
184
|
-
|
|
185
|
-
UNSAFE_CHARSET = [
|
|
186
|
-
"big5", "sjis", "filename", "gbk", "ucs2", "cp932",
|
|
187
|
-
]
|
|
185
|
+
UNSAFE_CHARSET = %w[big5 sjis filename gbk ucs2 cp932].freeze
|
|
188
186
|
|
|
189
|
-
# @private
|
|
190
187
|
NUMBER_TO_CHARSET = {}
|
|
191
|
-
# @private
|
|
192
188
|
COLLATION_TO_CHARSET = {}
|
|
193
|
-
# @private
|
|
194
189
|
CHARSET_DEFAULT = {}
|
|
190
|
+
|
|
195
191
|
CHARSETS.each do |number, csname, clname, default|
|
|
196
|
-
cs = Charset.new
|
|
197
|
-
cs.unsafe = true if UNSAFE_CHARSET.include?
|
|
192
|
+
cs = Charset.new(number, csname, clname)
|
|
193
|
+
cs.unsafe = true if UNSAFE_CHARSET.include?(csname)
|
|
198
194
|
NUMBER_TO_CHARSET[number] = cs
|
|
199
195
|
COLLATION_TO_CHARSET[clname] = cs
|
|
200
196
|
CHARSET_DEFAULT[csname] = cs if default
|
|
201
197
|
end
|
|
202
198
|
|
|
203
|
-
|
|
204
|
-
|
|
199
|
+
NUMBER_TO_CHARSET.freeze
|
|
200
|
+
COLLATION_TO_CHARSET.freeze
|
|
201
|
+
CHARSET_DEFAULT.freeze
|
|
205
202
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
#
|
|
203
|
+
BINARY_CHARSET_NUMBER = CHARSET_DEFAULT["binary"].number
|
|
204
|
+
|
|
205
|
+
# MySQL Charset -> Ruby Encoding mapping
|
|
206
|
+
CHARSET_ENCODING = {
|
|
207
|
+
"armscii8" => nil,
|
|
208
|
+
"ascii" => Encoding::US_ASCII,
|
|
209
|
+
"big5" => Encoding::Big5,
|
|
210
|
+
"binary" => Encoding::ASCII_8BIT,
|
|
211
|
+
"cp1250" => Encoding::Windows_1250,
|
|
212
|
+
"cp1251" => Encoding::Windows_1251,
|
|
213
|
+
"cp1256" => Encoding::Windows_1256,
|
|
214
|
+
"cp1257" => Encoding::Windows_1257,
|
|
215
|
+
"cp850" => Encoding::CP850,
|
|
216
|
+
"cp852" => Encoding::CP852,
|
|
217
|
+
"cp866" => Encoding::IBM866,
|
|
218
|
+
"cp932" => Encoding::Windows_31J,
|
|
219
|
+
"dec8" => nil,
|
|
220
|
+
"eucjpms" => Encoding::EucJP_ms,
|
|
221
|
+
"euckr" => Encoding::EUC_KR,
|
|
222
|
+
"gb2312" => Encoding::EUC_CN,
|
|
223
|
+
"gbk" => Encoding::GBK,
|
|
224
|
+
"geostd8" => nil,
|
|
225
|
+
"greek" => Encoding::ISO_8859_7,
|
|
226
|
+
"hebrew" => Encoding::ISO_8859_8,
|
|
227
|
+
"hp8" => nil,
|
|
228
|
+
"keybcs2" => nil,
|
|
229
|
+
"koi8r" => Encoding::KOI8_R,
|
|
230
|
+
"koi8u" => Encoding::KOI8_U,
|
|
231
|
+
"latin1" => Encoding::ISO_8859_1,
|
|
232
|
+
"latin2" => Encoding::ISO_8859_2,
|
|
233
|
+
"latin5" => Encoding::ISO_8859_9,
|
|
234
|
+
"latin7" => Encoding::ISO_8859_13,
|
|
235
|
+
"macce" => Encoding::MacCentEuro,
|
|
236
|
+
"macroman" => Encoding::MacRoman,
|
|
237
|
+
"sjis" => Encoding::SHIFT_JIS,
|
|
238
|
+
"swe7" => nil,
|
|
239
|
+
"tis620" => Encoding::TIS_620,
|
|
240
|
+
"ucs2" => Encoding::UTF_16BE,
|
|
241
|
+
"ujis" => Encoding::EucJP_ms,
|
|
242
|
+
"utf8" => Encoding::UTF_8,
|
|
243
|
+
"utf8mb4" => Encoding::UTF_8,
|
|
244
|
+
}.freeze
|
|
245
|
+
|
|
246
|
+
# @param [Integer] n charset number
|
|
247
|
+
# @return [MysqlPR::Charset]
|
|
209
248
|
def self.by_number(n)
|
|
210
|
-
raise ClientError, "unknown charset number: #{n}" unless NUMBER_TO_CHARSET.key?
|
|
249
|
+
raise ClientError, "unknown charset number: #{n}" unless NUMBER_TO_CHARSET.key?(n)
|
|
250
|
+
|
|
211
251
|
NUMBER_TO_CHARSET[n]
|
|
212
252
|
end
|
|
213
253
|
|
|
214
|
-
# @
|
|
215
|
-
# @
|
|
216
|
-
# @return [Mysql::Charset]
|
|
254
|
+
# @param [String] str charset or collation name
|
|
255
|
+
# @return [MysqlPR::Charset]
|
|
217
256
|
def self.by_name(str)
|
|
218
257
|
ret = COLLATION_TO_CHARSET[str] || CHARSET_DEFAULT[str]
|
|
219
258
|
raise ClientError, "unknown charset: #{str}" unless ret
|
|
259
|
+
|
|
220
260
|
ret
|
|
221
261
|
end
|
|
222
262
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
"armscii8" => nil,
|
|
229
|
-
"ascii" => Encoding::US_ASCII,
|
|
230
|
-
"big5" => Encoding::Big5,
|
|
231
|
-
"binary" => Encoding::ASCII_8BIT,
|
|
232
|
-
"cp1250" => Encoding::Windows_1250,
|
|
233
|
-
"cp1251" => Encoding::Windows_1251,
|
|
234
|
-
"cp1256" => Encoding::Windows_1256,
|
|
235
|
-
"cp1257" => Encoding::Windows_1257,
|
|
236
|
-
"cp850" => Encoding::CP850,
|
|
237
|
-
"cp852" => Encoding::CP852,
|
|
238
|
-
"cp866" => Encoding::IBM866,
|
|
239
|
-
"cp932" => Encoding::Windows_31J,
|
|
240
|
-
"dec8" => nil,
|
|
241
|
-
"eucjpms" => Encoding::EucJP_ms,
|
|
242
|
-
"euckr" => Encoding::EUC_KR,
|
|
243
|
-
"gb2312" => Encoding::EUC_CN,
|
|
244
|
-
"gbk" => Encoding::GBK,
|
|
245
|
-
"geostd8" => nil,
|
|
246
|
-
"greek" => Encoding::ISO_8859_7,
|
|
247
|
-
"hebrew" => Encoding::ISO_8859_8,
|
|
248
|
-
"hp8" => nil,
|
|
249
|
-
"keybcs2" => nil,
|
|
250
|
-
"koi8r" => Encoding::KOI8_R,
|
|
251
|
-
"koi8u" => Encoding::KOI8_U,
|
|
252
|
-
"latin1" => Encoding::ISO_8859_1,
|
|
253
|
-
"latin2" => Encoding::ISO_8859_2,
|
|
254
|
-
"latin5" => Encoding::ISO_8859_9,
|
|
255
|
-
"latin7" => Encoding::ISO_8859_13,
|
|
256
|
-
"macce" => Encoding::MacCentEuro,
|
|
257
|
-
"macroman" => Encoding::MacRoman,
|
|
258
|
-
"sjis" => Encoding::SHIFT_JIS,
|
|
259
|
-
"swe7" => nil,
|
|
260
|
-
"tis620" => Encoding::TIS_620,
|
|
261
|
-
"ucs2" => Encoding::UTF_16BE,
|
|
262
|
-
"ujis" => Encoding::EucJP_ms,
|
|
263
|
-
"utf8" => Encoding::UTF_8,
|
|
264
|
-
"utf8mb4" => Encoding::UTF_8,
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
# @private
|
|
268
|
-
# @param [String] value
|
|
269
|
-
# @return [String]
|
|
270
|
-
def self.to_binary(value)
|
|
271
|
-
value.force_encoding Encoding::ASCII_8BIT
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
# @private
|
|
275
|
-
# convert raw to encoding and convert to Encoding.default_internal
|
|
276
|
-
# @param [String] raw
|
|
277
|
-
# @param [Encoding] encoding
|
|
278
|
-
# @return [String] result
|
|
279
|
-
def self.convert_encoding(raw, encoding)
|
|
280
|
-
raw.force_encoding(encoding).encode
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
# @private
|
|
284
|
-
# retrun corresponding Ruby encoding
|
|
285
|
-
# @return [Encoding] encoding
|
|
286
|
-
def encoding
|
|
287
|
-
enc = CHARSET_ENCODING[@name.downcase]
|
|
288
|
-
raise Mysql::ClientError, "unsupported charset: #{@name}" unless enc
|
|
289
|
-
enc
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
# @private
|
|
293
|
-
# convert encoding to corrensponding to MySQL charset
|
|
294
|
-
# @param [String] value
|
|
295
|
-
# @return [String]
|
|
296
|
-
def convert(value)
|
|
297
|
-
if value.is_a? String and value.encoding != Encoding::ASCII_8BIT
|
|
298
|
-
value = value.encode encoding
|
|
299
|
-
end
|
|
300
|
-
value
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
else
|
|
304
|
-
# for Ruby 1.8
|
|
263
|
+
# @param [String] value
|
|
264
|
+
# @return [String] value with binary encoding
|
|
265
|
+
def self.to_binary(value)
|
|
266
|
+
value.dup.force_encoding(Encoding::ASCII_8BIT)
|
|
267
|
+
end
|
|
305
268
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
269
|
+
# Convert raw bytes to encoding and then to Encoding.default_internal
|
|
270
|
+
# @param [String] raw
|
|
271
|
+
# @param [Encoding] encoding
|
|
272
|
+
# @return [String]
|
|
273
|
+
def self.convert_encoding(raw, encoding)
|
|
274
|
+
raw.force_encoding(encoding).encode
|
|
275
|
+
end
|
|
309
276
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
277
|
+
# @return [Encoding] corresponding Ruby encoding
|
|
278
|
+
def encoding
|
|
279
|
+
enc = CHARSET_ENCODING[@name.downcase]
|
|
280
|
+
raise MysqlPR::ClientError, "unsupported charset: #{@name}" unless enc
|
|
313
281
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
end
|
|
282
|
+
enc
|
|
283
|
+
end
|
|
317
284
|
|
|
318
|
-
|
|
319
|
-
|
|
285
|
+
# Convert value encoding to match MySQL charset
|
|
286
|
+
# @param [String] value
|
|
287
|
+
# @return [String]
|
|
288
|
+
def convert(value)
|
|
289
|
+
if value.is_a?(String) && value.encoding != Encoding::ASCII_8BIT
|
|
290
|
+
value = value.encode(encoding)
|
|
320
291
|
end
|
|
321
|
-
|
|
292
|
+
value
|
|
322
293
|
end
|
|
323
294
|
end
|
|
324
295
|
end
|
data/lib/mysql-pr/constants.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# Copyright (C) 2003-2008 TOMITA Masahiro
|
|
2
4
|
# mailto:tommy@tmtm.org
|
|
3
5
|
|
|
@@ -52,6 +54,8 @@ class MysqlPR
|
|
|
52
54
|
CLIENT_SECURE_CONNECTION = 1 << 15 # New 4.1 authentication
|
|
53
55
|
CLIENT_MULTI_STATEMENTS = 1 << 16 # Enable/disable multi-stmt support
|
|
54
56
|
CLIENT_MULTI_RESULTS = 1 << 17 # Enable/disable multi-results
|
|
57
|
+
CLIENT_PS_MULTI_RESULTS = 1 << 18 # Multi-results in PS-protocol
|
|
58
|
+
CLIENT_PLUGIN_AUTH = 1 << 19 # Client supports plugin authentication
|
|
55
59
|
|
|
56
60
|
# Connection Option
|
|
57
61
|
OPT_CONNECT_TIMEOUT = 0
|
data/lib/mysql-pr/error.rb
CHANGED
data/lib/mysql-pr/packet.rb
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
class MysqlPR
|
|
4
|
+
# Binary packet parsing for MySQL protocol
|
|
2
5
|
class Packet
|
|
3
6
|
# convert Numeric to LengthCodedBinary
|
|
4
7
|
def self.lcb(num)
|
|
5
|
-
return "\xfb" if num.nil?
|
|
8
|
+
return "\xfb".b if num.nil?
|
|
6
9
|
return [num].pack("C") if num < 251
|
|
7
10
|
return [252, num].pack("Cv") if num < 65536
|
|
8
|
-
return [253, num&0xffff, num>>16].pack("CvC") if num <
|
|
9
|
-
return [254, num&0xffffffff, num>>32].pack("CVV")
|
|
11
|
+
return [253, num & 0xffff, num >> 16].pack("CvC") if num < 16_777_216
|
|
12
|
+
return [254, num & 0xffffffff, num >> 32].pack("CVV")
|
|
10
13
|
end
|
|
11
14
|
|
|
12
15
|
# convert String to LengthCodedString
|
|
@@ -66,7 +69,7 @@ class MysqlPR
|
|
|
66
69
|
end
|
|
67
70
|
|
|
68
71
|
def eof?
|
|
69
|
-
@data
|
|
72
|
+
@data.getbyte(0) == 0xfe && @data.length == 5
|
|
70
73
|
end
|
|
71
74
|
|
|
72
75
|
def to_s
|
data/lib/mysql-pr/protocol.rb
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# Copyright (C) 2008-2012 TOMITA Masahiro
|
|
2
4
|
# mailto:tommy@tmtm.org
|
|
3
5
|
|
|
4
6
|
require "socket"
|
|
5
7
|
require "timeout"
|
|
6
8
|
require "digest/sha1"
|
|
9
|
+
require "digest/sha2"
|
|
7
10
|
require "stringio"
|
|
11
|
+
require "openssl"
|
|
8
12
|
|
|
9
13
|
class MysqlPR
|
|
10
14
|
# MySQL network protocol
|
|
@@ -151,15 +155,18 @@ class MysqlPR
|
|
|
151
155
|
# conn_timeout :: [Integer] connect timeout (sec).
|
|
152
156
|
# read_timeout :: [Integer] read timeout (sec).
|
|
153
157
|
# write_timeout :: [Integer] write timeout (sec).
|
|
158
|
+
# ssl_options :: [Hash / nil] SSL options. nil means no SSL.
|
|
154
159
|
# === Exception
|
|
155
160
|
# [ClientError] :: connection timeout
|
|
156
|
-
def initialize(host, port, socket, conn_timeout, read_timeout, write_timeout)
|
|
161
|
+
def initialize(host, port, socket, conn_timeout, read_timeout, write_timeout, ssl_options = nil)
|
|
157
162
|
@insert_id = 0
|
|
158
163
|
@warning_count = 0
|
|
159
164
|
@gc_stmt_queue = [] # stmt id list which GC destroy.
|
|
160
165
|
set_state :INIT
|
|
161
166
|
@read_timeout = read_timeout
|
|
162
167
|
@write_timeout = write_timeout
|
|
168
|
+
@ssl_options = ssl_options
|
|
169
|
+
@ssl_enabled = false
|
|
163
170
|
begin
|
|
164
171
|
Timeout.timeout conn_timeout do
|
|
165
172
|
if host.nil? or host.empty? or host == "localhost"
|
|
@@ -175,6 +182,18 @@ class MysqlPR
|
|
|
175
182
|
end
|
|
176
183
|
end
|
|
177
184
|
|
|
185
|
+
# Returns true if SSL is enabled for this connection
|
|
186
|
+
def ssl_enabled?
|
|
187
|
+
@ssl_enabled
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Returns SSL cipher info if SSL is enabled
|
|
191
|
+
def ssl_cipher
|
|
192
|
+
return nil unless @ssl_enabled && @sock.respond_to?(:cipher)
|
|
193
|
+
|
|
194
|
+
@sock.cipher
|
|
195
|
+
end
|
|
196
|
+
|
|
178
197
|
def close
|
|
179
198
|
@sock.close
|
|
180
199
|
end
|
|
@@ -197,6 +216,7 @@ class MysqlPR
|
|
|
197
216
|
@server_version = init_packet.server_version.split(/\D/)[0,3].inject{|a,b|a.to_i*100+b.to_i}
|
|
198
217
|
@thread_id = init_packet.thread_id
|
|
199
218
|
client_flags = CLIENT_LONG_PASSWORD | CLIENT_LONG_FLAG | CLIENT_TRANSACTIONS | CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION
|
|
219
|
+
client_flags |= CLIENT_PLUGIN_AUTH
|
|
200
220
|
client_flags |= CLIENT_CONNECT_WITH_DB if db
|
|
201
221
|
client_flags |= flag
|
|
202
222
|
@charset = charset
|
|
@@ -204,12 +224,171 @@ class MysqlPR
|
|
|
204
224
|
@charset = Charset.by_number(init_packet.server_charset)
|
|
205
225
|
@charset.encoding # raise error if unsupported charset
|
|
206
226
|
end
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
227
|
+
|
|
228
|
+
# SSL handshake if requested and server supports it
|
|
229
|
+
if @ssl_options && (init_packet.server_capabilities & CLIENT_SSL) != 0
|
|
230
|
+
client_flags |= CLIENT_SSL
|
|
231
|
+
# Send SSL request packet (partial auth packet with SSL flag)
|
|
232
|
+
write SSLRequestPacket.serialize(client_flags, 1024**3, @charset.number)
|
|
233
|
+
# Upgrade connection to SSL
|
|
234
|
+
upgrade_to_ssl
|
|
235
|
+
elsif @ssl_options && @ssl_options[:required]
|
|
236
|
+
raise ClientError, "SSL required but server does not support SSL"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
auth_plugin = init_packet.auth_plugin_name || "mysql_native_password"
|
|
240
|
+
scramble = init_packet.scramble_buff
|
|
241
|
+
|
|
242
|
+
# Choose password encryption based on auth plugin
|
|
243
|
+
if auth_plugin == "caching_sha2_password"
|
|
244
|
+
netpw = encrypt_password_sha256(passwd, scramble)
|
|
245
|
+
else
|
|
246
|
+
netpw = encrypt_password(passwd, scramble)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
write AuthenticationPacket.serialize(client_flags, 1024**3, @charset.number, user, netpw, db, auth_plugin)
|
|
250
|
+
|
|
251
|
+
# Read response
|
|
252
|
+
response = read
|
|
253
|
+
response_data = response.to_s
|
|
254
|
+
|
|
255
|
+
# Handle different response types
|
|
256
|
+
case response_data.getbyte(0)
|
|
257
|
+
when 0x00
|
|
258
|
+
# OK packet - authentication successful
|
|
259
|
+
set_state :READY
|
|
260
|
+
when 0xfe
|
|
261
|
+
# Auth switch request
|
|
262
|
+
handle_auth_switch(response_data, passwd)
|
|
263
|
+
when 0x01
|
|
264
|
+
# More data - caching_sha2_password specific
|
|
265
|
+
handle_caching_sha2_more_data(response_data, passwd, scramble)
|
|
266
|
+
else
|
|
267
|
+
raise ProtocolError, "Unexpected auth response: #{response_data.getbyte(0)}"
|
|
268
|
+
end
|
|
211
269
|
end
|
|
212
270
|
|
|
271
|
+
# Handle auth switch request
|
|
272
|
+
def handle_auth_switch(response_data, passwd)
|
|
273
|
+
# Parse auth switch request: 0xfe + plugin_name + scramble
|
|
274
|
+
pkt = Packet.new(response_data[1..])
|
|
275
|
+
plugin_name = pkt.string
|
|
276
|
+
scramble = pkt.to_s
|
|
277
|
+
|
|
278
|
+
if plugin_name == "mysql_native_password"
|
|
279
|
+
netpw = encrypt_password(passwd, scramble)
|
|
280
|
+
write netpw
|
|
281
|
+
read # OK or error
|
|
282
|
+
set_state :READY
|
|
283
|
+
elsif plugin_name == "caching_sha2_password"
|
|
284
|
+
netpw = encrypt_password_sha256(passwd, scramble)
|
|
285
|
+
write netpw
|
|
286
|
+
response = read
|
|
287
|
+
if response.to_s.getbyte(0) == 0x01
|
|
288
|
+
handle_caching_sha2_more_data(response.to_s, passwd, scramble)
|
|
289
|
+
else
|
|
290
|
+
set_state :READY
|
|
291
|
+
end
|
|
292
|
+
else
|
|
293
|
+
raise ProtocolError, "Unsupported auth plugin: #{plugin_name}"
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Handle caching_sha2_password "more data" response
|
|
298
|
+
def handle_caching_sha2_more_data(response_data, passwd, scramble)
|
|
299
|
+
# 0x01 + status byte
|
|
300
|
+
status = response_data.getbyte(1)
|
|
301
|
+
|
|
302
|
+
case status
|
|
303
|
+
when 0x03
|
|
304
|
+
# Fast auth success - server already has cached password hash
|
|
305
|
+
read # Read the final OK packet
|
|
306
|
+
set_state :READY
|
|
307
|
+
when 0x04
|
|
308
|
+
# Full authentication required
|
|
309
|
+
if @ssl_enabled
|
|
310
|
+
# Send plaintext password over SSL
|
|
311
|
+
write "#{passwd}\x00"
|
|
312
|
+
read # OK or error
|
|
313
|
+
set_state :READY
|
|
314
|
+
else
|
|
315
|
+
# Need RSA encryption - request public key
|
|
316
|
+
write "\x02" # Request public key
|
|
317
|
+
pubkey_response = read
|
|
318
|
+
pubkey_data = pubkey_response.to_s
|
|
319
|
+
|
|
320
|
+
if pubkey_data.getbyte(0) == 0x01
|
|
321
|
+
# Got public key
|
|
322
|
+
public_key = pubkey_data[1..]
|
|
323
|
+
encrypted_password = rsa_encrypt_password(passwd, scramble, public_key)
|
|
324
|
+
write encrypted_password
|
|
325
|
+
read # OK or error
|
|
326
|
+
set_state :READY
|
|
327
|
+
else
|
|
328
|
+
raise ProtocolError, "Failed to get server public key"
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
else
|
|
332
|
+
raise ProtocolError, "Unknown caching_sha2_password status: #{status}"
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# RSA encrypt password for caching_sha2_password
|
|
337
|
+
def rsa_encrypt_password(passwd, scramble, public_key_pem)
|
|
338
|
+
# XOR password with scramble
|
|
339
|
+
passwd_bytes = "#{passwd}\x00".bytes
|
|
340
|
+
scramble_bytes = scramble.bytes
|
|
341
|
+
xored = passwd_bytes.each_with_index.map { |b, i| b ^ scramble_bytes[i % scramble_bytes.length] }
|
|
342
|
+
|
|
343
|
+
# Encrypt with RSA public key
|
|
344
|
+
rsa = OpenSSL::PKey::RSA.new(public_key_pem)
|
|
345
|
+
rsa.public_encrypt(xored.pack("C*"), OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
private
|
|
349
|
+
|
|
350
|
+
# Upgrade the connection to SSL/TLS
|
|
351
|
+
def upgrade_to_ssl
|
|
352
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
|
353
|
+
|
|
354
|
+
# Configure SSL context based on options
|
|
355
|
+
if @ssl_options[:ca]
|
|
356
|
+
ssl_context.ca_file = @ssl_options[:ca]
|
|
357
|
+
end
|
|
358
|
+
if @ssl_options[:cert]
|
|
359
|
+
ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(@ssl_options[:cert]))
|
|
360
|
+
end
|
|
361
|
+
if @ssl_options[:key]
|
|
362
|
+
ssl_context.key = OpenSSL::PKey::RSA.new(File.read(@ssl_options[:key]))
|
|
363
|
+
end
|
|
364
|
+
if @ssl_options[:ca_path]
|
|
365
|
+
ssl_context.ca_path = @ssl_options[:ca_path]
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Set verification mode
|
|
369
|
+
if @ssl_options[:verify] == false
|
|
370
|
+
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
371
|
+
else
|
|
372
|
+
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Set minimum TLS version if specified
|
|
376
|
+
if @ssl_options[:min_version]
|
|
377
|
+
ssl_context.min_version = @ssl_options[:min_version]
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Wrap socket in SSL
|
|
381
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(@sock, ssl_context)
|
|
382
|
+
ssl_socket.hostname = @ssl_options[:hostname] if @ssl_options[:hostname]
|
|
383
|
+
ssl_socket.sync_close = true
|
|
384
|
+
ssl_socket.connect
|
|
385
|
+
|
|
386
|
+
@sock = ssl_socket
|
|
387
|
+
@ssl_enabled = true
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
public
|
|
391
|
+
|
|
213
392
|
# Quit command
|
|
214
393
|
def quit_command
|
|
215
394
|
synchronize do
|
|
@@ -508,8 +687,8 @@ class MysqlPR
|
|
|
508
687
|
|
|
509
688
|
@sqlstate = "00000"
|
|
510
689
|
|
|
511
|
-
# Error packet
|
|
512
|
-
if ret
|
|
690
|
+
# Error packet (use getbyte for encoding-safe comparison)
|
|
691
|
+
if ret.getbyte(0) == 0xff
|
|
513
692
|
f, errno, marker, @sqlstate, message = ret.unpack("Cvaa5a*")
|
|
514
693
|
unless marker == "#"
|
|
515
694
|
f, errno, message = ret.unpack("Cva*") # Version 4.0 Error
|
|
@@ -575,7 +754,7 @@ class MysqlPR
|
|
|
575
754
|
end
|
|
576
755
|
end
|
|
577
756
|
|
|
578
|
-
# Encrypt password
|
|
757
|
+
# Encrypt password for mysql_native_password (SHA1)
|
|
579
758
|
# === Argument
|
|
580
759
|
# plain :: [String] plain password.
|
|
581
760
|
# scramble :: [String] scramble code from initial packet.
|
|
@@ -588,6 +767,19 @@ class MysqlPR
|
|
|
588
767
|
return hash_stage1.unpack("C*").zip(Digest::SHA1.digest(scramble+hash_stage2).unpack("C*")).map{|a,b| a^b}.pack("C*")
|
|
589
768
|
end
|
|
590
769
|
|
|
770
|
+
# Encrypt password for caching_sha2_password (SHA256)
|
|
771
|
+
# === Argument
|
|
772
|
+
# plain :: [String] plain password.
|
|
773
|
+
# scramble :: [String] scramble code from initial packet.
|
|
774
|
+
# === Return
|
|
775
|
+
# [String] encrypted password
|
|
776
|
+
def encrypt_password_sha256(plain, scramble)
|
|
777
|
+
return "" if plain.nil? or plain.empty?
|
|
778
|
+
hash_stage1 = Digest::SHA256.digest(plain)
|
|
779
|
+
hash_stage2 = Digest::SHA256.digest(hash_stage1)
|
|
780
|
+
hash_stage1.unpack("C*").zip(Digest::SHA256.digest(hash_stage2 + scramble).unpack("C*")).map { |a, b| a ^ b }.pack("C*")
|
|
781
|
+
end
|
|
782
|
+
|
|
591
783
|
# Initial packet
|
|
592
784
|
class InitialPacket
|
|
593
785
|
def self.parse(pkt)
|
|
@@ -599,18 +791,26 @@ class MysqlPR
|
|
|
599
791
|
server_capabilities = pkt.ushort
|
|
600
792
|
server_charset = pkt.utiny
|
|
601
793
|
server_status = pkt.ushort
|
|
602
|
-
|
|
603
|
-
|
|
794
|
+
server_capabilities_upper = pkt.ushort
|
|
795
|
+
auth_plugin_data_length = pkt.utiny
|
|
796
|
+
pkt.read(10) # reserved
|
|
797
|
+
# Read rest of scramble (12 bytes for caching_sha2_password, or variable)
|
|
798
|
+
rest_scramble_len = [auth_plugin_data_length - 8, 12].max
|
|
799
|
+
rest_scramble_buff = pkt.read(rest_scramble_len)
|
|
800
|
+
# Remove trailing null if present
|
|
801
|
+
rest_scramble_buff = rest_scramble_buff.sub(/\x00+\z/, "")
|
|
802
|
+
auth_plugin_name = pkt.string rescue "mysql_native_password"
|
|
604
803
|
raise ProtocolError, "unsupported version: #{protocol_version}" unless protocol_version == VERSION
|
|
605
804
|
raise ProtocolError, "invalid packet: f0=#{f0}" unless f0 == 0
|
|
606
805
|
scramble_buff.concat rest_scramble_buff
|
|
607
|
-
|
|
806
|
+
server_capabilities |= (server_capabilities_upper << 16)
|
|
807
|
+
self.new protocol_version, server_version, thread_id, server_capabilities, server_charset, server_status, scramble_buff, auth_plugin_name
|
|
608
808
|
end
|
|
609
809
|
|
|
610
|
-
attr_reader :protocol_version, :server_version, :thread_id, :server_capabilities, :server_charset, :server_status, :scramble_buff
|
|
810
|
+
attr_reader :protocol_version, :server_version, :thread_id, :server_capabilities, :server_charset, :server_status, :scramble_buff, :auth_plugin_name
|
|
611
811
|
|
|
612
812
|
def initialize(*args)
|
|
613
|
-
@protocol_version, @server_version, @thread_id, @server_capabilities, @server_charset, @server_status, @scramble_buff = args
|
|
813
|
+
@protocol_version, @server_version, @thread_id, @server_capabilities, @server_charset, @server_status, @scramble_buff, @auth_plugin_name = args
|
|
614
814
|
end
|
|
615
815
|
end
|
|
616
816
|
|
|
@@ -688,18 +888,34 @@ class MysqlPR
|
|
|
688
888
|
end
|
|
689
889
|
end
|
|
690
890
|
|
|
891
|
+
# SSL Request packet - sent before SSL handshake
|
|
892
|
+
class SSLRequestPacket
|
|
893
|
+
def self.serialize(client_flags, max_packet_size, charset_number)
|
|
894
|
+
[
|
|
895
|
+
client_flags,
|
|
896
|
+
max_packet_size,
|
|
897
|
+
charset_number,
|
|
898
|
+
"" # filler: 23 bytes of 0x00
|
|
899
|
+
].pack("VVCa23")
|
|
900
|
+
end
|
|
901
|
+
end
|
|
902
|
+
|
|
691
903
|
# Authentication packet
|
|
692
904
|
class AuthenticationPacket
|
|
693
|
-
def self.serialize(client_flags, max_packet_size, charset_number, username, scrambled_password, databasename)
|
|
694
|
-
[
|
|
905
|
+
def self.serialize(client_flags, max_packet_size, charset_number, username, scrambled_password, databasename, auth_plugin_name = nil)
|
|
906
|
+
packet = [
|
|
695
907
|
client_flags,
|
|
696
908
|
max_packet_size,
|
|
697
|
-
|
|
698
|
-
""
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
909
|
+
charset_number,
|
|
910
|
+
"" # reserved 23 bytes
|
|
911
|
+
].pack("VVCa23")
|
|
912
|
+
|
|
913
|
+
packet << "#{username}\x00"
|
|
914
|
+
packet << Packet.lcs(scrambled_password)
|
|
915
|
+
packet << "#{databasename}\x00" if databasename && (client_flags & MysqlPR::CLIENT_CONNECT_WITH_DB) != 0
|
|
916
|
+
packet << "#{auth_plugin_name}\x00" if auth_plugin_name && (client_flags & MysqlPR::CLIENT_PLUGIN_AUTH) != 0
|
|
917
|
+
|
|
918
|
+
packet
|
|
703
919
|
end
|
|
704
920
|
end
|
|
705
921
|
|