Compare commits

...

24 Commits
1.0.0 ... 1.0.3

Author SHA1 Message Date
747be30d2e 1.0.3 2019-08-26 12:42:34 +02:00
88a9583f4c Update CHANGELOG.rst 2019-07-20 13:03:30 +02:00
fd6e87914c README 2019-06-24 12:36:08 +02:00
6dee49357d Fix comments 2019-06-24 12:29:42 +02:00
df36d6255d Changed the way the output filename is generated
When ``--download`` without ``--output`` results in a redirect,
now only the initial URL is considered, not the final one.
2019-06-24 12:20:09 +02:00
e92b831e6e Create FUNDING.yml 2019-06-23 12:05:24 +02:00
fd44f1af93 Updated Readme to fix a typo (#767) 2019-04-10 13:21:37 +02:00
b6309547d5 Add a bash here string example 2019-03-11 08:41:24 +01:00
3a46149de1 Fix several ResourceWarning: unclosed file (#741)
Signed-off-by: Mickaël Schoentgen <contact@tiger-222.fr>
2019-02-04 10:00:30 +01:00
b7c8bf0800 Add animation by @loranallensmith 2019-02-03 15:27:17 +01:00
69d010a11b Brew cleanup 2019-02-03 15:08:29 +01:00
42ff243400 Add make brew-test 2019-02-03 14:58:23 +01:00
933b438e5f Bump dependency versions #742 2019-02-03 14:26:05 +01:00
358342d1c9 Update LICENSE 2019-01-09 12:30:44 +01:00
c591a3810d 1.0.3-dev 2018-11-14 16:36:47 +01:00
0eba037037 v1.0.2
Close #729
2018-11-14 16:36:19 +01:00
3898129e9c Changelog 2018-11-14 16:22:00 +01:00
b88e88d2e3 Fix tests for installation with pyOpenSSL #729 2018-11-14 16:10:08 +01:00
d1407baf76 Add make pdf 2018-11-14 13:06:10 +01:00
d5032ca859 Fix changelog 2018-11-14 11:45:57 +01:00
f6a19cf552 Don't call external URLs from tests #729 2018-11-14 11:42:59 +01:00
74979f3b33 Brew 2018-11-06 11:37:33 +01:00
698eb51e60 Update screenshot 2018-11-03 18:08:43 +01:00
ae8030c930 Homebrew formula for v1.0.0 2018-11-02 17:18:04 +01:00
18 changed files with 302 additions and 131 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: https://paypal.me/roztocil

View File

@ -6,6 +6,46 @@ This document records all notable changes to `HTTPie <http://httpie.org>`_.
This project adheres to `Semantic Versioning <http://semver.org/>`_. This project adheres to `Semantic Versioning <http://semver.org/>`_.
`1.0.3`_ (2019-08-26)
-------------------------
* Fixed CVE-2019-10751 — the way the output filename is generated for
``--download`` requests without ``--output`` resulting in a redirect has
been changed to only consider the initial URL as the base for the generated
filename, and not the final one. This fixes a potential security issue under
the following scenario:
1. A ``--download`` request with no explicit ``--output`` is made (e.g.,
``$ http -d example.org/file.txt``), instructing httpie to
`generate the output filename <https://httpie.org/doc#downloaded-file-name>`_
from the ``Content-Disposition`` response, or from the URL if the header
is not provided.
2. The server handling the request has been modified by an attacker and
instead of the expected response the URL returns a redirect to another
URL, e.g., ``attacker.example.org/.bash_profile``, whose response does
not provide a ``Content-Disposition`` header (i.e., the base for the
generated filename becomes ``.bash_profile`` instead of ``file.txt``).
3. Your current directory doesnt already contain ``.bash_profile``
(i.e., no unique suffix is added to the generated filename).
4. You dont notice the potentially unexpected output filename
as reported by httpie in the console output
(e.g., ``Downloading 100.00 B to ".bash_profile"``).
Reported by Raul Onitza and Giulio Comi.
`1.0.2`_ (2018-11-14)
-------------------------
* Fixed tests for installation with pyOpenSSL.
`1.0.1`_ (2018-11-14)
-------------------------
* Removed external URL calls from tests.
`1.0.0`_ (2018-11-02) `1.0.0`_ (2018-11-02)
------------------------- -------------------------
@ -308,13 +348,13 @@ This project adheres to `Semantic Versioning <http://semver.org/>`_.
* Many improvements and bug fixes * Many improvements and bug fixes
`0.1`_ (2012-02-25) `0.1.0`_ (2012-02-25)
------------------- ---------------------
* Initial public release * Initial public release
.. _`0.1`: https://github.com/jakubroztocil/httpie/commit/b966efa .. _`0.1.0`: https://github.com/jakubroztocil/httpie/commit/b966efa
.. _0.1.4: https://github.com/jakubroztocil/httpie/compare/b966efa...0.1.4 .. _0.1.4: https://github.com/jakubroztocil/httpie/compare/b966efa...0.1.4
.. _0.1.5: https://github.com/jakubroztocil/httpie/compare/0.1.4...0.1.5 .. _0.1.5: https://github.com/jakubroztocil/httpie/compare/0.1.4...0.1.5
.. _0.1.6: https://github.com/jakubroztocil/httpie/compare/0.1.5...0.1.6 .. _0.1.6: https://github.com/jakubroztocil/httpie/compare/0.1.5...0.1.6
@ -341,3 +381,6 @@ This project adheres to `Semantic Versioning <http://semver.org/>`_.
.. _0.9.8: https://github.com/jakubroztocil/httpie/compare/0.9.6...0.9.8 .. _0.9.8: https://github.com/jakubroztocil/httpie/compare/0.9.6...0.9.8
.. _0.9.9: https://github.com/jakubroztocil/httpie/compare/0.9.8...0.9.9 .. _0.9.9: https://github.com/jakubroztocil/httpie/compare/0.9.8...0.9.9
.. _1.0.0: https://github.com/jakubroztocil/httpie/compare/0.9.9...1.0.0 .. _1.0.0: https://github.com/jakubroztocil/httpie/compare/0.9.9...1.0.0
.. _1.0.1: https://github.com/jakubroztocil/httpie/compare/1.0.0...1.0.1
.. _1.0.2: https://github.com/jakubroztocil/httpie/compare/1.0.1...1.0.2
.. _1.0.3: https://github.com/jakubroztocil/httpie/compare/1.0.2...1.0.3

View File

@ -1,4 +1,4 @@
Copyright © 2012-2017 Jakub Roztocil <jakub@roztocil.co> Copyright © 2012-2019 Jakub Roztocil <jakub@roztocil.co>
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:

View File

@ -93,9 +93,8 @@ publish-no-test:
@echo $(TAG)Testing wheel build an installation$(END) @echo $(TAG)Testing wheel build an installation$(END)
@echo "$(VERSION)" @echo "$(VERSION)"
@echo "$(VERSION)" | grep -q "dev" && echo '!!!Not publishing dev version!!!' && exit 1 || echo ok @echo "$(VERSION)" | grep -q "dev" && echo '!!!Not publishing dev version!!!' && exit 1 || echo ok
python setup.py register python setup.py sdist bdist_wheel
python setup.py sdist upload twine upload dist/*
python setup.py bdist_wheel upload
@echo @echo
@ -125,9 +124,29 @@ uninstall-all: uninstall-httpie
############################################################################### ###############################################################################
# Utils # Docs
############################################################################### ###############################################################################
pdf:
# NOTE: rst2pdf needs to be installed manually and against a Python 2
@echo "Converting README.rst to PDF…"
rst2pdf \
--strip-elements-with-class=no-pdf \
README.rst \
-o README.pdf
@echo "Done"
@echo
homebrew-formula-vars:
extras/get-homebrew-formula-vars.py ###############################################################################
# Homebrew
###############################################################################
brew-deps:
extras/brew-deps.py
brew-test:
- brew uninstall httpie
brew install --build-from-source ./extras/httpie.rb
brew test httpie
brew audit --strict httpie

View File

@ -11,6 +11,12 @@ generally interacting with HTTP servers.
.. class:: no-web .. class:: no-web
.. image:: https://raw.githubusercontent.com/jakubroztocil/httpie/master/httpie.gif
:alt: HTTPie in action
:width: 100%
:align: center
.. image:: https://raw.githubusercontent.com/jakubroztocil/httpie/master/httpie.png .. image:: https://raw.githubusercontent.com/jakubroztocil/httpie/master/httpie.png
:alt: HTTPie compared to cURL :alt: HTTPie compared to cURL
:width: 100% :width: 100%
@ -134,6 +140,9 @@ You can also install the latest unreleased development version directly from
the ``master`` branch on GitHub. It is a work-in-progress of a future stable the ``master`` branch on GitHub. It is a work-in-progress of a future stable
release so the experience might be not as smooth. release so the experience might be not as smooth.
.. class:: no-pdf
|unix_build| |unix_build|
@ -1103,6 +1112,13 @@ You can use ``echo`` for simple data:
$ echo '{"name": "John"}' | http PATCH example.com/person/1 X-API-Token:123 $ echo '{"name": "John"}' | http PATCH example.com/person/1 X-API-Token:123
You can also use a Bash *here string*:
.. code-block:: bash
$ http example.com/ <<<'{"name": "John"}'
You can even pipe web services together using HTTPie: You can even pipe web services together using HTTPie:
.. code-block:: bash .. code-block:: bash
@ -1297,13 +1313,25 @@ is being saved to a file.
Done. 251.30 kB in 2.73862s (91.76 kB/s) Done. 251.30 kB in 2.73862s (91.76 kB/s)
Downloaded file name Downloaded filename
-------------------- --------------------
If not provided via ``--output, -o``, the output filename will be determined There are three mutually exclusive ways through which HTTPie determines
from ``Content-Disposition`` (if available), or from the URL and the output filename (with decreasing priority):
``Content-Type``. If the guessed filename already exists, HTTPie adds a unique
suffix to it. 1. You can explicitly provide it via ``--output, -o``.
The file gets overwritten if it already exists
(or appended to with ``--continue, -c``).
2. The server may specify the filename in the optional ``Content-Disposition``
response header. Any leading dots are stripped from a server-provided filename.
3. The last resort HTTPie uses is to generate the filename from a combination
of the request URL and the response ``Content-Type``.
The initial URL is always used as the basis for
the generated filename — even if there has been one or more redirects.
To prevent data loss by overwriting, HTTPie adds a unique numerical suffix to the
filename when necessary (unless specified with ``--output, -o``).
Piping while downloading Piping while downloading
@ -1550,7 +1578,7 @@ Best practices
-------------- --------------
The default behaviour of automatically reading ``stdin`` is typically not The default behaviour of automatically reading ``stdin`` is typically not
desirable during non-interactive invocations. You most likely want desirable during non-interactive invocations. You most likely want to
use the ``--ignore-stdin`` option to disable it. use the ``--ignore-stdin`` option to disable it.
It is a common gotcha that without this option HTTPie seemingly hangs. It is a common gotcha that without this option HTTPie seemingly hangs.
@ -1682,7 +1710,9 @@ See `CHANGELOG <https://github.com/jakubroztocil/httpie/blob/master/CHANGELOG.rs
Artwork Artwork
------- -------
See `claudiatd/httpie-artwork`_ * `Logo <https://github.com/claudiatd/httpie-artwork>`_ by `Cláudia Delgado <https://github.com/claudiatd>`_.
* `Animation <https://raw.githubusercontent.com/jakubroztocil/httpie/master/httpie.gif>`_ by `Allen Smith <https://github.com/loranallensmith>`_ of GitHub.
Licence Licence
@ -1704,7 +1734,6 @@ have contributed.
.. _these fine people: https://github.com/jakubroztocil/httpie/contributors .. _these fine people: https://github.com/jakubroztocil/httpie/contributors
.. _Jakub Roztocil: https://roztocil.co .. _Jakub Roztocil: https://roztocil.co
.. _@jakubroztocil: https://twitter.com/jakubroztocil .. _@jakubroztocil: https://twitter.com/jakubroztocil
.. _claudiatd/httpie-artwork: https://github.com/claudiatd/httpie-artwork
.. |pypi| image:: https://img.shields.io/pypi/v/httpie.svg?style=flat-square&label=latest%20stable%20version .. |pypi| image:: https://img.shields.io/pypi/v/httpie.svg?style=flat-square&label=latest%20stable%20version

View File

@ -1,9 +1,11 @@
#!/usr/bin/env python #!/usr/bin/env python3
""" """
Generate URLs and file hashes to be included in the Homebrew formula Generate Ruby code with URLs and file hashes for packages from PyPi
after a new release of HTTPie has been published on PyPi. (i.e., httpie itself as well as its dependencies) to be included
in the Homebrew formula after a new release of HTTPie has been published
on PyPi.
https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb <https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb>
""" """
import hashlib import hashlib
@ -12,8 +14,13 @@ import requests
PACKAGES = [ PACKAGES = [
'httpie', 'httpie',
'Pygments',
'requests', 'requests',
'pygments', 'certifi',
'urllib3',
'idna',
'chardet',
'PySocks',
] ]
@ -50,7 +57,7 @@ def main():
print(' url "{url}"'.format(url=dep_meta['url'])) print(' url "{url}"'.format(url=dep_meta['url']))
print(' sha256 "{sha256}"'.format(sha256=dep_meta['sha256'])) print(' sha256 "{sha256}"'.format(sha256=dep_meta['sha256']))
print(' end') print(' end')
print() print('')
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -5,40 +5,60 @@
# https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb # https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb
# #
class Httpie < Formula class Httpie < Formula
include Language::Python::Virtualenv
desc "User-friendly cURL replacement (command-line HTTP client)" desc "User-friendly cURL replacement (command-line HTTP client)"
homepage "https://httpie.org/" homepage "https://httpie.org/"
url "https://files.pythonhosted.org/packages/09/8d/581ef7bd9a09dc30b621638a4fa805a2073bbfb45fa06ed37f998f172419/httpie-1.0.2.tar.gz"
url "https://pypi.python.org/packages/85/95/7ccea3ae7fd1185e21629f6d14fa9c896d6250bb15fb492efa91edc741a2/httpie-0.9.8.tar.gz" sha256 "fc676c85febdf3d80abc1ef6fa71ec3764d8b838806a7ae4e55e5e5aa014a2ab"
sha256 "515870b15231530f56fe2164190581748e8799b66ef0fe36ec9da3396f0df6e1"
head "https://github.com/jakubroztocil/httpie.git" head "https://github.com/jakubroztocil/httpie.git"
depends_on :python3 bottle do
cellar :any_skip_relocation
resource "requests" do sha256 "158258be68ac93de13860be2bef02da6fd8b68aa24b2e6609bcff1ec3f93e7a0" => :mojave
url "https://pypi.python.org/packages/d9/03/155b3e67fe35fe5b6f4227a8d9e96a14fda828b18199800d161bcefc1359/requests-2.12.3.tar.gz" sha256 "54352116b6fa2c3bd65f26136fdcb57986dbff8a52de5febf7aea59c126d29e1" => :high_sierra
sha256 "de5d266953875e9647e37ef7bfe6ef1a46ff8ddfe61b5b3652edf7ea717ee2b2" sha256 "9cce71768fe388808e11b26d651b44a6b54219f5406845b4273b5099f5c1f76f" => :sierra
end end
resource "pygments" do depends_on "python"
url "https://pypi.python.org/packages/b8/67/ab177979be1c81bc99c8d0592ef22d547e70bb4c6815c383286ed5dec504/Pygments-2.1.3.tar.gz"
sha256 "88e4c8a91b2af5962bfa5ea2447ec6dd357018e86e94c7d14bd8cacbc5b55d81" resource "Pygments" do
url "https://files.pythonhosted.org/packages/64/69/413708eaf3a64a6abb8972644e0f20891a55e621c6759e2c3f3891e05d63/Pygments-2.3.1.tar.gz"
sha256 "5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a"
end
resource "requests" do
url "https://files.pythonhosted.org/packages/52/2c/514e4ac25da2b08ca5a464c50463682126385c4272c18193876e91f4bc38/requests-2.21.0.tar.gz"
sha256 "502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e"
end
resource "certifi" do
url "https://files.pythonhosted.org/packages/55/54/3ce77783acba5979ce16674fc98b1920d00b01d337cfaaf5db22543505ed/certifi-2018.11.29.tar.gz"
sha256 "47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7"
end
resource "urllib3" do
url "https://files.pythonhosted.org/packages/b1/53/37d82ab391393565f2f831b8eedbffd57db5a718216f82f1a8b4d381a1c1/urllib3-1.24.1.tar.gz"
sha256 "de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
end
resource "idna" do
url "https://files.pythonhosted.org/packages/ad/13/eb56951b6f7950cadb579ca166e448ba77f9d24efc03edd7e55fa57d04b7/idna-2.8.tar.gz"
sha256 "c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"
end
resource "chardet" do
url "https://files.pythonhosted.org/packages/fc/bb/a5768c230f9ddb03acc9ef3f0d4a3cf93462473795d18e9535498c8f929d/chardet-3.0.4.tar.gz"
sha256 "84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"
end
resource "PySocks" do
url "https://files.pythonhosted.org/packages/53/12/6bf1d764f128636cef7408e8156b7235b150ea31650d0260969215bb8e7d/PySocks-1.6.8.tar.gz"
sha256 "3fe52c55890a248676fd69dc9e3c4e811718b777834bcaab7a8125cf9deac672"
end end
def install def install
pyver = Language::Python.major_minor_version "python3" virtualenv_install_with_resources
ENV.prepend_create_path "PYTHONPATH", libexec/"vendor/lib/python#{pyver}/site-packages"
%w[pygments requests].each do |r|
resource(r).stage do
system "python3", *Language::Python.setup_install_args(libexec/"vendor")
end
end
ENV.prepend_create_path "PYTHONPATH", libexec/"lib/python#{pyver}/site-packages"
system "python3", *Language::Python.setup_install_args(libexec)
bin.install Dir["#{libexec}/bin/*"]
bin.env_script_all_files(libexec/"bin", :PYTHONPATH => ENV["PYTHONPATH"])
end end
test do test do

BIN
httpie.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 681 KiB

View File

@ -2,7 +2,7 @@
HTTPie - a CLI, cURL-like tool for humans. HTTPie - a CLI, cURL-like tool for humans.
""" """
__version__ = '1.0.0' __version__ = '1.0.3'
__author__ = 'Jakub Roztocil' __author__ = 'Jakub Roztocil'
__licence__ = 'BSD' __licence__ = 'BSD'

View File

@ -224,13 +224,13 @@ class Downloader(object):
request_headers['Range'] = 'bytes=%d-' % bytes_have request_headers['Range'] = 'bytes=%d-' % bytes_have
self._resumed_from = bytes_have self._resumed_from = bytes_have
def start(self, response): def start(self, final_response):
""" """
Initiate and return a stream for `response` body with progress Initiate and return a stream for `response` body with progress
callback attached. Can be called only once. callback attached. Can be called only once.
:param response: Initiated response object with headers already fetched :param final_response: Initiated response object with headers already fetched
:type response: requests.models.Response :type final_response: requests.models.Response
:return: RawStream, output_file :return: RawStream, output_file
@ -240,14 +240,18 @@ class Downloader(object):
# FIXME: some servers still might sent Content-Encoding: gzip # FIXME: some servers still might sent Content-Encoding: gzip
# <https://github.com/jakubroztocil/httpie/issues/423> # <https://github.com/jakubroztocil/httpie/issues/423>
try: try:
total_size = int(response.headers['Content-Length']) total_size = int(final_response.headers['Content-Length'])
except (KeyError, ValueError, TypeError): except (KeyError, ValueError, TypeError):
total_size = None total_size = None
if self._output_file: if not self._output_file:
if self._resume and response.status_code == PARTIAL_CONTENT: self._output_file = self._get_output_file_from_response(
final_response)
else:
# `--output, -o` provided
if self._resume and final_response.status_code == PARTIAL_CONTENT:
total_size = parse_content_range( total_size = parse_content_range(
response.headers.get('Content-Range'), final_response.headers.get('Content-Range'),
self._resumed_from self._resumed_from
) )
@ -258,19 +262,6 @@ class Downloader(object):
self._output_file.truncate() self._output_file.truncate()
except IOError: except IOError:
pass # stdout pass # stdout
else:
# TODO: Should the filename be taken from response.history[0].url?
# Output file not specified. Pick a name that doesn't exist yet.
filename = None
if 'Content-Disposition' in response.headers:
filename = filename_from_content_disposition(
response.headers['Content-Disposition'])
if not filename:
filename = filename_from_url(
url=response.url,
content_type=response.headers.get('Content-Type'),
)
self._output_file = open(get_unique_filename(filename), mode='a+b')
self.status.started( self.status.started(
resumed_from=self._resumed_from, resumed_from=self._resumed_from,
@ -278,7 +269,7 @@ class Downloader(object):
) )
stream = RawStream( stream = RawStream(
msg=HTTPResponse(response), msg=HTTPResponse(final_response),
with_headers=False, with_headers=False,
with_body=True, with_body=True,
on_body_chunk_downloaded=self.chunk_downloaded, on_body_chunk_downloaded=self.chunk_downloaded,
@ -324,6 +315,25 @@ class Downloader(object):
""" """
self.status.chunk_downloaded(len(chunk)) self.status.chunk_downloaded(len(chunk))
@staticmethod
def _get_output_file_from_response(final_response):
# Output file not specified. Pick a name that doesn't exist yet.
filename = None
if 'Content-Disposition' in final_response.headers:
filename = filename_from_content_disposition(
final_response.headers['Content-Disposition'])
if not filename:
initial_response = (
final_response.history[0] if final_response.history
else final_response
)
filename = filename_from_url(
url=initial_response.url,
content_type=final_response.headers.get('Content-Type'),
)
unique_filename = get_unique_filename(filename)
return open(unique_filename, mode='a+b')
class Status(object): class Status(object):
"""Holds details about the downland status.""" """Holds details about the downland status."""

View File

@ -752,7 +752,7 @@ def parse_items(items,
def readable_file_arg(filename): def readable_file_arg(filename):
try: try:
open(filename, 'rb') with open(filename, 'rb'):
return filename
except IOError as ex: except IOError as ex:
raise ArgumentTypeError('%s: %s' % (filename, ex.args[1])) raise ArgumentTypeError('%s: %s' % (filename, ex.args[1]))
return filename

View File

@ -6,3 +6,4 @@ pytest-httpbin>=0.0.6
docutils docutils
wheel wheel
pycodestyle pycodestyle
twine

View File

@ -35,8 +35,8 @@ tests_require = [
install_requires = [ install_requires = [
'requests>=2.18.4', 'requests>=2.21.0',
'Pygments>=2.1.3' 'Pygments>=2.3.1'
] ]

View File

@ -1,6 +1,7 @@
"""Tests for dealing with binary request and response data.""" """Tests for dealing with binary request and response data."""
import requests
from fixtures import BIN_FILE_PATH, BIN_FILE_CONTENT, BIN_FILE_PATH_ARG from fixtures import BIN_FILE_PATH, BIN_FILE_CONTENT, BIN_FILE_PATH_ARG
from httpie.compat import urlopen
from httpie.output.streams import BINARY_SUPPRESSED_NOTICE from httpie.output.streams import BINARY_SUPPRESSED_NOTICE
from utils import MockEnvironment, http from utils import MockEnvironment, http
@ -31,25 +32,19 @@ class TestBinaryRequestData:
class TestBinaryResponseData: class TestBinaryResponseData:
url = 'http://www.google.com/favicon.ico'
@property def test_binary_suppresses_when_terminal(self, httpbin):
def bindata(self): r = http('GET', httpbin + '/bytes/1024')
if not hasattr(self, '_bindata'):
self._bindata = urlopen(self.url).read()
return self._bindata
def test_binary_suppresses_when_terminal(self):
r = http('GET', self.url)
assert BINARY_SUPPRESSED_NOTICE.decode() in r assert BINARY_SUPPRESSED_NOTICE.decode() in r
def test_binary_suppresses_when_not_terminal_but_pretty(self): def test_binary_suppresses_when_not_terminal_but_pretty(self, httpbin):
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False) env = MockEnvironment(stdin_isatty=True, stdout_isatty=False)
r = http('--pretty=all', 'GET', self.url, r = http('--pretty=all', 'GET', httpbin + '/bytes/1024', env=env)
env=env)
assert BINARY_SUPPRESSED_NOTICE.decode() in r assert BINARY_SUPPRESSED_NOTICE.decode() in r
def test_binary_included_and_correct_when_suitable(self): def test_binary_included_and_correct_when_suitable(self, httpbin):
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False) env = MockEnvironment(stdin_isatty=True, stdout_isatty=False)
r = http('GET', self.url, env=env) url = httpbin + '/bytes/1024?seed=1'
assert r == self.bindata r = http('GET', url, env=env)
expected = requests.get(url).content
assert r == expected

View File

@ -1,4 +1,5 @@
import os import os
import tempfile
import time import time
import pytest import pytest
@ -22,6 +23,7 @@ class Response(object):
class TestDownloadUtils: class TestDownloadUtils:
def test_Content_Range_parsing(self): def test_Content_Range_parsing(self):
parse = parse_content_range parse = parse_content_range
@ -131,35 +133,50 @@ class TestDownloads:
assert body == r assert body == r
def test_download_with_Content_Length(self, httpbin_both): def test_download_with_Content_Length(self, httpbin_both):
devnull = open(os.devnull, 'w') with open(os.devnull, 'w') as devnull:
downloader = Downloader(output_file=devnull, progress_file=devnull) downloader = Downloader(output_file=devnull, progress_file=devnull)
downloader.start(Response( downloader.start(Response(
url=httpbin_both.url + '/', url=httpbin_both.url + '/',
headers={'Content-Length': 10} headers={'Content-Length': 10}
)) ))
time.sleep(1.1) time.sleep(1.1)
downloader.chunk_downloaded(b'12345') downloader.chunk_downloaded(b'12345')
time.sleep(1.1) time.sleep(1.1)
downloader.chunk_downloaded(b'12345') downloader.chunk_downloaded(b'12345')
downloader.finish() downloader.finish()
assert not downloader.interrupted assert not downloader.interrupted
downloader._progress_reporter.join()
def test_download_no_Content_Length(self, httpbin_both): def test_download_no_Content_Length(self, httpbin_both):
devnull = open(os.devnull, 'w') with open(os.devnull, 'w') as devnull:
downloader = Downloader(output_file=devnull, progress_file=devnull) downloader = Downloader(output_file=devnull, progress_file=devnull)
downloader.start(Response(url=httpbin_both.url + '/')) downloader.start(Response(url=httpbin_both.url + '/'))
time.sleep(1.1) time.sleep(1.1)
downloader.chunk_downloaded(b'12345') downloader.chunk_downloaded(b'12345')
downloader.finish() downloader.finish()
assert not downloader.interrupted assert not downloader.interrupted
downloader._progress_reporter.join()
def test_download_interrupted(self, httpbin_both): def test_download_interrupted(self, httpbin_both):
devnull = open(os.devnull, 'w') with open(os.devnull, 'w') as devnull:
downloader = Downloader(output_file=devnull, progress_file=devnull) downloader = Downloader(output_file=devnull, progress_file=devnull)
downloader.start(Response( downloader.start(Response(
url=httpbin_both.url + '/', url=httpbin_both.url + '/',
headers={'Content-Length': 5} headers={'Content-Length': 5}
)) ))
downloader.chunk_downloaded(b'1234') downloader.chunk_downloaded(b'1234')
downloader.finish() downloader.finish()
assert downloader.interrupted assert downloader.interrupted
downloader._progress_reporter.join()
def test_download_with_redirect_original_url_used_for_filename(self, httpbin):
# Redirect from `/redirect/1` to `/get`.
expected_filename = '1.json'
orig_cwd = os.getcwd()
os.chdir(tempfile.mkdtemp(prefix='httpie_download_test_'))
try:
assert os.listdir('.') == []
http('--download', httpbin.url + '/redirect/1')
assert os.listdir('.') == [expected_filename]
finally:
os.chdir(orig_cwd)

View File

@ -2,17 +2,31 @@ import os
import pytest import pytest
import pytest_httpbin.certs import pytest_httpbin.certs
from requests.exceptions import SSLError import requests.exceptions
from httpie import ExitStatus from httpie import ExitStatus
from httpie.input import SSL_VERSION_ARG_MAPPING from httpie.input import SSL_VERSION_ARG_MAPPING
from utils import http, HTTP_OK, TESTS_ROOT from utils import HTTP_OK, TESTS_ROOT, http
try:
# Handle OpenSSL errors, if installed.
# See <https://github.com/jakubroztocil/httpie/issues/729>
# noinspection PyUnresolvedReferences
import OpenSSL.SSL
ssl_errors = (
requests.exceptions.SSLError,
OpenSSL.SSL.Error,
)
except ImportError:
ssl_errors = (
requests.exceptions.SSLError,
)
CLIENT_CERT = os.path.join(TESTS_ROOT, 'client_certs', 'client.crt') CLIENT_CERT = os.path.join(TESTS_ROOT, 'client_certs', 'client.crt')
CLIENT_KEY = os.path.join(TESTS_ROOT, 'client_certs', 'client.key') CLIENT_KEY = os.path.join(TESTS_ROOT, 'client_certs', 'client.key')
CLIENT_PEM = os.path.join(TESTS_ROOT, 'client_certs', 'client.pem') CLIENT_PEM = os.path.join(TESTS_ROOT, 'client_certs', 'client.pem')
# FIXME: # FIXME:
# We test against a local httpbin instance which uses a self-signed cert. # We test against a local httpbin instance which uses a self-signed cert.
# Requests without --verify=<CA_BUNDLE> will fail with a verification error. # Requests without --verify=<CA_BUNDLE> will fail with a verification error.
@ -28,7 +42,7 @@ def test_ssl_version(httpbin_secure, ssl_version):
httpbin_secure + '/get' httpbin_secure + '/get'
) )
assert HTTP_OK in r assert HTTP_OK in r
except SSLError as e: except ssl_errors as e:
if ssl_version == 'ssl3': if ssl_version == 'ssl3':
# pytest-httpbin doesn't support ssl3 # pytest-httpbin doesn't support ssl3
assert 'SSLV3_ALERT_HANDSHAKE_FAILURE' in str(e) assert 'SSLV3_ALERT_HANDSHAKE_FAILURE' in str(e)
@ -57,12 +71,12 @@ class TestClientCert:
assert 'No such file or directory' in r.stderr assert 'No such file or directory' in r.stderr
def test_cert_file_invalid(self, httpbin_secure): def test_cert_file_invalid(self, httpbin_secure):
with pytest.raises(SSLError): with pytest.raises(ssl_errors):
http(httpbin_secure + '/get', http(httpbin_secure + '/get',
'--cert', __file__) '--cert', __file__)
def test_cert_ok_but_missing_key(self, httpbin_secure): def test_cert_ok_but_missing_key(self, httpbin_secure):
with pytest.raises(SSLError): with pytest.raises(ssl_errors):
http(httpbin_secure + '/get', http(httpbin_secure + '/get',
'--cert', CLIENT_CERT) '--cert', CLIENT_CERT)
@ -79,21 +93,23 @@ class TestServerCert:
assert HTTP_OK in r assert HTTP_OK in r
def test_verify_custom_ca_bundle_path( def test_verify_custom_ca_bundle_path(
self, httpbin_secure_untrusted): self, httpbin_secure_untrusted
):
r = http(httpbin_secure_untrusted + '/get', '--verify', CA_BUNDLE) r = http(httpbin_secure_untrusted + '/get', '--verify', CA_BUNDLE)
assert HTTP_OK in r assert HTTP_OK in r
def test_self_signed_server_cert_by_default_raises_ssl_error( def test_self_signed_server_cert_by_default_raises_ssl_error(
self, self,
httpbin_secure_untrusted): httpbin_secure_untrusted
with pytest.raises(SSLError): ):
with pytest.raises(ssl_errors):
http(httpbin_secure_untrusted.url + '/get') http(httpbin_secure_untrusted.url + '/get')
def test_verify_custom_ca_bundle_invalid_path(self, httpbin_secure): def test_verify_custom_ca_bundle_invalid_path(self, httpbin_secure):
# since 2.14.0 requests raises IOError # since 2.14.0 requests raises IOError
with pytest.raises((SSLError, IOError)): with pytest.raises(ssl_errors + (IOError,)):
http(httpbin_secure.url + '/get', '--verify', '/__not_found__') http(httpbin_secure.url + '/get', '--verify', '/__not_found__')
def test_verify_custom_ca_bundle_invalid_bundle(self, httpbin_secure): def test_verify_custom_ca_bundle_invalid_bundle(self, httpbin_secure):
with pytest.raises(SSLError): with pytest.raises(ssl_errors):
http(httpbin_secure.url + '/get', '--verify', __file__) http(httpbin_secure.url + '/get', '--verify', __file__)

View File

@ -62,6 +62,8 @@ class MockEnvironment(Environment):
return super(MockEnvironment, self).config return super(MockEnvironment, self).config
def cleanup(self): def cleanup(self):
self.stdout.close()
self.stderr.close()
if self._delete_config_dir: if self._delete_config_dir:
assert self.config_dir.startswith(tempfile.gettempdir()) assert self.config_dir.startswith(tempfile.gettempdir())
from shutil import rmtree from shutil import rmtree