Skip to content

Commit b157ce0

Browse files
committed
Add --insecure option to the CLI.
Fix #1675..
1 parent 32870aa commit b157ce0

3 files changed

Lines changed: 45 additions & 8 deletions

File tree

docs/project/changelog.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ New features
5656

5757
* Validated compatibility with Python 3.15.
5858

59+
* Added the ``--insecure`` option to the ``websockets`` CLI to disable TLS
60+
certificate validation.
61+
5962
.. _16.1:
6063

6164
16.1
@@ -75,7 +78,7 @@ Improvements
7578
* Stripped ``Authorization``, ``Cookie``, and ``Proxy-Authorization`` headers
7679
when clients follow cross-origin redirects, a security hardening measure.
7780

78-
* Escaped control characters in incoming messages in ``python -m websockets``.
81+
* Escaped control characters in incoming messages in the ``websockets`` CLI.
7982

8083
* Blocked non-ASCII values in :class:`~datastructures.Headers`.
8184

@@ -135,6 +138,11 @@ Bug fixes
135138

136139
*March 5, 2025*
137140

141+
Improvements
142+
............
143+
144+
* Added an entry point for running the CLI as ``websockets``.
145+
138146
Bug fixes
139147
.........
140148

src/websockets/cli.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
import asyncio
55
import itertools
66
import os
7+
import ssl
78
import sys
8-
from typing import Generator
9+
from typing import Any, Generator
910

1011
from .asyncio.client import ClientConnection, connect
1112
from .asyncio.messages import SimpleQueue
@@ -123,9 +124,9 @@ async def send_outgoing_messages(
123124
break
124125

125126

126-
async def interactive_client(uri: str) -> None:
127+
async def interactive_client(uri: str, **kwargs: Any) -> None:
127128
try:
128-
websocket = await connect(uri)
129+
websocket = await connect(uri, **kwargs)
129130
except Exception as exc:
130131
print(f"Failed to connect to {uri}: {exc}.")
131132
sys.exit(1)
@@ -175,6 +176,11 @@ def main(argv: list[str] | None = None) -> None:
175176
action="store_true",
176177
help="show usage and exit",
177178
)
179+
parser.add_argument(
180+
"--insecure",
181+
action="store_true",
182+
help="disable TLS certificate verification",
183+
)
178184
parser.add_argument(
179185
"--version",
180186
action="store_true",
@@ -209,7 +215,13 @@ def main(argv: list[str] | None = None) -> None:
209215
except ImportError: # readline isn't available on all platforms
210216
pass
211217

218+
kwargs = {}
219+
if args.insecure and args.uri.startswith("wss://"):
220+
# This isn't a public API but it's mentioned in the changelog:
221+
# https://docs.python.org/3/whatsnew/3.4.html#changed-in-3-4-3
222+
kwargs["ssl"] = ssl._create_unverified_context()
223+
212224
try:
213-
asyncio.run(interactive_client(args.uri))
225+
asyncio.run(interactive_client(args.uri, **kwargs))
214226
except KeyboardInterrupt: # pragma: no cover
215227
pass

tests/test_cli.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# Run a test server in a thread. This is easier than running an asyncio server
1212
# because we would have to run main() in a thread, due to using asyncio.run().
1313
from .sync.server import get_uri, run_server
14+
from .utils import SERVER_CONTEXT
1415

1516

1617
vt100_commands = re.compile(r"\x1b\[[A-Z]|\x1b[78]|\r")
@@ -24,6 +25,10 @@ def add_connection_messages(output, server_uri):
2425
return f"Connected to {server_uri}.\n{output}Connection closed: 1000 (OK).\n"
2526

2627

28+
def echo_handler(websocket):
29+
websocket.send(websocket.recv())
30+
31+
2732
class CLITests(unittest.TestCase):
2833
def run_main(self, argv, inputs="", close_input=False, expected_exit_code=None):
2934
# Replace sys.stdin with a file-like object backed by a file descriptor
@@ -93,9 +98,6 @@ def handler(websocket):
9398
)
9499

95100
def test_send_message(self):
96-
def echo_handler(websocket):
97-
websocket.send(websocket.recv())
98-
99101
with run_server(echo_handler) as server:
100102
server_uri = get_uri(server)
101103
output = self.run_main([server_uri], "hello\n")
@@ -117,6 +119,21 @@ def wait_handler(websocket):
117119
add_connection_messages("", server_uri),
118120
)
119121

122+
def test_insecure_connection_rejected(self):
123+
with run_server(echo_handler, ssl=SERVER_CONTEXT) as server:
124+
server_uri = get_uri(server, secure=True)
125+
output = self.run_main([server_uri], "hello\n", expected_exit_code=1)
126+
self.assertTrue(output.startswith(f"Failed to connect to {server_uri}: "))
127+
128+
def test_insecure_connection_accepted(self):
129+
with run_server(echo_handler, ssl=SERVER_CONTEXT) as server:
130+
server_uri = get_uri(server, secure=True)
131+
output = self.run_main([server_uri, "--insecure"], "hello\n")
132+
self.assertEqual(
133+
remove_commands_and_prompts(output),
134+
add_connection_messages("\n< hello\n", server_uri),
135+
)
136+
120137
def test_connection_failure(self):
121138
output = self.run_main(["ws://localhost:54321"], expected_exit_code=1)
122139
self.assertTrue(

0 commit comments

Comments
 (0)