From b9f28eb587a4ad52ea2e0f499aaa7bc53746c173 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 10 Jan 2005 22:52:14 +0000 Subject: Added support for the new protocol spoken by MySQL 4.1.1+ servers for the Ruby/MySQL adapter that ships with Rails #440 [Matt Mower] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@372 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- .../active_record/connection_adapters/mysql411.rb | 318 +++++++++++++++++++++ .../connection_adapters/mysql_adapter.rb | 1 + 2 files changed, 319 insertions(+) create mode 100644 activerecord/lib/active_record/connection_adapters/mysql411.rb (limited to 'activerecord/lib/active_record/connection_adapters') diff --git a/activerecord/lib/active_record/connection_adapters/mysql411.rb b/activerecord/lib/active_record/connection_adapters/mysql411.rb new file mode 100644 index 0000000000..15f8f03cec --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql411.rb @@ -0,0 +1,318 @@ +# +# mysq411.rb - 0.1 - Matt Mower +# +# 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. +# + +if __FILE__ == $0 + $:.unshift( File.join( File.dirname( __FILE__ ), "..", "vendor" ) ) + require 'mysql' +end + +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 ) + 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, 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 + +end + +if __FILE__ == $0 + io = Mysql::real_connect( "", "", "", "activerecord_unittest" ) + puts io.list_dbs + io.close +end \ No newline at end of file diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index f9c38470aa..a6374f7b73 100755 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -13,6 +13,7 @@ module ActiveRecord # Only use the supplied backup Ruby/MySQL driver if no driver is already in place begin require 'active_record/vendor/mysql' + require 'active_record/connection_adapters/mysql411' rescue LoadError raise cannot_require_mysql end -- cgit v1.2.3