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.
@@ -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
- # @private
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, @name, @csname = number, name, csname
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
- # @private
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 number, csname, clname
197
- cs.unsafe = true if UNSAFE_CHARSET.include? csname
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
- # @private
204
- BINARY_CHARSET_NUMBER = CHARSET_DEFAULT['binary'].number
199
+ NUMBER_TO_CHARSET.freeze
200
+ COLLATION_TO_CHARSET.freeze
201
+ CHARSET_DEFAULT.freeze
205
202
 
206
- # @private
207
- # @param [Integer] n
208
- # @return [Mysql::Charset]
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? n
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
- # @private
215
- # @param [String] str
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
- if defined? Encoding
224
-
225
- # @private
226
- # MySQL Charset -> Ruby's Encodeing
227
- CHARSET_ENCODING = {
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
- def self.to_binary(value)
307
- value
308
- end
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
- def self.convert_encoding(raw, encoding)
311
- raw
312
- end
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
- def encoding
315
- nil
316
- end
282
+ enc
283
+ end
317
284
 
318
- def convert(value)
319
- value
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
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Copyright (C) 2003-2010 TOMITA Masahiro
2
4
  # mailto:tommy@tmtm.org
3
5
 
@@ -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 < 16777216
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[0] == ?\xfe && @data.length == 5
72
+ @data.getbyte(0) == 0xfe && @data.length == 5
70
73
  end
71
74
 
72
75
  def to_s
@@ -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
- netpw = encrypt_password passwd, init_packet.scramble_buff
208
- write AuthenticationPacket.serialize(client_flags, 1024**3, @charset.number, user, netpw, db)
209
- raise ProtocolError, 'The old style password is not supported' if read.to_s == "\xfe"
210
- set_state :READY
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[0] == ?\xff
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
- f1 = pkt.read(13)
603
- rest_scramble_buff = pkt.string
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
- self.new protocol_version, server_version, thread_id, server_capabilities, server_charset, server_status, scramble_buff
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
- Packet.lcb(charset_number),
698
- "", # always 0x00 * 23
699
- username,
700
- Packet.lcs(scrambled_password),
701
- databasename
702
- ].pack("VVa*a23Z*A*Z*")
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
 
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MysqlPR
4
+ VERSION = "3.0.0"
5
+ end