Skip to content

Commit 4eeba6a

Browse files
committed
TLS connection
1 parent 14d1f8b commit 4eeba6a

File tree

5 files changed

+94
-11
lines changed

5 files changed

+94
-11
lines changed

lib/mysql.rb

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def initialize
9696
@last_error = nil
9797
@result_exist = false
9898
@local_infile = nil
99+
@ssl_mode = SSL_MODE_PREFERRED
99100
end
100101

101102
# Connect to mysqld.
@@ -112,7 +113,7 @@ def connect(host=nil, user=nil, passwd=nil, db=nil, port=nil, socket=nil, flag=0
112113
warn 'unsupported flag: CLIENT_COMPRESS' if $VERBOSE
113114
flag &= ~CLIENT_COMPRESS
114115
end
115-
@protocol = Protocol.new host, port, socket, @connect_timeout, @read_timeout, @write_timeout, @local_infile
116+
@protocol = Protocol.new host, port, socket, @connect_timeout, @read_timeout, @write_timeout, @local_infile, @ssl_mode
116117
@protocol.authenticate user, passwd, db, flag, @charset
117118
@charset ||= @protocol.charset
118119
@host_info = (host.nil? || host == "localhost") ? 'Localhost via UNIX socket' : "#{host} via TCP/IP"
@@ -145,37 +146,61 @@ def close!
145146
#
146147
# Available options:
147148
# Mysql::INIT_COMMAND, Mysql::OPT_CONNECT_TIMEOUT, Mysql::OPT_READ_TIMEOUT,
148-
# Mysql::OPT_WRITE_TIMEOUT, Mysql::SET_CHARSET_NAME
149+
# Mysql::OPT_SSL_MODE, Mysql::OPT_WRITE_TIMEOUT, Mysql::SET_CHARSET_NAME
149150
# @param [Integer] opt option
150151
# @param [Integer] value option value that is depend on opt
151152
# @return [Mysql] self
152153
def options(opt, value=nil)
153154
case opt
155+
# when Mysql::DEFAULT_AUTH
156+
# when Mysql::ENABLE_CLEARTEXT_PLUGIN
154157
when Mysql::INIT_COMMAND
155158
@init_command = value.to_s
159+
# when Mysql::OPT_BIND
160+
# when Mysql::OPT_CAN_HANDLE_EXPIRED_PASSWORDS
156161
# when Mysql::OPT_COMPRESS
162+
# when Mysql::OPT_COMPRESSION_ALGORITHMS
163+
# when Mysql::OPT_CONNECT_ATTR_ADD
164+
# when Mysql::OPT_CONNECT_ATTR_DELETE
165+
# when Mysql::OPT_CONNECT_ATTR_RESET
157166
when Mysql::OPT_CONNECT_TIMEOUT
158167
@connect_timeout = value
159-
# when Mysql::GUESS_CONNECTION
160-
when Mysql::OPT_LOCAL_INFILE
161-
@local_infile = value ? '' : nil
168+
# when Mysql::OPT_GET_SERVER_PUBLIC_KEY
162169
when Mysql::OPT_LOAD_DATA_LOCAL_DIR
163170
@local_infile = value
171+
when Mysql::OPT_LOCAL_INFILE
172+
@local_infile = value ? '' : nil
173+
# when Mysql::OPT_MAX_ALLOWED_PACKET
164174
# when Mysql::OPT_NAMED_PIPE
175+
# when Mysql::OPT_NET_BUFFER_LENGTH
176+
# when Mysql::OPT_OPTIONAL_RESULTSET_METADATA
165177
# when Mysql::OPT_PROTOCOL
166178
when Mysql::OPT_READ_TIMEOUT
167179
@read_timeout = value.to_i
168180
# when Mysql::OPT_RECONNECT
181+
# when Mysql::OPT_RETRY_COUNT
169182
# when Mysql::SET_CLIENT_IP
170-
# when Mysql::OPT_SSL_VERIFY_SERVER_CERT
171-
# when Mysql::OPT_USE_EMBEDDED_CONNECTION
172-
# when Mysql::OPT_USE_REMOTE_CONNECTION
183+
# when Mysql::OPT_SSL_CA
184+
# when Mysql::OPT_SSL_CAPATH
185+
# when Mysql::OPT_SSL_CERT
186+
# when Mysql::OPT_SSL_CIPHER
187+
# when Mysql::OPT_SSL_CRL
188+
# when Mysql::OPT_SSL_CRLPATH
189+
# when Mysql::OPT_SSL_FIPS_MODE
190+
# when Mysql::OPT_SSL_KEY
191+
when Mysql::OPT_SSL_MODE
192+
@ssl_mode = value
193+
# when Mysql::OPT_TLS_CIPHERSUITES
194+
# when Mysql::OPT_TLS_VERSION
195+
# when Mysql::OPT_USE_RESULT
173196
when Mysql::OPT_WRITE_TIMEOUT
174197
@write_timeout = value.to_i
198+
# when Mysql::OPT_ZSTD_COMPRESSION_LEVEL
199+
# when Mysql::PLUGIN_DIR
175200
# when Mysql::READ_DEFAULT_FILE
176201
# when Mysql::READ_DEFAULT_GROUP
177202
# when Mysql::REPORT_DATA_TRUNCATION
178-
# when Mysql::SECURE_AUTH
203+
# when Mysql::SERVER_PUBLIC_KEY
179204
# when Mysql::SET_CHARSET_DIR
180205
when Mysql::SET_CHARSET_NAME
181206
@charset = Charset.by_name value.to_s

lib/mysql/authenticator/caching_sha2_password.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ def authenticate(passwd, scramble)
2626
when "\x03" # fast_auth_success
2727
# OK
2828
when "\x04" # perform_full_authentication
29-
raise 'Authentication requires secure connection (not supported)'
29+
if @protocol.client_flags & CLIENT_SSL == 0
30+
raise 'Authentication requires secure connection'
31+
end
32+
@protocol.write passwd+"\0"
3033
else
3134
raise "invalid auth reply packet: #{data.inspect}"
3235
end

lib/mysql/authenticator/sha256_password.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ def name
1818
# @yield [String] hashed password
1919
# @return [Mysql::Packet]
2020
def authenticate(passwd, scramble)
21+
if @protocol.client_flags & CLIENT_SSL != 0
22+
yield passwd+"\0"
23+
return @protocol.read
24+
end
2125
yield "\x01" # request public key
2226
pkt = @protocol.read
2327
data = pkt.to_s

lib/mysql/constants.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ class Mysql
116116
OPT_ZSTD_COMPRESSION_LEVEL = 42
117117
OPT_LOAD_DATA_LOCAL_DIR = 43
118118

119+
# SSL Mode
120+
SSL_MODE_DISABLED = 1
121+
SSL_MODE_PREFERRED = 2
122+
SSL_MODE_REQUIRED = 3
123+
SSL_MODE_VERIFY_CA = 4
124+
SSL_MODE_VERIFY_IDENTITY = 5
125+
119126
# Server Option
120127
OPTION_MULTI_STATEMENTS_ON = 0
121128
OPTION_MULTI_STATEMENTS_OFF = 1

lib/mysql/protocol.rb

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require "socket"
66
require "timeout"
77
require "stringio"
8+
require "openssl"
89
require_relative 'authenticator.rb'
910

1011
class Mysql
@@ -136,16 +137,18 @@ def self.value2net(v)
136137
# read_timeout :: [Integer] read timeout (sec).
137138
# write_timeout :: [Integer] write timeout (sec).
138139
# local_infile :: [String] local infile path
140+
# ssl_mode :: [Integer]
139141
# === Exception
140142
# [ClientError] :: connection timeout
141-
def initialize(host, port, socket, conn_timeout, read_timeout, write_timeout, local_infile)
143+
def initialize(host, port, socket, conn_timeout, read_timeout, write_timeout, local_infile, ssl_mode)
142144
@insert_id = 0
143145
@warning_count = 0
144146
@gc_stmt_queue = [] # stmt id list which GC destroy.
145147
set_state :INIT
146148
@read_timeout = read_timeout
147149
@write_timeout = write_timeout
148150
@local_infile = local_infile
151+
@ssl_mode = ssl_mode
149152
begin
150153
Timeout.timeout conn_timeout do
151154
if host.nil? or host.empty? or host == "localhost"
@@ -181,6 +184,7 @@ def authenticate(user, passwd, db, flag, charset)
181184
init_packet = InitialPacket.parse read
182185
@server_info = init_packet.server_version
183186
@server_version = init_packet.server_version.split(/\D/)[0,3].inject{|a,b|a.to_i*100+b.to_i}
187+
@server_capabilities = init_packet.server_capabilities
184188
@thread_id = init_packet.thread_id
185189
@client_flags = CLIENT_LONG_PASSWORD | CLIENT_LONG_FLAG | CLIENT_TRANSACTIONS | CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION | CLIENT_PLUGIN_AUTH
186190
@client_flags |= CLIENT_LOCAL_FILES if @local_infile
@@ -191,10 +195,38 @@ def authenticate(user, passwd, db, flag, charset)
191195
@charset = Charset.by_number(init_packet.server_charset)
192196
@charset.encoding # raise error if unsupported charset
193197
end
198+
enable_ssl
194199
Authenticator.new(self).authenticate(user, passwd, db, init_packet.scramble_buff, init_packet.auth_plugin)
195200
set_state :READY
196201
end
197202

203+
def enable_ssl
204+
case @ssl_mode
205+
when SSL_MODE_DISABLED
206+
return
207+
when SSL_MODE_PREFERRED
208+
return if @sock.is_a? UNIXSocket
209+
return if @server_capabilities & CLIENT_SSL == 0
210+
when SSL_MODE_REQUIRED
211+
if @server_capabilities & CLIENT_SSL == 0
212+
raise ClientError::SslConnectionError, "SSL is required but the server doesn't support it"
213+
end
214+
else
215+
raise ClientError, "ssl_mode #{@ssl_mode} is not supported"
216+
end
217+
begin
218+
@client_flags |= CLIENT_SSL
219+
write Protocol::TlsAuthenticationPacket.serialize(@client_flags, 1024**3, @charset.number)
220+
@sock = OpenSSL::SSL::SSLSocket.new(@sock)
221+
@sock.sync_close = true
222+
@sock.connect
223+
rescue => e
224+
@client_flags &= ~CLIENT_SSL
225+
return if @ssl_mode == SSL_MODE_PREFERRED
226+
raise e
227+
end
228+
end
229+
198230
# Quit command
199231
def quit_command
200232
synchronize do
@@ -700,6 +732,18 @@ def self.serialize(client_flags, max_packet_size, charset_number, username, scra
700732
end
701733
end
702734

735+
# TLS Authentication packet
736+
class TlsAuthenticationPacket
737+
def self.serialize(client_flags, max_packet_size, charset_number)
738+
[
739+
client_flags,
740+
max_packet_size,
741+
charset_number,
742+
"", # always 0x00 * 23
743+
].pack("VVCa23")
744+
end
745+
end
746+
703747
# Execute packet
704748
class ExecutePacket
705749
def self.serialize(statement_id, cursor_type, values)

0 commit comments

Comments
 (0)