diff 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
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/Mapi.pm
@@ -0,0 +1,457 @@
+# 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 - 2016 MonetDB B.V.
+
+package Mapi;
+
+use strict;
+use Socket;
+use IO::Socket;
+use Digest::MD5 'md5_hex';
+use Digest::SHA qw(sha1_hex sha256_hex sha512_hex);
+
+sub pass_chal {
+  my ($passwd, @challenge) = @_;
+  if ($challenge[2] == 9) {
+    my $pwhash = $challenge[5];
+    if ($pwhash eq 'SHA512') {
+      $passwd = sha512_hex($passwd);
+    } elsif ($pwhash eq 'SHA256') {
+      $passwd = sha256_hex($passwd);
+    } elsif ($pwhash eq 'SHA1') {
+      $passwd = sha1_hex($passwd);
+    } elsif ($pwhash eq 'MD5') {
+      $passwd = md5_hex($passwd);
+    } else {
+      warn "unsupported password hash: ".$pwhash;
+      return;
+    }
+  } else {
+    warn "unsupported protocol version: ".$challenge[2];
+    return;
+  }
+
+  my @cyphers = split(/,/, $challenge[3]);
+  my $chal;
+  foreach (@cyphers) {
+    if ($_ eq 'SHA512') {
+      $chal = "{$_}".sha512_hex($passwd.$challenge[0]);
+      last;
+    } elsif ($_ eq 'SHA256') {
+      $chal = "{$_}".sha256_hex($passwd.$challenge[0]);
+      last;
+    } elsif ($_ eq 'SHA1') {
+      $chal = "{$_}".sha1_hex($passwd.$challenge[0]);
+      last;
+    } elsif ($_ eq 'MD5') {
+      $chal = "{$_}".md5_hex($passwd.$challenge[0]);
+      last;
+    }
+  }
+  if (!$chal) {
+    warn "unsupported hash algorithm necessary for login: ".$challenge[3];
+    return;
+  }
+
+  return $chal;
+}
+
+sub new {
+  my $mapi = shift;
+  my $host  = shift || 'localhost';
+  my $port  = shift || 50000;
+  my $user  = shift || 'monetdb';
+  my $passwd  = shift || 'monetdb';
+  my $lang  = shift || 'sql';
+  my $db  = shift || '';
+  my $trace  = shift || 0;
+  my $self = {};
+
+  bless( $self, $mapi );
+
+  $self->{trace} = $trace;
+
+  print "new:$host,$port,$user,$passwd,$lang,$db\n" if ($self->{trace});
+  $self->{host} = $host;
+  $self->{port} = $port;
+  $self->{user} = $user;
+  $self->{passwd} = $passwd;
+  $self->{lang} = $lang;
+  $self->{db} = $db;
+  $self->{socket} = IO::Socket::INET->new(
+        PeerAddr => $host,
+        PeerPort => $port,
+        Proto    => 'tcp'
+  ) || die "!ERROR can't connect to $host:$port $!";
+  $self->{piggyback} = [];
+  $self->{skip_in} = 0;
+
+  #binmode($self->{socket},":utf8");
+
+  #block challenge:mserver:9:cypher(s):content_byteorder(BIG/LIT):pwhash\n");
+  my $block = $self->getblock();
+  my @challenge = split(/:/, $block);
+  print "Connection to socket established ($block)\n" if ($self->{trace});
+
+  my $passchal = pass_chal($passwd, @challenge) || die;
+
+  # content_byteorder(BIG/LIT):user:{cypher_algo}mypasswordchallenge_cyphered:lang:database: 
+  $self->putblock("LIT:$user:$passchal:$lang:$db:\n");
+  my $prompt = $self->getblock();
+  if ($prompt =~ /^\^mapi:monetdb:/) {
+    # full reconnect
+    $self->{socket}->close;
+    print "Following redirect: $prompt\n" if ($self->{trace});
+    my @tokens = split(/[\n\/:\?]+/, $prompt); # dirty, but it's Perl anyway
+    return new Mapi($tokens[3], $tokens[4], $user, $passwd, $lang, $tokens[5], $trace);
+  } elsif ($prompt =~ /^\^mapi:merovingian:\/\/proxy/) {
+    # proxied redirect
+    do {
+      print "Being proxied by $host:$port\n" if ($self->{trace});
+      $block = $self->getblock();
+      @challenge = split(/:/, $block);
+      $passchal = pass_chal($passwd, @challenge) || die;
+      $self->putblock("LIT:$user:$passchal:$lang:$db:\n");
+      $prompt = $self->getblock();
+    } while ($prompt =~ /^\^mapi:merovingian:proxy/);
+  } # TODO: don't die on warnings (#)
+  die $prompt if ($prompt ne "");
+  print "Logged on $user\@$db with $lang\n" if ($self->{trace});
+  return $self;
+}
+
+# How to create a duplicate
+sub clone {
+  my ($self,$src)= @_;
+  bless($self,"Mapi");
+  print "cloning\n" if ($self->{trace});
+  $self->{host} = $src->{host};
+  $self->{port} = $src->{port};
+  $self->{user} = $src->{user};
+  $self->{passwd} = $src->{passwd};
+  $self->{lang} = $src->{lang};
+  $self->{db} = $src->{db};
+  $self->{socket} = $src->{socket};
+  $self->resetState();
+}
+
+sub mapiport_intern {
+  my $mapiport = 'localhost:50000';
+  $mapiport = $ENV{'MAPIPORT'} if defined($ENV{'MAPIPORT'});
+  return $mapiport;
+}
+
+sub hostname {
+  my ($hostname) = mapiport_intern() =~ /([^:]*)/;
+  $hostname = 'localhost' if ($hostname eq '');
+  return $hostname;
+}
+
+sub portnr {
+  my ($portnr) = mapiport_intern() =~ /:([^:]*)/;
+  $portnr = 50000 if ($portnr eq '');
+  return $portnr;
+}
+
+sub disconnect {
+  my ($self) = @_;
+  print "disconnect\n" if ($self->{trace});
+  $self->{socket}->close;
+  print "Disconnected from server\n" if ($self->{trace});
+}
+
+sub showState {
+  my ($self) = @_;
+  if ($self->{trace}) {
+    print "mapi.error :".$self->{error}."\n";
+    print "mapi.errstr:".$self->{errstr}."\n";
+    print "mapi.active:".$self->{active}."\n";
+    print "mapi.row[".length($self->{row})."]:".$self->{row}."\n";
+  }
+}
+
+sub resetState {
+  my ($self) = @_;
+  print "resetState\n" if ($self->{trace});
+  $self->{errstr}="";
+  $self->{error}=0;
+  $self->{active}=0;  
+}
+
+#packge the request and ship it, the back-end reads blocks!
+sub doRequest {
+  my($self,$cmd) = @_;
+
+  $cmd = "S" . $cmd if $self->{lang} eq 'sql';
+  # even if the query ends with a ;, this never hurts and fixes the -- on last line bug
+  $cmd = $cmd . "\n;\n";
+  print "doRequest:$cmd\n" if ($self->{trace});
+  $self->putblock($cmd); # TODO handle exceptions || die "!ERROR can't send $cmd: $!";
+  $self->resetState();
+}
+
+# Analyse a single line for errors
+sub error {
+  my ($self,$line) = @_;
+  my $err = $self->{errstr};
+  $err = "$err\n" if (length($err) > 0);
+  $line =~ s/^\!//;
+  $self->{errstr} = $err . $line;
+# $self->showState();
+  $self->{row}= "";
+  $self->{error} = 1;
+  print "Error found $self->{error}\n" if ($self->{trace});
+}
+
+# analyse commentary lines for auxiliary information
+sub propertyTest {
+  my ($self) =@_;
+  my $err= $self->{error};
+  my $row= $self->{row};
+#   $self->showState();
+  if ($row =~ /^\#---/) {
+    $self->{row}= "";
+    return 1;
+  }
+  if ($row =~ /^\#.*\#/) {
+    $self->{row}= "";
+    return 1;
+  }
+  return 0;
+}
+
+
+sub getRow {
+  my ($self)= @_;
+  my $row = $self->{lines}[$self->{next}++];
+  my @chars = split(//, $row,3);
+
+  if ($chars[0] eq '!') { 
+    $self->error($row);
+    my $i = 1;
+    while ($self->{lines}[$i] =~ '!') {
+      $self->error($self->{lines}[$i]);
+      $i++;
+    }
+    $self->{active} = 0;
+    return -1
+  } elsif ($chars[0] eq '&') {
+    # not expected
+  } elsif ($chars[0] eq '%') {
+    # header line
+  } elsif ($chars[0] eq '[') {
+    # row result
+    $self->{row} = $row;
+    if ($self->{nrcols} < 0) {
+      $self->{nrcols} = () = $row =~ /,\t/g;
+      $self->{nrcols}++;
+    }
+    $self->{active} = 1;
+  } elsif ($chars[0] eq '=') {
+    # xml result line
+    $self->{row} = substr($row, 1); # skip = 
+    $self->{active} = 1;
+  } elsif ($chars[0] eq '^') {
+    # ^ redirect, ie use different server
+  } elsif ($chars[0] eq '#') {
+    # warnings etc, skip, and return what follows
+    return $self->getRow;
+  }
+  return $self->{active};
+}
+
+sub getBlock {
+  my ($self)= @_;
+  print "getBlock $self->{active}\n" if ($self->{trace});
+  my $block = $self->getblock();
+  @{$self->{lines}} = split(/\n/, $block);
+
+  # skip diagnostic messages before the header
+  shift @{$self->{lines}} while @{$self->{lines}} && $self->{lines}[0] =~ /\A#/;
+
+  die "implausible return from MonetDB: $self->{lines}[0]\n" if $self->{lines}[0] =~ /\A[^ -~]/;
+
+  my $header = $self->{lines}[0];
+  my @chars = split(//, $header);
+
+  $self->{id} = -1;
+  $self->{nrcols} = -1;
+  $self->{replysize} = scalar(@{$self->{lines}});
+  $self->{active} = 0;
+  $self->{skip} = 0; # next+skip is current result row
+  $self->{next} = 0; # all done
+  $self->{offset} = 0;
+  $self->{hdrs} = [];
+
+  if ($chars[0] eq '&') {
+    if ($chars[1] eq '1' || $chars[1] eq 6) {
+      if ($chars[1] eq '1') {
+        # &1 id result-count nr-cols rows-in-this-block
+        my ($dummy,$id,$cnt,$nrcols,$replysize) = split(' ', $header);
+        $self->{id} = $id;
+        $self->{count} = $cnt;
+        $self->{nrcols} = $nrcols;
+        $self->{replysize} = $replysize;
+      } else {
+        # &6 id nr-cols,rows-in-this-block,offset
+        my ($dummy,$id,$nrcols,$replysize,$offset) = split(' ', $header);
+        $self->{id} = $id;
+        $self->{nrcols} = $nrcols;
+        $self->{replysize} = $replysize;
+        $self->{offset} = $offset;
+      }
+      # for now skip table header information
+      my $i = 1;
+      while ($self->{lines}[$i] =~ /\A%/) {
+        $self->{hdrs}[$i - 1] = $self->{lines}[$i];
+        $i++;
+      }
+      $self->{skip} = $i;
+      $self->{next} = $i;
+      $self->{row} = $self->{lines}[$self->{next}++];
+
+      $self->{active} = 1;
+    } elsif ($chars[1] eq '2') { # updates
+      my ($dummy,$cnt) = split(' ', $header);
+      $self->{count} = $cnt;
+      $self->{nrcols} = 1;
+      $self->{replysize} = 1;
+      $self->{row} = "" . $cnt;
+      $self->{next} = $cnt; # all done
+      return -2;
+    } elsif ($chars[1] eq '3') { # transaction 
+      # nothing todo
+    } elsif ($chars[1] eq '4') { # auto_commit 
+      my ($dummy,$ac) = split(' ', $header);
+      if ($ac eq 't') {
+        $self->{auto_commit} = 1;
+      } else {
+        $self->{auto_commit} = 0;
+      }
+    } elsif ($chars[1] eq '5') { # prepare 
+      my ($dummy,$id,$cnt,$nrcols,$replysize) = split(' ', $header);
+      # TODO parse result, rows (type, digits, scale)
+      $self->{count} = $cnt;
+      $self->{nrcols} = $nrcols;
+      $self->{replysize} = $replysize;
+      $self->{row} = "";
+      $self->{next} = $cnt; # all done
+    }
+  } else {
+    return $self->getRow();
+  } 
+  return $self->{active};
+}
+
+sub getReply {
+  my ($self)= @_;
+
+  if ($self->{active} == 0) {
+    return $self->getBlock();
+  } elsif ($self->{next} < $self->{replysize} + $self->{skip}) {
+    return $self->getRow();
+  } elsif (${self}->{offset} + $self->{replysize} < $self->{count}) {
+    # get next slice
+    my $rs = $self->{replysize};
+    my $offset = $self->{offset} + $rs;
+    $self->putblock("Xexport $self->{id} $offset $rs");
+    return $self->getBlock();
+  } else {
+    # close large results, but only send on next query
+    if ($self->{id} > 0 && $self->{count} != $self->{replysize}) {
+      push @{$self->{piggyback}}, "Xclose $self->{id}";
+      $self->{skip_in}++;
+    }
+    $self->{active} = 0;
+  } 
+  return $self->{active};
+
+}
+
+sub readFromSocket {
+  my ($self, $ref, $count) = @_;
+
+  die "invalid buffer reference" unless (ref($ref) eq 'SCALAR');
+
+  my $rcount = 0;
+  $$ref ||= "";
+
+  while ($count > 0) {
+    $rcount = $self->{socket}->sysread($$ref, $count, length($$ref));
+
+    die "read error: $!" unless (defined($rcount));
+    die "no more data on socket" if ($rcount == 0);
+
+    $count -= $rcount;
+  }
+}
+
+sub getblock {
+  my ($self) = @_;
+
+  # now read back the same way
+  my $result = "";
+  my $last_block = 0;
+  do {
+    my $flag;
+
+    $self->readFromSocket(\$flag, 2); # read block info
+
+    my $unpacked = unpack( 'v', $flag );  # unpack (little endian short)
+    my $len = ( $unpacked >> 1 );    # get length
+    $last_block = $unpacked & 1;    # get last-block-flag
+
+    print "getblock: $last_block $len\n" if ($self->{trace});
+    if ($len > 0 ) {
+      my $data;
+      $self->readFromSocket(\$data, $len); # read
+      $result .= $data;
+      print "getblock: $data\n" if ($self->{trace});
+    }
+  } while ( !$last_block );
+  print "IN:\n$result\n" if $ENV{MAPI_TRACE};
+
+  if ($self->{skip_in}) {
+    $self->{skip_in}--;
+    goto &getblock;
+  }
+
+  return $result;
+}
+
+sub putblock {
+  my $self = shift;
+
+  # there maybe something in the piggyback buffer
+  my @blocks = (\(@{ $self->{piggyback} }), \(@_));
+  @{ $self->{piggyback} } = ();
+
+  # create blocks of data with max 0xffff length,
+  # then loop over the data and send it.
+  my $out = '';
+  for my $blk (@blocks) {
+    print "OUT:\n$$blk\n" if $ENV{MAPI_TRACE};
+    utf8::downgrade($$blk); # deny wide chars
+    my $pos        = 0;
+    my $last_block = 0;
+    my $blocksize  = 0x7fff >> 1;       # max len per block
+    my $data;
+
+    while ( !$last_block ) {
+      my $data = substr($$blk, 0, $blocksize, "");
+      my $len = length($data);
+      # set last-block-flag
+      $last_block = 1 if !length $$blk;
+      my $flag = pack( 'v', ( $len << 1 ) + $last_block );
+      print "putblock: $last_block ".$data."\n" if ($self->{trace});
+      $out .= $flag . $data;
+    }
+  }
+  $self->{socket}->syswrite($out); #send it
+}
+
+1;
+
+# vim: set ts=2 sw=2 expandtab: