ruby-mysql 2.9.14 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|