Mercurial > hg > monetdb-perl
comparison MonetDB-CLI-MapiPP/MonetDB/CLI/Mapi.pm @ 1:f899cb48b4cc
Turned into independent repository.
Updated version numbers to 1.00; added Fedora spec file; added
top-level Makefile.
author | Sjoerd Mullender <sjoerd@acm.org> |
---|---|
date | Mon, 19 Sep 2016 15:20:16 +0200 (2016-09-19) |
parents | Mapi.pm@cedccb7e0143 |
children | a0ec9e080a5b |
comparison
equal
deleted
inserted
replaced
0:cedccb7e0143 | 1:f899cb48b4cc |
---|---|
1 # This Source Code Form is subject to the terms of the Mozilla Public | |
2 # License, v. 2.0. If a copy of the MPL was not distributed with this | |
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. | |
4 # | |
5 # Copyright 1997 - July 2008 CWI, August 2008 - 2016 MonetDB B.V. | |
6 | |
7 package MonetDB::CLI::Mapi; | |
8 | |
9 use strict; | |
10 use Socket; | |
11 use IO::Socket; | |
12 use Digest::MD5 'md5_hex'; | |
13 use Digest::SHA qw(sha1_hex sha256_hex sha512_hex); | |
14 | |
15 our $VERSION = '1.00'; | |
16 | |
17 sub pass_chal { | |
18 my ($passwd, @challenge) = @_; | |
19 if ($challenge[2] == 9) { | |
20 my $pwhash = $challenge[5]; | |
21 if ($pwhash eq 'SHA512') { | |
22 $passwd = sha512_hex($passwd); | |
23 } elsif ($pwhash eq 'SHA256') { | |
24 $passwd = sha256_hex($passwd); | |
25 } elsif ($pwhash eq 'SHA1') { | |
26 $passwd = sha1_hex($passwd); | |
27 } elsif ($pwhash eq 'MD5') { | |
28 $passwd = md5_hex($passwd); | |
29 } else { | |
30 warn "unsupported password hash: ".$pwhash; | |
31 return; | |
32 } | |
33 } else { | |
34 warn "unsupported protocol version: ".$challenge[2]; | |
35 return; | |
36 } | |
37 | |
38 my @cyphers = split(/,/, $challenge[3]); | |
39 my $chal; | |
40 foreach (@cyphers) { | |
41 if ($_ eq 'SHA512') { | |
42 $chal = "{$_}".sha512_hex($passwd.$challenge[0]); | |
43 last; | |
44 } elsif ($_ eq 'SHA256') { | |
45 $chal = "{$_}".sha256_hex($passwd.$challenge[0]); | |
46 last; | |
47 } elsif ($_ eq 'SHA1') { | |
48 $chal = "{$_}".sha1_hex($passwd.$challenge[0]); | |
49 last; | |
50 } elsif ($_ eq 'MD5') { | |
51 $chal = "{$_}".md5_hex($passwd.$challenge[0]); | |
52 last; | |
53 } | |
54 } | |
55 if (!$chal) { | |
56 warn "unsupported hash algorithm necessary for login: ".$challenge[3]; | |
57 return; | |
58 } | |
59 | |
60 return $chal; | |
61 } | |
62 | |
63 sub new { | |
64 my $mapi = shift; | |
65 my $host = shift || 'localhost'; | |
66 my $port = shift || 50000; | |
67 my $user = shift || 'monetdb'; | |
68 my $passwd = shift || 'monetdb'; | |
69 my $lang = shift || 'sql'; | |
70 my $db = shift || ''; | |
71 my $trace = shift || 0; | |
72 my $self = {}; | |
73 | |
74 bless( $self, $mapi ); | |
75 | |
76 $self->{trace} = $trace; | |
77 | |
78 print "new:$host,$port,$user,$passwd,$lang,$db\n" if ($self->{trace}); | |
79 $self->{host} = $host; | |
80 $self->{port} = $port; | |
81 $self->{user} = $user; | |
82 $self->{passwd} = $passwd; | |
83 $self->{lang} = $lang; | |
84 $self->{db} = $db; | |
85 $self->{socket} = IO::Socket::INET->new( | |
86 PeerAddr => $host, | |
87 PeerPort => $port, | |
88 Proto => 'tcp' | |
89 ) || die "!ERROR can't connect to $host:$port $!"; | |
90 $self->{piggyback} = []; | |
91 $self->{skip_in} = 0; | |
92 | |
93 #binmode($self->{socket},":utf8"); | |
94 | |
95 #block challenge:mserver:9:cypher(s):content_byteorder(BIG/LIT):pwhash\n"); | |
96 my $block = $self->getblock(); | |
97 my @challenge = split(/:/, $block); | |
98 print "Connection to socket established ($block)\n" if ($self->{trace}); | |
99 | |
100 my $passchal = pass_chal($passwd, @challenge) || die; | |
101 | |
102 # content_byteorder(BIG/LIT):user:{cypher_algo}mypasswordchallenge_cyphered:lang:database: | |
103 $self->putblock("LIT:$user:$passchal:$lang:$db:\n"); | |
104 my $prompt = $self->getblock(); | |
105 if ($prompt =~ /^\^mapi:monetdb:/) { | |
106 # full reconnect | |
107 $self->{socket}->close; | |
108 print "Following redirect: $prompt\n" if ($self->{trace}); | |
109 my @tokens = split(/[\n\/:\?]+/, $prompt); # dirty, but it's Perl anyway | |
110 return new MonetDB::CLI::Mapi($tokens[3], $tokens[4], $user, $passwd, $lang, $tokens[5], $trace); | |
111 } elsif ($prompt =~ /^\^mapi:merovingian:\/\/proxy/) { | |
112 # proxied redirect | |
113 do { | |
114 print "Being proxied by $host:$port\n" if ($self->{trace}); | |
115 $block = $self->getblock(); | |
116 @challenge = split(/:/, $block); | |
117 $passchal = pass_chal($passwd, @challenge) || die; | |
118 $self->putblock("LIT:$user:$passchal:$lang:$db:\n"); | |
119 $prompt = $self->getblock(); | |
120 } while ($prompt =~ /^\^mapi:merovingian:proxy/); | |
121 } # TODO: don't die on warnings (#) | |
122 die $prompt if ($prompt ne ""); | |
123 print "Logged on $user\@$db with $lang\n" if ($self->{trace}); | |
124 return $self; | |
125 } | |
126 | |
127 # How to create a duplicate | |
128 sub clone { | |
129 my ($self,$src)= @_; | |
130 bless($self,"MonetDB::CLI::Mapi"); | |
131 print "cloning\n" if ($self->{trace}); | |
132 $self->{host} = $src->{host}; | |
133 $self->{port} = $src->{port}; | |
134 $self->{user} = $src->{user}; | |
135 $self->{passwd} = $src->{passwd}; | |
136 $self->{lang} = $src->{lang}; | |
137 $self->{db} = $src->{db}; | |
138 $self->{socket} = $src->{socket}; | |
139 $self->resetState(); | |
140 } | |
141 | |
142 sub mapiport_intern { | |
143 my $mapiport = 'localhost:50000'; | |
144 $mapiport = $ENV{'MAPIPORT'} if defined($ENV{'MAPIPORT'}); | |
145 return $mapiport; | |
146 } | |
147 | |
148 sub hostname { | |
149 my ($hostname) = mapiport_intern() =~ /([^:]*)/; | |
150 $hostname = 'localhost' if ($hostname eq ''); | |
151 return $hostname; | |
152 } | |
153 | |
154 sub portnr { | |
155 my ($portnr) = mapiport_intern() =~ /:([^:]*)/; | |
156 $portnr = 50000 if ($portnr eq ''); | |
157 return $portnr; | |
158 } | |
159 | |
160 sub disconnect { | |
161 my ($self) = @_; | |
162 print "disconnect\n" if ($self->{trace}); | |
163 $self->{socket}->close; | |
164 print "Disconnected from server\n" if ($self->{trace}); | |
165 } | |
166 | |
167 sub showState { | |
168 my ($self) = @_; | |
169 if ($self->{trace}) { | |
170 print "mapi.error :".$self->{error}."\n"; | |
171 print "mapi.errstr:".$self->{errstr}."\n"; | |
172 print "mapi.active:".$self->{active}."\n"; | |
173 print "mapi.row[".length($self->{row})."]:".$self->{row}."\n"; | |
174 } | |
175 } | |
176 | |
177 sub resetState { | |
178 my ($self) = @_; | |
179 print "resetState\n" if ($self->{trace}); | |
180 $self->{errstr}=""; | |
181 $self->{error}=0; | |
182 $self->{active}=0; | |
183 } | |
184 | |
185 #packge the request and ship it, the back-end reads blocks! | |
186 sub doRequest { | |
187 my($self,$cmd) = @_; | |
188 | |
189 $cmd = "S" . $cmd if $self->{lang} eq 'sql'; | |
190 # even if the query ends with a ;, this never hurts and fixes the -- on last line bug | |
191 $cmd = $cmd . "\n;\n"; | |
192 print "doRequest:$cmd\n" if ($self->{trace}); | |
193 $self->putblock($cmd); # TODO handle exceptions || die "!ERROR can't send $cmd: $!"; | |
194 $self->resetState(); | |
195 } | |
196 | |
197 # Analyse a single line for errors | |
198 sub error { | |
199 my ($self,$line) = @_; | |
200 my $err = $self->{errstr}; | |
201 $err = "$err\n" if (length($err) > 0); | |
202 $line =~ s/^\!//; | |
203 $self->{errstr} = $err . $line; | |
204 # $self->showState(); | |
205 $self->{row}= ""; | |
206 $self->{error} = 1; | |
207 print "Error found $self->{error}\n" if ($self->{trace}); | |
208 } | |
209 | |
210 # analyse commentary lines for auxiliary information | |
211 sub propertyTest { | |
212 my ($self) =@_; | |
213 my $err= $self->{error}; | |
214 my $row= $self->{row}; | |
215 # $self->showState(); | |
216 if ($row =~ /^\#---/) { | |
217 $self->{row}= ""; | |
218 return 1; | |
219 } | |
220 if ($row =~ /^\#.*\#/) { | |
221 $self->{row}= ""; | |
222 return 1; | |
223 } | |
224 return 0; | |
225 } | |
226 | |
227 | |
228 sub getRow { | |
229 my ($self)= @_; | |
230 my $row = $self->{lines}[$self->{next}++]; | |
231 my @chars = split(//, $row,3); | |
232 | |
233 if ($chars[0] eq '!') { | |
234 $self->error($row); | |
235 my $i = 1; | |
236 while ($self->{lines}[$i] =~ '!') { | |
237 $self->error($self->{lines}[$i]); | |
238 $i++; | |
239 } | |
240 $self->{active} = 0; | |
241 return -1 | |
242 } elsif ($chars[0] eq '&') { | |
243 # not expected | |
244 } elsif ($chars[0] eq '%') { | |
245 # header line | |
246 } elsif ($chars[0] eq '[') { | |
247 # row result | |
248 $self->{row} = $row; | |
249 if ($self->{nrcols} < 0) { | |
250 $self->{nrcols} = () = $row =~ /,\t/g; | |
251 $self->{nrcols}++; | |
252 } | |
253 $self->{active} = 1; | |
254 } elsif ($chars[0] eq '=') { | |
255 # xml result line | |
256 $self->{row} = substr($row, 1); # skip = | |
257 $self->{active} = 1; | |
258 } elsif ($chars[0] eq '^') { | |
259 # ^ redirect, ie use different server | |
260 } elsif ($chars[0] eq '#') { | |
261 # warnings etc, skip, and return what follows | |
262 return $self->getRow; | |
263 } | |
264 return $self->{active}; | |
265 } | |
266 | |
267 sub getBlock { | |
268 my ($self)= @_; | |
269 print "getBlock $self->{active}\n" if ($self->{trace}); | |
270 my $block = $self->getblock(); | |
271 @{$self->{lines}} = split(/\n/, $block); | |
272 | |
273 # skip diagnostic messages before the header | |
274 shift @{$self->{lines}} while @{$self->{lines}} && $self->{lines}[0] =~ /\A#/; | |
275 | |
276 die "implausible return from MonetDB: $self->{lines}[0]\n" if $self->{lines}[0] =~ /\A[^ -~]/; | |
277 | |
278 my $header = $self->{lines}[0]; | |
279 my @chars = split(//, $header); | |
280 | |
281 $self->{id} = -1; | |
282 $self->{nrcols} = -1; | |
283 $self->{replysize} = scalar(@{$self->{lines}}); | |
284 $self->{active} = 0; | |
285 $self->{skip} = 0; # next+skip is current result row | |
286 $self->{next} = 0; # all done | |
287 $self->{offset} = 0; | |
288 $self->{hdrs} = []; | |
289 | |
290 if ($chars[0] eq '&') { | |
291 if ($chars[1] eq '1' || $chars[1] eq 6) { | |
292 if ($chars[1] eq '1') { | |
293 # &1 id result-count nr-cols rows-in-this-block | |
294 my ($dummy,$id,$cnt,$nrcols,$replysize) = split(' ', $header); | |
295 $self->{id} = $id; | |
296 $self->{count} = $cnt; | |
297 $self->{nrcols} = $nrcols; | |
298 $self->{replysize} = $replysize; | |
299 } else { | |
300 # &6 id nr-cols,rows-in-this-block,offset | |
301 my ($dummy,$id,$nrcols,$replysize,$offset) = split(' ', $header); | |
302 $self->{id} = $id; | |
303 $self->{nrcols} = $nrcols; | |
304 $self->{replysize} = $replysize; | |
305 $self->{offset} = $offset; | |
306 } | |
307 # for now skip table header information | |
308 my $i = 1; | |
309 while ($self->{lines}[$i] =~ /\A%/) { | |
310 $self->{hdrs}[$i - 1] = $self->{lines}[$i]; | |
311 $i++; | |
312 } | |
313 $self->{skip} = $i; | |
314 $self->{next} = $i; | |
315 $self->{row} = $self->{lines}[$self->{next}++]; | |
316 | |
317 $self->{active} = 1; | |
318 } elsif ($chars[1] eq '2') { # updates | |
319 my ($dummy,$cnt) = split(' ', $header); | |
320 $self->{count} = $cnt; | |
321 $self->{nrcols} = 1; | |
322 $self->{replysize} = 1; | |
323 $self->{row} = "" . $cnt; | |
324 $self->{next} = $cnt; # all done | |
325 return -2; | |
326 } elsif ($chars[1] eq '3') { # transaction | |
327 # nothing todo | |
328 } elsif ($chars[1] eq '4') { # auto_commit | |
329 my ($dummy,$ac) = split(' ', $header); | |
330 if ($ac eq 't') { | |
331 $self->{auto_commit} = 1; | |
332 } else { | |
333 $self->{auto_commit} = 0; | |
334 } | |
335 } elsif ($chars[1] eq '5') { # prepare | |
336 my ($dummy,$id,$cnt,$nrcols,$replysize) = split(' ', $header); | |
337 # TODO parse result, rows (type, digits, scale) | |
338 $self->{count} = $cnt; | |
339 $self->{nrcols} = $nrcols; | |
340 $self->{replysize} = $replysize; | |
341 $self->{row} = ""; | |
342 $self->{next} = $cnt; # all done | |
343 } | |
344 } else { | |
345 return $self->getRow(); | |
346 } | |
347 return $self->{active}; | |
348 } | |
349 | |
350 sub getReply { | |
351 my ($self)= @_; | |
352 | |
353 if ($self->{active} == 0) { | |
354 return $self->getBlock(); | |
355 } elsif ($self->{next} < $self->{replysize} + $self->{skip}) { | |
356 return $self->getRow(); | |
357 } elsif (${self}->{offset} + $self->{replysize} < $self->{count}) { | |
358 # get next slice | |
359 my $rs = $self->{replysize}; | |
360 my $offset = $self->{offset} + $rs; | |
361 $self->putblock("Xexport $self->{id} $offset $rs"); | |
362 return $self->getBlock(); | |
363 } else { | |
364 # close large results, but only send on next query | |
365 if ($self->{id} > 0 && $self->{count} != $self->{replysize}) { | |
366 push @{$self->{piggyback}}, "Xclose $self->{id}"; | |
367 $self->{skip_in}++; | |
368 } | |
369 $self->{active} = 0; | |
370 } | |
371 return $self->{active}; | |
372 | |
373 } | |
374 | |
375 sub readFromSocket { | |
376 my ($self, $ref, $count) = @_; | |
377 | |
378 die "invalid buffer reference" unless (ref($ref) eq 'SCALAR'); | |
379 | |
380 my $rcount = 0; | |
381 $$ref ||= ""; | |
382 | |
383 while ($count > 0) { | |
384 $rcount = $self->{socket}->sysread($$ref, $count, length($$ref)); | |
385 | |
386 die "read error: $!" unless (defined($rcount)); | |
387 die "no more data on socket" if ($rcount == 0); | |
388 | |
389 $count -= $rcount; | |
390 } | |
391 } | |
392 | |
393 sub getblock { | |
394 my ($self) = @_; | |
395 | |
396 # now read back the same way | |
397 my $result = ""; | |
398 my $last_block = 0; | |
399 do { | |
400 my $flag; | |
401 | |
402 $self->readFromSocket(\$flag, 2); # read block info | |
403 | |
404 my $unpacked = unpack( 'v', $flag ); # unpack (little endian short) | |
405 my $len = ( $unpacked >> 1 ); # get length | |
406 $last_block = $unpacked & 1; # get last-block-flag | |
407 | |
408 print "getblock: $last_block $len\n" if ($self->{trace}); | |
409 if ($len > 0 ) { | |
410 my $data; | |
411 $self->readFromSocket(\$data, $len); # read | |
412 $result .= $data; | |
413 print "getblock: $data\n" if ($self->{trace}); | |
414 } | |
415 } while ( !$last_block ); | |
416 print "IN:\n$result\n" if $ENV{MAPI_TRACE}; | |
417 | |
418 if ($self->{skip_in}) { | |
419 $self->{skip_in}--; | |
420 goto &getblock; | |
421 } | |
422 | |
423 return $result; | |
424 } | |
425 | |
426 sub putblock { | |
427 my $self = shift; | |
428 | |
429 # there maybe something in the piggyback buffer | |
430 my @blocks = (\(@{ $self->{piggyback} }), \(@_)); | |
431 @{ $self->{piggyback} } = (); | |
432 | |
433 # create blocks of data with max 0xffff length, | |
434 # then loop over the data and send it. | |
435 my $out = ''; | |
436 for my $blk (@blocks) { | |
437 print "OUT:\n$$blk\n" if $ENV{MAPI_TRACE}; | |
438 utf8::downgrade($$blk); # deny wide chars | |
439 my $pos = 0; | |
440 my $last_block = 0; | |
441 my $blocksize = 0x7fff >> 1; # max len per block | |
442 my $data; | |
443 | |
444 while ( !$last_block ) { | |
445 my $data = substr($$blk, 0, $blocksize, ""); | |
446 my $len = length($data); | |
447 # set last-block-flag | |
448 $last_block = 1 if !length $$blk; | |
449 my $flag = pack( 'v', ( $len << 1 ) + $last_block ); | |
450 print "putblock: $last_block ".$data."\n" if ($self->{trace}); | |
451 $out .= $flag . $data; | |
452 } | |
453 } | |
454 $self->{socket}->syswrite($out); #send it | |
455 } | |
456 | |
457 1; | |
458 | |
459 # vim: set ts=2 sw=2 expandtab: |