monetdb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,443 @@
1
+ require "socket"
2
+ require "time"
3
+ require "uri"
4
+
5
+ #
6
+ # Implements the MAPI communication protocol
7
+ #
8
+
9
+ class MonetDB
10
+ class Connection
11
+
12
+ # enable debug output
13
+ @@DEBUG = false
14
+
15
+ # hour in seconds, used for timezone calculation
16
+ @@HOUR = 3600
17
+
18
+ # maximum size (in bytes) for a monetdb message to be sent
19
+ @@MAX_MESSAGE_SIZE = 32766
20
+
21
+ # endianness of a message sent to the server
22
+ @@CLIENT_ENDIANNESS = "BIG"
23
+
24
+ # MAPI protocols supported by the driver
25
+ @@SUPPORTED_PROTOCOLS = [8, 9]
26
+
27
+ attr_reader :socket, :auto_commit, :transactions, :lang
28
+
29
+ # A new instance of MonetDB::Connection.
30
+ # * user : username (default is monetdb)
31
+ # * passwd : password (default is monetdb)
32
+ # * lang : language (default is sql)
33
+ # * host : server hostanme or ip (default is localhost)
34
+ # * port : server port (default is 50000)
35
+ def initialize(user = "monetdb", passwd = "monetdb", lang = "sql", host = "127.0.0.1", port = "50000")
36
+ @user = user
37
+ @passwd = passwd
38
+ @lang = lang.downcase
39
+ @host = host
40
+ @port = port
41
+
42
+ @client_endianness = @@CLIENT_ENDIANNESS
43
+ @auth_iteration = 0
44
+ @connection_established = false
45
+ @transactions = MonetDB::Transaction.new
46
+ end
47
+
48
+ # Connect to the database, creates a new socket.
49
+ def connect(db_name = "demo", auth_type = "SHA1")
50
+ @database = db_name
51
+ @auth_type = auth_type
52
+ @socket = TCPSocket.new(@host, @port.to_i)
53
+
54
+ if real_connect
55
+ if @lang == LANG_SQL
56
+ set_timezone
57
+ set_reply_size
58
+ elsif (@lang == LANG_XQUERY) and XQUERY_OUTPUT_SEQ
59
+ # require xquery output to be in seq format
60
+ send(format_command("output seq"))
61
+ end
62
+ true
63
+ else
64
+ false
65
+ end
66
+ end
67
+
68
+ # Perform a real connection; retrieve challenge, proxy through merovinginan, build challenge and set the timezone.
69
+ def real_connect
70
+ server_challenge = retrieve_server_challenge()
71
+
72
+ if server_challenge != nil
73
+ salt = server_challenge.split(':')[0]
74
+ @server_name = server_challenge.split(':')[1]
75
+ @protocol = server_challenge.split(':')[2].to_i
76
+ @supported_auth_types = server_challenge.split(':')[3].split(',')
77
+ @server_endianness = server_challenge.split(':')[4]
78
+ if @protocol == 9
79
+ @pwhash = server_challenge.split(':')[5]
80
+ end
81
+ else
82
+ raise MonetDB::ConnectionError, "Error: server returned an empty challenge string."
83
+ end
84
+
85
+ # The server supports only RIPMED168 or crypt as an authentication hash function, but the driver does not.
86
+ if @supported_auth_types.length == 1
87
+ auth = @supported_auth_types[0]
88
+ if auth.upcase == "RIPEMD160" or auth.upcase == "CRYPT"
89
+ raise MonetDB::ConnectionError, auth.upcase + " " + ": algorithm not supported by ruby-monetdb."
90
+ end
91
+ end
92
+
93
+ # If the server protocol version is not 8: abort and notify the user.
94
+ if @@SUPPORTED_PROTOCOLS.include?(@protocol) == false
95
+ raise MonetDB::ProtocolError, "Protocol not supported. The current implementation of ruby-monetdb works with MAPI protocols #{@@SUPPORTED_PROTOCOLS} only."
96
+
97
+ elsif mapi_proto_v8?
98
+ reply = build_auth_string_v8(@auth_type, salt, @database)
99
+ elsif mapi_proto_v9?
100
+ reply = build_auth_string_v9(@auth_type, salt, @database)
101
+ end
102
+
103
+ if @socket != nil
104
+ @connection_established = true
105
+
106
+ send(reply)
107
+ monetdb_auth = receive
108
+
109
+ if monetdb_auth.length == 0
110
+ # auth succedeed
111
+ true
112
+ else
113
+ if monetdb_auth[0].chr == MSG_REDIRECT
114
+ #redirection
115
+
116
+ redirects = [] # store a list of possible redirects
117
+
118
+ monetdb_auth.split('\n').each do |m|
119
+ # strip the trailing ^mapi:
120
+ # if the redirect string start with something != "^mapi:" or is empty, the redirect is invalid and shall not be included.
121
+ if m[0..5] == "^mapi:"
122
+ redir = m[6..m.length]
123
+ # url parse redir
124
+ redirects.push(redir)
125
+ else
126
+ $stderr.print "Warning: Invalid Redirect #{m}"
127
+ end
128
+ end
129
+
130
+ if redirects.size == 0
131
+ raise MonetDB::ConnectionError, "No valid redirect received"
132
+ else
133
+ begin
134
+ uri = URI.split(redirects[0])
135
+ # Splits the string on following parts and returns array with result:
136
+ #
137
+ # * Scheme
138
+ # * Userinfo
139
+ # * Host
140
+ # * Port
141
+ # * Registry
142
+ # * Path
143
+ # * Opaque
144
+ # * Query
145
+ # * Fragment
146
+ server_name = uri[0]
147
+ host = uri[2]
148
+ port = uri[3]
149
+ database = uri[5].gsub(/^\//, '') if uri[5] != nil
150
+ rescue URI::InvalidURIError
151
+ raise MonetDB::ConnectionError, "Invalid redirect: #{redirects[0]}"
152
+ end
153
+ end
154
+
155
+ if server_name == "merovingian"
156
+ if @auth_iteration <= 10
157
+ @auth_iteration += 1
158
+ real_connect
159
+ else
160
+ raise MonetDB::ConnectionError, "Merovingian: too many iterations while proxying."
161
+ end
162
+ elsif server_name == "monetdb"
163
+ begin
164
+ @socket.close
165
+ rescue
166
+ raise MonetDB::ConnectionError, "I/O error while closing connection to #{@socket}"
167
+ end
168
+ # reinitialize a connection
169
+ @host = host
170
+ @port = port
171
+
172
+ connect(database, @auth_type)
173
+ else
174
+ @connection_established = false
175
+ raise MonetDB::ConnectionError, monetdb_auth
176
+ end
177
+ elsif monetdb_auth[0].chr == MSG_INFO
178
+ raise MonetDB::ConnectionError, monetdb_auth
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ def savepoint
185
+ @transactions.savepoint
186
+ end
187
+
188
+ # Formats a <i>command</i> string so that it can be parsed by the server.
189
+ def format_command(x)
190
+ "X" + x + "\n"
191
+ end
192
+
193
+ # Send an 'export' command to the server.
194
+ def set_export(id, idx, offset)
195
+ send(format_command("export " + id.to_s + " " + idx.to_s + " " + offset.to_s ))
196
+ end
197
+
198
+ # Send a 'reply_size' command to the server.
199
+ def set_reply_size
200
+ send(format_command(("reply_size " + REPLY_SIZE)))
201
+ response = receive
202
+
203
+ if response == MSG_PROMPT
204
+ true
205
+ elsif response[0] == MSG_INFO
206
+ raise MonetDB::CommandError, "Unable to set reply_size: #{response}"
207
+ end
208
+ end
209
+
210
+ def set_output_seq
211
+ send(format_command("output seq"))
212
+ end
213
+
214
+ # Disconnect from server.
215
+ def disconnect
216
+ if @connection_established
217
+ begin
218
+ @socket.close
219
+ rescue => e
220
+ $stderr.print e
221
+ end
222
+ else
223
+ raise MonetDB::ConnectionError, "No connection established."
224
+ end
225
+ end
226
+
227
+ # Send data to a monetdb5 server instance and returns server response.
228
+ def send(data)
229
+ encode_message(data).each do |m|
230
+ @socket.write(m)
231
+ end
232
+ end
233
+
234
+ # Receive data from a monetdb5 server instance.
235
+ def receive
236
+ is_final, chunk_size = recv_decode_hdr
237
+
238
+ if chunk_size == 0
239
+ return "" # needed on ruby-1.8.6 linux/64bit; recv(0) hangs on this configuration.
240
+ end
241
+
242
+ data = @socket.recv(chunk_size)
243
+
244
+ if is_final == false
245
+ while is_final == false
246
+ is_final, chunk_size = recv_decode_hdr
247
+ data += @socket.recv(chunk_size)
248
+ end
249
+ end
250
+
251
+ data
252
+ end
253
+
254
+ # Build and authentication string given the parameters submitted by the user (MAPI protocol v8).
255
+ def build_auth_string_v8(auth_type, salt, db_name)
256
+ # seed = password + salt
257
+ if (auth_type.upcase == "MD5" or auth_type.upcase == "SHA1") and @supported_auth_types.include?(auth_type.upcase)
258
+ auth_type = auth_type.upcase
259
+ digest = Hasher.new(auth_type, @passwd+salt)
260
+ hashsum = digest.hashsum
261
+ elsif auth_type.downcase == "plain" or not @supported_auth_types.include?(auth_type.upcase)
262
+ auth_type = 'plain'
263
+ hashsum = @passwd + salt
264
+
265
+ elsif auth_type.downcase == "crypt"
266
+ auth_type = @supported_auth_types[@supported_auth_types.index(auth_type)+1]
267
+ $stderr.print "The selected hashing algorithm is not supported by the Ruby driver. #{auth_type} will be used instead."
268
+ digest = Hasher.new(auth_type, @passwd+salt)
269
+ hashsum = digest.hashsum
270
+ else
271
+ # The user selected an auth type not supported by the server.
272
+ raise MonetDB::ConnectionError, "#{auth_type} not supported by the server. Please choose one from #{@supported_auth_types}"
273
+
274
+ end
275
+ # Build the reply message with header
276
+ reply = @client_endianness + ":" + @user + ":{" + auth_type + "}" + hashsum + ":" + @lang + ":" + db_name + ":"
277
+ end
278
+
279
+ # Build and authentication string given the parameters submitted by the user (MAPI protocol v9).
280
+ def build_auth_string_v9(auth_type, salt, db_name)
281
+ if (auth_type.upcase == "MD5" or auth_type.upcase == "SHA1") and @supported_auth_types.include?(auth_type.upcase)
282
+ auth_type = auth_type.upcase
283
+ # Hash the password
284
+ pwhash = Hasher.new(@pwhash, @passwd)
285
+
286
+ digest = Hasher.new(auth_type, pwhash.hashsum + salt)
287
+ hashsum = digest.hashsum
288
+
289
+ elsif auth_type.downcase == "plain" # or not @supported_auth_types.include?(auth_type.upcase)
290
+ # Keep it for compatibility with merovingian
291
+ auth_type = 'plain'
292
+ hashsum = @passwd + salt
293
+ elsif @supported_auth_types.include?(auth_type.upcase)
294
+ if auth_type.upcase == "RIPEMD160"
295
+ auth_type = @supported_auth_types[@supported_auth_types.index(auth_type)+1]
296
+ $stderr.print "The selected hashing algorithm is not supported by the Ruby driver. #{auth_type} will be used instead."
297
+ end
298
+ # Hash the password
299
+ pwhash = Hasher.new(@pwhash, @passwd)
300
+
301
+ digest = Hasher.new(auth_type, pwhash.hashsum + salt)
302
+ hashsum = digest.hashsum
303
+ else
304
+ # The user selected an auth type not supported by the server.
305
+ raise MonetDB::ConnectionError, "#{auth_type} not supported by the server. Please choose one from #{@supported_auth_types}"
306
+ end
307
+ # Build the reply message with header
308
+ reply = @client_endianness + ":" + @user + ":{" + auth_type + "}" + hashsum + ":" + @lang + ":" + db_name + ":"
309
+ end
310
+
311
+ # Build a message to be sent to the server.
312
+ def encode_message(msg = "")
313
+ message = Array.new
314
+ data = ""
315
+
316
+ hdr = 0 # package header
317
+ pos = 0
318
+ is_final = false # last package in the stream
319
+
320
+ while (! is_final)
321
+ data = msg[pos..pos+[@@MAX_MESSAGE_SIZE.to_i, (msg.length - pos).to_i].min]
322
+ pos += data.length
323
+
324
+ if (msg.length - pos) == 0
325
+ last_bit = 1
326
+ is_final = true
327
+ else
328
+ last_bit = 0
329
+ end
330
+
331
+ hdr = [(data.length << 1) | last_bit].pack('v')
332
+
333
+ message << hdr + data.to_s # Short Little Endian Encoding
334
+ end
335
+
336
+ message.freeze # freeze and return the encode message
337
+ end
338
+
339
+ # Used as the first step in the authentication phase; retrieves a challenge string from the server.
340
+ def retrieve_server_challenge
341
+ server_challenge = receive
342
+ end
343
+
344
+ # Reads and decodes the header of a server message.
345
+ def recv_decode_hdr
346
+ if @socket != nil
347
+ fb = @socket.recv(1)
348
+ sb = @socket.recv(1)
349
+
350
+ # Use execeptions handling to keep compatibility between different ruby
351
+ # versions.
352
+ #
353
+ # Chars are treated differently in ruby 1.8 and 1.9
354
+ # try do to ascii to int conversion using ord (ruby 1.9)
355
+ # and if it fail fallback to character.to_i (ruby 1.8)
356
+ begin
357
+ fb = fb[0].ord
358
+ sb = sb[0].ord
359
+ rescue NoMethodError => one_eight
360
+ fb = fb[0].to_i
361
+ sb = sb[0].to_i
362
+ end
363
+
364
+ chunk_size = (sb << 7) | (fb >> 1)
365
+
366
+ is_final = false
367
+ if ( (fb & 1) == 1 )
368
+ is_final = true
369
+
370
+ end
371
+ # return the size of the chunk (in bytes)
372
+ return is_final, chunk_size
373
+ else
374
+ raise MonetDB::SocketError, "Error while receiving data\n"
375
+ end
376
+ end
377
+
378
+ # Sets the time zone according to the Operating System settings.
379
+ def set_timezone
380
+ tz = Time.new
381
+ tz_offset = tz.gmt_offset / @@HOUR
382
+
383
+ if tz_offset <= 9 # verify minute count!
384
+ tz_offset = "'+0" + tz_offset.to_s + ":00'"
385
+ else
386
+ tz_offset = "'+" + tz_offset.to_s + ":00'"
387
+ end
388
+ query_tz = "sSET TIME ZONE INTERVAL " + tz_offset + " HOUR TO MINUTE;"
389
+
390
+ # Perform the query directly within the method
391
+ send(query_tz)
392
+ response = receive
393
+
394
+ if response == MSG_PROMPT
395
+ true
396
+ elsif response[0].chr == MSG_INFO
397
+ raise MonetDB::QueryError, response
398
+ end
399
+ end
400
+
401
+ # Turns auto commit on/off.
402
+ def set_auto_commit(flag = true)
403
+ if flag == false
404
+ ac = " 0"
405
+ else
406
+ ac = " 1"
407
+ end
408
+
409
+ send(format_command("auto_commit " + ac))
410
+ response = receive
411
+
412
+ if response == MSG_PROMPT
413
+ @auto_commit = flag
414
+ elsif response[0].chr == MSG_INFO
415
+ raise MonetDB::CommandError, response
416
+ end
417
+ end
418
+
419
+ # Check the auto commit status (on/off).
420
+ def auto_commit?
421
+ !!@auto_commit
422
+ end
423
+
424
+ # Check if monetdb is running behind the merovingian proxy and forward the connection in case.
425
+ def merovingian?
426
+ @server_name.downcase == "merovingian"
427
+ end
428
+
429
+ def mserver?
430
+ @server_name.downcase == "monetdb"
431
+ end
432
+
433
+ # Check which protocol is spoken by the server.
434
+ def mapi_proto_v8?
435
+ @protocol == 8
436
+ end
437
+
438
+ def mapi_proto_v9?
439
+ @protocol == 9
440
+ end
441
+
442
+ end
443
+ end
@@ -0,0 +1 @@
1
+ require "monetdb/core_ext/string"
@@ -0,0 +1,67 @@
1
+ #
2
+ # Overload the class string to convert monetdb to ruby types.
3
+ #
4
+
5
+ class String
6
+
7
+ def getInt
8
+ to_i
9
+ end
10
+
11
+ def getFloat
12
+ to_f
13
+ end
14
+
15
+ def getString
16
+ gsub(/^"/, "").gsub(/"$/, "")
17
+ end
18
+
19
+ # Convert from HEX to the origianl binary data.
20
+ def getBlob
21
+ blob = ""
22
+ scan(/../) { |tuple| blob += tuple.hex.chr }
23
+ blob
24
+ end
25
+
26
+ # Ruby currently supports only time formatted timestamps; treat TIME as string.
27
+ def getTime
28
+ gsub(/^"/, "").gsub(/"$/, "")
29
+ end
30
+
31
+ # Ruby currently supports only date formatted timestamps; treat DATE as string.
32
+ def getDate
33
+ gsub(/^"/, "").gsub(/"$/, "")
34
+ end
35
+
36
+ def getDateTime
37
+ date = split(" ")[0].split("-")
38
+ time = split(" ")[1].split(":")
39
+ Time.gm(date[0], date[1], date[2], time[0], time[1], time[2])
40
+ end
41
+
42
+ def getChar
43
+ # Ruby < 1.9 does not have a char datatype
44
+ begin
45
+ ord
46
+ rescue
47
+ self
48
+ end
49
+ end
50
+
51
+ def getBool
52
+ if %w(1 y t true).include?(self)
53
+ true
54
+ elsif %w(0 n f false).include?(self)
55
+ false
56
+ end
57
+ end
58
+
59
+ def getNull
60
+ if upcase == "NONE"
61
+ nil
62
+ else
63
+ raise "Unknown value"
64
+ end
65
+ end
66
+
67
+ end