diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 293a32df..2faf4516 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,7 @@ This project adheres to `Semantic Versioning `_. `2.1.0-dev`_ (unreleased) ------------------------- +* Add ``--path-as-is`` to bypass dot segment (``/../`` or ``/./``) URL squashing. * Fixed ``--form`` file upload mixed with redirected ``stdin`` error handling. diff --git a/httpie/cli/definition.py b/httpie/cli/definition.py index 61ed3d3b..43fb5348 100644 --- a/httpie/cli/definition.py +++ b/httpie/cli/definition.py @@ -552,6 +552,15 @@ network.add_argument( """ ) +network.add_argument( + '--path-as-is', + default=False, + action='store_true', + help=""" + Bypass dot segment (/../ or /./) URL squashing. + + """ +) ####################################################################### # SSL diff --git a/httpie/client.py b/httpie/client.py index 431bee4f..6c412e73 100644 --- a/httpie/client.py +++ b/httpie/client.py @@ -6,6 +6,7 @@ import zlib from contextlib import contextmanager from pathlib import Path from typing import Iterable, Union +from urllib.parse import urlparse, urlunparse import requests from requests.adapters import HTTPAdapter @@ -77,6 +78,11 @@ def collect_messages( request = requests.Request(**request_kwargs) prepared_request = requests_session.prepare_request(request) + if args.path_as_is: + prepared_request.url = ensure_path_as_is( + orig_url=args.url, + prepped_url=prepared_request.url, + ) if args.compress and prepared_request.body: compress_body(prepared_request, always=args.compress > 1) response_count = 0 @@ -278,3 +284,30 @@ def make_request_kwargs( } return kwargs + + +def ensure_path_as_is(orig_url: str, prepped_url: str) -> str: + """ + Handle `--path-as-is` by replacing the path component of the prepared + URL with the path component from the original URL. Other parts stay + untouched because other (welcome) processing on the URL might have + taken place. + + + + + + + + >>> ensure_path_as_is('http://foo/../', 'http://foo/?foo=bar') + 'http://foo/../?foo=bar' + + """ + parsed_orig, parsed_prepped = urlparse(orig_url), urlparse(prepped_url) + final_dict = { + # noinspection PyProtectedMember + **parsed_prepped._asdict(), + 'path': parsed_orig.path, + } + final_url = urlunparse(tuple(final_dict.values())) + return final_url diff --git a/tests/test_httpie.py b/tests/test_httpie.py index 70bc2f9d..66caba52 100644 --- a/tests/test_httpie.py +++ b/tests/test_httpie.py @@ -55,6 +55,25 @@ def test_GET(httpbin_both): assert HTTP_OK in r +def test_path_dot_normalization(): + r = http( + '--offline', + 'example.org/../../etc/password', + 'param==value' + ) + assert 'GET /etc/password?param=value' in r + + +def test_path_as_is(): + r = http( + '--offline', + '--path-as-is', + 'example.org/../../etc/password', + 'param==value' + ) + assert 'GET /../../etc/password?param=value' in r + + def test_DELETE(httpbin_both): r = http('DELETE', httpbin_both + '/delete') assert HTTP_OK in r