Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Changes for crash
Unreleased
==========

- Honor ``?verify_ssl=<bool>`` in the host URLs to toggle TLS certificate verification. The CLI ``--verify-ssl`` flag overrides the URL param. Closes #482.

2026/02/09 0.32.0
=================

Expand Down
16 changes: 16 additions & 0 deletions docs/run.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ The ``crash`` executable supports multiple command-line options:
| | ``<HOSTS>`` can be a single host, or it can |
| | be a space separated list of hosts. |
| | |
| | Each host may also be given as a full URL, |
| | e.g. |
| | ``https://user:pw@node:4200/?verify_ssl=0``. |
| | Userinfo is used for authentication and the |
| | ``verify_ssl`` query parameter toggles TLS |
| | verification. Explicit CLI flags |
| | (``--username``, ``--verify-ssl``, …) |
| | always override values taken from the URL. |
| | |
| | If multiple hosts are specified, Crash will |
| | attempt to connect to all of them. The |
| | command will succeed if at least one |
Expand Down Expand Up @@ -153,6 +162,13 @@ Here, we're using:
- ``~/.certs/client.key`` as the client certificate key
- ``~/.certs/server-ca.crt`` as the server CA certificate

The same connection settings can also be given inline via the host URL:

.. code-block:: console

sh$ crash --hosts "https://crate:secret@node1.example.com:4200/?verify_ssl=false" \
-c "SELECT * FROM sys.nodes"

.. _user-conf-dir:

User configuration directory
Expand Down
47 changes: 44 additions & 3 deletions src/crate/crash/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from getpass import getpass
from operator import itemgetter
from typing import Optional, Union
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse

import sqlparse
import urllib3
Expand Down Expand Up @@ -147,8 +148,10 @@ def _conf_or_default(key, value):
'--verify-ssl',
choices=(True, False),
type=boolean,
default=True,
help='Enable or disable the verification of the server SSL certificate'
default=None,
help='Enable or disable the verification of the server SSL certificate '
'(default: enabled). Can also be set as a `verify_ssl` query '
'parameter on a host URL, with this cli option having priority over the URL params.'
)
parser.add_argument('--cert-file', type=file_with_permissions, metavar='FILENAME',
help='use FILENAME as the client certificate file')
Expand Down Expand Up @@ -563,6 +566,35 @@ def host_and_port(host_or_port):
return host_or_port + ':4200'


def extract_url_params(host):
# lift recognized connection params (verify_ssl) off one host url;
# return (cleaned_host, params). non-http urls pass through untouched.
parsed = urlparse(host)
if parsed.scheme not in ('http', 'https') or not parsed.query:
return host, {}

lifted = {}
kept = []
for key, value in parse_qsl(parsed.query, keep_blank_values=True):
if key == 'verify_ssl':
lifted[key] = boolean(value)
else:
kept.append((key, value))
return urlunparse(parsed._replace(query=urlencode(kept))), lifted


def collect_url_params(hosts):
# fold extract_url_params over a host list; first occurrence wins.
cleaned = []
params = {}
for host in hosts:
c, lifted = extract_url_params(host)
cleaned.append(c)
for key, value in lifted.items():
params.setdefault(key, value)
return cleaned, params


def get_information_schema_query(lowest_server_version):
schema_name = \
"table_schema" if lowest_server_version >= \
Expand Down Expand Up @@ -614,7 +646,16 @@ def main():
printer.info(crash_version)
sys.exit(0)

crate_hosts = [host_and_port(h) for h in args.hosts]
try:
cleaned_hosts, url_params = collect_url_params(args.hosts)
except ArgumentTypeError as e:
printer.warn(str(e))
sys.exit(1)
crate_hosts = [host_and_port(h) for h in cleaned_hosts]

if args.verify_ssl is None:
args.verify_ssl = url_params.get('verify_ssl', True)

error_trace = args.verbose > 0

password = _resolve_password(is_tty, args.force_passwd_prompt)
Expand Down
50 changes: 50 additions & 0 deletions tests/test_command.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# vim: set fileencodings=utf-8

from argparse import ArgumentTypeError
from unittest import TestCase

from verlib2 import Version
Expand All @@ -9,7 +10,10 @@
Result,
_decode_timeout,
_decode_timeouts,
collect_url_params,
extract_url_params,
get_information_schema_query,
get_parser,
host_and_port,
stmt_type,
)
Expand Down Expand Up @@ -103,6 +107,52 @@ def test_short_hostnames(self):
self.assertEqual(host_and_port(':'), 'localhost:4200')


class UrlParamsTest(TestCase):

def test_extract_url_params(self):
for value, expected in [
('false', False), ('False', False), ('0', False), ('no', False),
('true', True), ('1', True), ('yes', True),
]:
cleaned, params = extract_url_params(
f'https://u:p@h:4200/?foo=bar&verify_ssl={value}')
self.assertEqual(cleaned, 'https://u:p@h:4200/?foo=bar')
self.assertEqual(params, {'verify_ssl': expected})

for host in ('localhost:4200', ':4200', ':', 'localhost',
'https://h:4200/', 'postgresql://h/?verify_ssl=false',
'localhost:4200?foo=bar'):
self.assertEqual(extract_url_params(host), (host, {}))

with self.assertRaises(ArgumentTypeError):
extract_url_params('https://h/?verify_ssl=maybe')

def test_first_host_wins(self):
_, params = collect_url_params([
'https://a/?verify_ssl=false',
'https://b/?verify_ssl=true',
])
self.assertIs(params['verify_ssl'], False)

def test_verify_ssl_precedence(self):
# --verify-ssl > url > default True.
def resolve(argv):
args = get_parser().parse_args(argv)
_, url_params = collect_url_params(args.hosts)
return url_params.get('verify_ssl', True) \
if args.verify_ssl is None else args.verify_ssl

self.assertIs(resolve(['--hosts', 'h:4200']), True)
self.assertIs(
resolve(['--hosts', 'https://h/?verify_ssl=false']), False)
self.assertIs(
resolve(['--hosts', 'https://h/?verify_ssl=false',
'--verify-ssl', 'true']), True)
self.assertIs(
resolve(['--hosts', 'https://h/?verify_ssl=true',
'--verify-ssl', 'false']), False)


class CommandUtilsTest(TestCase):

def test_stmt_type(self):
Expand Down
Loading