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