Mercurial > hg > monetdb-ruby
view lib/MonetDBConnection.rb @ 10:2ebc526bc7dd
Updated copyright.
author | Sjoerd Mullender <sjoerd@acm.org> |
---|---|
date | Fri, 06 Jan 2017 13:16:10 +0100 (2017-01-06) |
parents | b4cf00b05ef1 |
children | 53aae47a9483 |
line wrap: on
line source
# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # # Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. # Implements the MAPI communication protocol require 'socket' require 'time' require_relative 'hasher' require_relative 'MonetDBExceptions' require 'uri' # parse merovingian redirects class MonetDBConnection Q_TABLE = "1" # SELECT operation Q_UPDATE = "2" # INSERT/UPDATE operations Q_CREATE = "3" # CREATE/DROP TABLE operations Q_TRANSACTION = "4" # TRANSACTION Q_PREPARE = "5" Q_BLOCK = "6" # QBLOCK message MSG_REDIRECT = '^' # auth redirection through merovingian MSG_QUERY = '&' MSG_SCHEMA_HEADER = '%' MSG_INFO = '!' # info response from mserver MSG_TUPLE = '[' MSG_PROMPT = "" REPLY_SIZE = '-1' MAX_AUTH_ITERATION = 10 # maximum number of atuh iterations (thorough merovingian) allowed MONET_ERROR = -1 LANG_SQL = "sql" # Protocols MAPIv9 = 9 MONETDB_MEROVINGIAN = "merovingian" MONETDB_MSERVER = "monetdb" MEROVINGIAN_MAX_ITERATIONS = 10 # enable debug output @@DEBUG = false # hour in seconds, used for timezone calculation @@HOUR = 3600 # maximum size (in bytes) for a monetdb message to be sent @@MAX_MESSAGE_SIZE = 32766 # endianness of a message sent to the server @@CLIENT_ENDIANNESS = "BIG" # MAPI protocols supported by the driver @@SUPPORTED_PROTOCOLS = [ MonetDBConnection::MAPIv9 ] attr_reader :socket, :auto_commit, :transactions, :lang # Instantiates a new MonetDBConnection object # * 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 = MonetDBTransaction.new # handles a pool of transactions (generates and keeps track of savepoints) if @@DEBUG == true require 'logger' end if @lang[0, 3] == 'sql' @lang = "sql" end 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 == MonetDBConnection::LANG_SQL set_timezone set_reply_size end true end false 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 @@SUPPORTED_PROTOCOLS.include?(@protocol) == false raise MonetDBProtocolError, "Protocol not supported. The current implementation of ruby-monetdb works with MAPI protocols #{@@SUPPORTED_PROTOCOLS} only." end @pwhash = server_challenge.split(':')[5] else raise MonetDBConnectionError, "Error: server returned an empty challenge string." end # The server supports only RIPMED160 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" raise MonetDBConnectionError, auth.upcase + " " + ": algorithm not supported by ruby-monetdb." end end reply = build_auth_string_v9(@auth_type, salt, @database) 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 == MonetDBConnection::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 MonetDBConnectionError, "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 MonetDBConnectionError, "Invalid redirect: #{redirects[0]}" end end if server_name == MonetDBConnection::MONETDB_MEROVINGIAN if @auth_iteration <= MonetDBConnection::MEROVINGIAN_MAX_ITERATIONS @auth_iteration += 1 real_connect else raise MonetDBConnectionError, "Merovingian: too many iterations while proxying." end elsif server_name == MonetDBConnection::MONETDB_MSERVER begin @socket.close rescue raise MonetDBConnectionError, "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 MonetDBConnectionError, monetdb_auth end elsif monetdb_auth[0].chr == MonetDBConnection::MSG_INFO raise MonetDBConnectionError, monetdb_auth end end end end def savepoint @transactions.savepoint end # Formats a <i>command</i> string so that it can be parsed by the server def format_command(x) return "X" + x + "\n" end # 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 )) end # send a 'reply_size' command to the server def set_reply_size send(format_command(("reply_size " + MonetDBConnection::REPLY_SIZE))) response = receive if response == MonetDBConnection::MSG_PROMPT true elsif response[0] == MonetDBConnection::MSG_INFO raise MonetDBCommandError, "Unable to set reply_size: #{response}" end end def set_output_seq send(format_command("output seq")) end # Disconnect from server def disconnect() if @connection_established begin @socket.close rescue => e $stderr.print e end else raise MonetDBConnectionError, "No connection established." end end # send data to a monetdb5 server instance and returns server's response def send(data) encode_message(data).each do |m| @socket.write(m) end end # 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. end 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 return data end # # Builds 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 MonetDBConnectionError, "#{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 + ":" end # builds 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; retrives 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 MonetDBSocketError, "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 # verify minute count! if tz_offset <= -10 tz_offset = "'" + tz_offset.to_s + ":00'" elsif tz_offset < 0 tz_offset = -tz_offset tz_offset = "'-0" + tz_offset.to_s + ":00'" elsif tz_offset <= 9 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 == MonetDBConnection::MSG_PROMPT true elsif response[0].chr == MonetDBConnection::MSG_INFO raise MonetDBQueryError, 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 == MonetDBConnection::MSG_PROMPT @auto_commit = flag elsif response[0].chr == MonetDBConnection::MSG_INFO raise MonetDBCommandError, response return 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? if @server_name.downcase == MonetDBConnection::MONETDB_MEROVINGIAN true else false end end def mserver? if @server_name.downcase == MonetDBConnection::MONETDB_MSERVER true else false end end end # handles transactions and savepoints. Can be used to simulate nested transactions. class MonetDBTransaction SAVEPOINT_STRING = "monetdbsp" def initialize @id = 0 @savepoint = "" end def savepoint @savepoint = SAVEPOINT_STRING + @id.to_s end def release prev_id end def save next_id end private def next_id @id += 1 end def prev_id @id -= 1 end end