lib/monetdb/connection.rb in monetdb-0.1.3 vs lib/monetdb/connection.rb in monetdb-0.2.0

- old
+ new

@@ -1,443 +1,125 @@ require "socket" -require "time" -require "uri" +require "monetdb/connection/messages" +require "monetdb/connection/setup" +require "monetdb/connection/query" -# -# Implements the MAPI communication protocol -# - -class MonetDB +module MonetDB class Connection - # enable debug output - @@DEBUG = false + include Messages + include Setup + include Query - # hour in seconds, used for timezone calculation - @@HOUR = 3600 + Q_TABLE = "1" # SELECT statement + Q_UPDATE = "2" # INSERT/UPDATE statement + Q_CREATE = "3" # CREATE/DROP TABLE statement + Q_TRANSACTION = "4" # TRANSACTION + Q_PREPARE = "5" # QPREPARE message + Q_BLOCK = "6" # QBLOCK message - # maximum size (in bytes) for a monetdb message to be sent - @@MAX_MESSAGE_SIZE = 32766 + MSG_PROMPT = "" + MSG_ERROR = "!" + MSG_REDIRECT = "^" + MSG_QUERY = "&" + MSG_SCHEME = "%" + MSG_TUPLE = "[" - # endianness of a message sent to the server - @@CLIENT_ENDIANNESS = "BIG" + ENDIANNESS = "BIG" + LANG = "sql" + REPLY_SIZE = "-1" + MAX_MSG_SIZE = 32766 - # MAPI protocols supported by the driver - @@SUPPORTED_PROTOCOLS = [8, 9] + MAPI_V8 = "8" + MAPI_V9 = "9" + PROTOCOLS = [MAPI_V8, MAPI_V9] - attr_reader :socket, :auto_commit, :transactions, :lang + AUTH_MD5 = "MD5" + AUTH_SHA512 = "SHA512" + AUTH_SHA384 = "SHA384" + AUTH_SHA256 = "SHA256" + AUTH_SHA1 = "SHA1" + AUTH_PLAIN = "PLAIN" + AUTH_TYPES = [AUTH_MD5, AUTH_SHA512, AUTH_SHA384, AUTH_SHA256, AUTH_SHA1, AUTH_PLAIN] - # A new instance of MonetDB::Connection. - # * user : username (default is monetdb) - # * passwd : password (default is monetdb) - # * lang : language (default is sql) - # * host : server hostanme or ip (default is localhost) - # * port : server port (default is 50000) - def initialize(user = "monetdb", passwd = "monetdb", lang = "sql", host = "127.0.0.1", port = "50000") - @user = user - @passwd = passwd - @lang = lang.downcase - @host = host - @port = port - - @client_endianness = @@CLIENT_ENDIANNESS - @auth_iteration = 0 - @connection_established = false - @transactions = MonetDB::Transaction.new + def initialize(config = {}) + @config = { + :host => "localhost", + :port => 50000, + :username => "monetdb", + :password => "monetdb" + }.merge( + config.inject({}){|h, (k, v)| h[k.to_sym] = v; h} + ) end - # Connect to the database, creates a new socket. - def connect(db_name = "demo", auth_type = "SHA1") - @database = db_name - @auth_type = auth_type - @socket = TCPSocket.new(@host, @port.to_i) - - if real_connect - if @lang == LANG_SQL - set_timezone - set_reply_size - elsif (@lang == LANG_XQUERY) and XQUERY_OUTPUT_SEQ - # require xquery output to be in seq format - send(format_command("output seq")) - end - true - else - false - end + def connect + disconnect if connected? + @socket = TCPSocket.new config[:host], config[:port].to_i + setup + true end - # Perform a real connection; retrieve challenge, proxy through merovinginan, build challenge and set the timezone. - def real_connect - server_challenge = retrieve_server_challenge() - - if server_challenge != nil - salt = server_challenge.split(':')[0] - @server_name = server_challenge.split(':')[1] - @protocol = server_challenge.split(':')[2].to_i - @supported_auth_types = server_challenge.split(':')[3].split(',') - @server_endianness = server_challenge.split(':')[4] - if @protocol == 9 - @pwhash = server_challenge.split(':')[5] - end - else - raise MonetDB::ConnectionError, "Error: server returned an empty challenge string." - end - - # The server supports only RIPMED168 or crypt as an authentication hash function, but the driver does not. - if @supported_auth_types.length == 1 - auth = @supported_auth_types[0] - if auth.upcase == "RIPEMD160" or auth.upcase == "CRYPT" - raise MonetDB::ConnectionError, auth.upcase + " " + ": algorithm not supported by ruby-monetdb." - end - end - - # If the server protocol version is not 8: abort and notify the user. - if @@SUPPORTED_PROTOCOLS.include?(@protocol) == false - raise MonetDB::ProtocolError, "Protocol not supported. The current implementation of ruby-monetdb works with MAPI protocols #{@@SUPPORTED_PROTOCOLS} only." - - elsif mapi_proto_v8? - reply = build_auth_string_v8(@auth_type, salt, @database) - elsif mapi_proto_v9? - reply = build_auth_string_v9(@auth_type, salt, @database) - end - - if @socket != nil - @connection_established = true - - send(reply) - monetdb_auth = receive - - if monetdb_auth.length == 0 - # auth succedeed - true - else - if monetdb_auth[0].chr == MSG_REDIRECT - #redirection - - redirects = [] # store a list of possible redirects - - monetdb_auth.split('\n').each do |m| - # strip the trailing ^mapi: - # if the redirect string start with something != "^mapi:" or is empty, the redirect is invalid and shall not be included. - if m[0..5] == "^mapi:" - redir = m[6..m.length] - # url parse redir - redirects.push(redir) - else - $stderr.print "Warning: Invalid Redirect #{m}" - end - end - - if redirects.size == 0 - raise MonetDB::ConnectionError, "No valid redirect received" - else - begin - uri = URI.split(redirects[0]) - # Splits the string on following parts and returns array with result: - # - # * Scheme - # * Userinfo - # * Host - # * Port - # * Registry - # * Path - # * Opaque - # * Query - # * Fragment - server_name = uri[0] - host = uri[2] - port = uri[3] - database = uri[5].gsub(/^\//, '') if uri[5] != nil - rescue URI::InvalidURIError - raise MonetDB::ConnectionError, "Invalid redirect: #{redirects[0]}" - end - end - - if server_name == "merovingian" - if @auth_iteration <= 10 - @auth_iteration += 1 - real_connect - else - raise MonetDB::ConnectionError, "Merovingian: too many iterations while proxying." - end - elsif server_name == "monetdb" - begin - @socket.close - rescue - raise MonetDB::ConnectionError, "I/O error while closing connection to #{@socket}" - end - # reinitialize a connection - @host = host - @port = port - - connect(database, @auth_type) - else - @connection_established = false - raise MonetDB::ConnectionError, monetdb_auth - end - elsif monetdb_auth[0].chr == MSG_INFO - raise MonetDB::ConnectionError, monetdb_auth - end - end - end + def connected? + !socket.nil? end - def savepoint - @transactions.savepoint + def disconnect + socket.disconnect if connected? + @socket = nil end - # Formats a <i>command</i> string so that it can be parsed by the server. - def format_command(x) - "X" + x + "\n" - end + private - # Send an 'export' command to the server. - def set_export(id, idx, offset) - send(format_command("export " + id.to_s + " " + idx.to_s + " " + offset.to_s )) + def config + @config end - # Send a 'reply_size' command to the server. - def set_reply_size - send(format_command(("reply_size " + REPLY_SIZE))) - response = receive - - if response == MSG_PROMPT - true - elsif response[0] == MSG_INFO - raise MonetDB::CommandError, "Unable to set reply_size: #{response}" - end + def socket + @socket end - def set_output_seq - send(format_command("output seq")) + def log(type, msg) + MonetDB.logger.send(type, msg) if MonetDB.logger end - # Disconnect from server. - def disconnect - if @connection_established - begin - @socket.close - rescue => e - $stderr.print e - end - else - raise MonetDB::ConnectionError, "No connection established." - end - end + def read + raise ConnectionError, "Not connected to server" unless connected? - # Send data to a monetdb5 server instance and returns server response. - def send(data) - encode_message(data).each do |m| - @socket.write(m) - end - end + length, last_chunk = read_length + data, iterations = "", 0 - # Receive data from a monetdb5 server instance. - def receive - is_final, chunk_size = recv_decode_hdr - - if chunk_size == 0 - return "" # needed on ruby-1.8.6 linux/64bit; recv(0) hangs on this configuration. + while (length > 0) && (iterations < 1000) do + received = socket.recv(length) + data << received + length -= received.bytesize + iterations += 1 end + data << read unless last_chunk - data = @socket.recv(chunk_size) - - if is_final == false - while is_final == false - is_final, chunk_size = recv_decode_hdr - data += @socket.recv(chunk_size) - end - end - data end - # Build and authentication string given the parameters submitted by the user (MAPI protocol v8). - def build_auth_string_v8(auth_type, salt, db_name) - # seed = password + salt - if (auth_type.upcase == "MD5" or auth_type.upcase == "SHA1") and @supported_auth_types.include?(auth_type.upcase) - auth_type = auth_type.upcase - digest = Hasher.new(auth_type, @passwd+salt) - hashsum = digest.hashsum - elsif auth_type.downcase == "plain" or not @supported_auth_types.include?(auth_type.upcase) - auth_type = 'plain' - hashsum = @passwd + salt - - elsif auth_type.downcase == "crypt" - auth_type = @supported_auth_types[@supported_auth_types.index(auth_type)+1] - $stderr.print "The selected hashing algorithm is not supported by the Ruby driver. #{auth_type} will be used instead." - digest = Hasher.new(auth_type, @passwd+salt) - hashsum = digest.hashsum - else - # The user selected an auth type not supported by the server. - raise MonetDB::ConnectionError, "#{auth_type} not supported by the server. Please choose one from #{@supported_auth_types}" - - end - # Build the reply message with header - reply = @client_endianness + ":" + @user + ":{" + auth_type + "}" + hashsum + ":" + @lang + ":" + db_name + ":" + def read_length + bytes = socket.recv(2).unpack("v")[0] + [(bytes >> 1), (bytes & 1) == 1] end - # Build and authentication string given the parameters submitted by the user (MAPI protocol v9). - def build_auth_string_v9(auth_type, salt, db_name) - if (auth_type.upcase == "MD5" or auth_type.upcase == "SHA1") and @supported_auth_types.include?(auth_type.upcase) - auth_type = auth_type.upcase - # Hash the password - pwhash = Hasher.new(@pwhash, @passwd) - - digest = Hasher.new(auth_type, pwhash.hashsum + salt) - hashsum = digest.hashsum - - elsif auth_type.downcase == "plain" # or not @supported_auth_types.include?(auth_type.upcase) - # Keep it for compatibility with merovingian - auth_type = 'plain' - hashsum = @passwd + salt - elsif @supported_auth_types.include?(auth_type.upcase) - if auth_type.upcase == "RIPEMD160" - auth_type = @supported_auth_types[@supported_auth_types.index(auth_type)+1] - $stderr.print "The selected hashing algorithm is not supported by the Ruby driver. #{auth_type} will be used instead." - end - # Hash the password - pwhash = Hasher.new(@pwhash, @passwd) - - digest = Hasher.new(auth_type, pwhash.hashsum + salt) - hashsum = digest.hashsum - else - # The user selected an auth type not supported by the server. - raise MonetDB::ConnectionError, "#{auth_type} not supported by the server. Please choose one from #{@supported_auth_types}" + def write(message) + raise ConnectionError, "Not connected to server" unless connected? + pack(message).each do |chunk| + socket.write(chunk) end - # Build the reply message with header - reply = @client_endianness + ":" + @user + ":{" + auth_type + "}" + hashsum + ":" + @lang + ":" + db_name + ":" + true end - # Build a message to be sent to the server. - def encode_message(msg = "") - message = Array.new - data = "" - - hdr = 0 # package header - pos = 0 - is_final = false # last package in the stream - - while (! is_final) - data = msg[pos..pos+[@@MAX_MESSAGE_SIZE.to_i, (msg.length - pos).to_i].min] - pos += data.length - - if (msg.length - pos) == 0 - last_bit = 1 - is_final = true - else - last_bit = 0 - end - - hdr = [(data.length << 1) | last_bit].pack('v') - - message << hdr + data.to_s # Short Little Endian Encoding - end - - message.freeze # freeze and return the encode message - end - - # Used as the first step in the authentication phase; retrieves a challenge string from the server. - def retrieve_server_challenge - server_challenge = receive - end - - # Reads and decodes the header of a server message. - def recv_decode_hdr - if @socket != nil - fb = @socket.recv(1) - sb = @socket.recv(1) - - # Use execeptions handling to keep compatibility between different ruby - # versions. - # - # Chars are treated differently in ruby 1.8 and 1.9 - # try do to ascii to int conversion using ord (ruby 1.9) - # and if it fail fallback to character.to_i (ruby 1.8) - begin - fb = fb[0].ord - sb = sb[0].ord - rescue NoMethodError => one_eight - fb = fb[0].to_i - sb = sb[0].to_i - end - - chunk_size = (sb << 7) | (fb >> 1) - - is_final = false - if ( (fb & 1) == 1 ) - is_final = true - - end - # return the size of the chunk (in bytes) - return is_final, chunk_size - else - raise MonetDB::SocketError, "Error while receiving data\n" - end - end - - # Sets the time zone according to the Operating System settings. - def set_timezone - tz = Time.new - tz_offset = tz.gmt_offset / @@HOUR - - if tz_offset <= 9 # verify minute count! - tz_offset = "'+0" + tz_offset.to_s + ":00'" - else - tz_offset = "'+" + tz_offset.to_s + ":00'" - end - query_tz = "sSET TIME ZONE INTERVAL " + tz_offset + " HOUR TO MINUTE;" - - # Perform the query directly within the method - send(query_tz) - response = receive - - if response == MSG_PROMPT - true - elsif response[0].chr == MSG_INFO - raise MonetDB::QueryError, response - end - end - - # Turns auto commit on/off. - def set_auto_commit(flag = true) - if flag == false - ac = " 0" - else - ac = " 1" - end - - send(format_command("auto_commit " + ac)) - response = receive - - if response == MSG_PROMPT - @auto_commit = flag - elsif response[0].chr == MSG_INFO - raise MonetDB::CommandError, response - end - end - - # Check the auto commit status (on/off). - def auto_commit? - !!@auto_commit - end - - # Check if monetdb is running behind the merovingian proxy and forward the connection in case. - def merovingian? - @server_name.downcase == "merovingian" - end - - def mserver? - @server_name.downcase == "monetdb" - end - - # Check which protocol is spoken by the server. - def mapi_proto_v8? - @protocol == 8 - end - - def mapi_proto_v9? - @protocol == 9 + def pack(message) + chunks = message.scan(/.{1,#{MAX_MSG_SIZE}}/m) + chunks.each_with_index.to_a.collect do |chunk, index| + last_bit = (index == chunks.size - 1) ? 1 : 0 + length = [(chunk.size << 1) | last_bit].pack("v") + "#{length}#{chunk}" + end.freeze end end end