aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/vendor/mysql411.rb
blob: 5bdd09c9d01245f7ed16a723b35c20ea0bd84a99 (plain) (blame)
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
#
# 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 )
    @use_411 = (version.strip >= '4.1.1')

    @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 @use_411
      # 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

  # 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 = @use_411 ? 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 @use_411
      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 @use_411
      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 @use_411
      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 @use_411
      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