1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
|
#
# mysq411.rb - 0.1 - Matt Mower <self@mattmower.com>
#
# The native Ruby MySQL client (mysql.rb) by Tomita Masahiro does not (yet) handle the new MySQL
# protocol introduced in MySQL 4.1.1. This protocol introduces a new authentication scheme as
# well as modifications to the client/server exchanges themselves.
#
# mysql411.rb modifies the Mysql class to add MySQL 4.1.x support. It modifies the connection
# algorithm to detect a 4.1.1 server and respond with the new authentication scheme, otherwise using
# the original one. Similarly for the changes to packet structures and field definitions, etc...
#
# It redefines serveral methods which behave differently depending upon the server context. The
# way I have implemented this is to alias the old method, create a new alternative method, and redefine
# the original method as a selector which calls the appropriate method based upon the server version.
# There may have been a neater way to do this.
#
# In general I've tried not to change the original code any more than necessary, i.e. even where I
# redefine a method I have made the smallest number of changes possible, rather than rewriting from
# scratch.
#
# *Caveat Lector* This code passes all current ActiveRecord unit tests however this is no guarantee that
# full & correct MySQL 4.1 support has been achieved.
#
require 'digest/sha1'
#
# Extend the Mysql class to work with MySQL 4.1.1+ servers. After version
# 4.1.1 the password hashing function (and some other connection details) have
# changed rendering the previous Mysql class unable to connect:
#
#
class Mysql
CLIENT_PROTOCOL_41 = 512
CLIENT_SECURE_CONNECTION = 32768
def real_connect( host=nil, user=nil, passwd=nil, db=nil, port=nil, socket=nil, flag=nil )
@server_status = SERVER_STATUS_AUTOCOMMIT
if( host == nil || host == "localhost" ) && defined? UNIXSocket
unix_socket = socket || ENV["MYSQL_UNIX_PORT"] || MYSQL_UNIX_ADDR
sock = UNIXSocket::new( unix_socket )
@host_info = Error::err( Error::CR_LOCALHOST_CONNECTION )
@unix_socket = unix_socket
else
sock = TCPSocket::new(host, port||ENV["MYSQL_TCP_PORT"]||(Socket::getservbyname("mysql","tcp") rescue MYSQL_PORT))
@host_info = sprintf Error::err(Error::CR_TCP_CONNECTION), host
end
@host = host ? host.dup : nil
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true
@net = Net::new sock
a = read
@protocol_version = a.slice!(0)
@server_version, a = a.split(/\0/,2)
# Store the version number components for speedy comparison
version, ostag = @server_version.split( /-/, 2 )
@major_ver, @minor_ver, @revision_num = version.split( /\./ ).map { |v| v.to_i }
@thread_id, @scramble_buff = a.slice!(0,13).unpack("La8")
if a.size >= 2 then
@server_capabilities, = a.slice!(0,2).unpack("v")
end
if a.size >= 16 then
@server_language, @server_status = a.unpack("cv")
end
# Set the flags we'll send back to the server
flag = 0 if flag == nil
flag |= @client_flag | CLIENT_CAPABILITIES
flag |= CLIENT_CONNECT_WITH_DB if db
if version_meets_minimum?( 4, 1, 1 )
# In 4.1.1+ the seed comes in two parts which must be combined
a.slice!( 0, 16 )
seed_part_2 = a.slice!( 0, 12 );
@scramble_buff << seed_part_2
flag |= CLIENT_FOUND_ROWS
flag |= CLIENT_PROTOCOL_41
flag |= CLIENT_SECURE_CONNECTION if @server_capabilities & CLIENT_SECURE_CONNECTION;
if db && @server_capabilities & CLIENT_CONNECT_WITH_DB != 0
@db = db.dup
end
scrambled_password = scramble411( passwd, @scramble_buff, @protocol_version==9 )
data = make_client_auth_packet_41( flag, user, scrambled_password, db )
else
scrambled_password = scramble( passwd, @scramble_buff, @protocol_version == 9 )
data = Net::int2str(flag)+Net::int3str(@max_allowed_packet)+(user||"")+"\0"+scrambled_password
if db and @server_capabilities & CLIENT_CONNECT_WITH_DB != 0 then
data << "\0"+db
@db = db.dup
end
end
write data
read
self
end
alias :connect :real_connect
# Pack the authentication information into depending upon whether an initial database has
# been specified
def make_client_auth_packet_41( flag, user, password, db )
if db && @server_capabilities & CLIENT_CONNECT_WITH_DB != 0
template = "VVcx23a#{user.size+1}cA#{password.size}a#{db.size+1}"
else
template = "VVcx23a#{user.size+1}cA#{password.size}x"
end
[ flag, @max_allowed_packet, @server_language, user, password.size, password, db ].pack( template )
end
def version_meets_minimum?( major, minor, revision )
@major_ver >= major && @minor_ver >= minor && @revision_num >= revision
end
# SERVER: public_seed=create_random_string()
# send(public_seed)
#
# CLIENT: recv(public_seed)
# hash_stage1=sha1("password")
# hash_stage2=sha1(hash_stage1)
# reply=xor(hash_stage1, sha1(public_seed,hash_stage2)
#
# #this three steps are done in scramble()
#
# send(reply)
#
#
# SERVER: recv(reply)
# hash_stage1=xor(reply, sha1(public_seed,hash_stage2))
# candidate_hash2=sha1(hash_stage1)
# check(candidate_hash2==hash_stage2)
def scramble411( password, seed, old_ver )
return "" if password == nil or password == ""
raise "old version password is not implemented" if old_ver
# print "Seed Bytes = "
# seed.each_byte { |b| print "0x#{b.to_s( 16 )}, " }
# puts
stage1 = Digest::SHA1.digest( password )
stage2 = Digest::SHA1.digest( stage1 )
dgst = Digest::SHA1.new
dgst << seed
dgst << stage2
stage3 = dgst.digest
# stage1.zip( stage3 ).map { |a, b| (a ^ b).chr }.join
scrambled = ( 0 ... stage3.size ).map { |i| stage3[i] ^ stage1[i] }
scrambled = scrambled.map { |x| x.chr }
scrambled.join
end
def change_user(user="", passwd="", db="")
scrambled_password = version_meets_minimum?( 4, 1, 1 ) ? scramble411( passwd, @scramble_buff, @protocol_version==9 ) : scramble( passwd, @scramble_buff, @protocol_version==9 )
data = user+"\0"+scrambled_password+"\0"+db
command COM_CHANGE_USER, data
@user = user
@passwd = passwd
@db = db
end
#
# The 4.1 protocol changed the length of the END packet
#
alias_method :old_read_one_row, :read_one_row
def read_one_row( field_count )
if version_meets_minimum?( 4, 1, 1 )
read_one_row_41( field_count )
else
old_read_one_row( field_count )
end
end
def read_one_row_41( field_count )
data = read
return if data[0] == 254 and data.length < 9
rec = []
field_count.times do
len = get_length data
if len == nil then
rec << len
else
rec << data.slice!(0,len)
end
end
rec
end
#
# The 4.1 protocol changed the length of the END packet
#
alias_method :old_skip_result, :skip_result
def skip_result
if version_meets_minimum?( 4, 1, 1 )
skip_result_41
else
old_skip_result
end
end
def skip_result_41()
if @status == :STATUS_USE_RESULT then
loop do
data = read
break if data[0] == 254 and data.length == 1
end
@status = :STATUS_READY
end
end
# The field description structure is changed for the 4.1 protocol passing
# more data and a different packing form. NOTE: The 4.1 protocol now passes
# back a "catalog" name for each field which is a new feature. Since AR has
# nowhere to put it I'm throwing it away. Possibly this is not the best
# idea?
#
alias_method :old_unpack_fields, :unpack_fields
def unpack_fields( data, long_flag_protocol )
if version_meets_minimum?( 4, 1, 1 )
unpack_fields_41( data, long_flag_protocol )
else
old_unpack_fields( data, long_flag_protocol )
end
end
def unpack_fields_41( data, long_flag_protocol )
ret = []
data.each do |f|
catalog_name = f[0]
database_name = f[1]
table_name_alias = f[2]
table_name = f[3]
column_name_alias = f[4]
column_name = f[5]
charset = f[6][0] + f[6][1]*256
length = f[6][2] + f[6][3]*256 + f[6][4]*256*256 + f[6][5]*256*256*256
type = f[6][6]
flags = f[6][7] + f[6][8]*256
decimals = f[6][9]
def_value = f[7]
max_length = 0
ret << Field::new(table_name, table_name, column_name_alias, length, type, flags, decimals, def_value, max_length)
end
ret
end
# In this instance the read_query_result method in mysql is bound to read 5 field parameters which
# is expanded to 7 in the 4.1 protocol. So in this case we redefine this entire method in order
# to write "read_rows 7" instead of "read_rows 5"!
#
alias_method :old_read_query_result, :read_query_result
def read_query_result
if version_meets_minimum?( 4, 1, 1 )
read_query_result_41
else
old_read_query_result
end
end
def read_query_result_41
data = read
@field_count = get_length(data)
if @field_count == nil then # LOAD DATA LOCAL INFILE
File::open(data) do |f|
write f.read
end
write "" # mark EOF
data = read
@field_count = get_length(data)
end
if @field_count == 0 then
@affected_rows = get_length(data, true)
@insert_id = get_length(data, true)
if @server_capabilities & CLIENT_TRANSACTIONS != 0 then
a = data.slice!(0,2)
@server_status = a[0]+a[1]*256
end
if data.size > 0 and get_length(data) then
@info = data
end
else
@extra_info = get_length(data, true)
fields = read_rows 7
@fields = unpack_fields(fields, @server_capabilities & CLIENT_LONG_FLAG != 0)
@status = :STATUS_GET_RESULT
end
self
end
# Get rid of GC.start in #free.
class Result
def free
@handle.skip_result
@handle = @fields = @data = nil
end
end
end
|