#!/usr/bin/env python """ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ """ Minimal pure-python MonetDB MAPI client (stdlib only, no pymonetdb). MAPI is a block-framed text protocol: a 2-byte little-endian header (length << 1 | last-flag) wraps each block; login is a colon-separated challenge/response with a chosen password hash; queries are sent with an 's' prefix and results come back as '&' headers, '%' metadata and '[' tuples. Paging is disabled up front (Xreply_size -1) so a whole result set arrives at once. """ import hashlib import re import socket import struct from extra.dbwire import InterfaceError from extra.dbwire import NotSupportedError from extra.dbwire import OperationalError from extra.dbwire import ProgrammingError _MAX_BLOCK = 0xffff >> 1 def _recvn(sock, n): buf = b"" while len(buf) < n: chunk = sock.recv(n - len(buf)) if not chunk: raise InterfaceError("connection closed by server") buf += chunk return buf def _getblock(sock): out = b"" while True: (header,) = struct.unpack("> 1, header & 1 out += _recvn(sock, length) if last: break return out.decode("utf-8", "replace") def _putblock(sock, text): data = text.encode("utf-8") off = 0 while True: chunk = data[off:off + _MAX_BLOCK] off += _MAX_BLOCK last = off >= len(data) sock.sendall(struct.pack("= 2 and value[0] == '"' and value[-1] == '"': body = value[1:-1] if "\\" not in body: return body # MonetDB renders control bytes as C octal escapes (\ooo) etc.; decode with unicode_escape but only # on the ASCII runs so raw multibyte (> 0x7f) is preserved (mirrors pymonetdb's result decoding) return "".join(seg.encode("utf-8").decode("unicode_escape") if "\\" in seg else seg for seg in re.split(r"([\x00-\x7f]+)", body)) return value def _parse_result(text): description, rows = None, [] for line in text.split("\n"): if not line: continue marker = line[0] if marker == "!": # error raise ProgrammingError("(remote) %s" % line[1:].strip()) elif marker == "%": # metadata: " # " payload, _, kind = line[1:].rpartition("#") if kind.strip() == "name": description = [(name.strip(), None, None, None, None, None, None) for name in payload.split(",\t")] elif marker == "[": # tuple: "[ v1,\tv2,\t... ]" body = line.strip() if body.startswith("[") and body.endswith("]"): body = body[1:-1].strip() rows.append(tuple(_unquote(v.strip()) for v in body.split(",\t"))) # "&" result headers, "#" info, "=" no-slice tuples are ignored for our purposes return description, rows class Cursor(object): def __init__(self, connection): self.connection = connection self.description = None self.rowcount = -1 self._rows = [] self._pos = 0 def execute(self, query, params=None): if params is not None: raise NotSupportedError("parameter binding is not supported; pass a fully-formed query string") self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0 self.description, self._rows = self.connection._query(query) self.rowcount = len(self._rows) return self def fetchall(self): retVal = self._rows[self._pos:] self._pos = len(self._rows) return retVal def fetchone(self): if self._pos >= len(self._rows): return None retVal = self._rows[self._pos] self._pos += 1 return retVal def close(self): self._rows = [] class Connection(object): def __init__(self, sock): self._sock = sock def cursor(self): return Cursor(self) def commit(self): pass # sqlmap runs autonomous statements; MonetDB SQL is auto-committed unless a transaction is opened def rollback(self): pass def close(self): try: self._sock.close() except Exception: pass def _command(self, text): _putblock(self._sock, text) reply = _getblock(self._sock) if reply.startswith("!"): raise OperationalError("(remote) %s" % reply[1:].strip()) def _query(self, query): try: _putblock(self._sock, "s" + query + ";\n") return _parse_result(_getblock(self._sock)) except (socket.error, socket.timeout) as ex: raise OperationalError("connection error: %s" % ex) except (struct.error, IndexError, ValueError) as ex: raise InterfaceError("malformed server response: %s" % ex) def connect(host=None, port=50000, user=None, password=None, database=None, connect_timeout=None, **kwargs): host, port = host or "localhost", int(port or 50000) try: sock = socket.create_connection((host, port), timeout=connect_timeout) sock.settimeout(None) except (socket.error, socket.timeout) as ex: raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex)) try: for _ in range(10): # bounded: merovingian proxy stage + optional redirect + mserver challenge block = _getblock(sock) if block == "": # login accepted break if block[0] == "^": # redirect url = block[1:].strip() m = re.match(r"mapi:monetdb://([^:/]+):(\d+)/(\S*)", url) if m and (m.group(1) != host or int(m.group(2)) != port): sock.close() host, port, database = m.group(1), int(m.group(2)), m.group(3) or database sock = socket.create_connection((host, port), timeout=connect_timeout) sock.settimeout(None) continue # merovingian proxy redirect: keep reading the next challenge on this socket if block[0] == "!": raise OperationalError("(remote) %s" % block[1:].strip()) _putblock(sock, _challenge_response(block, user, password, database)) else: raise OperationalError("MonetDB login did not converge") except (OperationalError, NotSupportedError, InterfaceError): try: sock.close() except Exception: pass raise except (socket.error, socket.timeout) as ex: # I/O error during the login/redirect exchange try: sock.close() except Exception: pass raise OperationalError("connection error: %s" % ex) connection = Connection(sock) try: connection._command("Xreply_size -1\n") # disable row paging so a whole result set is returned at once except (socket.error, socket.timeout) as ex: connection.close() raise OperationalError("connection error: %s" % ex) return connection