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
data/lib/mysql/protocol.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# coding: ascii-8bit
|
2
|
-
# Copyright (C) 2008
|
2
|
+
# Copyright (C) 2008 TOMITA Masahiro
|
3
3
|
# mailto:tommy@tmtm.org
|
4
4
|
|
5
5
|
require "socket"
|
6
|
-
require "timeout"
|
7
|
-
require "digest/sha1"
|
8
6
|
require "stringio"
|
7
|
+
require "openssl"
|
8
|
+
require_relative 'authenticator.rb'
|
9
9
|
|
10
10
|
class Mysql
|
11
11
|
# MySQL network protocol
|
@@ -15,12 +15,10 @@ class Mysql
|
|
15
15
|
MAX_PACKET_LENGTH = 2**24-1
|
16
16
|
|
17
17
|
# Convert netdata to Ruby value
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
# === Return
|
23
|
-
# Object :: converted value.
|
18
|
+
# @param data [Packet] packet data
|
19
|
+
# @param type [Integer] field type
|
20
|
+
# @param unsigned [true or false] true if value is unsigned
|
21
|
+
# @return [Object] converted value.
|
24
22
|
def self.net2value(pkt, type, unsigned)
|
25
23
|
case type
|
26
24
|
when Field::TYPE_STRING, Field::TYPE_VAR_STRING, Field::TYPE_NEWDECIMAL, Field::TYPE_BLOB, Field::TYPE_JSON
|
@@ -45,17 +43,18 @@ class Mysql
|
|
45
43
|
when Field::TYPE_DATE
|
46
44
|
len = pkt.utiny
|
47
45
|
y, m, d = pkt.read(len).unpack("vCC")
|
48
|
-
t =
|
46
|
+
t = Time.new(y, m, d) rescue nil
|
49
47
|
return t
|
50
48
|
when Field::TYPE_DATETIME, Field::TYPE_TIMESTAMP
|
51
49
|
len = pkt.utiny
|
52
50
|
y, m, d, h, mi, s, sp = pkt.read(len).unpack("vCCCCCV")
|
53
|
-
return
|
51
|
+
return Time.new(y, m, d, h, mi, Rational((s.to_i*1000000+sp.to_i)/1000000)) rescue nil
|
54
52
|
when Field::TYPE_TIME
|
55
53
|
len = pkt.utiny
|
56
54
|
sign, d, h, mi, s, sp = pkt.read(len).unpack("CVCCCV")
|
57
|
-
|
58
|
-
|
55
|
+
r = d.to_i*86400 + h.to_i*3600 + mi.to_i*60 + s.to_i + sp.to_f/1000000
|
56
|
+
r *= -1 if sign != 0
|
57
|
+
return r
|
59
58
|
when Field::TYPE_YEAR
|
60
59
|
return pkt.ushort
|
61
60
|
when Field::TYPE_BIT
|
@@ -66,13 +65,10 @@ class Mysql
|
|
66
65
|
end
|
67
66
|
|
68
67
|
# convert Ruby value to netdata
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
# String :: netdata
|
74
|
-
# === Exception
|
75
|
-
# ProtocolError :: value too large / value is not supported
|
68
|
+
# @param v [Object] Ruby value.
|
69
|
+
# @return [Integer] type of column. Field::TYPE_*
|
70
|
+
# @return [String] netdata
|
71
|
+
# @raise [ProtocolError] value too large / value is not supported
|
76
72
|
def self.value2net(v)
|
77
73
|
case v
|
78
74
|
when nil
|
@@ -97,12 +93,9 @@ class Mysql
|
|
97
93
|
when String
|
98
94
|
type = Field::TYPE_STRING
|
99
95
|
val = Packet.lcs(v)
|
100
|
-
when
|
96
|
+
when Time
|
101
97
|
type = Field::TYPE_DATETIME
|
102
98
|
val = [11, v.year, v.month, v.day, v.hour, v.min, v.sec, v.usec].pack("CvCCCCCV")
|
103
|
-
when Mysql::Time
|
104
|
-
type = Field::TYPE_DATETIME
|
105
|
-
val = [11, v.year, v.month, v.day, v.hour, v.min, v.sec, v.second_part].pack("CvCCCCCV")
|
106
99
|
else
|
107
100
|
raise ProtocolError, "class #{v.class} is not supported"
|
108
101
|
end
|
@@ -112,12 +105,14 @@ class Mysql
|
|
112
105
|
attr_reader :server_info
|
113
106
|
attr_reader :server_version
|
114
107
|
attr_reader :thread_id
|
108
|
+
attr_reader :client_flags
|
115
109
|
attr_reader :sqlstate
|
116
110
|
attr_reader :affected_rows
|
117
111
|
attr_reader :insert_id
|
118
112
|
attr_reader :server_status
|
119
113
|
attr_reader :warning_count
|
120
114
|
attr_reader :message
|
115
|
+
attr_reader :get_server_public_key
|
121
116
|
attr_accessor :charset
|
122
117
|
|
123
118
|
# @state variable keep state for connection.
|
@@ -127,72 +122,101 @@ class Mysql
|
|
127
122
|
# :RESULT :: After retr_fields(), retr_all_records() or stmt_retr_all_records() is needed.
|
128
123
|
|
129
124
|
# make socket connection to server.
|
130
|
-
#
|
131
|
-
# host
|
132
|
-
#
|
133
|
-
#
|
134
|
-
#
|
135
|
-
#
|
136
|
-
#
|
137
|
-
#
|
138
|
-
# [
|
139
|
-
|
125
|
+
# @param opts [Hash]
|
126
|
+
# @option :host [String] hostname mysqld running
|
127
|
+
# @option :username [String] username to connect to mysqld
|
128
|
+
# @option :password [String] password to connect to mysqld
|
129
|
+
# @option :database [String] initial database name
|
130
|
+
# @option :port [String] port number (used if host is not 'localhost' or nil)
|
131
|
+
# @option :socket [String] socket filename (used if host is 'localhost' or nil)
|
132
|
+
# @option :flags [Integer] connection flag. Mysql::CLIENT_* ORed
|
133
|
+
# @option :charset [Mysql::Charset] character set
|
134
|
+
# @option :connect_timeout [Numeric, nil]
|
135
|
+
# @option :read_timeout [Numeric, nil]
|
136
|
+
# @option :write_timeout [Numeric, nil]
|
137
|
+
# @option :local_infile [Boolean]
|
138
|
+
# @option :load_data_local_dir [String]
|
139
|
+
# @option :ssl_mode [Integer]
|
140
|
+
# @option :get_server_public_key [Boolean]
|
141
|
+
# @raise [ClientError] connection timeout
|
142
|
+
def initialize(opts)
|
143
|
+
@opts = opts
|
144
|
+
@charset = Mysql::Charset.by_name("utf8mb4")
|
140
145
|
@insert_id = 0
|
141
146
|
@warning_count = 0
|
142
147
|
@gc_stmt_queue = [] # stmt id list which GC destroy.
|
143
148
|
set_state :INIT
|
144
|
-
@
|
145
|
-
@write_timeout = write_timeout
|
149
|
+
@get_server_public_key = @opts[:get_server_public_key]
|
146
150
|
begin
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
@sock = TCPSocket.new host, port
|
154
|
-
end
|
151
|
+
if @opts[:host].nil? or @opts[:host].empty? or @opts[:host] == "localhost"
|
152
|
+
socket = @opts[:socket] || ENV["MYSQL_UNIX_PORT"] || MYSQL_UNIX_PORT
|
153
|
+
@socket = Socket.unix(socket)
|
154
|
+
else
|
155
|
+
port = @opts[:port] || ENV["MYSQL_TCP_PORT"] || (Socket.getservbyname("mysql","tcp") rescue MYSQL_TCP_PORT)
|
156
|
+
@socket = Socket.tcp(@opts[:host], port, connect_timeout: @opts[:connect_timeout])
|
155
157
|
end
|
156
|
-
rescue
|
158
|
+
rescue Errno::ETIMEDOUT
|
157
159
|
raise ClientError, "connection timeout"
|
158
160
|
end
|
159
161
|
end
|
160
162
|
|
161
163
|
def close
|
162
|
-
@
|
164
|
+
@socket.close
|
163
165
|
end
|
164
166
|
|
165
167
|
# initial negotiate and authenticate.
|
166
|
-
#
|
167
|
-
#
|
168
|
-
|
169
|
-
# db :: [String / nil] default database name. nil: no default.
|
170
|
-
# flag :: [Integer] client flag
|
171
|
-
# charset :: [Mysql::Charset / nil] charset for connection. nil: use server's charset
|
172
|
-
# === Exception
|
173
|
-
# ProtocolError :: The old style password is not supported
|
174
|
-
def authenticate(user, passwd, db, flag, charset)
|
168
|
+
# @param charset [Mysql::Charset, nil] charset for connection. nil: use server's charset
|
169
|
+
# @raise [ProtocolError] The old style password is not supported
|
170
|
+
def authenticate
|
175
171
|
check_state :INIT
|
176
|
-
@authinfo = [user, passwd, db, flag, charset]
|
177
172
|
reset
|
178
173
|
init_packet = InitialPacket.parse read
|
179
174
|
@server_info = init_packet.server_version
|
180
175
|
@server_version = init_packet.server_version.split(/\D/)[0,3].inject{|a,b|a.to_i*100+b.to_i}
|
176
|
+
@server_capabilities = init_packet.server_capabilities
|
181
177
|
@thread_id = init_packet.thread_id
|
182
|
-
client_flags = CLIENT_LONG_PASSWORD | CLIENT_LONG_FLAG | CLIENT_TRANSACTIONS | CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION
|
183
|
-
client_flags |=
|
184
|
-
client_flags |=
|
185
|
-
@
|
186
|
-
|
178
|
+
@client_flags = CLIENT_LONG_PASSWORD | CLIENT_LONG_FLAG | CLIENT_TRANSACTIONS | CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION | CLIENT_PLUGIN_AUTH
|
179
|
+
@client_flags |= CLIENT_LOCAL_FILES if @opts[:local_infile] || @opts[:load_data_local_dir]
|
180
|
+
@client_flags |= CLIENT_CONNECT_WITH_DB if @opts[:database]
|
181
|
+
@client_flags |= @opts[:flags]
|
182
|
+
if @opts[:charset]
|
183
|
+
@charset = @opts[:charset].is_a?(Charset) ? @opts[:charset] : Charset.by_name(@opts[:charset])
|
184
|
+
else
|
187
185
|
@charset = Charset.by_number(init_packet.server_charset)
|
188
186
|
@charset.encoding # raise error if unsupported charset
|
189
187
|
end
|
190
|
-
|
191
|
-
|
192
|
-
raise ProtocolError, 'The old style password is not supported' if read.to_s == "\xfe"
|
188
|
+
enable_ssl
|
189
|
+
Authenticator.new(self).authenticate(@opts[:username], @opts[:password].to_s, @opts[:database], init_packet.scramble_buff, init_packet.auth_plugin)
|
193
190
|
set_state :READY
|
194
191
|
end
|
195
192
|
|
193
|
+
def enable_ssl
|
194
|
+
case @opts[:ssl_mode]
|
195
|
+
when SSL_MODE_DISABLED, '1', 'disabled'
|
196
|
+
return
|
197
|
+
when SSL_MODE_PREFERRED, '2', 'preferred'
|
198
|
+
return if @socket.local_address.unix?
|
199
|
+
return if @server_capabilities & CLIENT_SSL == 0
|
200
|
+
when SSL_MODE_REQUIRED, '3', 'required'
|
201
|
+
if @server_capabilities & CLIENT_SSL == 0
|
202
|
+
raise ClientError::SslConnectionError, "SSL is required but the server doesn't support it"
|
203
|
+
end
|
204
|
+
else
|
205
|
+
raise ClientError, "ssl_mode #{@opts[:ssl_mode]} is not supported"
|
206
|
+
end
|
207
|
+
begin
|
208
|
+
@client_flags |= CLIENT_SSL
|
209
|
+
write Protocol::TlsAuthenticationPacket.serialize(@client_flags, 1024**3, @charset.number)
|
210
|
+
@socket = OpenSSL::SSL::SSLSocket.new(@socket)
|
211
|
+
@socket.sync_close = true
|
212
|
+
@socket.connect
|
213
|
+
rescue => e
|
214
|
+
@client_flags &= ~CLIENT_SSL
|
215
|
+
return if @opts[:ssl_mode] == SSL_MODE_PREFERRED
|
216
|
+
raise e
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
196
220
|
# Quit command
|
197
221
|
def quit_command
|
198
222
|
synchronize do
|
@@ -203,10 +227,8 @@ class Mysql
|
|
203
227
|
end
|
204
228
|
|
205
229
|
# Query command
|
206
|
-
#
|
207
|
-
#
|
208
|
-
# === Return
|
209
|
-
# [Integer / nil] number of fields of results. nil if no results.
|
230
|
+
# @param query [String] query string
|
231
|
+
# @return [Integer, nil] number of fields of results. nil if no results.
|
210
232
|
def query_command(query)
|
211
233
|
check_state :READY
|
212
234
|
begin
|
@@ -220,8 +242,7 @@ class Mysql
|
|
220
242
|
end
|
221
243
|
|
222
244
|
# get result of query.
|
223
|
-
#
|
224
|
-
# [integer / nil] number of fields of results. nil if no results.
|
245
|
+
# @return [integer, nil] number of fields of results. nil if no results.
|
225
246
|
def get_result
|
226
247
|
begin
|
227
248
|
res_packet = ResultPacket.parse read
|
@@ -230,10 +251,7 @@ class Mysql
|
|
230
251
|
return res_packet.field_count
|
231
252
|
end
|
232
253
|
if res_packet.field_count.nil? # LOAD DATA LOCAL INFILE
|
233
|
-
|
234
|
-
File.open(filename){|f| write f}
|
235
|
-
write nil # EOF mark
|
236
|
-
read
|
254
|
+
send_local_file(res_packet.message)
|
237
255
|
end
|
238
256
|
@affected_rows, @insert_id, @server_status, @warning_count, @message =
|
239
257
|
res_packet.affected_rows, res_packet.insert_id, res_packet.server_status, res_packet.warning_count, res_packet.message
|
@@ -245,11 +263,22 @@ class Mysql
|
|
245
263
|
end
|
246
264
|
end
|
247
265
|
|
266
|
+
# send local file to server
|
267
|
+
def send_local_file(filename)
|
268
|
+
filename = File.absolute_path(filename)
|
269
|
+
if @opts[:local_infile] || @opts[:load_data_local_dir] && filename.start_with?(@opts[:load_data_local_dir])
|
270
|
+
File.open(filename){|f| write f}
|
271
|
+
else
|
272
|
+
raise ClientError::LoadDataLocalInfileRejected, 'LOAD DATA LOCAL INFILE file request rejected due to restrictions on access.'
|
273
|
+
end
|
274
|
+
ensure
|
275
|
+
write nil # EOF mark
|
276
|
+
read
|
277
|
+
end
|
278
|
+
|
248
279
|
# Retrieve n fields
|
249
|
-
#
|
250
|
-
#
|
251
|
-
# === Return
|
252
|
-
# [Array of Mysql::Field] field list
|
280
|
+
# @param n [Integer] number of fields
|
281
|
+
# @return [Array<Mysql::Field>] field list
|
253
282
|
def retr_fields(n)
|
254
283
|
check_state :FIELD
|
255
284
|
begin
|
@@ -264,10 +293,8 @@ class Mysql
|
|
264
293
|
end
|
265
294
|
|
266
295
|
# Retrieve all records for simple query
|
267
|
-
#
|
268
|
-
#
|
269
|
-
# === Return
|
270
|
-
# [Array of Array of String] all records
|
296
|
+
# @param fields [Array<Mysql::Field>] number of fields
|
297
|
+
# @return [Array<Array<String>>] all records
|
271
298
|
def retr_all_records(fields)
|
272
299
|
check_state :RESULT
|
273
300
|
enc = charset.encoding
|
@@ -284,43 +311,6 @@ class Mysql
|
|
284
311
|
end
|
285
312
|
end
|
286
313
|
|
287
|
-
# Field list command
|
288
|
-
# === Argument
|
289
|
-
# table :: [String] table name.
|
290
|
-
# field :: [String / nil] field name that may contain wild card.
|
291
|
-
# === Return
|
292
|
-
# [Array of Field] field list
|
293
|
-
def field_list_command(table, field)
|
294
|
-
synchronize do
|
295
|
-
reset
|
296
|
-
write [COM_FIELD_LIST, table, 0, field].pack("Ca*Ca*")
|
297
|
-
fields = []
|
298
|
-
until (data = read).eof?
|
299
|
-
fields.push Field.new(FieldPacket.parse(data))
|
300
|
-
end
|
301
|
-
return fields
|
302
|
-
end
|
303
|
-
end
|
304
|
-
|
305
|
-
# Process info command
|
306
|
-
# === Return
|
307
|
-
# [Array of Field] field list
|
308
|
-
def process_info_command
|
309
|
-
check_state :READY
|
310
|
-
begin
|
311
|
-
reset
|
312
|
-
write [COM_PROCESS_INFO].pack("C")
|
313
|
-
field_count = read.lcb
|
314
|
-
fields = field_count.times.map{Field.new FieldPacket.parse(read)}
|
315
|
-
read_eof_packet
|
316
|
-
set_state :RESULT
|
317
|
-
return fields
|
318
|
-
rescue
|
319
|
-
set_state :READY
|
320
|
-
raise
|
321
|
-
end
|
322
|
-
end
|
323
|
-
|
324
314
|
# Ping command
|
325
315
|
def ping_command
|
326
316
|
simple_command [COM_PING].pack("C")
|
@@ -352,12 +342,8 @@ class Mysql
|
|
352
342
|
end
|
353
343
|
|
354
344
|
# Stmt prepare command
|
355
|
-
#
|
356
|
-
#
|
357
|
-
# === Return
|
358
|
-
# [Integer] statement id
|
359
|
-
# [Integer] number of parameters
|
360
|
-
# [Array of Field] field list
|
345
|
+
# @param stmt [String] prepared statement
|
346
|
+
# @return [Array<Integer, Integer, Array<Field>>] statement id, number of parameters, field list
|
361
347
|
def stmt_prepare_command(stmt)
|
362
348
|
synchronize do
|
363
349
|
reset
|
@@ -378,11 +364,9 @@ class Mysql
|
|
378
364
|
end
|
379
365
|
|
380
366
|
# Stmt execute command
|
381
|
-
#
|
382
|
-
#
|
383
|
-
#
|
384
|
-
# === Return
|
385
|
-
# [Integer] number of fields
|
367
|
+
# @param stmt_id [Integer] statement id
|
368
|
+
# @param values [Array] parameters
|
369
|
+
# @return [Integer] number of fields
|
386
370
|
def stmt_execute_command(stmt_id, values)
|
387
371
|
check_state :READY
|
388
372
|
begin
|
@@ -396,11 +380,9 @@ class Mysql
|
|
396
380
|
end
|
397
381
|
|
398
382
|
# Retrieve all records for prepared statement
|
399
|
-
#
|
400
|
-
#
|
401
|
-
#
|
402
|
-
# === Return
|
403
|
-
# [Array of Array of Object] all records
|
383
|
+
# @param fields [Array of Mysql::Fields] field list
|
384
|
+
# @param charset [Mysql::Charset]
|
385
|
+
# @return [Array<Array<Object>>] all records
|
404
386
|
def stmt_retr_all_records(fields, charset)
|
405
387
|
check_state :RESULT
|
406
388
|
enc = charset.encoding
|
@@ -416,8 +398,7 @@ class Mysql
|
|
416
398
|
end
|
417
399
|
|
418
400
|
# Stmt close command
|
419
|
-
#
|
420
|
-
# stmt_id :: [Integer] statement id
|
401
|
+
# @param stmt_id [Integer] statement id
|
421
402
|
def stmt_close_command(stmt_id)
|
422
403
|
synchronize do
|
423
404
|
reset
|
@@ -429,15 +410,13 @@ class Mysql
|
|
429
410
|
@gc_stmt_queue.push stmt_id
|
430
411
|
end
|
431
412
|
|
432
|
-
private
|
433
|
-
|
434
413
|
def check_state(st)
|
435
414
|
raise 'command out of sync' unless @state == st
|
436
415
|
end
|
437
416
|
|
438
417
|
def set_state(st)
|
439
418
|
@state = st
|
440
|
-
if st == :READY
|
419
|
+
if st == :READY && !@gc_stmt_queue.empty?
|
441
420
|
gc_disabled = GC.disable
|
442
421
|
begin
|
443
422
|
while st = @gc_stmt_queue.shift
|
@@ -465,28 +444,24 @@ class Mysql
|
|
465
444
|
end
|
466
445
|
|
467
446
|
# Read one packet data
|
468
|
-
#
|
469
|
-
# [
|
470
|
-
# === Exception
|
471
|
-
# [ProtocolError] invalid packet sequence number
|
447
|
+
# @return [Packet] packet data
|
448
|
+
# @rails [ProtocolError] invalid packet sequence number
|
472
449
|
def read
|
473
450
|
data = ''
|
474
451
|
len = nil
|
475
452
|
begin
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
data.concat ret
|
486
|
-
end
|
453
|
+
header = read_timeout(4, @opts[:read_timeout])
|
454
|
+
raise EOFError unless header && header.length == 4
|
455
|
+
len1, len2, seq = header.unpack("CvC")
|
456
|
+
len = (len2 << 8) + len1
|
457
|
+
raise ProtocolError, "invalid packet: sequence number mismatch(#{seq} != #{@seq}(expected))" if @seq != seq
|
458
|
+
@seq = (@seq + 1) % 256
|
459
|
+
ret = read_timeout(len, @opts[:read_timeout])
|
460
|
+
raise EOFError unless ret && ret.length == len
|
461
|
+
data.concat ret
|
487
462
|
rescue EOFError
|
488
463
|
raise ClientError::ServerGoneError, 'MySQL server has gone away'
|
489
|
-
rescue
|
464
|
+
rescue Errno::ETIMEDOUT
|
490
465
|
raise ClientError, "read timeout"
|
491
466
|
end while len == MAX_PACKET_LENGTH
|
492
467
|
|
@@ -494,64 +469,95 @@ class Mysql
|
|
494
469
|
|
495
470
|
# Error packet
|
496
471
|
if data[0] == ?\xff
|
497
|
-
|
472
|
+
_, errno, marker, @sqlstate, message = data.unpack("Cvaa5a*")
|
498
473
|
unless marker == "#"
|
499
|
-
|
474
|
+
_, errno, message = data.unpack("Cva*") # Version 4.0 Error
|
500
475
|
@sqlstate = ""
|
501
476
|
end
|
502
477
|
message.force_encoding(@charset.encoding)
|
503
478
|
if Mysql::ServerError::ERROR_MAP.key? errno
|
504
479
|
raise Mysql::ServerError::ERROR_MAP[errno].new(message, @sqlstate)
|
505
480
|
end
|
506
|
-
raise Mysql::ServerError.new(message, @sqlstate)
|
481
|
+
raise Mysql::ServerError.new(message, @sqlstate, errno)
|
507
482
|
end
|
508
483
|
Packet.new(data)
|
509
484
|
end
|
510
485
|
|
486
|
+
def read_timeout(len, timeout)
|
487
|
+
return @socket.read(len) if timeout.nil? || timeout == 0
|
488
|
+
result = ''
|
489
|
+
e = Time.now + timeout
|
490
|
+
while result.size < len
|
491
|
+
now = Time.now
|
492
|
+
raise Errno::ETIMEDOUT if now > e
|
493
|
+
r = @socket.read_nonblock(len - result.size, exception: false)
|
494
|
+
case r
|
495
|
+
when :wait_readable
|
496
|
+
IO.select([@socket], nil, nil, e - now)
|
497
|
+
next
|
498
|
+
when :wait_writable
|
499
|
+
IO.select(nil, [@socket], nil, e - now)
|
500
|
+
next
|
501
|
+
else
|
502
|
+
result << r
|
503
|
+
end
|
504
|
+
end
|
505
|
+
return result
|
506
|
+
end
|
507
|
+
|
511
508
|
# Write one packet data
|
512
|
-
#
|
513
|
-
# data :: [String / IO] packet data. If data is nil, write empty packet.
|
509
|
+
# @param data [String, IO, nil] packet data. If data is nil, write empty packet.
|
514
510
|
def write(data)
|
515
511
|
begin
|
516
|
-
@
|
512
|
+
@socket.sync = false
|
517
513
|
if data.nil?
|
518
|
-
|
519
|
-
@sock.write [0, 0, @seq].pack("CvC")
|
520
|
-
end
|
514
|
+
write_timeout([0, 0, @seq].pack("CvC"), @opts[:write_timeout])
|
521
515
|
@seq = (@seq + 1) % 256
|
522
516
|
else
|
523
517
|
data = StringIO.new data if data.is_a? String
|
524
518
|
while d = data.read(MAX_PACKET_LENGTH)
|
525
|
-
|
526
|
-
@sock.write [d.length%256, d.length/256, @seq].pack("CvC")
|
527
|
-
@sock.write d
|
528
|
-
end
|
519
|
+
write_timeout([d.length%256, d.length/256, @seq].pack("CvC")+d, @opts[:write_timeout])
|
529
520
|
@seq = (@seq + 1) % 256
|
530
521
|
end
|
531
522
|
end
|
532
|
-
@
|
533
|
-
|
534
|
-
@sock.flush
|
535
|
-
end
|
523
|
+
@socket.sync = true
|
524
|
+
@socket.flush
|
536
525
|
rescue Errno::EPIPE
|
537
526
|
raise ClientError::ServerGoneError, 'MySQL server has gone away'
|
538
|
-
rescue
|
527
|
+
rescue Errno::ETIMEDOUT
|
539
528
|
raise ClientError, "write timeout"
|
540
529
|
end
|
541
530
|
end
|
542
531
|
|
532
|
+
def write_timeout(data, timeout)
|
533
|
+
return @socket.write(data) if timeout.nil? || timeout == 0
|
534
|
+
len = 0
|
535
|
+
e = Time.now + timeout
|
536
|
+
while len < data.size
|
537
|
+
now = Time.now
|
538
|
+
raise Errno::ETIMEDOUT if now > e
|
539
|
+
l = @socket.write_nonblock(data[len..-1], exception: false)
|
540
|
+
case l
|
541
|
+
when :wait_readable
|
542
|
+
IO.select([@socket], nil, nil, e - now)
|
543
|
+
when :wait_writable
|
544
|
+
IO.select(nil, [@socket], nil, e - now)
|
545
|
+
else
|
546
|
+
len += l
|
547
|
+
end
|
548
|
+
end
|
549
|
+
return len
|
550
|
+
end
|
551
|
+
|
543
552
|
# Read EOF packet
|
544
|
-
#
|
545
|
-
# [ProtocolError] packet is not EOF
|
553
|
+
# @raise [ProtocolError] packet is not EOF
|
546
554
|
def read_eof_packet
|
547
555
|
raise ProtocolError, "packet is not EOF" unless read.eof?
|
548
556
|
end
|
549
557
|
|
550
558
|
# Send simple command
|
551
|
-
#
|
552
|
-
#
|
553
|
-
# === Return
|
554
|
-
# [String] received data
|
559
|
+
# @param packet :: [String] packet data
|
560
|
+
# @return [String] received data
|
555
561
|
def simple_command(packet)
|
556
562
|
synchronize do
|
557
563
|
reset
|
@@ -560,19 +566,6 @@ class Mysql
|
|
560
566
|
end
|
561
567
|
end
|
562
568
|
|
563
|
-
# Encrypt password
|
564
|
-
# === Argument
|
565
|
-
# plain :: [String] plain password.
|
566
|
-
# scramble :: [String] scramble code from initial packet.
|
567
|
-
# === Return
|
568
|
-
# [String] encrypted password
|
569
|
-
def encrypt_password(plain, scramble)
|
570
|
-
return "" if plain.nil? or plain.empty?
|
571
|
-
hash_stage1 = Digest::SHA1.digest plain
|
572
|
-
hash_stage2 = Digest::SHA1.digest hash_stage1
|
573
|
-
return hash_stage1.unpack("C*").zip(Digest::SHA1.digest(scramble+hash_stage2).unpack("C*")).map{|a,b| a^b}.pack("C*")
|
574
|
-
end
|
575
|
-
|
576
569
|
# Initial packet
|
577
570
|
class InitialPacket
|
578
571
|
def self.parse(pkt)
|
@@ -584,18 +577,26 @@ class Mysql
|
|
584
577
|
server_capabilities = pkt.ushort
|
585
578
|
server_charset = pkt.utiny
|
586
579
|
server_status = pkt.ushort
|
587
|
-
|
580
|
+
server_capabilities2 = pkt.ushort
|
581
|
+
scramble_length = pkt.utiny
|
582
|
+
_f1 = pkt.read(10)
|
588
583
|
rest_scramble_buff = pkt.string
|
584
|
+
auth_plugin = pkt.string
|
585
|
+
|
586
|
+
server_capabilities |= server_capabilities2 << 16
|
587
|
+
scramble_buff.concat rest_scramble_buff
|
588
|
+
|
589
589
|
raise ProtocolError, "unsupported version: #{protocol_version}" unless protocol_version == VERSION
|
590
590
|
raise ProtocolError, "invalid packet: f0=#{f0}" unless f0 == 0
|
591
|
-
scramble_buff.
|
592
|
-
|
591
|
+
raise ProtocolError, "invalid packet: scramble_length(#{scramble_length}) != length of scramble(#{scramble_buff.size + 1})" unless scramble_length == scramble_buff.size + 1
|
592
|
+
|
593
|
+
self.new protocol_version, server_version, thread_id, server_capabilities, server_charset, server_status, scramble_buff, auth_plugin
|
593
594
|
end
|
594
595
|
|
595
|
-
attr_reader :protocol_version, :server_version, :thread_id, :server_capabilities, :server_charset, :server_status, :scramble_buff
|
596
|
+
attr_reader :protocol_version, :server_version, :thread_id, :server_capabilities, :server_charset, :server_status, :scramble_buff, :auth_plugin
|
596
597
|
|
597
598
|
def initialize(*args)
|
598
|
-
@protocol_version, @server_version, @thread_id, @server_capabilities, @server_charset, @server_status, @scramble_buff = args
|
599
|
+
@protocol_version, @server_version, @thread_id, @server_capabilities, @server_charset, @server_status, @scramble_buff, @auth_plugin = args
|
599
600
|
end
|
600
601
|
end
|
601
602
|
|
@@ -675,16 +676,35 @@ class Mysql
|
|
675
676
|
|
676
677
|
# Authentication packet
|
677
678
|
class AuthenticationPacket
|
678
|
-
def self.serialize(client_flags, max_packet_size, charset_number, username, scrambled_password, databasename)
|
679
|
-
[
|
679
|
+
def self.serialize(client_flags, max_packet_size, charset_number, username, scrambled_password, databasename, auth_plugin)
|
680
|
+
data = [
|
680
681
|
client_flags,
|
681
682
|
max_packet_size,
|
682
|
-
|
683
|
+
charset_number,
|
683
684
|
"", # always 0x00 * 23
|
684
685
|
username,
|
685
686
|
Packet.lcs(scrambled_password),
|
686
|
-
|
687
|
-
|
687
|
+
]
|
688
|
+
pack = "VVCa23Z*A*"
|
689
|
+
if databasename
|
690
|
+
data.push databasename
|
691
|
+
pack.concat "Z*"
|
692
|
+
end
|
693
|
+
data.push auth_plugin
|
694
|
+
pack.concat "Z*"
|
695
|
+
data.pack(pack)
|
696
|
+
end
|
697
|
+
end
|
698
|
+
|
699
|
+
# TLS Authentication packet
|
700
|
+
class TlsAuthenticationPacket
|
701
|
+
def self.serialize(client_flags, max_packet_size, charset_number)
|
702
|
+
[
|
703
|
+
client_flags,
|
704
|
+
max_packet_size,
|
705
|
+
charset_number,
|
706
|
+
"", # always 0x00 * 23
|
707
|
+
].pack("VVCa23")
|
688
708
|
end
|
689
709
|
end
|
690
710
|
|
@@ -712,6 +732,21 @@ class Mysql
|
|
712
732
|
end
|
713
733
|
|
714
734
|
end
|
735
|
+
|
736
|
+
class AuthenticationResultPacket
|
737
|
+
def self.parse(pkt)
|
738
|
+
result = pkt.utiny
|
739
|
+
auth_plugin = pkt.string
|
740
|
+
scramble = pkt.string
|
741
|
+
self.new(result, auth_plugin, scramble)
|
742
|
+
end
|
743
|
+
|
744
|
+
attr_reader :result, :auth_plugin, :scramble
|
745
|
+
|
746
|
+
def initialize(*args)
|
747
|
+
@result, @auth_plugin, @scramble = args
|
748
|
+
end
|
749
|
+
end
|
715
750
|
end
|
716
751
|
|
717
752
|
class RawRecord
|
@@ -732,17 +767,15 @@ class Mysql
|
|
732
767
|
end
|
733
768
|
|
734
769
|
class StmtRawRecord
|
735
|
-
#
|
736
|
-
#
|
737
|
-
#
|
738
|
-
# encoding:: [Encoding]
|
770
|
+
# @param pkt [Packet]
|
771
|
+
# @param fields [Array of Fields]
|
772
|
+
# @param encoding [Encoding]
|
739
773
|
def initialize(packet, fields, encoding)
|
740
774
|
@packet, @fields, @encoding = packet, fields, encoding
|
741
775
|
end
|
742
776
|
|
743
777
|
# Parse statement result packet
|
744
|
-
#
|
745
|
-
# [Array of Object] one record
|
778
|
+
# @return [Array<Object>] one record
|
746
779
|
def parse_record_packet
|
747
780
|
@packet.utiny # skip first byte
|
748
781
|
null_bit_map = @packet.read((@fields.length+7+2)/8).unpack("b*").first
|
@@ -752,7 +785,7 @@ class Mysql
|
|
752
785
|
else
|
753
786
|
unsigned = f.flags & Field::UNSIGNED_FLAG != 0
|
754
787
|
v = Protocol.net2value(@packet, f.type, unsigned)
|
755
|
-
if v.is_a? Numeric or v.is_a?
|
788
|
+
if v.nil? or v.is_a? Numeric or v.is_a? Time
|
756
789
|
v
|
757
790
|
elsif f.type == Field::TYPE_BIT or f.charsetnr == Charset::BINARY_CHARSET_NUMBER
|
758
791
|
Charset.to_binary(v)
|