Combine cookies from original request and session file

Close #932
Co-authored-by: kbanc <katherine.bancoft@gmail.com>
Co-authored-by: Gabriel Cruz <gabs.oficial98@gmail.com>
This commit is contained in:
Katherine Bancroft 2020-06-18 17:17:33 -04:00 committed by GitHub
parent 93d07cfe57
commit 9500ce136a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 178 additions and 28 deletions

View File

@ -530,7 +530,6 @@ Simple example:
$ http PUT httpbin.org/put name=John email=john@example.org $ http PUT httpbin.org/put name=John email=john@example.org
.. code-block:: http .. code-block:: http
PUT / HTTP/1.1 PUT / HTTP/1.1
Accept: application/json, */*;q=0.5 Accept: application/json, */*;q=0.5
Accept-Encoding: gzip, deflate Accept-Encoding: gzip, deflate
@ -1745,6 +1744,60 @@ exchange after it has been created, specify the session name via
# But it is not updated: # But it is not updated:
$ http --session-read-only=./ro-session.json httpbin.org/headers Custom-Header:new-value $ http --session-read-only=./ro-session.json httpbin.org/headers Custom-Header:new-value
Cookie Storage Behaviour
------------------------
**TL;DR:** Cookie storage priority: Server response > Command line request > Session file
To set a cookie within a Session there are three options:
1. Get a `Set-Cookie` header in a response from a server
.. code-block:: bash
$ http --session=./session.json httpbin.org/cookie/set?foo=bar
2. Set the cookie name and value through the command line as seen in `cookies`_
.. code-block:: bash
$ http --session=./session.json httpbin.org/headers Cookie:foo=bar
3. Manually set cookie parameters in the json file of the session
.. code-block:: json
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.org/doc#sessions",
"httpie": "2.2.0-dev"
},
"auth": {
"password": null,
"type": null,
"username": null
},
"cookies": {
"foo": {
"expires": null,
"path": "/",
"secure": false,
"value": "bar"
}
}
}
Cookies will be set in the session file with the priority specified above. For example, a cookie
set through the command line will overwrite a cookie of the same name stored
in the session file. If the server returns a `Set-Cookie` header with a
cookie of the same name, the returned cookie will overwrite the preexisting cookie.
Expired cookies are never stored. If a cookie in a session file expires, it will be removed before
sending a new request. If the server expires an existing cookie, it will also be removed from the
session file.
Config Config
====== ======

View File

@ -4,6 +4,8 @@ Persistent, JSON-serialized sessions.
""" """
import os import os
import re import re
from http.cookies import SimpleCookie
from pathlib import Path from pathlib import Path
from typing import Iterable, Optional, Union from typing import Iterable, Optional, Union
from urllib.parse import urlsplit from urllib.parse import urlsplit
@ -76,7 +78,13 @@ class Session(BaseConfigDict):
continue # Ignore explicitly unset headers continue # Ignore explicitly unset headers
value = value.decode('utf8') value = value.decode('utf8')
if name == 'User-Agent' and value.startswith('HTTPie/'): if name.lower() == 'user-agent' and value.startswith('HTTPie/'):
continue
if name.lower() == 'cookie':
for cookie_name, morsel in SimpleCookie(value).items():
self['cookies'][cookie_name] = {'value': morsel.value}
del request_headers[name]
continue continue
for prefix in SESSION_IGNORED_HEADER_PREFIXES: for prefix in SESSION_IGNORED_HEADER_PREFIXES:

View File

@ -35,6 +35,27 @@ class SessionTestBase:
return MockEnvironment(config_dir=self.config_dir) return MockEnvironment(config_dir=self.config_dir)
class CookieTestBase:
def setup_method(self, method):
self.config_dir = mk_config_dir()
orig_session = {
'cookies': {
'cookie1': {
'value': 'foo',
},
'cookie2': {
'value': 'foo',
}
}
}
self.session_path = self.config_dir / 'test-session.json'
self.session_path.write_text(json.dumps(orig_session))
def teardown_method(self, method):
shutil.rmtree(self.config_dir)
class TestSessionFlow(SessionTestBase): class TestSessionFlow(SessionTestBase):
""" """
These tests start with an existing session created in `setup_method()`. These tests start with an existing session created in `setup_method()`.
@ -191,13 +212,7 @@ class TestSession(SessionTestBase):
os.chdir(cwd) os.chdir(cwd)
class TestExpiredCookies: class TestExpiredCookies(CookieTestBase):
def setup_method(self, method):
self.config_dir = mk_config_dir()
def teardown_method(self, method):
shutil.rmtree(self.config_dir)
@pytest.mark.parametrize( @pytest.mark.parametrize(
argnames=['initial_cookie', 'expired_cookie'], argnames=['initial_cookie', 'expired_cookie'],
@ -213,29 +228,16 @@ class TestExpiredCookies:
assert expired_cookie not in session.cookies assert expired_cookie not in session.cookies
def test_expired_cookies(self, httpbin): def test_expired_cookies(self, httpbin):
orig_session = {
'cookies': {
'to_expire': {
'value': 'foo'
},
'to_stay': {
'value': 'foo'
},
}
}
session_path = self.config_dir / 'test-session.json'
session_path.write_text(json.dumps(orig_session))
r = http( r = http(
'--session', str(session_path), '--session', str(self.session_path),
'--print=H', '--print=H',
httpbin.url + '/cookies/delete?to_expire', httpbin.url + '/cookies/delete?cookie2',
) )
assert 'Cookie: to_expire=foo; to_stay=foo' in r assert 'Cookie: cookie1=foo; cookie2=foo' in r
updated_session = json.loads(session_path.read_text()) updated_session = json.loads(self.session_path.read_text())
assert 'to_stay' in updated_session['cookies'] assert 'cookie1' in updated_session['cookies']
assert 'to_expire' not in updated_session['cookies'] assert 'cookie2' not in updated_session['cookies']
@pytest.mark.parametrize( @pytest.mark.parametrize(
argnames=['headers', 'now', 'expected_expired'], argnames=['headers', 'now', 'expected_expired'],
@ -277,3 +279,90 @@ class TestExpiredCookies:
) )
def test_get_expired_cookies_manages_multiple_cookie_headers(self, headers, now, expected_expired): def test_get_expired_cookies_manages_multiple_cookie_headers(self, headers, now, expected_expired):
assert get_expired_cookies(headers, now=now) == expected_expired assert get_expired_cookies(headers, now=now) == expected_expired
class TestCookieStorage(CookieTestBase):
@pytest.mark.parametrize(
argnames=['new_cookies', 'new_cookies_dict', 'expected'],
argvalues=[(
'new=bar',
{'new': 'bar'},
'cookie1=foo; cookie2=foo; new=bar'
),
(
'new=bar;chocolate=milk',
{'new': 'bar', 'chocolate': 'milk'},
'chocolate=milk; cookie1=foo; cookie2=foo; new=bar'
),
(
'new=bar; chocolate=milk',
{'new': 'bar', 'chocolate': 'milk'},
'chocolate=milk; cookie1=foo; cookie2=foo; new=bar'
),
(
'new=bar;; chocolate=milk;;;',
{'new': 'bar', 'chocolate': 'milk'},
'cookie1=foo; cookie2=foo; new=bar'
),
(
'new=bar; chocolate=milk;;;',
{'new': 'bar', 'chocolate': 'milk'},
'chocolate=milk; cookie1=foo; cookie2=foo; new=bar'
)
]
)
def test_existing_and_new_cookies_sent_in_request(self, new_cookies, new_cookies_dict, expected, httpbin):
r = http(
'--session', str(self.session_path),
'--print=H',
httpbin.url,
'Cookie:' + new_cookies,
)
# Note: cookies in response are in alphabetical order
assert 'Cookie: ' + expected in r
updated_session = json.loads(self.session_path.read_text())
for name, value in new_cookies_dict.items():
assert name, value in updated_session['cookies']
assert 'Cookie' not in updated_session['headers']
@pytest.mark.parametrize(
argnames=['cli_cookie', 'set_cookie', 'expected'],
argvalues=[(
'',
'/cookies/set/cookie1/bar',
'bar'
),
(
'cookie1=not_foo',
'/cookies/set/cookie1/bar',
'bar'
),
(
'cookie1=not_foo',
'',
'not_foo'
),
(
'',
'',
'foo'
)
]
)
def test_cookie_storage_priority(self, cli_cookie, set_cookie, expected, httpbin):
"""
Expected order of priority for cookie storage in session file:
1. set-cookie (from server)
2. command line arg
3. cookie already stored in session file
"""
r = http(
'--session', str(self.session_path),
httpbin.url + set_cookie,
'Cookie:' + cli_cookie,
)
updated_session = json.loads(self.session_path.read_text())
assert updated_session['cookies']['cookie1']['value'] == expected