Skip to content

Commit

Permalink
Move port and interface validation to the CLI layer
Browse files Browse the repository at this point in the history
  • Loading branch information
jkbrzt committed Oct 26, 2024
1 parent 0eab08a commit 4cea2e8
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 158 deletions.
13 changes: 11 additions & 2 deletions httpie/cli/argtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ def parse_format_options(s: str, defaults: Optional[dict]) -> dict:
)


def response_charset_type(encoding: str) -> str:
def response_charset_arg_type(encoding: str) -> str:
try:
''.encode(encoding)
except LookupError:
Expand All @@ -268,8 +268,17 @@ def response_charset_type(encoding: str) -> str:
return encoding


def response_mime_type(mime_type: str) -> str:
def response_mime_arg_type(mime_type: str) -> str:
if mime_type.count('/') != 1:
raise argparse.ArgumentTypeError(
f'{mime_type!r} doesn’t look like a mime type; use type/subtype')
return mime_type


def interface_arg_type(interface: str) -> str:
import ipaddress
try:
ipaddress.ip_interface(interface)
except ValueError as e:
raise argparse.ArgumentTypeError(str(e))
return interface
60 changes: 41 additions & 19 deletions httpie/cli/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,50 @@
from argparse import FileType

from httpie import __doc__, __version__
from httpie.cli.argtypes import (KeyValueArgType, SessionNameValidator,
SSLCredentials, readable_file_arg,
response_charset_type, response_mime_type)
from httpie.cli.constants import (BASE_OUTPUT_OPTIONS, DEFAULT_FORMAT_OPTIONS,
OUT_REQ_BODY, OUT_REQ_HEAD, OUT_RESP_BODY,
OUT_RESP_HEAD, OUT_RESP_META, OUTPUT_OPTIONS,
OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP,
PRETTY_STDOUT_TTY_ONLY,
SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY,
SORTED_FORMAT_OPTIONS_STRING,
UNSORTED_FORMAT_OPTIONS_STRING, RequestType)
from httpie.cli.options import ParserSpec, Qualifiers, to_argparse
from httpie.output.formatters.colors import (AUTO_STYLE, DEFAULT_STYLE, BUNDLED_STYLES,
get_available_styles)
from httpie.output.formatters.colors import (
AUTO_STYLE,
BUNDLED_STYLES,
DEFAULT_STYLE,
get_available_styles,
)
from httpie.plugins.builtin import BuiltinAuthPlugin
from httpie.plugins.registry import plugin_manager
from httpie.ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS_STRING
from .argtypes import (
KeyValueArgType,
SSLCredentials,
SessionNameValidator,
interface_arg_type,
readable_file_arg,
response_charset_arg_type,
response_mime_arg_type,
)
from .constants import (
BASE_OUTPUT_OPTIONS,
DEFAULT_FORMAT_OPTIONS,
OUTPUT_OPTIONS,
OUTPUT_OPTIONS_DEFAULT,
OUT_REQ_BODY,
OUT_REQ_HEAD,
OUT_RESP_BODY,
OUT_RESP_HEAD,
OUT_RESP_META,
PRETTY_MAP,
PRETTY_STDOUT_TTY_ONLY,
RequestType,
SEPARATOR_GROUP_ALL_ITEMS,
SEPARATOR_PROXY,
SORTED_FORMAT_OPTIONS_STRING,
UNSORTED_FORMAT_OPTIONS_STRING,
)
from .options import ParserSpec, Qualifiers, to_argparse
from .ports import local_port_arg_type


# Man pages are static (built when making a release).
# We use this check to not include generated, system-specific information there (e.g., default --ciphers).
IS_MAN_PAGE = bool(os.environ.get('HTTPIE_BUILDING_MAN_PAGES'))


options = ParserSpec(
'http',
description=f'{__doc__.strip()} <https://httpie.io>',
Expand Down Expand Up @@ -349,7 +369,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False):
output_processing.add_argument(
'--response-charset',
metavar='ENCODING',
type=response_charset_type,
type=response_charset_arg_type,
short_help='Override the response encoding for terminal display purposes.',
help="""
Override the response encoding for terminal display purposes, e.g.:
Expand All @@ -362,7 +382,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False):
output_processing.add_argument(
'--response-mime',
metavar='MIME_TYPE',
type=response_mime_type,
type=response_mime_arg_type,
short_help='Override the response mime type for coloring and formatting for the terminal.',
help="""
Override the response mime type for coloring and formatting for the terminal, e.g.:
Expand Down Expand Up @@ -894,12 +914,14 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False):
)
network.add_argument(
"--interface",
default=None,
type=interface_arg_type,
default='0.0.0.0',
short_help="Bind to a specific network interface.",
)
network.add_argument(
"--local-port",
default=None,
type=local_port_arg_type,
default=0,
short_help="Set the local port to be used for the outgoing request.",
help="""
It can be either a port range (e.g. "11221-14555") or a single port.
Expand Down
50 changes: 50 additions & 0 deletions httpie/cli/ports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import argparse
from random import randint
from typing import Tuple


MIN_PORT = 0
MAX_PORT = 65535
OUTSIDE_VALID_PORT_RANGE_ERROR = f'outside valid port range {MIN_PORT}-{MAX_PORT}'


def local_port_arg_type(port: str) -> int:
port = parse_local_port_arg(port)
if isinstance(port, tuple):
port = randint(*port)
return port


def parse_local_port_arg(port: str) -> int | Tuple[int, int]:
if '-' in port[1:]: # Don’t treat negative port as range.
return _clean_port_range(port)
return _clean_port(port)


def _clean_port_range(port_range: str) -> Tuple[int, int]:
"""
We allow two digits separated by a hyphen to represent a port range.
The parsing is done so that even negative numbers get parsed correctly, allowing us to
give a more specific outside-range error message.
"""
sep_pos = port_range.find('-', 1)
start, end = port_range[:sep_pos], port_range[sep_pos + 1:]
start = _clean_port(start)
end = _clean_port(end)
if start > end:
raise argparse.ArgumentTypeError(f'{port_range!r} is not a valid port range')
return start, end


def _clean_port(port: str) -> int:
try:
port = int(port)
except ValueError:
raise argparse.ArgumentTypeError(f'{port!r} is not a number')
if not (MIN_PORT <= port <= MAX_PORT):
raise argparse.ArgumentTypeError(
f'{port!r} is {OUTSIDE_VALID_PORT_RANGE_ERROR}'
)
return port
43 changes: 2 additions & 41 deletions httpie/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import sys
import typing
from pathlib import Path
from random import randint
from time import monotonic
from typing import Any, Dict, Callable, Iterable
from urllib.parse import urlparse, urlunparse
Expand Down Expand Up @@ -68,48 +67,10 @@ def collect_messages(
)
send_kwargs = make_send_kwargs(args)
send_kwargs_mergeable_from_env = make_send_kwargs_mergeable_from_env(args)

source_address = None

if args.interface:
# automatically raises ValueError upon invalid IP
ipaddress.ip_address(args.interface)

source_address = (args.interface, 0)
if args.local_port:

if '-' not in args.local_port:
try:
parsed_port = int(args.local_port)
except ValueError:
raise ValueError(f'"{args.local_port}" is not a valid port number.')

source_address = (args.interface or "0.0.0.0", parsed_port)
else:
if args.local_port.count('-') != 1:
raise ValueError(f'"{args.local_port}" is not a valid port range. i.e. we accept value like "25441-65540".')

try:
min_port, max_port = args.local_port.split('-', 1)
except ValueError:
raise ValueError(f'The port range you gave in input "{args.local_port}" is not a valid range.')

if min_port == "":
raise ValueError("Negative port number are all invalid values.")
if max_port == "":
raise ValueError('Port range requires both start and end ports to be specified. e.g. "25441-65540".')

try:
min_port, max_port = int(min_port), int(max_port)
except ValueError:
raise ValueError(f'Either "{min_port}" or/and "{max_port}" is an invalid port number.')

source_address = (args.interface or "0.0.0.0", randint(int(min_port), int(max_port)))

parsed_url = parse_url(args.url)
resolver = args.resolver or None

# we want to make sure every ".localhost" host resolve to loopback
# We want to make sure every ".localhost" host resolve to loopback
if parsed_url.host and parsed_url.host.endswith(".localhost"):
ensure_resolver = f"in-memory://default/?hosts={parsed_url.host}:127.0.0.1&hosts={parsed_url.host}:[::1]"

Expand Down Expand Up @@ -157,7 +118,7 @@ def collect_messages(
resolver=resolver,
disable_ipv6=args.ipv4,
disable_ipv4=args.ipv6,
source_address=source_address,
source_address=(args.interface, args.local_port),
quic_cache=env.config.quic_file,
happy_eyeballs=args.happy_eyeballs,
)
Expand Down
6 changes: 3 additions & 3 deletions tests/test_binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ def test_binary_file_form(self, httpbin):


class TestBinaryResponseData:
"""local httpbin crash due to an unfixed bug.
See https://github.com/psf/httpbin/pull/41
It is merged but not yet released."""
# Local httpbin crashes due to an unfixed bug — it is merged but not yet released.
# <https://github.com/psf/httpbin/pull/41>
# TODO: switch to the local `httpbin` fixture when the fix is released.

def test_binary_suppresses_when_terminal(self, remote_httpbin):
r = http('GET', remote_httpbin + '/bytes/1024?seed=1')
Expand Down
Loading

0 comments on commit 4cea2e8

Please sign in to comment.