mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-08-04 14:55:40 +00:00
139 lines
5.1 KiB
Python
139 lines
5.1 KiB
Python
#!/usr/bin/env python
|
|
|
|
"""
|
|
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
|
See the file 'LICENSE' for copying permission
|
|
"""
|
|
|
|
"""
|
|
Minimal pure-python ClickHouse client over its native HTTP interface (stdlib only, no clickhouse_connect).
|
|
|
|
ClickHouse exposes an HTTP endpoint that runs a query in the request body and streams the result back in a
|
|
chosen format; we use TabSeparatedWithNames (first line = column names, then tab-separated rows with
|
|
backslash escaping and \\N for NULL). Covers ClickHouse and its HTTP-compatible forks.
|
|
"""
|
|
|
|
import base64
|
|
import socket
|
|
|
|
try:
|
|
from urllib.request import Request, urlopen # Python 3
|
|
from urllib.error import HTTPError, URLError
|
|
except ImportError:
|
|
from urllib2 import Request, urlopen, HTTPError, URLError # Python 2
|
|
|
|
from extra.dbwire import OperationalError
|
|
from extra.dbwire import ProgrammingError
|
|
|
|
# TabSeparated backslash escapes -> the literal byte they denote
|
|
_ESCAPE = {ord("t"): 9, ord("n"): 10, ord("r"): 13, ord("0"): 0, ord("b"): 8,
|
|
ord("f"): 12, ord("a"): 7, ord("v"): 11, ord("\\"): 92, ord("'"): 39}
|
|
|
|
def _unescape(value):
|
|
# value: the raw bytes of one TSV field -> None (\N) or the unescaped bytes. Operates on bytes because a
|
|
# String/FixedString column can hold arbitrary non-UTF-8 data, which a whole-body utf-8 decode would destroy.
|
|
if value == b"\\N":
|
|
return None
|
|
if b"\\" not in value:
|
|
return value
|
|
src, out, i, n = bytearray(value), bytearray(), 0, len(value)
|
|
while i < n:
|
|
c = src[i]
|
|
if c == 0x5c and i + 1 < n: # backslash
|
|
out.append(_ESCAPE.get(src[i + 1], src[i + 1])); i += 2
|
|
else:
|
|
out.append(c); i += 1
|
|
return bytes(out)
|
|
|
|
def _decode_cell(value):
|
|
# keep text as str; hand back raw bytes only when a value is not valid UTF-8 (sqlmap then hex-encodes it)
|
|
if value is None:
|
|
return None
|
|
try:
|
|
return value.decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
return value
|
|
|
|
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 ProgrammingError("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, host, port, user, password, database, timeout):
|
|
self._url = "http://%s:%d/?database=%s&default_format=TabSeparatedWithNames" % (host, port, database or "default")
|
|
self._headers = {}
|
|
if user or password:
|
|
token = base64.b64encode(("%s:%s" % (user or "", password or "")).encode("utf-8")).decode("ascii")
|
|
self._headers["Authorization"] = "Basic %s" % token
|
|
self._timeout = timeout
|
|
|
|
def cursor(self):
|
|
return Cursor(self)
|
|
|
|
def commit(self):
|
|
pass # ClickHouse statements are executed immediately (no client-side transaction)
|
|
|
|
def rollback(self):
|
|
pass
|
|
|
|
def close(self):
|
|
pass # HTTP is stateless
|
|
|
|
def _query(self, query):
|
|
req = Request(self._url, data=query.encode("utf-8"), headers=self._headers)
|
|
try:
|
|
body = urlopen(req, timeout=self._timeout).read() # bytes: column data may be non-UTF-8
|
|
except HTTPError as ex:
|
|
raise ProgrammingError("(remote) %s" % ex.read().decode("utf-8", "replace").strip())
|
|
except URLError as ex:
|
|
raise OperationalError("(remote) %s" % ex)
|
|
except (socket.timeout, socket.error) as ex:
|
|
raise OperationalError("(remote) %s" % ex)
|
|
|
|
if not body:
|
|
return None, []
|
|
lines = body.split(b"\n")
|
|
if lines and lines[-1] == b"":
|
|
lines.pop()
|
|
if not lines:
|
|
return None, []
|
|
description = [(name, None, None, None, None, None, None) for name in (_decode_cell(_unescape(_)) for _ in lines[0].split(b"\t"))]
|
|
rows = [tuple(_decode_cell(_unescape(_)) for _ in line.split(b"\t")) for line in lines[1:]]
|
|
return description, rows
|
|
|
|
def connect(host=None, port=8123, user=None, password=None, database=None, connect_timeout=None, **kwargs):
|
|
connection = Connection(host or "localhost", int(port or 8123), user, password, database, connect_timeout)
|
|
try:
|
|
connection._query("SELECT 1") # verify connectivity/credentials up front
|
|
except ProgrammingError:
|
|
raise
|
|
except Exception as ex:
|
|
raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex))
|
|
return connection
|