sqlmap/extra/dbwire/clickhouse.py
Miroslav Štampar a61e35980a
Some checks are pending
/ build (macos-latest, 3.8) (push) Waiting to run
/ build (ubuntu-latest, pypy-2.7) (push) Waiting to run
/ build (windows-latest, 3.14) (push) Waiting to run
Bug fixes for dbwire
2026-07-12 11:36:35 +02:00

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