mirror of
https://github.com/httpie/cli.git
synced 2025-08-12 23:07:13 +02:00
Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
747be30d2e | |||
88a9583f4c | |||
fd6e87914c | |||
6dee49357d | |||
df36d6255d | |||
e92b831e6e | |||
fd44f1af93 | |||
b6309547d5 | |||
3a46149de1 | |||
b7c8bf0800 | |||
69d010a11b | |||
42ff243400 | |||
933b438e5f | |||
358342d1c9 | |||
c591a3810d | |||
0eba037037 | |||
3898129e9c | |||
b88e88d2e3 | |||
d1407baf76 | |||
d5032ca859 | |||
f6a19cf552 | |||
74979f3b33 | |||
698eb51e60 | |||
ae8030c930 |
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal 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
|
@ -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 doesn’t already contain ``.bash_profile``
|
||||||
|
(i.e., no unique suffix is added to the generated filename).
|
||||||
|
4. You don’t 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
|
||||||
|
2
LICENSE
2
LICENSE
@ -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:
|
||||||
|
31
Makefile
31
Makefile
@ -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
|
||||||
|
45
README.rst
45
README.rst
@ -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
|
||||||
|
@ -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__':
|
@ -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
BIN
httpie.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1019 KiB |
BIN
httpie.png
BIN
httpie.png
Binary file not shown.
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 681 KiB |
@ -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'
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
|
||||||
|
@ -6,3 +6,4 @@ pytest-httpbin>=0.0.6
|
|||||||
docutils
|
docutils
|
||||||
wheel
|
wheel
|
||||||
pycodestyle
|
pycodestyle
|
||||||
|
twine
|
||||||
|
4
setup.py
4
setup.py
@ -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'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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__)
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user