ruby-mysql 2.9.14 → 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 +5 -5
- data/CHANGELOG.md +50 -0
- data/README.md +28 -0
- data/lib/mysql/authenticator/caching_sha2_password.rb +62 -0
- data/lib/mysql/authenticator/mysql_native_password.rb +37 -0
- data/lib/mysql/authenticator/sha256_password.rb +40 -0
- data/lib/mysql/authenticator.rb +84 -0
- data/lib/mysql/charset.rb +379 -328
- data/lib/mysql/constants.rb +91 -44
- data/lib/mysql/error.rb +13 -7
- data/lib/mysql/protocol.rb +243 -210
- data/lib/mysql.rb +183 -338
- data/test/test_mysql.rb +264 -550
- metadata +18 -12
- data/README.rdoc +0 -68
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f95fb29b93c207b23bbc1378ce78b10ab297269f0cf24409116a69502cdf9ae8
|
4
|
+
data.tar.gz: 7979ccfd21194e81a4c98f90920b7dc3cdb1165201bdf7bc0d640d16fbe8ebea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '068b6f6008f4b77dc6d34b660c8f06b73db2c06e63fc98d69d5455ceb6ab61c3924d158de4398d6b897cc2fa44d460a879b60355dbb16ac90f0af666f750467d'
|
7
|
+
data.tar.gz: 769105fbd5f16a4b8ab9c55c4a1243c962db3a238f05baa16d970cb2cbaadba0f0487c2daf194b53ce0bfa48639aac37cb79527da02518607a4a0a861cdd6caf
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
## [3.0.0] - 2021-11-16
|
2
|
+
|
3
|
+
- `Mysql.new` no longer connect. use `Mysql.connect` or `Mysql#connect`.
|
4
|
+
|
5
|
+
- `Mysql.init` is removed. use `Mysql.new` instead.
|
6
|
+
|
7
|
+
- `Mysql.new`, `Mysql.conncet` and `Mysql#connect` takes URI object or URI string or Hash object.
|
8
|
+
example:
|
9
|
+
Mysql.connect('mysql://user:password@hostname:port/dbname?charset=ascii')
|
10
|
+
Mysql.connect('mysql://user:password@%2Ftmp%2Fmysql.sock/dbname?charset=ascii') # for UNIX socket
|
11
|
+
Mysql.connect('hostname', 'user', 'password', 'dbname')
|
12
|
+
Mysql.connect(host: 'hostname', username: 'user', password: 'password', database: 'dbname')
|
13
|
+
|
14
|
+
- `Mysql.options` is removed. use `Mysql#param = value` instead.
|
15
|
+
For example:
|
16
|
+
m = Mysql.init
|
17
|
+
m.options(Mysql::OPT_LOCAL_INFILE, true)
|
18
|
+
m.connect(host, user, passwd)
|
19
|
+
change to
|
20
|
+
m = Mysql.new
|
21
|
+
m.local_infile = true
|
22
|
+
m.connect(host, user, passwd)
|
23
|
+
or
|
24
|
+
m = Mysql.connect(host, user, passwd, local_infile: true)
|
25
|
+
|
26
|
+
- `Mysql::Time` is removed.
|
27
|
+
Instead, `Time` object is returned for the DATE, DATETIME, TIMESTAMP data,
|
28
|
+
and `Integer` object is returned for the TIME data.
|
29
|
+
If DATE, DATETIME, TIMESTAMP are invalid values for Time, nil is returned.
|
30
|
+
|
31
|
+
- meaningless methods are removed:
|
32
|
+
* `bind_result`
|
33
|
+
* `client_info`
|
34
|
+
* `client_version`
|
35
|
+
* `get_proto_info`
|
36
|
+
* `get_server_info`
|
37
|
+
* `get_server_version`
|
38
|
+
* `proto_info`
|
39
|
+
* `query_with_result`
|
40
|
+
|
41
|
+
- alias method are removed:
|
42
|
+
* `get_host_info`: use `host_info`
|
43
|
+
* `real_connect`: use `connect`
|
44
|
+
* `real_query`: use `query`
|
45
|
+
|
46
|
+
- methods corresponding to deprecated APIs in MySQL are removed:
|
47
|
+
* `list_dbs`: use `SHOW DATABASES`
|
48
|
+
* `list_fields`: use `SHOW COLUMNS`
|
49
|
+
* `list_processes`: use `SHOW PROCESSLIST`
|
50
|
+
* `list_tables`: use `SHOW TABLES`
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# ruby-mysql
|
2
|
+
|
3
|
+
## Description
|
4
|
+
|
5
|
+
MySQL connector for Ruby.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
```
|
10
|
+
gem install ruby-mysql
|
11
|
+
```
|
12
|
+
|
13
|
+
## Synopsis
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
my = Mysql.connect('mysql://username:password@hostname:port/dbname?charset=utf8mb4')
|
17
|
+
my.query("select col1, col2 from tblname").each do |col1, col2|
|
18
|
+
p col1, col2
|
19
|
+
end
|
20
|
+
stmt = my.prepare('insert into tblname (col1,col2) values (?,?)')
|
21
|
+
stmt.execute 123, 'abc'
|
22
|
+
```
|
23
|
+
|
24
|
+
## Copyright
|
25
|
+
|
26
|
+
* Author: TOMITA Masahiro <tommy@tmtm.org>
|
27
|
+
* Copyright: Copyright 2008 TOMITA Masahiro
|
28
|
+
* License: MIT
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'digest/sha2'
|
2
|
+
|
3
|
+
class Mysql
|
4
|
+
class Authenticator
|
5
|
+
class CachingSha2Password
|
6
|
+
# @param protocol [Mysql::Protocol]
|
7
|
+
def initialize(protocol)
|
8
|
+
@protocol = protocol
|
9
|
+
end
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
def name
|
13
|
+
'caching_sha2_password'
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param passwd [String]
|
17
|
+
# @param scramble [String]
|
18
|
+
# @yield [String] hashed password
|
19
|
+
# @return [Mysql::Packet]
|
20
|
+
def authenticate(passwd, scramble)
|
21
|
+
yield hash_password(passwd, scramble)
|
22
|
+
pkt = @protocol.read
|
23
|
+
data = pkt.to_s
|
24
|
+
if data.size == 2 && data[0] == "\x01"
|
25
|
+
case data[1]
|
26
|
+
when "\x03" # fast_auth_success
|
27
|
+
# OK
|
28
|
+
when "\x04" # perform_full_authentication
|
29
|
+
if @protocol.client_flags & CLIENT_SSL != 0
|
30
|
+
@protocol.write passwd+"\0"
|
31
|
+
elsif !@protocol.get_server_public_key
|
32
|
+
raise 'Authentication requires secure connection'
|
33
|
+
else
|
34
|
+
@protocol.write "\2" # request public key
|
35
|
+
pkt = @protocol.read
|
36
|
+
pkt.utiny # skip
|
37
|
+
pubkey = pkt.to_s
|
38
|
+
hash = (passwd+"\0").unpack("C*").zip(scramble.unpack("C*")).map{|a, b| a ^ b}.pack("C*")
|
39
|
+
enc = OpenSSL::PKey::RSA.new(pubkey).public_encrypt(hash, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
|
40
|
+
@protocol.write enc
|
41
|
+
end
|
42
|
+
else
|
43
|
+
raise "invalid auth reply packet: #{data.inspect}"
|
44
|
+
end
|
45
|
+
pkt = @protocol.read
|
46
|
+
end
|
47
|
+
return pkt
|
48
|
+
end
|
49
|
+
|
50
|
+
# @param passwd [String]
|
51
|
+
# @param scramble [String]
|
52
|
+
# @return [String] hashed password
|
53
|
+
def hash_password(passwd, scramble)
|
54
|
+
return '' if passwd.nil? or passwd.empty?
|
55
|
+
hash1 = Digest::SHA256.digest(passwd)
|
56
|
+
hash2 = Digest::SHA256.digest(hash1)
|
57
|
+
hash3 = Digest::SHA256.digest(hash2 + scramble)
|
58
|
+
hash1.unpack("C*").zip(hash3.unpack("C*")).map{|a, b| a ^ b}.pack("C*")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
3
|
+
class Mysql
|
4
|
+
class Authenticator
|
5
|
+
class MysqlNativePassword
|
6
|
+
# @param protocol [Mysql::Protocol]
|
7
|
+
def initialize(protocol)
|
8
|
+
@protocol = protocol
|
9
|
+
end
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
def name
|
13
|
+
'mysql_native_password'
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param passwd [String]
|
17
|
+
# @param scramble [String]
|
18
|
+
# @yield [String] hashed password
|
19
|
+
# @return [Mysql::Packet]
|
20
|
+
def authenticate(passwd, scramble)
|
21
|
+
yield hash_password(passwd, scramble)
|
22
|
+
@protocol.read
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param passwd [String]
|
26
|
+
# @param scramble [String]
|
27
|
+
# @return [String] hashed password
|
28
|
+
def hash_password(passwd, scramble)
|
29
|
+
return '' if passwd.nil? or passwd.empty?
|
30
|
+
hash1 = Digest::SHA1.digest(passwd)
|
31
|
+
hash2 = Digest::SHA1.digest(hash1)
|
32
|
+
hash3 = Digest::SHA1.digest(scramble + hash2)
|
33
|
+
hash1.unpack("C*").zip(hash3.unpack("C*")).map{|a, b| a ^ b}.pack("C*")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
class Mysql
|
4
|
+
class Authenticator
|
5
|
+
class Sha256Password
|
6
|
+
# @param protocol [Mysql::Protocol]
|
7
|
+
def initialize(protocol)
|
8
|
+
@protocol = protocol
|
9
|
+
end
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
def name
|
13
|
+
'sha256_password'
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param passwd [String]
|
17
|
+
# @param scramble [String]
|
18
|
+
# @yield [String] hashed password
|
19
|
+
# @return [Mysql::Packet]
|
20
|
+
def authenticate(passwd, scramble)
|
21
|
+
if @protocol.client_flags & CLIENT_SSL != 0
|
22
|
+
yield passwd+"\0"
|
23
|
+
return @protocol.read
|
24
|
+
end
|
25
|
+
yield "\x01" # request public key
|
26
|
+
pkt = @protocol.read
|
27
|
+
data = pkt.to_s
|
28
|
+
if data[0] == "\x01"
|
29
|
+
pkt.utiny # skip
|
30
|
+
pubkey = pkt.to_s
|
31
|
+
hash = (passwd+"\0").unpack("C*").zip(scramble.unpack("C*")).map{|a, b| a ^ b}.pack("C*")
|
32
|
+
enc = OpenSSL::PKey::RSA.new(pubkey).public_encrypt(hash, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
|
33
|
+
@protocol.write enc
|
34
|
+
pkt = @protocol.read
|
35
|
+
end
|
36
|
+
return pkt
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
class Mysql
|
2
|
+
class Authenticator
|
3
|
+
@plugins = {}
|
4
|
+
|
5
|
+
# @param plugin [String]
|
6
|
+
def self.plugin_class(plugin)
|
7
|
+
return @plugins[plugin] if @plugins[plugin]
|
8
|
+
|
9
|
+
raise ClientError, "invalid plugin name: #{plugin}" unless plugin.match?(/\A\w+\z/)
|
10
|
+
begin
|
11
|
+
require_relative "authenticator/#{plugin}"
|
12
|
+
rescue LoadError
|
13
|
+
return nil
|
14
|
+
end
|
15
|
+
class_name = plugin.gsub(/(?:^|_)(.)/){$1.upcase}
|
16
|
+
raise ClientError, "#{class_name} is undefined" unless self.const_defined? class_name
|
17
|
+
klass = self.const_get(class_name)
|
18
|
+
@plugins[plugin] = klass
|
19
|
+
return klass
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(protocol)
|
23
|
+
@protocol = protocol
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param plugin [String]
|
27
|
+
def get(plugin)
|
28
|
+
self.class.plugin_class(plugin)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param plugin [String]
|
32
|
+
def get!(plugin)
|
33
|
+
get(plugin) or raise ClientError, "unknown plugin: #{plugin}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def authenticate(user, passwd, db, scramble, plugin_name)
|
37
|
+
plugin = (get(plugin_name) || DummyPlugin).new(@protocol)
|
38
|
+
pkt = plugin.authenticate(passwd, scramble) do |hashed|
|
39
|
+
@protocol.write Protocol::AuthenticationPacket.serialize(@protocol.client_flags, 1024**3, @protocol.charset.number, user, hashed, db, plugin.name)
|
40
|
+
end
|
41
|
+
while true
|
42
|
+
res = Protocol::AuthenticationResultPacket.parse(pkt)
|
43
|
+
case res.result
|
44
|
+
when 0 # OK
|
45
|
+
break
|
46
|
+
when 2 # multi factor auth
|
47
|
+
raise ClientError, 'multi factor authentication is not supported'
|
48
|
+
when 254 # change auth plugin
|
49
|
+
plugin = get!(res.auth_plugin).new(@protocol)
|
50
|
+
pkt = plugin.authenticate(passwd, res.scramble) do |hashed|
|
51
|
+
if passwd.nil? || passwd.empty?
|
52
|
+
@protocol.write "\0"
|
53
|
+
else
|
54
|
+
@protocol.write hashed
|
55
|
+
end
|
56
|
+
end
|
57
|
+
else
|
58
|
+
raise ClientError, "invalid packet: #{pkt.to_s}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class DummyPlugin
|
64
|
+
# @param protocol [Mysql::Protocol]
|
65
|
+
def initialize(protocol)
|
66
|
+
@protocol = protocol
|
67
|
+
end
|
68
|
+
|
69
|
+
# @return [String]
|
70
|
+
def name
|
71
|
+
''
|
72
|
+
end
|
73
|
+
|
74
|
+
# @param passwd [String]
|
75
|
+
# @param scramble [String]
|
76
|
+
# @yield [String] hashed password
|
77
|
+
# @return [Mysql::Packet]
|
78
|
+
def authenticate(passwd, scramble)
|
79
|
+
yield ''
|
80
|
+
@protocol.read
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|