2013-09-20 01:00:54 +08:00
|
|
|
# Copyright (c) 2012 OpenStack Foundation
|
2012-09-04 14:02:19 -07:00
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
|
|
# implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
|
|
|
|
"Tests for swift.common.swob"
|
|
|
|
|
|
|
|
import datetime
|
2014-03-04 11:52:48 -08:00
|
|
|
import unittest
|
2012-11-01 20:45:11 -04:00
|
|
|
import re
|
Allow pre-1970 dates in If-[Un]Modified-Since
If I want to fetch an object only if it is newer than the first moon
landing, I send a GET request with header:
If-Modified-Since: Sun, 20 Jul 1969 20:18:00 UTC
Since that date is older than Swift, I expect a 2xx response. However,
I get a 412, which isn't even a valid thing to do for
If-Modified-Since; it should either be 2xx or 304. This is because of
two problems:
(a) Swift treats pre-1970 dates as invalid, and
(b) Swift returns 412 when a date is invalid instead of ignoring it.
This commit makes it so any time between datetime.datetime.min and
datetime.datetime.max is an acceptable value for If-Modified-Since and
If-Unmodified-Since. Dates outside that date range are treated as
invalid headers and thus are ignored, as RFC 2616 section 14.28
requires ("If the specified date is invalid, the header is ignored").
This only works for dates that the Python standard library can parse,
which on my machine is 01 Jan 1 to 31 Dec 9999. Eliminating those
restrictions would require implementing our own date parsing and
comparison, and that's almost certainly not worth it.
Change-Id: I4cb4903c4e5e3b6b3c9506c2cabbfbda62e82f35
2014-03-03 11:25:43 -08:00
|
|
|
import time
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2015-05-27 18:01:37 +02:00
|
|
|
from six import BytesIO
|
2015-10-08 15:03:52 +02:00
|
|
|
from six.moves.urllib.parse import quote
|
2015-05-27 17:27:47 +02:00
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
import swift.common.swob
|
2014-06-10 22:17:47 -07:00
|
|
|
from swift.common import utils, exceptions
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
|
|
|
|
class TestHeaderEnvironProxy(unittest.TestCase):
|
|
|
|
def test_proxy(self):
|
|
|
|
environ = {}
|
|
|
|
proxy = swift.common.swob.HeaderEnvironProxy(environ)
|
|
|
|
proxy['Content-Length'] = 20
|
|
|
|
proxy['Content-Type'] = 'text/plain'
|
|
|
|
proxy['Something-Else'] = 'somevalue'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(
|
2012-09-04 14:02:19 -07:00
|
|
|
proxy.environ, {'CONTENT_LENGTH': '20',
|
|
|
|
'CONTENT_TYPE': 'text/plain',
|
|
|
|
'HTTP_SOMETHING_ELSE': 'somevalue'})
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(proxy['content-length'], '20')
|
|
|
|
self.assertEqual(proxy['content-type'], 'text/plain')
|
|
|
|
self.assertEqual(proxy['something-else'], 'somevalue')
|
2015-08-27 01:00:26 +02:00
|
|
|
self.assertEqual(set(['Something-Else',
|
|
|
|
'Content-Length', 'Content-Type']),
|
|
|
|
set(proxy.keys()))
|
|
|
|
self.assertEqual(list(iter(proxy)), proxy.keys())
|
|
|
|
self.assertEqual(3, len(proxy))
|
|
|
|
|
|
|
|
def test_ignored_keys(self):
|
|
|
|
# Constructor doesn't normalize keys
|
|
|
|
key = 'wsgi.input'
|
|
|
|
environ = {key: ''}
|
|
|
|
proxy = swift.common.swob.HeaderEnvironProxy(environ)
|
|
|
|
self.assertEqual([], list(iter(proxy)))
|
|
|
|
self.assertEqual([], proxy.keys())
|
|
|
|
self.assertEqual(0, len(proxy))
|
|
|
|
self.assertRaises(KeyError, proxy.__getitem__, key)
|
|
|
|
self.assertNotIn(key, proxy)
|
|
|
|
|
|
|
|
proxy['Content-Type'] = 'text/plain'
|
|
|
|
self.assertEqual(['Content-Type'], list(iter(proxy)))
|
|
|
|
self.assertEqual(['Content-Type'], proxy.keys())
|
|
|
|
self.assertEqual(1, len(proxy))
|
|
|
|
self.assertEqual('text/plain', proxy['Content-Type'])
|
|
|
|
self.assertIn('Content-Type', proxy)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
def test_del(self):
|
|
|
|
environ = {}
|
|
|
|
proxy = swift.common.swob.HeaderEnvironProxy(environ)
|
|
|
|
proxy['Content-Length'] = 20
|
|
|
|
proxy['Content-Type'] = 'text/plain'
|
|
|
|
proxy['Something-Else'] = 'somevalue'
|
|
|
|
del proxy['Content-Length']
|
|
|
|
del proxy['Content-Type']
|
|
|
|
del proxy['Something-Else']
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(proxy.environ, {})
|
2015-08-27 01:00:26 +02:00
|
|
|
self.assertEqual(0, len(proxy))
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
def test_contains(self):
|
|
|
|
environ = {}
|
|
|
|
proxy = swift.common.swob.HeaderEnvironProxy(environ)
|
|
|
|
proxy['Content-Length'] = 20
|
|
|
|
proxy['Content-Type'] = 'text/plain'
|
|
|
|
proxy['Something-Else'] = 'somevalue'
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('content-length' in proxy)
|
|
|
|
self.assertTrue('content-type' in proxy)
|
|
|
|
self.assertTrue('something-else' in proxy)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
def test_keys(self):
|
|
|
|
environ = {}
|
|
|
|
proxy = swift.common.swob.HeaderEnvironProxy(environ)
|
|
|
|
proxy['Content-Length'] = 20
|
|
|
|
proxy['Content-Type'] = 'text/plain'
|
|
|
|
proxy['Something-Else'] = 'somevalue'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(
|
2012-09-04 14:02:19 -07:00
|
|
|
set(proxy.keys()),
|
|
|
|
set(('Content-Length', 'Content-Type', 'Something-Else')))
|
|
|
|
|
|
|
|
|
|
|
|
class TestRange(unittest.TestCase):
|
|
|
|
def test_range(self):
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=1-7')
|
|
|
|
self.assertEqual(swob_range.ranges[0], (1, 7))
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
def test_upsidedown_range(self):
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=5-10')
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(2), [])
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
def test_str(self):
|
|
|
|
for range_str in ('bytes=1-7', 'bytes=1-', 'bytes=-1',
|
|
|
|
'bytes=1-7,9-12', 'bytes=-7,9-'):
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range(range_str)
|
|
|
|
self.assertEqual(str(swob_range), range_str)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2012-11-01 20:45:11 -04:00
|
|
|
def test_ranges_for_length(self):
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=1-7')
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(10), [(1, 8)])
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(5), [(1, 5)])
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(None), None)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2012-11-01 20:45:11 -04:00
|
|
|
def test_ranges_for_large_length(self):
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=-100000000000000000000000')
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(100), [(0, 100)])
|
2012-11-01 20:45:11 -04:00
|
|
|
|
|
|
|
def test_ranges_for_length_no_end(self):
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=1-')
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(10), [(1, 10)])
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(5), [(1, 5)])
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(None), None)
|
2012-10-03 14:20:52 -07:00
|
|
|
# This used to freak out:
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=100-')
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(5), [])
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(None), None)
|
2012-11-01 20:45:11 -04:00
|
|
|
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=4-6,100-')
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(5), [(4, 5)])
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2012-11-01 20:45:11 -04:00
|
|
|
def test_ranges_for_length_no_start(self):
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=-7')
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(10), [(3, 10)])
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(5), [(0, 5)])
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(None), None)
|
2012-11-01 20:45:11 -04:00
|
|
|
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=4-6,-100')
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(5), [(4, 5), (0, 5)])
|
2012-11-01 20:45:11 -04:00
|
|
|
|
|
|
|
def test_ranges_for_length_multi(self):
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=-20,4-')
|
|
|
|
self.assertEqual(len(swob_range.ranges_for_length(200)), 2)
|
2012-11-01 20:45:11 -04:00
|
|
|
|
Reject overly-taxing ranged-GET requests
RFC 7233 says that servers MAY reject egregious range-GET requests
such as requests with hundreds of ranges, requests with non-ascending
ranges, and so on.
Such requests are fairly hard for Swift to process. Consider a Range
header that asks for the first byte of every 10th MiB in a 4 GiB
object, but in some random order. That'll cause a lot of seeks on the
object server, but the corresponding response body is quite small in
comparison to the workload.
This commit makes Swift reject, with a 416 response, any ranged GET
request with more than fifty ranges, more than three overlapping
ranges, or more than eight non-increasing ranges.
This is a necessary prerequisite for supporting multi-range GETs on
large objects. Otherwise, a malicious user could construct a Range
header with hundreds of byte ranges where each individual byterange
requires the proxy to contact a different object server. If seeking
all over a disk is bad, connecting all over the cluster is way worse.
DocImpact
Change-Id: I4dcedcaae6c3deada06a0223479e611094d57234
2014-08-28 09:39:38 -08:00
|
|
|
# the actual length greater than each range element
|
2016-01-20 16:06:26 -08:00
|
|
|
self.assertEqual(swob_range.ranges_for_length(200),
|
|
|
|
[(180, 200), (4, 200)])
|
Reject overly-taxing ranged-GET requests
RFC 7233 says that servers MAY reject egregious range-GET requests
such as requests with hundreds of ranges, requests with non-ascending
ranges, and so on.
Such requests are fairly hard for Swift to process. Consider a Range
header that asks for the first byte of every 10th MiB in a 4 GiB
object, but in some random order. That'll cause a lot of seeks on the
object server, but the corresponding response body is quite small in
comparison to the workload.
This commit makes Swift reject, with a 416 response, any ranged GET
request with more than fifty ranges, more than three overlapping
ranges, or more than eight non-increasing ranges.
This is a necessary prerequisite for supporting multi-range GETs on
large objects. Otherwise, a malicious user could construct a Range
header with hundreds of byte ranges where each individual byterange
requires the proxy to contact a different object server. If seeking
all over a disk is bad, connecting all over the cluster is way worse.
DocImpact
Change-Id: I4dcedcaae6c3deada06a0223479e611094d57234
2014-08-28 09:39:38 -08:00
|
|
|
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=30-150,-10')
|
|
|
|
self.assertEqual(len(swob_range.ranges_for_length(200)), 2)
|
Reject overly-taxing ranged-GET requests
RFC 7233 says that servers MAY reject egregious range-GET requests
such as requests with hundreds of ranges, requests with non-ascending
ranges, and so on.
Such requests are fairly hard for Swift to process. Consider a Range
header that asks for the first byte of every 10th MiB in a 4 GiB
object, but in some random order. That'll cause a lot of seeks on the
object server, but the corresponding response body is quite small in
comparison to the workload.
This commit makes Swift reject, with a 416 response, any ranged GET
request with more than fifty ranges, more than three overlapping
ranges, or more than eight non-increasing ranges.
This is a necessary prerequisite for supporting multi-range GETs on
large objects. Otherwise, a malicious user could construct a Range
header with hundreds of byte ranges where each individual byterange
requires the proxy to contact a different object server. If seeking
all over a disk is bad, connecting all over the cluster is way worse.
DocImpact
Change-Id: I4dcedcaae6c3deada06a0223479e611094d57234
2014-08-28 09:39:38 -08:00
|
|
|
|
|
|
|
# the actual length lands in the middle of a range
|
2016-01-20 16:06:26 -08:00
|
|
|
self.assertEqual(swob_range.ranges_for_length(90),
|
|
|
|
[(30, 90), (80, 90)])
|
2012-11-01 20:45:11 -04:00
|
|
|
|
|
|
|
# the actual length greater than any of the range
|
2016-01-20 16:06:26 -08:00
|
|
|
self.assertEqual(swob_range.ranges_for_length(200),
|
2015-08-05 23:58:14 +05:30
|
|
|
[(30, 151), (190, 200)])
|
2012-11-01 20:45:11 -04:00
|
|
|
|
2016-01-20 16:06:26 -08:00
|
|
|
self.assertEqual(swob_range.ranges_for_length(None), None)
|
2012-11-01 20:45:11 -04:00
|
|
|
|
|
|
|
def test_ranges_for_length_edges(self):
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=0-1, -7')
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(10),
|
2015-08-05 23:58:14 +05:30
|
|
|
[(0, 2), (3, 10)])
|
2012-11-01 20:45:11 -04:00
|
|
|
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=-7, 0-1')
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(10),
|
2015-08-05 23:58:14 +05:30
|
|
|
[(3, 10), (0, 2)])
|
2012-11-01 20:45:11 -04:00
|
|
|
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=-7, 0-1')
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(5),
|
2015-08-05 23:58:14 +05:30
|
|
|
[(0, 5), (0, 2)])
|
2012-09-04 14:02:19 -07:00
|
|
|
|
Reject overly-taxing ranged-GET requests
RFC 7233 says that servers MAY reject egregious range-GET requests
such as requests with hundreds of ranges, requests with non-ascending
ranges, and so on.
Such requests are fairly hard for Swift to process. Consider a Range
header that asks for the first byte of every 10th MiB in a 4 GiB
object, but in some random order. That'll cause a lot of seeks on the
object server, but the corresponding response body is quite small in
comparison to the workload.
This commit makes Swift reject, with a 416 response, any ranged GET
request with more than fifty ranges, more than three overlapping
ranges, or more than eight non-increasing ranges.
This is a necessary prerequisite for supporting multi-range GETs on
large objects. Otherwise, a malicious user could construct a Range
header with hundreds of byte ranges where each individual byterange
requires the proxy to contact a different object server. If seeking
all over a disk is bad, connecting all over the cluster is way worse.
DocImpact
Change-Id: I4dcedcaae6c3deada06a0223479e611094d57234
2014-08-28 09:39:38 -08:00
|
|
|
def test_ranges_for_length_overlapping(self):
|
|
|
|
# Fewer than 3 overlaps is okay
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=10-19,15-24')
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(100),
|
2015-08-05 23:58:14 +05:30
|
|
|
[(10, 20), (15, 25)])
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=10-19,15-24,20-29')
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(100),
|
2015-08-05 23:58:14 +05:30
|
|
|
[(10, 20), (15, 25), (20, 30)])
|
Reject overly-taxing ranged-GET requests
RFC 7233 says that servers MAY reject egregious range-GET requests
such as requests with hundreds of ranges, requests with non-ascending
ranges, and so on.
Such requests are fairly hard for Swift to process. Consider a Range
header that asks for the first byte of every 10th MiB in a 4 GiB
object, but in some random order. That'll cause a lot of seeks on the
object server, but the corresponding response body is quite small in
comparison to the workload.
This commit makes Swift reject, with a 416 response, any ranged GET
request with more than fifty ranges, more than three overlapping
ranges, or more than eight non-increasing ranges.
This is a necessary prerequisite for supporting multi-range GETs on
large objects. Otherwise, a malicious user could construct a Range
header with hundreds of byte ranges where each individual byterange
requires the proxy to contact a different object server. If seeking
all over a disk is bad, connecting all over the cluster is way worse.
DocImpact
Change-Id: I4dcedcaae6c3deada06a0223479e611094d57234
2014-08-28 09:39:38 -08:00
|
|
|
|
|
|
|
# Adjacent ranges, though suboptimal, don't overlap
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=10-19,20-29,30-39')
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(100),
|
2015-08-05 23:58:14 +05:30
|
|
|
[(10, 20), (20, 30), (30, 40)])
|
Reject overly-taxing ranged-GET requests
RFC 7233 says that servers MAY reject egregious range-GET requests
such as requests with hundreds of ranges, requests with non-ascending
ranges, and so on.
Such requests are fairly hard for Swift to process. Consider a Range
header that asks for the first byte of every 10th MiB in a 4 GiB
object, but in some random order. That'll cause a lot of seeks on the
object server, but the corresponding response body is quite small in
comparison to the workload.
This commit makes Swift reject, with a 416 response, any ranged GET
request with more than fifty ranges, more than three overlapping
ranges, or more than eight non-increasing ranges.
This is a necessary prerequisite for supporting multi-range GETs on
large objects. Otherwise, a malicious user could construct a Range
header with hundreds of byte ranges where each individual byterange
requires the proxy to contact a different object server. If seeking
all over a disk is bad, connecting all over the cluster is way worse.
DocImpact
Change-Id: I4dcedcaae6c3deada06a0223479e611094d57234
2014-08-28 09:39:38 -08:00
|
|
|
|
|
|
|
# Ranges that share a byte do overlap
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=10-20,20-30,30-40,40-50')
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(100), [])
|
Reject overly-taxing ranged-GET requests
RFC 7233 says that servers MAY reject egregious range-GET requests
such as requests with hundreds of ranges, requests with non-ascending
ranges, and so on.
Such requests are fairly hard for Swift to process. Consider a Range
header that asks for the first byte of every 10th MiB in a 4 GiB
object, but in some random order. That'll cause a lot of seeks on the
object server, but the corresponding response body is quite small in
comparison to the workload.
This commit makes Swift reject, with a 416 response, any ranged GET
request with more than fifty ranges, more than three overlapping
ranges, or more than eight non-increasing ranges.
This is a necessary prerequisite for supporting multi-range GETs on
large objects. Otherwise, a malicious user could construct a Range
header with hundreds of byte ranges where each individual byterange
requires the proxy to contact a different object server. If seeking
all over a disk is bad, connecting all over the cluster is way worse.
DocImpact
Change-Id: I4dcedcaae6c3deada06a0223479e611094d57234
2014-08-28 09:39:38 -08:00
|
|
|
|
|
|
|
# With suffix byte range specs (e.g. bytes=-2), make sure that we
|
|
|
|
# correctly determine overlapping-ness based on the entity length
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range('bytes=10-15,15-20,30-39,-9')
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(100),
|
2015-08-05 23:58:14 +05:30
|
|
|
[(10, 16), (15, 21), (30, 40), (91, 100)])
|
2016-01-20 16:06:26 -08:00
|
|
|
self.assertEqual(swob_range.ranges_for_length(20), [])
|
Reject overly-taxing ranged-GET requests
RFC 7233 says that servers MAY reject egregious range-GET requests
such as requests with hundreds of ranges, requests with non-ascending
ranges, and so on.
Such requests are fairly hard for Swift to process. Consider a Range
header that asks for the first byte of every 10th MiB in a 4 GiB
object, but in some random order. That'll cause a lot of seeks on the
object server, but the corresponding response body is quite small in
comparison to the workload.
This commit makes Swift reject, with a 416 response, any ranged GET
request with more than fifty ranges, more than three overlapping
ranges, or more than eight non-increasing ranges.
This is a necessary prerequisite for supporting multi-range GETs on
large objects. Otherwise, a malicious user could construct a Range
header with hundreds of byte ranges where each individual byterange
requires the proxy to contact a different object server. If seeking
all over a disk is bad, connecting all over the cluster is way worse.
DocImpact
Change-Id: I4dcedcaae6c3deada06a0223479e611094d57234
2014-08-28 09:39:38 -08:00
|
|
|
|
|
|
|
def test_ranges_for_length_nonascending(self):
|
|
|
|
few_ranges = ("bytes=100-109,200-209,300-309,500-509,"
|
|
|
|
"400-409,600-609,700-709")
|
|
|
|
many_ranges = few_ranges + ",800-809"
|
|
|
|
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range(few_ranges)
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(100000),
|
2015-08-05 23:58:14 +05:30
|
|
|
[(100, 110), (200, 210), (300, 310), (500, 510),
|
|
|
|
(400, 410), (600, 610), (700, 710)])
|
Reject overly-taxing ranged-GET requests
RFC 7233 says that servers MAY reject egregious range-GET requests
such as requests with hundreds of ranges, requests with non-ascending
ranges, and so on.
Such requests are fairly hard for Swift to process. Consider a Range
header that asks for the first byte of every 10th MiB in a 4 GiB
object, but in some random order. That'll cause a lot of seeks on the
object server, but the corresponding response body is quite small in
comparison to the workload.
This commit makes Swift reject, with a 416 response, any ranged GET
request with more than fifty ranges, more than three overlapping
ranges, or more than eight non-increasing ranges.
This is a necessary prerequisite for supporting multi-range GETs on
large objects. Otherwise, a malicious user could construct a Range
header with hundreds of byte ranges where each individual byterange
requires the proxy to contact a different object server. If seeking
all over a disk is bad, connecting all over the cluster is way worse.
DocImpact
Change-Id: I4dcedcaae6c3deada06a0223479e611094d57234
2014-08-28 09:39:38 -08:00
|
|
|
|
2016-01-20 16:06:26 -08:00
|
|
|
swob_range = swift.common.swob.Range(many_ranges)
|
|
|
|
self.assertEqual(swob_range.ranges_for_length(100000), [])
|
Reject overly-taxing ranged-GET requests
RFC 7233 says that servers MAY reject egregious range-GET requests
such as requests with hundreds of ranges, requests with non-ascending
ranges, and so on.
Such requests are fairly hard for Swift to process. Consider a Range
header that asks for the first byte of every 10th MiB in a 4 GiB
object, but in some random order. That'll cause a lot of seeks on the
object server, but the corresponding response body is quite small in
comparison to the workload.
This commit makes Swift reject, with a 416 response, any ranged GET
request with more than fifty ranges, more than three overlapping
ranges, or more than eight non-increasing ranges.
This is a necessary prerequisite for supporting multi-range GETs on
large objects. Otherwise, a malicious user could construct a Range
header with hundreds of byte ranges where each individual byterange
requires the proxy to contact a different object server. If seeking
all over a disk is bad, connecting all over the cluster is way worse.
DocImpact
Change-Id: I4dcedcaae6c3deada06a0223479e611094d57234
2014-08-28 09:39:38 -08:00
|
|
|
|
|
|
|
def test_ranges_for_length_too_many(self):
|
|
|
|
at_the_limit_ranges = (
|
|
|
|
"bytes=" + ",".join("%d-%d" % (x * 1000, x * 1000 + 10)
|
|
|
|
for x in range(50)))
|
|
|
|
too_many_ranges = at_the_limit_ranges + ",10000000-10000009"
|
|
|
|
|
|
|
|
rng = swift.common.swob.Range(at_the_limit_ranges)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(len(rng.ranges_for_length(1000000000)), 50)
|
Reject overly-taxing ranged-GET requests
RFC 7233 says that servers MAY reject egregious range-GET requests
such as requests with hundreds of ranges, requests with non-ascending
ranges, and so on.
Such requests are fairly hard for Swift to process. Consider a Range
header that asks for the first byte of every 10th MiB in a 4 GiB
object, but in some random order. That'll cause a lot of seeks on the
object server, but the corresponding response body is quite small in
comparison to the workload.
This commit makes Swift reject, with a 416 response, any ranged GET
request with more than fifty ranges, more than three overlapping
ranges, or more than eight non-increasing ranges.
This is a necessary prerequisite for supporting multi-range GETs on
large objects. Otherwise, a malicious user could construct a Range
header with hundreds of byte ranges where each individual byterange
requires the proxy to contact a different object server. If seeking
all over a disk is bad, connecting all over the cluster is way worse.
DocImpact
Change-Id: I4dcedcaae6c3deada06a0223479e611094d57234
2014-08-28 09:39:38 -08:00
|
|
|
|
|
|
|
rng = swift.common.swob.Range(too_many_ranges)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(rng.ranges_for_length(1000000000), [])
|
Reject overly-taxing ranged-GET requests
RFC 7233 says that servers MAY reject egregious range-GET requests
such as requests with hundreds of ranges, requests with non-ascending
ranges, and so on.
Such requests are fairly hard for Swift to process. Consider a Range
header that asks for the first byte of every 10th MiB in a 4 GiB
object, but in some random order. That'll cause a lot of seeks on the
object server, but the corresponding response body is quite small in
comparison to the workload.
This commit makes Swift reject, with a 416 response, any ranged GET
request with more than fifty ranges, more than three overlapping
ranges, or more than eight non-increasing ranges.
This is a necessary prerequisite for supporting multi-range GETs on
large objects. Otherwise, a malicious user could construct a Range
header with hundreds of byte ranges where each individual byterange
requires the proxy to contact a different object server. If seeking
all over a disk is bad, connecting all over the cluster is way worse.
DocImpact
Change-Id: I4dcedcaae6c3deada06a0223479e611094d57234
2014-08-28 09:39:38 -08:00
|
|
|
|
2012-10-03 14:20:52 -07:00
|
|
|
def test_range_invalid_syntax(self):
|
2012-11-01 20:45:11 -04:00
|
|
|
|
2016-04-15 17:22:44 -07:00
|
|
|
def _assert_invalid_range(range_value):
|
2012-11-01 20:45:11 -04:00
|
|
|
try:
|
|
|
|
swift.common.swob.Range(range_value)
|
2016-04-15 17:22:44 -07:00
|
|
|
self.fail("Expected %r to be invalid, but wasn't" %
|
|
|
|
(range_value,))
|
2012-11-01 20:45:11 -04:00
|
|
|
except ValueError:
|
2016-04-15 17:22:44 -07:00
|
|
|
pass
|
2012-11-01 20:45:11 -04:00
|
|
|
|
|
|
|
"""
|
|
|
|
All the following cases should result ValueError exception
|
|
|
|
1. value not starts with bytes=
|
|
|
|
2. range value start is greater than the end, eg. bytes=5-3
|
|
|
|
3. range does not have start or end, eg. bytes=-
|
|
|
|
4. range does not have hyphen, eg. bytes=45
|
|
|
|
5. range value is non numeric
|
|
|
|
6. any combination of the above
|
|
|
|
"""
|
|
|
|
|
2016-04-15 17:22:44 -07:00
|
|
|
_assert_invalid_range('nonbytes=foobar,10-2')
|
|
|
|
_assert_invalid_range('bytes=5-3')
|
|
|
|
_assert_invalid_range('bytes=-')
|
|
|
|
_assert_invalid_range('bytes=45')
|
|
|
|
_assert_invalid_range('bytes=foo-bar,3-5')
|
|
|
|
_assert_invalid_range('bytes=4-10,45')
|
|
|
|
_assert_invalid_range('bytes=foobar,3-5')
|
|
|
|
_assert_invalid_range('bytes=nonumber-5')
|
|
|
|
_assert_invalid_range('bytes=nonumber')
|
|
|
|
_assert_invalid_range('bytes=--1')
|
2012-10-03 14:20:52 -07:00
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
class TestMatch(unittest.TestCase):
|
|
|
|
def test_match(self):
|
|
|
|
match = swift.common.swob.Match('"a", "b"')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(match.tags, set(('a', 'b')))
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('a' in match)
|
|
|
|
self.assertTrue('b' in match)
|
|
|
|
self.assertTrue('c' not in match)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
def test_match_star(self):
|
|
|
|
match = swift.common.swob.Match('"a", "*"')
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('a' in match)
|
|
|
|
self.assertTrue('b' in match)
|
|
|
|
self.assertTrue('c' in match)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
def test_match_noquote(self):
|
|
|
|
match = swift.common.swob.Match('a, b')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(match.tags, set(('a', 'b')))
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('a' in match)
|
|
|
|
self.assertTrue('b' in match)
|
|
|
|
self.assertTrue('c' not in match)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
|
2015-11-17 16:15:59 +09:00
|
|
|
class TestTransferEncoding(unittest.TestCase):
|
|
|
|
def test_is_chunked(self):
|
|
|
|
headers = {}
|
|
|
|
self.assertFalse(swift.common.swob.is_chunked(headers))
|
|
|
|
|
|
|
|
headers['Transfer-Encoding'] = 'chunked'
|
|
|
|
self.assertTrue(swift.common.swob.is_chunked(headers))
|
|
|
|
|
|
|
|
headers['Transfer-Encoding'] = 'gzip,chunked'
|
|
|
|
try:
|
|
|
|
swift.common.swob.is_chunked(headers)
|
|
|
|
except AttributeError as e:
|
|
|
|
self.assertEqual(str(e), "Unsupported Transfer-Coding header"
|
|
|
|
" value specified in Transfer-Encoding header")
|
|
|
|
else:
|
|
|
|
self.fail("Expected an AttributeError raised for 'gzip'")
|
|
|
|
|
|
|
|
headers['Transfer-Encoding'] = 'gzip'
|
|
|
|
try:
|
|
|
|
swift.common.swob.is_chunked(headers)
|
|
|
|
except ValueError as e:
|
|
|
|
self.assertEqual(str(e), "Invalid Transfer-Encoding header value")
|
|
|
|
else:
|
|
|
|
self.fail("Expected a ValueError raised for 'gzip'")
|
|
|
|
|
|
|
|
headers['Transfer-Encoding'] = 'gzip,identity'
|
|
|
|
try:
|
|
|
|
swift.common.swob.is_chunked(headers)
|
|
|
|
except AttributeError as e:
|
|
|
|
self.assertEqual(str(e), "Unsupported Transfer-Coding header"
|
|
|
|
" value specified in Transfer-Encoding header")
|
|
|
|
else:
|
|
|
|
self.fail("Expected an AttributeError raised for 'gzip,identity'")
|
|
|
|
|
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
class TestAccept(unittest.TestCase):
|
|
|
|
def test_accept_json(self):
|
|
|
|
for accept in ('application/json', 'application/json;q=1.0,*/*;q=0.9',
|
2012-11-29 13:29:00 -08:00
|
|
|
'*/*;q=0.9,application/json;q=1.0', 'application/*',
|
|
|
|
'text/*,application/json', 'application/*,text/*',
|
|
|
|
'application/json,text/xml'):
|
2012-09-04 14:02:19 -07:00
|
|
|
acc = swift.common.swob.Accept(accept)
|
|
|
|
match = acc.best_match(['text/plain', 'application/json',
|
2012-11-29 13:29:00 -08:00
|
|
|
'application/xml', 'text/xml'])
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(match, 'application/json')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
def test_accept_plain(self):
|
|
|
|
for accept in ('', 'text/plain', 'application/xml;q=0.8,*/*;q=0.9',
|
|
|
|
'*/*;q=0.9,application/xml;q=0.8', '*/*',
|
|
|
|
'text/plain,application/xml'):
|
|
|
|
acc = swift.common.swob.Accept(accept)
|
|
|
|
match = acc.best_match(['text/plain', 'application/json',
|
2012-11-29 13:29:00 -08:00
|
|
|
'application/xml', 'text/xml'])
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(match, 'text/plain')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
def test_accept_xml(self):
|
|
|
|
for accept in ('application/xml', 'application/xml;q=1.0,*/*;q=0.9',
|
2013-07-18 16:33:01 -07:00
|
|
|
'*/*;q=0.9,application/xml;q=1.0',
|
|
|
|
'application/xml;charset=UTF-8',
|
|
|
|
'application/xml;charset=UTF-8;qws="quoted with space"',
|
|
|
|
'application/xml; q=0.99 ; qws="quoted with space"'):
|
2012-09-04 14:02:19 -07:00
|
|
|
acc = swift.common.swob.Accept(accept)
|
|
|
|
match = acc.best_match(['text/plain', 'application/xml',
|
2012-11-29 13:29:00 -08:00
|
|
|
'text/xml'])
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(match, 'application/xml')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2012-11-29 13:29:00 -08:00
|
|
|
def test_accept_invalid(self):
|
|
|
|
for accept in ('*', 'text/plain,,', 'some stuff',
|
|
|
|
'application/xml;q=1.0;q=1.1', 'text/plain,*',
|
2013-07-18 16:33:01 -07:00
|
|
|
'text /plain', 'text\x7f/plain',
|
|
|
|
'text/plain;a=b=c',
|
|
|
|
'text/plain;q=1;q=2',
|
|
|
|
'text/plain; ubq="unbalanced " quotes"'):
|
2012-11-29 13:29:00 -08:00
|
|
|
acc = swift.common.swob.Accept(accept)
|
|
|
|
match = acc.best_match(['text/plain', 'application/xml',
|
|
|
|
'text/xml'])
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(match, None)
|
2012-11-29 13:29:00 -08:00
|
|
|
|
Rework to support RFC 2616 Sec 4.4 Message Length
RFC 2616 Sec 4.4 Message Length describes how the content-length and
transfer-encoding headers interact. Basically, if chunked transfer
encoding is used, the content-length header value is ignored and if
the content-length header is present, and the request is not using
chunked transfer-encoding, then the content-length must match the body
length.
The only Transfer-Coding value we support in the Transfer-Encoding
header (to date) is "chunked". RFC 2616 Sec 14.41 specifies that if
"multiple encodings have been applied to an entity, the
transfer-codings MUST be listed in the order in which they were
applied." Since we only supported "chunked". If the Transfer-Encoding
header value has multiple transfer-codings, we return a 501 (Not
Implemented) (see RFC 2616 Sec 3.6) without checking if chunked is the
last one specified. Finally, if transfer-encoding is anything but
"chunked", we return a 400 (Bad Request) to the client.
This patch adds a new method, message_length, to the swob request
object which will apply an algorithm based on RFC 2616 Sec 4.4
leveraging the existing content_length property.
In addition to these changes, the proxy server will now notice when
the message length specified by the content-length header is greater
than the configured object maximum size and fail the request with a
413, "Request Entity Too Large", before reading the entire body.
This work flows from https://review.openstack.org/27152.
Change-Id: I5d2a30b89092680dee9d946e1aafd017eaaef8c0
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2013-04-19 00:10:38 -04:00
|
|
|
def test_repr(self):
|
|
|
|
acc = swift.common.swob.Accept("application/json")
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(repr(acc), "application/json")
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2013-07-18 16:33:01 -07:00
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
class TestRequest(unittest.TestCase):
|
|
|
|
def test_blank(self):
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', environ={'REQUEST_METHOD': 'POST'},
|
|
|
|
headers={'Content-Type': 'text/plain'}, body='hi')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.path_info, '/')
|
|
|
|
self.assertEqual(req.body, 'hi')
|
|
|
|
self.assertEqual(req.headers['Content-Type'], 'text/plain')
|
|
|
|
self.assertEqual(req.method, 'POST')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2013-08-28 23:29:18 -07:00
|
|
|
def test_blank_req_environ_property_args(self):
|
|
|
|
blank = swift.common.swob.Request.blank
|
|
|
|
req = blank('/', method='PATCH')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.method, 'PATCH')
|
|
|
|
self.assertEqual(req.environ['REQUEST_METHOD'], 'PATCH')
|
2013-08-28 23:29:18 -07:00
|
|
|
req = blank('/', referer='http://example.com')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.referer, 'http://example.com')
|
|
|
|
self.assertEqual(req.referrer, 'http://example.com')
|
|
|
|
self.assertEqual(req.environ['HTTP_REFERER'], 'http://example.com')
|
|
|
|
self.assertEqual(req.headers['Referer'], 'http://example.com')
|
2013-08-28 23:29:18 -07:00
|
|
|
req = blank('/', script_name='/application')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.script_name, '/application')
|
|
|
|
self.assertEqual(req.environ['SCRIPT_NAME'], '/application')
|
2013-08-28 23:29:18 -07:00
|
|
|
req = blank('/', host='www.example.com')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.host, 'www.example.com')
|
|
|
|
self.assertEqual(req.environ['HTTP_HOST'], 'www.example.com')
|
|
|
|
self.assertEqual(req.headers['Host'], 'www.example.com')
|
2013-08-28 23:29:18 -07:00
|
|
|
req = blank('/', remote_addr='127.0.0.1')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.remote_addr, '127.0.0.1')
|
|
|
|
self.assertEqual(req.environ['REMOTE_ADDR'], '127.0.0.1')
|
2013-08-28 23:29:18 -07:00
|
|
|
req = blank('/', remote_user='username')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.remote_user, 'username')
|
|
|
|
self.assertEqual(req.environ['REMOTE_USER'], 'username')
|
2013-08-28 23:29:18 -07:00
|
|
|
req = blank('/', user_agent='curl/7.22.0 (x86_64-pc-linux-gnu)')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.user_agent, 'curl/7.22.0 (x86_64-pc-linux-gnu)')
|
|
|
|
self.assertEqual(req.environ['HTTP_USER_AGENT'],
|
|
|
|
'curl/7.22.0 (x86_64-pc-linux-gnu)')
|
|
|
|
self.assertEqual(req.headers['User-Agent'],
|
|
|
|
'curl/7.22.0 (x86_64-pc-linux-gnu)')
|
2013-08-28 23:29:18 -07:00
|
|
|
req = blank('/', query_string='a=b&c=d')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.query_string, 'a=b&c=d')
|
|
|
|
self.assertEqual(req.environ['QUERY_STRING'], 'a=b&c=d')
|
2013-08-28 23:29:18 -07:00
|
|
|
req = blank('/', if_match='*')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.environ['HTTP_IF_MATCH'], '*')
|
|
|
|
self.assertEqual(req.headers['If-Match'], '*')
|
2013-08-28 23:29:18 -07:00
|
|
|
|
|
|
|
# multiple environ property kwargs
|
|
|
|
req = blank('/', method='PATCH', referer='http://example.com',
|
|
|
|
script_name='/application', host='www.example.com',
|
|
|
|
remote_addr='127.0.0.1', remote_user='username',
|
|
|
|
user_agent='curl/7.22.0 (x86_64-pc-linux-gnu)',
|
|
|
|
query_string='a=b&c=d', if_match='*')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.method, 'PATCH')
|
|
|
|
self.assertEqual(req.referer, 'http://example.com')
|
|
|
|
self.assertEqual(req.script_name, '/application')
|
|
|
|
self.assertEqual(req.host, 'www.example.com')
|
|
|
|
self.assertEqual(req.remote_addr, '127.0.0.1')
|
|
|
|
self.assertEqual(req.remote_user, 'username')
|
|
|
|
self.assertEqual(req.user_agent, 'curl/7.22.0 (x86_64-pc-linux-gnu)')
|
|
|
|
self.assertEqual(req.query_string, 'a=b&c=d')
|
|
|
|
self.assertEqual(req.environ['QUERY_STRING'], 'a=b&c=d')
|
2013-08-28 23:29:18 -07:00
|
|
|
|
|
|
|
def test_invalid_req_environ_property_args(self):
|
|
|
|
# getter only property
|
|
|
|
try:
|
2015-02-18 11:59:31 +05:30
|
|
|
swift.common.swob.Request.blank(
|
|
|
|
'/', host_url='http://example.com:8080/v1/a/c/o')
|
2013-08-28 23:29:18 -07:00
|
|
|
except TypeError as e:
|
2015-02-18 11:59:31 +05:30
|
|
|
self.assertEqual("got unexpected keyword argument 'host_url'",
|
2015-08-05 23:58:14 +05:30
|
|
|
str(e))
|
2013-08-28 23:29:18 -07:00
|
|
|
else:
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue(False, "invalid req_environ_property "
|
|
|
|
"didn't raise error!")
|
2013-08-28 23:29:18 -07:00
|
|
|
# regular attribute
|
|
|
|
try:
|
2013-08-31 23:13:15 -04:00
|
|
|
swift.common.swob.Request.blank('/', _params_cache={'a': 'b'})
|
2013-08-28 23:29:18 -07:00
|
|
|
except TypeError as e:
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual("got unexpected keyword "
|
|
|
|
"argument '_params_cache'", str(e))
|
2013-08-28 23:29:18 -07:00
|
|
|
else:
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue(False, "invalid req_environ_property "
|
|
|
|
"didn't raise error!")
|
2014-04-25 19:54:49 -07:00
|
|
|
# non-existent attribute
|
2013-08-28 23:29:18 -07:00
|
|
|
try:
|
2013-08-31 23:13:15 -04:00
|
|
|
swift.common.swob.Request.blank('/', params_cache={'a': 'b'})
|
2013-08-28 23:29:18 -07:00
|
|
|
except TypeError as e:
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual("got unexpected keyword "
|
|
|
|
"argument 'params_cache'", str(e))
|
2013-08-28 23:29:18 -07:00
|
|
|
else:
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue(False, "invalid req_environ_property "
|
|
|
|
"didn't raise error!")
|
2013-08-28 23:29:18 -07:00
|
|
|
# method
|
|
|
|
try:
|
2013-08-31 23:13:15 -04:00
|
|
|
swift.common.swob.Request.blank(
|
2013-08-28 23:29:18 -07:00
|
|
|
'/', as_referer='GET http://example.com')
|
|
|
|
except TypeError as e:
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual("got unexpected keyword "
|
|
|
|
"argument 'as_referer'", str(e))
|
2013-08-28 23:29:18 -07:00
|
|
|
else:
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue(False, "invalid req_environ_property "
|
|
|
|
"didn't raise error!")
|
2013-08-28 23:29:18 -07:00
|
|
|
|
|
|
|
def test_blank_path_info_precedence(self):
|
|
|
|
blank = swift.common.swob.Request.blank
|
|
|
|
req = blank('/a')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.path_info, '/a')
|
2013-08-28 23:29:18 -07:00
|
|
|
req = blank('/a', environ={'PATH_INFO': '/a/c'})
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.path_info, '/a/c')
|
2013-08-28 23:29:18 -07:00
|
|
|
req = blank('/a', environ={'PATH_INFO': '/a/c'}, path_info='/a/c/o')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.path_info, '/a/c/o')
|
2013-08-28 23:29:18 -07:00
|
|
|
req = blank('/a', path_info='/a/c/o')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.path_info, '/a/c/o')
|
2013-08-28 23:29:18 -07:00
|
|
|
|
2012-10-08 15:45:40 -07:00
|
|
|
def test_blank_body_precedence(self):
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', environ={'REQUEST_METHOD': 'POST',
|
2015-05-27 18:01:37 +02:00
|
|
|
'wsgi.input': BytesIO(b'')},
|
2012-10-08 15:45:40 -07:00
|
|
|
headers={'Content-Type': 'text/plain'}, body='hi')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.path_info, '/')
|
|
|
|
self.assertEqual(req.body, 'hi')
|
|
|
|
self.assertEqual(req.headers['Content-Type'], 'text/plain')
|
|
|
|
self.assertEqual(req.method, 'POST')
|
2015-05-27 18:01:37 +02:00
|
|
|
body_file = BytesIO(b'asdf')
|
2013-08-28 23:29:18 -07:00
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', environ={'REQUEST_METHOD': 'POST',
|
2015-05-27 18:01:37 +02:00
|
|
|
'wsgi.input': BytesIO(b'')},
|
2013-08-28 23:29:18 -07:00
|
|
|
headers={'Content-Type': 'text/plain'}, body='hi',
|
|
|
|
body_file=body_file)
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue(req.body_file is body_file)
|
2013-08-28 23:29:18 -07:00
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', environ={'REQUEST_METHOD': 'POST',
|
2015-05-27 18:01:37 +02:00
|
|
|
'wsgi.input': BytesIO(b'')},
|
2013-08-28 23:29:18 -07:00
|
|
|
headers={'Content-Type': 'text/plain'}, body='hi',
|
|
|
|
content_length=3)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.content_length, 3)
|
|
|
|
self.assertEqual(len(req.body), 2)
|
2012-10-08 15:45:40 -07:00
|
|
|
|
2013-01-16 10:00:18 -08:00
|
|
|
def test_blank_parsing(self):
|
|
|
|
req = swift.common.swob.Request.blank('http://test.com/')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.environ['wsgi.url_scheme'], 'http')
|
|
|
|
self.assertEqual(req.environ['SERVER_PORT'], '80')
|
|
|
|
self.assertEqual(req.environ['SERVER_NAME'], 'test.com')
|
2013-01-16 10:00:18 -08:00
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank('https://test.com:456/')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.environ['wsgi.url_scheme'], 'https')
|
|
|
|
self.assertEqual(req.environ['SERVER_PORT'], '456')
|
2013-01-16 10:00:18 -08:00
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank('test.com/')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.environ['wsgi.url_scheme'], 'http')
|
|
|
|
self.assertEqual(req.environ['SERVER_PORT'], '80')
|
|
|
|
self.assertEqual(req.environ['PATH_INFO'], 'test.com/')
|
2013-01-16 10:00:18 -08:00
|
|
|
|
|
|
|
self.assertRaises(TypeError, swift.common.swob.Request.blank,
|
|
|
|
'ftp://test.com/')
|
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
def test_params(self):
|
|
|
|
req = swift.common.swob.Request.blank('/?a=b&c=d')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.params['a'], 'b')
|
|
|
|
self.assertEqual(req.params['c'], 'd')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2015-02-18 11:59:31 +05:30
|
|
|
new_params = {'e': 'f', 'g': 'h'}
|
|
|
|
req.params = new_params
|
|
|
|
self.assertDictEqual(new_params, req.params)
|
|
|
|
|
|
|
|
new_params = (('i', 'j'), ('k', 'l'))
|
|
|
|
req.params = new_params
|
|
|
|
self.assertDictEqual(dict(new_params), req.params)
|
|
|
|
|
2014-06-10 22:17:47 -07:00
|
|
|
def test_timestamp_missing(self):
|
|
|
|
req = swift.common.swob.Request.blank('/')
|
|
|
|
self.assertRaises(exceptions.InvalidTimestamp,
|
|
|
|
getattr, req, 'timestamp')
|
|
|
|
|
|
|
|
def test_timestamp_invalid(self):
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'X-Timestamp': 'asdf'})
|
|
|
|
self.assertRaises(exceptions.InvalidTimestamp,
|
|
|
|
getattr, req, 'timestamp')
|
|
|
|
|
|
|
|
def test_timestamp(self):
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'X-Timestamp': '1402447134.13507_00000001'})
|
|
|
|
expected = utils.Timestamp('1402447134.13507', offset=1)
|
|
|
|
self.assertEqual(req.timestamp, expected)
|
|
|
|
self.assertEqual(req.timestamp.normal, expected.normal)
|
|
|
|
self.assertEqual(req.timestamp.internal, expected.internal)
|
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
def test_path(self):
|
|
|
|
req = swift.common.swob.Request.blank('/hi?a=b&c=d')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.path, '/hi')
|
2012-09-04 14:02:19 -07:00
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', environ={'SCRIPT_NAME': '/hi', 'PATH_INFO': '/there'})
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.path, '/hi/there')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2012-11-30 21:31:21 +00:00
|
|
|
def test_path_question_mark(self):
|
|
|
|
req = swift.common.swob.Request.blank('/test%3Ffile')
|
|
|
|
# This tests that .blank unquotes the path when setting PATH_INFO
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.environ['PATH_INFO'], '/test?file')
|
2012-11-30 21:31:21 +00:00
|
|
|
# This tests that .path requotes it
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.path, '/test%3Ffile')
|
2012-11-30 21:31:21 +00:00
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
def test_path_info_pop(self):
|
|
|
|
req = swift.common.swob.Request.blank('/hi/there')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.path_info_pop(), 'hi')
|
|
|
|
self.assertEqual(req.path_info, '/there')
|
|
|
|
self.assertEqual(req.script_name, '/hi')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
def test_bad_path_info_pop(self):
|
|
|
|
req = swift.common.swob.Request.blank('blahblah')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.path_info_pop(), None)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2012-11-28 00:08:26 +00:00
|
|
|
def test_path_info_pop_last(self):
|
|
|
|
req = swift.common.swob.Request.blank('/last')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.path_info_pop(), 'last')
|
|
|
|
self.assertEqual(req.path_info, '')
|
|
|
|
self.assertEqual(req.script_name, '/last')
|
2012-11-28 00:08:26 +00:00
|
|
|
|
|
|
|
def test_path_info_pop_none(self):
|
|
|
|
req = swift.common.swob.Request.blank('/')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.path_info_pop(), '')
|
|
|
|
self.assertEqual(req.path_info, '')
|
|
|
|
self.assertEqual(req.script_name, '/')
|
2012-11-28 00:08:26 +00:00
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
def test_copy_get(self):
|
2012-10-08 15:45:40 -07:00
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/hi/there', environ={'REQUEST_METHOD': 'POST'})
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.method, 'POST')
|
2012-09-04 14:02:19 -07:00
|
|
|
req2 = req.copy_get()
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req2.method, 'GET')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
def test_get_response(self):
|
|
|
|
def test_app(environ, start_response):
|
|
|
|
start_response('200 OK', [])
|
|
|
|
return ['hi']
|
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank('/')
|
|
|
|
resp = req.get_response(test_app)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 200)
|
|
|
|
self.assertEqual(resp.body, 'hi')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2013-08-23 15:03:08 +01:00
|
|
|
def test_401_unauthorized(self):
|
|
|
|
# No request environment
|
|
|
|
resp = swift.common.swob.HTTPUnauthorized()
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 401)
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Www-Authenticate' in resp.headers)
|
2013-08-23 15:03:08 +01:00
|
|
|
# Request environment
|
|
|
|
req = swift.common.swob.Request.blank('/')
|
|
|
|
resp = swift.common.swob.HTTPUnauthorized(request=req)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 401)
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Www-Authenticate' in resp.headers)
|
2013-08-23 15:03:08 +01:00
|
|
|
|
|
|
|
def test_401_valid_account_path(self):
|
|
|
|
|
|
|
|
def test_app(environ, start_response):
|
|
|
|
start_response('401 Unauthorized', [])
|
|
|
|
return ['hi']
|
|
|
|
|
|
|
|
# Request environment contains valid account in path
|
|
|
|
req = swift.common.swob.Request.blank('/v1/account-name')
|
|
|
|
resp = req.get_response(test_app)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 401)
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Www-Authenticate' in resp.headers)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual('Swift realm="account-name"',
|
|
|
|
resp.headers['Www-Authenticate'])
|
2013-08-23 15:03:08 +01:00
|
|
|
|
|
|
|
# Request environment contains valid account/container in path
|
|
|
|
req = swift.common.swob.Request.blank('/v1/account-name/c')
|
|
|
|
resp = req.get_response(test_app)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 401)
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Www-Authenticate' in resp.headers)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual('Swift realm="account-name"',
|
|
|
|
resp.headers['Www-Authenticate'])
|
2013-08-23 15:03:08 +01:00
|
|
|
|
|
|
|
def test_401_invalid_path(self):
|
|
|
|
|
|
|
|
def test_app(environ, start_response):
|
|
|
|
start_response('401 Unauthorized', [])
|
|
|
|
return ['hi']
|
|
|
|
|
|
|
|
# Request environment contains bad path
|
|
|
|
req = swift.common.swob.Request.blank('/random')
|
|
|
|
resp = req.get_response(test_app)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 401)
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Www-Authenticate' in resp.headers)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual('Swift realm="unknown"',
|
|
|
|
resp.headers['Www-Authenticate'])
|
2013-08-23 15:03:08 +01:00
|
|
|
|
|
|
|
def test_401_non_keystone_auth_path(self):
|
|
|
|
|
|
|
|
def test_app(environ, start_response):
|
|
|
|
start_response('401 Unauthorized', [])
|
|
|
|
return ['no creds in request']
|
|
|
|
|
|
|
|
# Request to get token
|
|
|
|
req = swift.common.swob.Request.blank('/v1.0/auth')
|
|
|
|
resp = req.get_response(test_app)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 401)
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Www-Authenticate' in resp.headers)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual('Swift realm="unknown"',
|
|
|
|
resp.headers['Www-Authenticate'])
|
2013-08-23 15:03:08 +01:00
|
|
|
|
|
|
|
# Other form of path
|
|
|
|
req = swift.common.swob.Request.blank('/auth/v1.0')
|
|
|
|
resp = req.get_response(test_app)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 401)
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Www-Authenticate' in resp.headers)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual('Swift realm="unknown"',
|
|
|
|
resp.headers['Www-Authenticate'])
|
2013-08-23 15:03:08 +01:00
|
|
|
|
|
|
|
def test_401_www_authenticate_exists(self):
|
|
|
|
|
|
|
|
def test_app(environ, start_response):
|
|
|
|
start_response('401 Unauthorized', {
|
|
|
|
'Www-Authenticate': 'Me realm="whatever"'})
|
|
|
|
return ['no creds in request']
|
|
|
|
|
|
|
|
# Auth middleware sets own Www-Authenticate
|
|
|
|
req = swift.common.swob.Request.blank('/auth/v1.0')
|
|
|
|
resp = req.get_response(test_app)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 401)
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Www-Authenticate' in resp.headers)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual('Me realm="whatever"',
|
|
|
|
resp.headers['Www-Authenticate'])
|
2013-08-23 15:03:08 +01:00
|
|
|
|
2014-06-06 11:46:41 -07:00
|
|
|
def test_401_www_authenticate_is_quoted(self):
|
|
|
|
|
|
|
|
def test_app(environ, start_response):
|
|
|
|
start_response('401 Unauthorized', [])
|
|
|
|
return ['hi']
|
|
|
|
|
|
|
|
hacker = 'account-name\n\n<b>foo<br>' # url injection test
|
|
|
|
quoted_hacker = quote(hacker)
|
|
|
|
req = swift.common.swob.Request.blank('/v1/' + hacker)
|
|
|
|
resp = req.get_response(test_app)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 401)
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Www-Authenticate' in resp.headers)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual('Swift realm="%s"' % quoted_hacker,
|
|
|
|
resp.headers['Www-Authenticate'])
|
2014-06-06 11:46:41 -07:00
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank('/v1/' + quoted_hacker)
|
|
|
|
resp = req.get_response(test_app)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 401)
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Www-Authenticate' in resp.headers)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual('Swift realm="%s"' % quoted_hacker,
|
|
|
|
resp.headers['Www-Authenticate'])
|
2014-06-06 11:46:41 -07:00
|
|
|
|
2013-08-23 15:03:08 +01:00
|
|
|
def test_not_401(self):
|
|
|
|
|
|
|
|
# Other status codes should not have WWW-Authenticate in response
|
|
|
|
def test_app(environ, start_response):
|
|
|
|
start_response('200 OK', [])
|
|
|
|
return ['hi']
|
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank('/')
|
|
|
|
resp = req.get_response(test_app)
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Www-Authenticate' not in resp.headers)
|
2013-08-23 15:03:08 +01:00
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
def test_properties(self):
|
|
|
|
req = swift.common.swob.Request.blank('/hi/there', body='hi')
|
|
|
|
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.body, 'hi')
|
|
|
|
self.assertEqual(req.content_length, 2)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
req.remote_addr = 'something'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.environ['REMOTE_ADDR'], 'something')
|
2012-09-04 14:02:19 -07:00
|
|
|
req.body = 'whatever'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.content_length, 8)
|
|
|
|
self.assertEqual(req.body, 'whatever')
|
|
|
|
self.assertEqual(req.method, 'GET')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
req.range = 'bytes=1-7'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.range.ranges[0], (1, 7))
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Range' in req.headers)
|
2012-09-04 14:02:19 -07:00
|
|
|
req.range = None
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Range' not in req.headers)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
def test_datetime_properties(self):
|
|
|
|
req = swift.common.swob.Request.blank('/hi/there', body='hi')
|
|
|
|
|
|
|
|
req.if_unmodified_since = 0
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue(isinstance(req.if_unmodified_since, datetime.datetime))
|
2012-09-04 14:02:19 -07:00
|
|
|
if_unmodified_since = req.if_unmodified_since
|
|
|
|
req.if_unmodified_since = if_unmodified_since
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(if_unmodified_since, req.if_unmodified_since)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
req.if_unmodified_since = 'something'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.headers['If-Unmodified-Since'], 'something')
|
|
|
|
self.assertEqual(req.if_unmodified_since, None)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('If-Unmodified-Since' in req.headers)
|
2012-09-04 14:02:19 -07:00
|
|
|
req.if_unmodified_since = None
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('If-Unmodified-Since' not in req.headers)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
Allow pre-1970 dates in If-[Un]Modified-Since
If I want to fetch an object only if it is newer than the first moon
landing, I send a GET request with header:
If-Modified-Since: Sun, 20 Jul 1969 20:18:00 UTC
Since that date is older than Swift, I expect a 2xx response. However,
I get a 412, which isn't even a valid thing to do for
If-Modified-Since; it should either be 2xx or 304. This is because of
two problems:
(a) Swift treats pre-1970 dates as invalid, and
(b) Swift returns 412 when a date is invalid instead of ignoring it.
This commit makes it so any time between datetime.datetime.min and
datetime.datetime.max is an acceptable value for If-Modified-Since and
If-Unmodified-Since. Dates outside that date range are treated as
invalid headers and thus are ignored, as RFC 2616 section 14.28
requires ("If the specified date is invalid, the header is ignored").
This only works for dates that the Python standard library can parse,
which on my machine is 01 Jan 1 to 31 Dec 9999. Eliminating those
restrictions would require implementing our own date parsing and
comparison, and that's almost certainly not worth it.
Change-Id: I4cb4903c4e5e3b6b3c9506c2cabbfbda62e82f35
2014-03-03 11:25:43 -08:00
|
|
|
too_big_date_list = list(datetime.datetime.max.timetuple())
|
|
|
|
too_big_date_list[0] += 1 # bump up the year
|
|
|
|
too_big_date = time.strftime(
|
|
|
|
"%a, %d %b %Y %H:%M:%S UTC", time.struct_time(too_big_date_list))
|
|
|
|
|
|
|
|
req.if_unmodified_since = too_big_date
|
|
|
|
self.assertEqual(req.if_unmodified_since, None)
|
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
def test_bad_range(self):
|
|
|
|
req = swift.common.swob.Request.blank('/hi/there', body='hi')
|
|
|
|
req.range = 'bad range'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.range, None)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2012-10-12 16:46:48 +04:00
|
|
|
def test_accept_header(self):
|
|
|
|
req = swift.common.swob.Request({'REQUEST_METHOD': 'GET',
|
|
|
|
'PATH_INFO': '/',
|
|
|
|
'HTTP_ACCEPT': 'application/json'})
|
|
|
|
self.assertEqual(
|
|
|
|
req.accept.best_match(['application/json', 'text/plain']),
|
|
|
|
'application/json')
|
|
|
|
self.assertEqual(
|
|
|
|
req.accept.best_match(['text/plain', 'application/json']),
|
|
|
|
'application/json')
|
|
|
|
|
Stop mutating PATH_INFO in proxy server
The proxy server was calling swob.Request.path_info_pop() prior to
instantiating a controller so that req.path_info was just /a/c/o (sans
/v1). The version got moved over into SCRIPT_NAME.
This lead to some unfortunate behavior when trying to re-use a request
from middleware. Something like this:
# Imagine we're a WSGIContext object here.
#
# To start, SCRIPT_NAME = '' and PATH_INFO='/v1/a/c/o'
resp_iter = self._app_call(env, start_response)
# Now SCRIPT_NAME='/v1' and PATH_INFO ='/a/c/o'
if something_special in self._response_headers:
env['REQUEST_METHOD'] = 'GET'
env.pop('HTTP_RANGE', None)
# 404 SURPRISE! The proxy calls path_info_pop() again,
# and now SCRIPT_NAME='/v1/a' and PATH_INFO='/c/o', so this
# gets treated as a container request. Yikes.
resp_iter = self._app_call(env, start_response)
Now we just leave SCRIPT_NAME and PATH_INFO alone. To make life easy
for everyone who does want just /a/c/o, I defined
swob.Request.swift_entity_path, which just strips off the /v1.
Note that there's still one call to path_info_pop() in tempauth, but
that's only for requests going to /auth, so it won't affect Swift API
requests. It might be a good idea to remove that one later, but let's
do one thing at a time.
Change-Id: I87557a11c01f3f3889b610578cda6ba7d3933e7a
2013-12-03 22:18:46 -08:00
|
|
|
def test_swift_entity_path(self):
|
|
|
|
req = swift.common.swob.Request.blank('/v1/a/c/o')
|
|
|
|
self.assertEqual(req.swift_entity_path, '/a/c/o')
|
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank('/v1/a/c')
|
|
|
|
self.assertEqual(req.swift_entity_path, '/a/c')
|
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank('/v1/a')
|
|
|
|
self.assertEqual(req.swift_entity_path, '/a')
|
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank('/v1')
|
|
|
|
self.assertEqual(req.swift_entity_path, None)
|
|
|
|
|
2012-10-15 19:00:36 +04:00
|
|
|
def test_path_qs(self):
|
|
|
|
req = swift.common.swob.Request.blank('/hi/there?hello=equal&acl')
|
|
|
|
self.assertEqual(req.path_qs, '/hi/there?hello=equal&acl')
|
|
|
|
|
|
|
|
req = swift.common.swob.Request({'PATH_INFO': '/hi/there',
|
|
|
|
'QUERY_STRING': 'hello=equal&acl'})
|
|
|
|
self.assertEqual(req.path_qs, '/hi/there?hello=equal&acl')
|
|
|
|
|
Rework to support RFC 2616 Sec 4.4 Message Length
RFC 2616 Sec 4.4 Message Length describes how the content-length and
transfer-encoding headers interact. Basically, if chunked transfer
encoding is used, the content-length header value is ignored and if
the content-length header is present, and the request is not using
chunked transfer-encoding, then the content-length must match the body
length.
The only Transfer-Coding value we support in the Transfer-Encoding
header (to date) is "chunked". RFC 2616 Sec 14.41 specifies that if
"multiple encodings have been applied to an entity, the
transfer-codings MUST be listed in the order in which they were
applied." Since we only supported "chunked". If the Transfer-Encoding
header value has multiple transfer-codings, we return a 501 (Not
Implemented) (see RFC 2616 Sec 3.6) without checking if chunked is the
last one specified. Finally, if transfer-encoding is anything but
"chunked", we return a 400 (Bad Request) to the client.
This patch adds a new method, message_length, to the swob request
object which will apply an algorithm based on RFC 2616 Sec 4.4
leveraging the existing content_length property.
In addition to these changes, the proxy server will now notice when
the message length specified by the content-length header is greater
than the configured object maximum size and fail the request with a
413, "Request Entity Too Large", before reading the entire body.
This work flows from https://review.openstack.org/27152.
Change-Id: I5d2a30b89092680dee9d946e1aafd017eaaef8c0
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2013-04-19 00:10:38 -04:00
|
|
|
def test_url(self):
|
|
|
|
req = swift.common.swob.Request.blank('/hi/there?hello=equal&acl')
|
|
|
|
self.assertEqual(req.url,
|
|
|
|
'http://localhost/hi/there?hello=equal&acl')
|
|
|
|
|
2013-01-24 15:21:41 -08:00
|
|
|
def test_wsgify(self):
|
|
|
|
used_req = []
|
|
|
|
|
|
|
|
@swift.common.swob.wsgify
|
|
|
|
def _wsgi_func(req):
|
|
|
|
used_req.append(req)
|
|
|
|
return swift.common.swob.Response('200 OK')
|
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank('/hi/there')
|
|
|
|
resp = req.get_response(_wsgi_func)
|
|
|
|
self.assertEqual(used_req[0].path, '/hi/there')
|
|
|
|
self.assertEqual(resp.status_int, 200)
|
|
|
|
|
|
|
|
def test_wsgify_raise(self):
|
|
|
|
used_req = []
|
|
|
|
|
|
|
|
@swift.common.swob.wsgify
|
|
|
|
def _wsgi_func(req):
|
|
|
|
used_req.append(req)
|
|
|
|
raise swift.common.swob.HTTPServerError()
|
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank('/hi/there')
|
|
|
|
resp = req.get_response(_wsgi_func)
|
|
|
|
self.assertEqual(used_req[0].path, '/hi/there')
|
|
|
|
self.assertEqual(resp.status_int, 500)
|
|
|
|
|
|
|
|
def test_split_path(self):
|
|
|
|
"""
|
|
|
|
Copied from swift.common.utils.split_path
|
|
|
|
"""
|
|
|
|
def _test_split_path(path, minsegs=1, maxsegs=None, rwl=False):
|
|
|
|
req = swift.common.swob.Request.blank(path)
|
|
|
|
return req.split_path(minsegs, maxsegs, rwl)
|
|
|
|
self.assertRaises(ValueError, _test_split_path, '')
|
|
|
|
self.assertRaises(ValueError, _test_split_path, '/')
|
|
|
|
self.assertRaises(ValueError, _test_split_path, '//')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(_test_split_path('/a'), ['a'])
|
2013-01-24 15:21:41 -08:00
|
|
|
self.assertRaises(ValueError, _test_split_path, '//a')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(_test_split_path('/a/'), ['a'])
|
2013-01-24 15:21:41 -08:00
|
|
|
self.assertRaises(ValueError, _test_split_path, '/a/c')
|
|
|
|
self.assertRaises(ValueError, _test_split_path, '//c')
|
|
|
|
self.assertRaises(ValueError, _test_split_path, '/a/c/')
|
|
|
|
self.assertRaises(ValueError, _test_split_path, '/a//')
|
|
|
|
self.assertRaises(ValueError, _test_split_path, '/a', 2)
|
|
|
|
self.assertRaises(ValueError, _test_split_path, '/a', 2, 3)
|
|
|
|
self.assertRaises(ValueError, _test_split_path, '/a', 2, 3, True)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(_test_split_path('/a/c', 2), ['a', 'c'])
|
|
|
|
self.assertEqual(_test_split_path('/a/c/o', 3), ['a', 'c', 'o'])
|
2013-01-24 15:21:41 -08:00
|
|
|
self.assertRaises(ValueError, _test_split_path, '/a/c/o/r', 3, 3)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(_test_split_path('/a/c/o/r', 3, 3, True),
|
|
|
|
['a', 'c', 'o/r'])
|
|
|
|
self.assertEqual(_test_split_path('/a/c', 2, 3, True),
|
|
|
|
['a', 'c', None])
|
2013-01-24 15:21:41 -08:00
|
|
|
self.assertRaises(ValueError, _test_split_path, '/a', 5, 4)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(_test_split_path('/a/c/', 2), ['a', 'c'])
|
|
|
|
self.assertEqual(_test_split_path('/a/c/', 2, 3), ['a', 'c', ''])
|
2013-01-24 15:21:41 -08:00
|
|
|
try:
|
|
|
|
_test_split_path('o\nn e', 2)
|
2013-08-28 21:16:08 +02:00
|
|
|
except ValueError as err:
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(str(err), 'Invalid path: o%0An%20e')
|
2013-01-24 15:21:41 -08:00
|
|
|
try:
|
|
|
|
_test_split_path('o\nn e', 2, 3, True)
|
2013-08-28 21:16:08 +02:00
|
|
|
except ValueError as err:
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(str(err), 'Invalid path: o%0An%20e')
|
2013-01-24 15:21:41 -08:00
|
|
|
|
2013-02-13 12:31:55 -08:00
|
|
|
def test_unicode_path(self):
|
|
|
|
req = swift.common.swob.Request.blank(u'/\u2661')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.path, quote(u'/\u2661'.encode('utf-8')))
|
2013-02-13 12:31:55 -08:00
|
|
|
|
|
|
|
def test_unicode_query(self):
|
|
|
|
req = swift.common.swob.Request.blank(u'/')
|
|
|
|
req.query_string = u'x=\u2661'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.params['x'], u'\u2661'.encode('utf-8'))
|
2013-01-24 15:21:41 -08:00
|
|
|
|
2013-07-23 16:41:45 -07:00
|
|
|
def test_url2(self):
|
Enhance log msg to report referer and user-agent
Enhance internally logged messages to report referer and user-agent.
Pass the referering URL and METHOD between internal servers (when
known), and set the user-agent to be the server type (obj-server,
container-server, proxy-server, obj-updater, obj-replicator,
container-updater, direct-client, etc.) with the process PID. In
conjunction with the transaction ID, it helps to track down which PID
from a given system was responsible for initiating the request and
what that server was working on to make this request.
This has been helpful in tracking down interactions between object,
container and account servers.
We also take things a bit further performaing a bit of refactoring to
consolidate calls to transfer_headers() now that we have a helper
method for constructing them.
Finally we performed further changes to avoid header key duplication
due to string literal header key values and the various objects
representing headers for requests and responses. See below for more
details.
====
Header Keys
There seems to be a bit of a problem with the case of the various
string literals used for header keys and the interchangable way
standard Python dictionaries, HeaderKeyDict() and HeaderEnvironProxy()
objects are used.
If one is not careful, a header object of some sort (one that does not
normalize its keys, and that is not necessarily a dictionary) can be
constructed containing header keys which differ only by the case of
their string literals. E.g.:
{ 'x-trans-id': '1234', 'X-Trans-Id': '5678' }
Such an object, when passed to http_connect() will result in an
on-the-wire header where the key values are merged together, comma
separated, that looks something like:
HTTP_X_TRANS_ID: 1234,5678
For some headers in some contexts, this is behavior is desirable. For
example, one can also use a list of tuples which enumerate the multiple
values a single header should have.
However, in almost all of the contexts used in the code base, this is
not desirable.
This behavior arises from a combination of factors:
1. Header strings are not constants and different lower-case and
title-case header strings values are used interchangably in the
code at times
It might be worth the effort to make a pass through the code to
stop using string literals and use constants instead, but there
are plusses and minuses to doing that, so this was not attempted
in this effort
2. HeaderEnvironProxy() objects report their keys in ".title()"
case, but normalize all other key references to the form
expected by the Request class's environ field
swob.Request.headers fields are HeaderEnvironProxy() objects.
3. HeaderKeyDict() objects report their keys in ".lower()" case,
and normalize all other key references to ".lower()" case
swob.Response.headers fields are HeaderKeyDict() objects.
Depending on which object is used and how it is used, one can end up
with such a mismatch.
This commit takes the following steps as a (PROPOSED) solution:
1. Change HeaderKeyDict() to normalize using ".title()" case to
match HeaderEnvironProxy()
2. Replace standard python dictionary objects with HeaderKeyDict()
objects where possible
This gives us an object that normalizes key references to avoid
fixing the code to normalize the string literals.
3. Fix up a few places to use title case string literals to match
the new defaults
Change-Id: Ied56a1df83ffac793ee85e796424d7d20f18f469
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2012-11-15 16:34:45 -05:00
|
|
|
pi = '/hi/there'
|
|
|
|
path = pi
|
|
|
|
req = swift.common.swob.Request.blank(path)
|
|
|
|
sche = 'http'
|
2013-09-07 10:14:00 +02:00
|
|
|
exp_url = '%s://localhost%s' % (sche, pi)
|
Enhance log msg to report referer and user-agent
Enhance internally logged messages to report referer and user-agent.
Pass the referering URL and METHOD between internal servers (when
known), and set the user-agent to be the server type (obj-server,
container-server, proxy-server, obj-updater, obj-replicator,
container-updater, direct-client, etc.) with the process PID. In
conjunction with the transaction ID, it helps to track down which PID
from a given system was responsible for initiating the request and
what that server was working on to make this request.
This has been helpful in tracking down interactions between object,
container and account servers.
We also take things a bit further performaing a bit of refactoring to
consolidate calls to transfer_headers() now that we have a helper
method for constructing them.
Finally we performed further changes to avoid header key duplication
due to string literal header key values and the various objects
representing headers for requests and responses. See below for more
details.
====
Header Keys
There seems to be a bit of a problem with the case of the various
string literals used for header keys and the interchangable way
standard Python dictionaries, HeaderKeyDict() and HeaderEnvironProxy()
objects are used.
If one is not careful, a header object of some sort (one that does not
normalize its keys, and that is not necessarily a dictionary) can be
constructed containing header keys which differ only by the case of
their string literals. E.g.:
{ 'x-trans-id': '1234', 'X-Trans-Id': '5678' }
Such an object, when passed to http_connect() will result in an
on-the-wire header where the key values are merged together, comma
separated, that looks something like:
HTTP_X_TRANS_ID: 1234,5678
For some headers in some contexts, this is behavior is desirable. For
example, one can also use a list of tuples which enumerate the multiple
values a single header should have.
However, in almost all of the contexts used in the code base, this is
not desirable.
This behavior arises from a combination of factors:
1. Header strings are not constants and different lower-case and
title-case header strings values are used interchangably in the
code at times
It might be worth the effort to make a pass through the code to
stop using string literals and use constants instead, but there
are plusses and minuses to doing that, so this was not attempted
in this effort
2. HeaderEnvironProxy() objects report their keys in ".title()"
case, but normalize all other key references to the form
expected by the Request class's environ field
swob.Request.headers fields are HeaderEnvironProxy() objects.
3. HeaderKeyDict() objects report their keys in ".lower()" case,
and normalize all other key references to ".lower()" case
swob.Response.headers fields are HeaderKeyDict() objects.
Depending on which object is used and how it is used, one can end up
with such a mismatch.
This commit takes the following steps as a (PROPOSED) solution:
1. Change HeaderKeyDict() to normalize using ".title()" case to
match HeaderEnvironProxy()
2. Replace standard python dictionary objects with HeaderKeyDict()
objects where possible
This gives us an object that normalizes key references to avoid
fixing the code to normalize the string literals.
3. Fix up a few places to use title case string literals to match
the new defaults
Change-Id: Ied56a1df83ffac793ee85e796424d7d20f18f469
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2012-11-15 16:34:45 -05:00
|
|
|
self.assertEqual(req.url, exp_url)
|
|
|
|
|
|
|
|
qs = 'hello=equal&acl'
|
|
|
|
path = '%s?%s' % (pi, qs)
|
|
|
|
s, p = 'unit.test.example.com', '90'
|
|
|
|
req = swift.common.swob.Request({'PATH_INFO': pi,
|
|
|
|
'QUERY_STRING': qs,
|
|
|
|
'SERVER_NAME': s,
|
|
|
|
'SERVER_PORT': p})
|
2013-09-07 10:14:00 +02:00
|
|
|
exp_url = '%s://%s:%s%s?%s' % (sche, s, p, pi, qs)
|
Enhance log msg to report referer and user-agent
Enhance internally logged messages to report referer and user-agent.
Pass the referering URL and METHOD between internal servers (when
known), and set the user-agent to be the server type (obj-server,
container-server, proxy-server, obj-updater, obj-replicator,
container-updater, direct-client, etc.) with the process PID. In
conjunction with the transaction ID, it helps to track down which PID
from a given system was responsible for initiating the request and
what that server was working on to make this request.
This has been helpful in tracking down interactions between object,
container and account servers.
We also take things a bit further performaing a bit of refactoring to
consolidate calls to transfer_headers() now that we have a helper
method for constructing them.
Finally we performed further changes to avoid header key duplication
due to string literal header key values and the various objects
representing headers for requests and responses. See below for more
details.
====
Header Keys
There seems to be a bit of a problem with the case of the various
string literals used for header keys and the interchangable way
standard Python dictionaries, HeaderKeyDict() and HeaderEnvironProxy()
objects are used.
If one is not careful, a header object of some sort (one that does not
normalize its keys, and that is not necessarily a dictionary) can be
constructed containing header keys which differ only by the case of
their string literals. E.g.:
{ 'x-trans-id': '1234', 'X-Trans-Id': '5678' }
Such an object, when passed to http_connect() will result in an
on-the-wire header where the key values are merged together, comma
separated, that looks something like:
HTTP_X_TRANS_ID: 1234,5678
For some headers in some contexts, this is behavior is desirable. For
example, one can also use a list of tuples which enumerate the multiple
values a single header should have.
However, in almost all of the contexts used in the code base, this is
not desirable.
This behavior arises from a combination of factors:
1. Header strings are not constants and different lower-case and
title-case header strings values are used interchangably in the
code at times
It might be worth the effort to make a pass through the code to
stop using string literals and use constants instead, but there
are plusses and minuses to doing that, so this was not attempted
in this effort
2. HeaderEnvironProxy() objects report their keys in ".title()"
case, but normalize all other key references to the form
expected by the Request class's environ field
swob.Request.headers fields are HeaderEnvironProxy() objects.
3. HeaderKeyDict() objects report their keys in ".lower()" case,
and normalize all other key references to ".lower()" case
swob.Response.headers fields are HeaderKeyDict() objects.
Depending on which object is used and how it is used, one can end up
with such a mismatch.
This commit takes the following steps as a (PROPOSED) solution:
1. Change HeaderKeyDict() to normalize using ".title()" case to
match HeaderEnvironProxy()
2. Replace standard python dictionary objects with HeaderKeyDict()
objects where possible
This gives us an object that normalizes key references to avoid
fixing the code to normalize the string literals.
3. Fix up a few places to use title case string literals to match
the new defaults
Change-Id: Ied56a1df83ffac793ee85e796424d7d20f18f469
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2012-11-15 16:34:45 -05:00
|
|
|
self.assertEqual(req.url, exp_url)
|
|
|
|
|
|
|
|
host = 'unit.test.example.com'
|
|
|
|
req = swift.common.swob.Request({'PATH_INFO': pi,
|
|
|
|
'QUERY_STRING': qs,
|
|
|
|
'HTTP_HOST': host + ':80'})
|
2013-09-07 10:14:00 +02:00
|
|
|
exp_url = '%s://%s%s?%s' % (sche, host, pi, qs)
|
Enhance log msg to report referer and user-agent
Enhance internally logged messages to report referer and user-agent.
Pass the referering URL and METHOD between internal servers (when
known), and set the user-agent to be the server type (obj-server,
container-server, proxy-server, obj-updater, obj-replicator,
container-updater, direct-client, etc.) with the process PID. In
conjunction with the transaction ID, it helps to track down which PID
from a given system was responsible for initiating the request and
what that server was working on to make this request.
This has been helpful in tracking down interactions between object,
container and account servers.
We also take things a bit further performaing a bit of refactoring to
consolidate calls to transfer_headers() now that we have a helper
method for constructing them.
Finally we performed further changes to avoid header key duplication
due to string literal header key values and the various objects
representing headers for requests and responses. See below for more
details.
====
Header Keys
There seems to be a bit of a problem with the case of the various
string literals used for header keys and the interchangable way
standard Python dictionaries, HeaderKeyDict() and HeaderEnvironProxy()
objects are used.
If one is not careful, a header object of some sort (one that does not
normalize its keys, and that is not necessarily a dictionary) can be
constructed containing header keys which differ only by the case of
their string literals. E.g.:
{ 'x-trans-id': '1234', 'X-Trans-Id': '5678' }
Such an object, when passed to http_connect() will result in an
on-the-wire header where the key values are merged together, comma
separated, that looks something like:
HTTP_X_TRANS_ID: 1234,5678
For some headers in some contexts, this is behavior is desirable. For
example, one can also use a list of tuples which enumerate the multiple
values a single header should have.
However, in almost all of the contexts used in the code base, this is
not desirable.
This behavior arises from a combination of factors:
1. Header strings are not constants and different lower-case and
title-case header strings values are used interchangably in the
code at times
It might be worth the effort to make a pass through the code to
stop using string literals and use constants instead, but there
are plusses and minuses to doing that, so this was not attempted
in this effort
2. HeaderEnvironProxy() objects report their keys in ".title()"
case, but normalize all other key references to the form
expected by the Request class's environ field
swob.Request.headers fields are HeaderEnvironProxy() objects.
3. HeaderKeyDict() objects report their keys in ".lower()" case,
and normalize all other key references to ".lower()" case
swob.Response.headers fields are HeaderKeyDict() objects.
Depending on which object is used and how it is used, one can end up
with such a mismatch.
This commit takes the following steps as a (PROPOSED) solution:
1. Change HeaderKeyDict() to normalize using ".title()" case to
match HeaderEnvironProxy()
2. Replace standard python dictionary objects with HeaderKeyDict()
objects where possible
This gives us an object that normalizes key references to avoid
fixing the code to normalize the string literals.
3. Fix up a few places to use title case string literals to match
the new defaults
Change-Id: Ied56a1df83ffac793ee85e796424d7d20f18f469
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2012-11-15 16:34:45 -05:00
|
|
|
self.assertEqual(req.url, exp_url)
|
|
|
|
|
|
|
|
host = 'unit.test.example.com'
|
|
|
|
sche = 'https'
|
|
|
|
req = swift.common.swob.Request({'PATH_INFO': pi,
|
|
|
|
'QUERY_STRING': qs,
|
|
|
|
'HTTP_HOST': host + ':443',
|
|
|
|
'wsgi.url_scheme': sche})
|
2013-09-07 10:14:00 +02:00
|
|
|
exp_url = '%s://%s%s?%s' % (sche, host, pi, qs)
|
Enhance log msg to report referer and user-agent
Enhance internally logged messages to report referer and user-agent.
Pass the referering URL and METHOD between internal servers (when
known), and set the user-agent to be the server type (obj-server,
container-server, proxy-server, obj-updater, obj-replicator,
container-updater, direct-client, etc.) with the process PID. In
conjunction with the transaction ID, it helps to track down which PID
from a given system was responsible for initiating the request and
what that server was working on to make this request.
This has been helpful in tracking down interactions between object,
container and account servers.
We also take things a bit further performaing a bit of refactoring to
consolidate calls to transfer_headers() now that we have a helper
method for constructing them.
Finally we performed further changes to avoid header key duplication
due to string literal header key values and the various objects
representing headers for requests and responses. See below for more
details.
====
Header Keys
There seems to be a bit of a problem with the case of the various
string literals used for header keys and the interchangable way
standard Python dictionaries, HeaderKeyDict() and HeaderEnvironProxy()
objects are used.
If one is not careful, a header object of some sort (one that does not
normalize its keys, and that is not necessarily a dictionary) can be
constructed containing header keys which differ only by the case of
their string literals. E.g.:
{ 'x-trans-id': '1234', 'X-Trans-Id': '5678' }
Such an object, when passed to http_connect() will result in an
on-the-wire header where the key values are merged together, comma
separated, that looks something like:
HTTP_X_TRANS_ID: 1234,5678
For some headers in some contexts, this is behavior is desirable. For
example, one can also use a list of tuples which enumerate the multiple
values a single header should have.
However, in almost all of the contexts used in the code base, this is
not desirable.
This behavior arises from a combination of factors:
1. Header strings are not constants and different lower-case and
title-case header strings values are used interchangably in the
code at times
It might be worth the effort to make a pass through the code to
stop using string literals and use constants instead, but there
are plusses and minuses to doing that, so this was not attempted
in this effort
2. HeaderEnvironProxy() objects report their keys in ".title()"
case, but normalize all other key references to the form
expected by the Request class's environ field
swob.Request.headers fields are HeaderEnvironProxy() objects.
3. HeaderKeyDict() objects report their keys in ".lower()" case,
and normalize all other key references to ".lower()" case
swob.Response.headers fields are HeaderKeyDict() objects.
Depending on which object is used and how it is used, one can end up
with such a mismatch.
This commit takes the following steps as a (PROPOSED) solution:
1. Change HeaderKeyDict() to normalize using ".title()" case to
match HeaderEnvironProxy()
2. Replace standard python dictionary objects with HeaderKeyDict()
objects where possible
This gives us an object that normalizes key references to avoid
fixing the code to normalize the string literals.
3. Fix up a few places to use title case string literals to match
the new defaults
Change-Id: Ied56a1df83ffac793ee85e796424d7d20f18f469
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2012-11-15 16:34:45 -05:00
|
|
|
self.assertEqual(req.url, exp_url)
|
|
|
|
|
|
|
|
host = 'unit.test.example.com:81'
|
|
|
|
req = swift.common.swob.Request({'PATH_INFO': pi,
|
|
|
|
'QUERY_STRING': qs,
|
|
|
|
'HTTP_HOST': host,
|
|
|
|
'wsgi.url_scheme': sche})
|
2013-09-07 10:14:00 +02:00
|
|
|
exp_url = '%s://%s%s?%s' % (sche, host, pi, qs)
|
Enhance log msg to report referer and user-agent
Enhance internally logged messages to report referer and user-agent.
Pass the referering URL and METHOD between internal servers (when
known), and set the user-agent to be the server type (obj-server,
container-server, proxy-server, obj-updater, obj-replicator,
container-updater, direct-client, etc.) with the process PID. In
conjunction with the transaction ID, it helps to track down which PID
from a given system was responsible for initiating the request and
what that server was working on to make this request.
This has been helpful in tracking down interactions between object,
container and account servers.
We also take things a bit further performaing a bit of refactoring to
consolidate calls to transfer_headers() now that we have a helper
method for constructing them.
Finally we performed further changes to avoid header key duplication
due to string literal header key values and the various objects
representing headers for requests and responses. See below for more
details.
====
Header Keys
There seems to be a bit of a problem with the case of the various
string literals used for header keys and the interchangable way
standard Python dictionaries, HeaderKeyDict() and HeaderEnvironProxy()
objects are used.
If one is not careful, a header object of some sort (one that does not
normalize its keys, and that is not necessarily a dictionary) can be
constructed containing header keys which differ only by the case of
their string literals. E.g.:
{ 'x-trans-id': '1234', 'X-Trans-Id': '5678' }
Such an object, when passed to http_connect() will result in an
on-the-wire header where the key values are merged together, comma
separated, that looks something like:
HTTP_X_TRANS_ID: 1234,5678
For some headers in some contexts, this is behavior is desirable. For
example, one can also use a list of tuples which enumerate the multiple
values a single header should have.
However, in almost all of the contexts used in the code base, this is
not desirable.
This behavior arises from a combination of factors:
1. Header strings are not constants and different lower-case and
title-case header strings values are used interchangably in the
code at times
It might be worth the effort to make a pass through the code to
stop using string literals and use constants instead, but there
are plusses and minuses to doing that, so this was not attempted
in this effort
2. HeaderEnvironProxy() objects report their keys in ".title()"
case, but normalize all other key references to the form
expected by the Request class's environ field
swob.Request.headers fields are HeaderEnvironProxy() objects.
3. HeaderKeyDict() objects report their keys in ".lower()" case,
and normalize all other key references to ".lower()" case
swob.Response.headers fields are HeaderKeyDict() objects.
Depending on which object is used and how it is used, one can end up
with such a mismatch.
This commit takes the following steps as a (PROPOSED) solution:
1. Change HeaderKeyDict() to normalize using ".title()" case to
match HeaderEnvironProxy()
2. Replace standard python dictionary objects with HeaderKeyDict()
objects where possible
This gives us an object that normalizes key references to avoid
fixing the code to normalize the string literals.
3. Fix up a few places to use title case string literals to match
the new defaults
Change-Id: Ied56a1df83ffac793ee85e796424d7d20f18f469
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2012-11-15 16:34:45 -05:00
|
|
|
self.assertEqual(req.url, exp_url)
|
|
|
|
|
|
|
|
def test_as_referer(self):
|
|
|
|
pi = '/hi/there'
|
|
|
|
qs = 'hello=equal&acl'
|
|
|
|
sche = 'https'
|
|
|
|
host = 'unit.test.example.com:81'
|
|
|
|
req = swift.common.swob.Request({'REQUEST_METHOD': 'POST',
|
|
|
|
'PATH_INFO': pi,
|
|
|
|
'QUERY_STRING': qs,
|
|
|
|
'HTTP_HOST': host,
|
|
|
|
'wsgi.url_scheme': sche})
|
2013-09-07 10:14:00 +02:00
|
|
|
exp_url = '%s://%s%s?%s' % (sche, host, pi, qs)
|
Enhance log msg to report referer and user-agent
Enhance internally logged messages to report referer and user-agent.
Pass the referering URL and METHOD between internal servers (when
known), and set the user-agent to be the server type (obj-server,
container-server, proxy-server, obj-updater, obj-replicator,
container-updater, direct-client, etc.) with the process PID. In
conjunction with the transaction ID, it helps to track down which PID
from a given system was responsible for initiating the request and
what that server was working on to make this request.
This has been helpful in tracking down interactions between object,
container and account servers.
We also take things a bit further performaing a bit of refactoring to
consolidate calls to transfer_headers() now that we have a helper
method for constructing them.
Finally we performed further changes to avoid header key duplication
due to string literal header key values and the various objects
representing headers for requests and responses. See below for more
details.
====
Header Keys
There seems to be a bit of a problem with the case of the various
string literals used for header keys and the interchangable way
standard Python dictionaries, HeaderKeyDict() and HeaderEnvironProxy()
objects are used.
If one is not careful, a header object of some sort (one that does not
normalize its keys, and that is not necessarily a dictionary) can be
constructed containing header keys which differ only by the case of
their string literals. E.g.:
{ 'x-trans-id': '1234', 'X-Trans-Id': '5678' }
Such an object, when passed to http_connect() will result in an
on-the-wire header where the key values are merged together, comma
separated, that looks something like:
HTTP_X_TRANS_ID: 1234,5678
For some headers in some contexts, this is behavior is desirable. For
example, one can also use a list of tuples which enumerate the multiple
values a single header should have.
However, in almost all of the contexts used in the code base, this is
not desirable.
This behavior arises from a combination of factors:
1. Header strings are not constants and different lower-case and
title-case header strings values are used interchangably in the
code at times
It might be worth the effort to make a pass through the code to
stop using string literals and use constants instead, but there
are plusses and minuses to doing that, so this was not attempted
in this effort
2. HeaderEnvironProxy() objects report their keys in ".title()"
case, but normalize all other key references to the form
expected by the Request class's environ field
swob.Request.headers fields are HeaderEnvironProxy() objects.
3. HeaderKeyDict() objects report their keys in ".lower()" case,
and normalize all other key references to ".lower()" case
swob.Response.headers fields are HeaderKeyDict() objects.
Depending on which object is used and how it is used, one can end up
with such a mismatch.
This commit takes the following steps as a (PROPOSED) solution:
1. Change HeaderKeyDict() to normalize using ".title()" case to
match HeaderEnvironProxy()
2. Replace standard python dictionary objects with HeaderKeyDict()
objects where possible
This gives us an object that normalizes key references to avoid
fixing the code to normalize the string literals.
3. Fix up a few places to use title case string literals to match
the new defaults
Change-Id: Ied56a1df83ffac793ee85e796424d7d20f18f469
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2012-11-15 16:34:45 -05:00
|
|
|
self.assertEqual(req.as_referer(), 'POST ' + exp_url)
|
|
|
|
|
Rework to support RFC 2616 Sec 4.4 Message Length
RFC 2616 Sec 4.4 Message Length describes how the content-length and
transfer-encoding headers interact. Basically, if chunked transfer
encoding is used, the content-length header value is ignored and if
the content-length header is present, and the request is not using
chunked transfer-encoding, then the content-length must match the body
length.
The only Transfer-Coding value we support in the Transfer-Encoding
header (to date) is "chunked". RFC 2616 Sec 14.41 specifies that if
"multiple encodings have been applied to an entity, the
transfer-codings MUST be listed in the order in which they were
applied." Since we only supported "chunked". If the Transfer-Encoding
header value has multiple transfer-codings, we return a 501 (Not
Implemented) (see RFC 2616 Sec 3.6) without checking if chunked is the
last one specified. Finally, if transfer-encoding is anything but
"chunked", we return a 400 (Bad Request) to the client.
This patch adds a new method, message_length, to the swob request
object which will apply an algorithm based on RFC 2616 Sec 4.4
leveraging the existing content_length property.
In addition to these changes, the proxy server will now notice when
the message length specified by the content-length header is greater
than the configured object maximum size and fail the request with a
413, "Request Entity Too Large", before reading the entire body.
This work flows from https://review.openstack.org/27152.
Change-Id: I5d2a30b89092680dee9d946e1aafd017eaaef8c0
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2013-04-19 00:10:38 -04:00
|
|
|
def test_message_length_just_content_length(self):
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
u'/',
|
|
|
|
environ={'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/'})
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.message_length(), None)
|
Rework to support RFC 2616 Sec 4.4 Message Length
RFC 2616 Sec 4.4 Message Length describes how the content-length and
transfer-encoding headers interact. Basically, if chunked transfer
encoding is used, the content-length header value is ignored and if
the content-length header is present, and the request is not using
chunked transfer-encoding, then the content-length must match the body
length.
The only Transfer-Coding value we support in the Transfer-Encoding
header (to date) is "chunked". RFC 2616 Sec 14.41 specifies that if
"multiple encodings have been applied to an entity, the
transfer-codings MUST be listed in the order in which they were
applied." Since we only supported "chunked". If the Transfer-Encoding
header value has multiple transfer-codings, we return a 501 (Not
Implemented) (see RFC 2616 Sec 3.6) without checking if chunked is the
last one specified. Finally, if transfer-encoding is anything but
"chunked", we return a 400 (Bad Request) to the client.
This patch adds a new method, message_length, to the swob request
object which will apply an algorithm based on RFC 2616 Sec 4.4
leveraging the existing content_length property.
In addition to these changes, the proxy server will now notice when
the message length specified by the content-length header is greater
than the configured object maximum size and fail the request with a
413, "Request Entity Too Large", before reading the entire body.
This work flows from https://review.openstack.org/27152.
Change-Id: I5d2a30b89092680dee9d946e1aafd017eaaef8c0
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2013-04-19 00:10:38 -04:00
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
u'/',
|
|
|
|
environ={'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/'},
|
2013-08-31 23:13:15 -04:00
|
|
|
body='x' * 42)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.message_length(), 42)
|
Rework to support RFC 2616 Sec 4.4 Message Length
RFC 2616 Sec 4.4 Message Length describes how the content-length and
transfer-encoding headers interact. Basically, if chunked transfer
encoding is used, the content-length header value is ignored and if
the content-length header is present, and the request is not using
chunked transfer-encoding, then the content-length must match the body
length.
The only Transfer-Coding value we support in the Transfer-Encoding
header (to date) is "chunked". RFC 2616 Sec 14.41 specifies that if
"multiple encodings have been applied to an entity, the
transfer-codings MUST be listed in the order in which they were
applied." Since we only supported "chunked". If the Transfer-Encoding
header value has multiple transfer-codings, we return a 501 (Not
Implemented) (see RFC 2616 Sec 3.6) without checking if chunked is the
last one specified. Finally, if transfer-encoding is anything but
"chunked", we return a 400 (Bad Request) to the client.
This patch adds a new method, message_length, to the swob request
object which will apply an algorithm based on RFC 2616 Sec 4.4
leveraging the existing content_length property.
In addition to these changes, the proxy server will now notice when
the message length specified by the content-length header is greater
than the configured object maximum size and fail the request with a
413, "Request Entity Too Large", before reading the entire body.
This work flows from https://review.openstack.org/27152.
Change-Id: I5d2a30b89092680dee9d946e1aafd017eaaef8c0
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2013-04-19 00:10:38 -04:00
|
|
|
|
|
|
|
req.headers['Content-Length'] = 'abc'
|
|
|
|
try:
|
2013-07-23 16:41:45 -07:00
|
|
|
req.message_length()
|
Rework to support RFC 2616 Sec 4.4 Message Length
RFC 2616 Sec 4.4 Message Length describes how the content-length and
transfer-encoding headers interact. Basically, if chunked transfer
encoding is used, the content-length header value is ignored and if
the content-length header is present, and the request is not using
chunked transfer-encoding, then the content-length must match the body
length.
The only Transfer-Coding value we support in the Transfer-Encoding
header (to date) is "chunked". RFC 2616 Sec 14.41 specifies that if
"multiple encodings have been applied to an entity, the
transfer-codings MUST be listed in the order in which they were
applied." Since we only supported "chunked". If the Transfer-Encoding
header value has multiple transfer-codings, we return a 501 (Not
Implemented) (see RFC 2616 Sec 3.6) without checking if chunked is the
last one specified. Finally, if transfer-encoding is anything but
"chunked", we return a 400 (Bad Request) to the client.
This patch adds a new method, message_length, to the swob request
object which will apply an algorithm based on RFC 2616 Sec 4.4
leveraging the existing content_length property.
In addition to these changes, the proxy server will now notice when
the message length specified by the content-length header is greater
than the configured object maximum size and fail the request with a
413, "Request Entity Too Large", before reading the entire body.
This work flows from https://review.openstack.org/27152.
Change-Id: I5d2a30b89092680dee9d946e1aafd017eaaef8c0
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2013-04-19 00:10:38 -04:00
|
|
|
except ValueError as e:
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(str(e), "Invalid Content-Length header value")
|
Rework to support RFC 2616 Sec 4.4 Message Length
RFC 2616 Sec 4.4 Message Length describes how the content-length and
transfer-encoding headers interact. Basically, if chunked transfer
encoding is used, the content-length header value is ignored and if
the content-length header is present, and the request is not using
chunked transfer-encoding, then the content-length must match the body
length.
The only Transfer-Coding value we support in the Transfer-Encoding
header (to date) is "chunked". RFC 2616 Sec 14.41 specifies that if
"multiple encodings have been applied to an entity, the
transfer-codings MUST be listed in the order in which they were
applied." Since we only supported "chunked". If the Transfer-Encoding
header value has multiple transfer-codings, we return a 501 (Not
Implemented) (see RFC 2616 Sec 3.6) without checking if chunked is the
last one specified. Finally, if transfer-encoding is anything but
"chunked", we return a 400 (Bad Request) to the client.
This patch adds a new method, message_length, to the swob request
object which will apply an algorithm based on RFC 2616 Sec 4.4
leveraging the existing content_length property.
In addition to these changes, the proxy server will now notice when
the message length specified by the content-length header is greater
than the configured object maximum size and fail the request with a
413, "Request Entity Too Large", before reading the entire body.
This work flows from https://review.openstack.org/27152.
Change-Id: I5d2a30b89092680dee9d946e1aafd017eaaef8c0
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2013-04-19 00:10:38 -04:00
|
|
|
else:
|
|
|
|
self.fail("Expected a ValueError raised for 'abc'")
|
|
|
|
|
|
|
|
def test_message_length_transfer_encoding(self):
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
u'/',
|
|
|
|
environ={'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/'},
|
|
|
|
headers={'transfer-encoding': 'chunked'},
|
2013-08-31 23:13:15 -04:00
|
|
|
body='x' * 42)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(req.message_length(), None)
|
Rework to support RFC 2616 Sec 4.4 Message Length
RFC 2616 Sec 4.4 Message Length describes how the content-length and
transfer-encoding headers interact. Basically, if chunked transfer
encoding is used, the content-length header value is ignored and if
the content-length header is present, and the request is not using
chunked transfer-encoding, then the content-length must match the body
length.
The only Transfer-Coding value we support in the Transfer-Encoding
header (to date) is "chunked". RFC 2616 Sec 14.41 specifies that if
"multiple encodings have been applied to an entity, the
transfer-codings MUST be listed in the order in which they were
applied." Since we only supported "chunked". If the Transfer-Encoding
header value has multiple transfer-codings, we return a 501 (Not
Implemented) (see RFC 2616 Sec 3.6) without checking if chunked is the
last one specified. Finally, if transfer-encoding is anything but
"chunked", we return a 400 (Bad Request) to the client.
This patch adds a new method, message_length, to the swob request
object which will apply an algorithm based on RFC 2616 Sec 4.4
leveraging the existing content_length property.
In addition to these changes, the proxy server will now notice when
the message length specified by the content-length header is greater
than the configured object maximum size and fail the request with a
413, "Request Entity Too Large", before reading the entire body.
This work flows from https://review.openstack.org/27152.
Change-Id: I5d2a30b89092680dee9d946e1aafd017eaaef8c0
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2013-04-19 00:10:38 -04:00
|
|
|
|
|
|
|
req.headers['Transfer-Encoding'] = 'gzip,chunked'
|
|
|
|
try:
|
2013-07-23 16:41:45 -07:00
|
|
|
req.message_length()
|
Rework to support RFC 2616 Sec 4.4 Message Length
RFC 2616 Sec 4.4 Message Length describes how the content-length and
transfer-encoding headers interact. Basically, if chunked transfer
encoding is used, the content-length header value is ignored and if
the content-length header is present, and the request is not using
chunked transfer-encoding, then the content-length must match the body
length.
The only Transfer-Coding value we support in the Transfer-Encoding
header (to date) is "chunked". RFC 2616 Sec 14.41 specifies that if
"multiple encodings have been applied to an entity, the
transfer-codings MUST be listed in the order in which they were
applied." Since we only supported "chunked". If the Transfer-Encoding
header value has multiple transfer-codings, we return a 501 (Not
Implemented) (see RFC 2616 Sec 3.6) without checking if chunked is the
last one specified. Finally, if transfer-encoding is anything but
"chunked", we return a 400 (Bad Request) to the client.
This patch adds a new method, message_length, to the swob request
object which will apply an algorithm based on RFC 2616 Sec 4.4
leveraging the existing content_length property.
In addition to these changes, the proxy server will now notice when
the message length specified by the content-length header is greater
than the configured object maximum size and fail the request with a
413, "Request Entity Too Large", before reading the entire body.
This work flows from https://review.openstack.org/27152.
Change-Id: I5d2a30b89092680dee9d946e1aafd017eaaef8c0
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2013-04-19 00:10:38 -04:00
|
|
|
except AttributeError as e:
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(str(e), "Unsupported Transfer-Coding header"
|
|
|
|
" value specified in Transfer-Encoding header")
|
Rework to support RFC 2616 Sec 4.4 Message Length
RFC 2616 Sec 4.4 Message Length describes how the content-length and
transfer-encoding headers interact. Basically, if chunked transfer
encoding is used, the content-length header value is ignored and if
the content-length header is present, and the request is not using
chunked transfer-encoding, then the content-length must match the body
length.
The only Transfer-Coding value we support in the Transfer-Encoding
header (to date) is "chunked". RFC 2616 Sec 14.41 specifies that if
"multiple encodings have been applied to an entity, the
transfer-codings MUST be listed in the order in which they were
applied." Since we only supported "chunked". If the Transfer-Encoding
header value has multiple transfer-codings, we return a 501 (Not
Implemented) (see RFC 2616 Sec 3.6) without checking if chunked is the
last one specified. Finally, if transfer-encoding is anything but
"chunked", we return a 400 (Bad Request) to the client.
This patch adds a new method, message_length, to the swob request
object which will apply an algorithm based on RFC 2616 Sec 4.4
leveraging the existing content_length property.
In addition to these changes, the proxy server will now notice when
the message length specified by the content-length header is greater
than the configured object maximum size and fail the request with a
413, "Request Entity Too Large", before reading the entire body.
This work flows from https://review.openstack.org/27152.
Change-Id: I5d2a30b89092680dee9d946e1aafd017eaaef8c0
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2013-04-19 00:10:38 -04:00
|
|
|
else:
|
|
|
|
self.fail("Expected an AttributeError raised for 'gzip'")
|
|
|
|
|
|
|
|
req.headers['Transfer-Encoding'] = 'gzip'
|
|
|
|
try:
|
2013-07-23 16:41:45 -07:00
|
|
|
req.message_length()
|
Rework to support RFC 2616 Sec 4.4 Message Length
RFC 2616 Sec 4.4 Message Length describes how the content-length and
transfer-encoding headers interact. Basically, if chunked transfer
encoding is used, the content-length header value is ignored and if
the content-length header is present, and the request is not using
chunked transfer-encoding, then the content-length must match the body
length.
The only Transfer-Coding value we support in the Transfer-Encoding
header (to date) is "chunked". RFC 2616 Sec 14.41 specifies that if
"multiple encodings have been applied to an entity, the
transfer-codings MUST be listed in the order in which they were
applied." Since we only supported "chunked". If the Transfer-Encoding
header value has multiple transfer-codings, we return a 501 (Not
Implemented) (see RFC 2616 Sec 3.6) without checking if chunked is the
last one specified. Finally, if transfer-encoding is anything but
"chunked", we return a 400 (Bad Request) to the client.
This patch adds a new method, message_length, to the swob request
object which will apply an algorithm based on RFC 2616 Sec 4.4
leveraging the existing content_length property.
In addition to these changes, the proxy server will now notice when
the message length specified by the content-length header is greater
than the configured object maximum size and fail the request with a
413, "Request Entity Too Large", before reading the entire body.
This work flows from https://review.openstack.org/27152.
Change-Id: I5d2a30b89092680dee9d946e1aafd017eaaef8c0
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2013-04-19 00:10:38 -04:00
|
|
|
except ValueError as e:
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(str(e), "Invalid Transfer-Encoding header value")
|
Rework to support RFC 2616 Sec 4.4 Message Length
RFC 2616 Sec 4.4 Message Length describes how the content-length and
transfer-encoding headers interact. Basically, if chunked transfer
encoding is used, the content-length header value is ignored and if
the content-length header is present, and the request is not using
chunked transfer-encoding, then the content-length must match the body
length.
The only Transfer-Coding value we support in the Transfer-Encoding
header (to date) is "chunked". RFC 2616 Sec 14.41 specifies that if
"multiple encodings have been applied to an entity, the
transfer-codings MUST be listed in the order in which they were
applied." Since we only supported "chunked". If the Transfer-Encoding
header value has multiple transfer-codings, we return a 501 (Not
Implemented) (see RFC 2616 Sec 3.6) without checking if chunked is the
last one specified. Finally, if transfer-encoding is anything but
"chunked", we return a 400 (Bad Request) to the client.
This patch adds a new method, message_length, to the swob request
object which will apply an algorithm based on RFC 2616 Sec 4.4
leveraging the existing content_length property.
In addition to these changes, the proxy server will now notice when
the message length specified by the content-length header is greater
than the configured object maximum size and fail the request with a
413, "Request Entity Too Large", before reading the entire body.
This work flows from https://review.openstack.org/27152.
Change-Id: I5d2a30b89092680dee9d946e1aafd017eaaef8c0
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2013-04-19 00:10:38 -04:00
|
|
|
else:
|
|
|
|
self.fail("Expected a ValueError raised for 'gzip'")
|
|
|
|
|
|
|
|
req.headers['Transfer-Encoding'] = 'gzip,identity'
|
|
|
|
try:
|
2013-07-23 16:41:45 -07:00
|
|
|
req.message_length()
|
Rework to support RFC 2616 Sec 4.4 Message Length
RFC 2616 Sec 4.4 Message Length describes how the content-length and
transfer-encoding headers interact. Basically, if chunked transfer
encoding is used, the content-length header value is ignored and if
the content-length header is present, and the request is not using
chunked transfer-encoding, then the content-length must match the body
length.
The only Transfer-Coding value we support in the Transfer-Encoding
header (to date) is "chunked". RFC 2616 Sec 14.41 specifies that if
"multiple encodings have been applied to an entity, the
transfer-codings MUST be listed in the order in which they were
applied." Since we only supported "chunked". If the Transfer-Encoding
header value has multiple transfer-codings, we return a 501 (Not
Implemented) (see RFC 2616 Sec 3.6) without checking if chunked is the
last one specified. Finally, if transfer-encoding is anything but
"chunked", we return a 400 (Bad Request) to the client.
This patch adds a new method, message_length, to the swob request
object which will apply an algorithm based on RFC 2616 Sec 4.4
leveraging the existing content_length property.
In addition to these changes, the proxy server will now notice when
the message length specified by the content-length header is greater
than the configured object maximum size and fail the request with a
413, "Request Entity Too Large", before reading the entire body.
This work flows from https://review.openstack.org/27152.
Change-Id: I5d2a30b89092680dee9d946e1aafd017eaaef8c0
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2013-04-19 00:10:38 -04:00
|
|
|
except AttributeError as e:
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(str(e), "Unsupported Transfer-Coding header"
|
|
|
|
" value specified in Transfer-Encoding header")
|
Rework to support RFC 2616 Sec 4.4 Message Length
RFC 2616 Sec 4.4 Message Length describes how the content-length and
transfer-encoding headers interact. Basically, if chunked transfer
encoding is used, the content-length header value is ignored and if
the content-length header is present, and the request is not using
chunked transfer-encoding, then the content-length must match the body
length.
The only Transfer-Coding value we support in the Transfer-Encoding
header (to date) is "chunked". RFC 2616 Sec 14.41 specifies that if
"multiple encodings have been applied to an entity, the
transfer-codings MUST be listed in the order in which they were
applied." Since we only supported "chunked". If the Transfer-Encoding
header value has multiple transfer-codings, we return a 501 (Not
Implemented) (see RFC 2616 Sec 3.6) without checking if chunked is the
last one specified. Finally, if transfer-encoding is anything but
"chunked", we return a 400 (Bad Request) to the client.
This patch adds a new method, message_length, to the swob request
object which will apply an algorithm based on RFC 2616 Sec 4.4
leveraging the existing content_length property.
In addition to these changes, the proxy server will now notice when
the message length specified by the content-length header is greater
than the configured object maximum size and fail the request with a
413, "Request Entity Too Large", before reading the entire body.
This work flows from https://review.openstack.org/27152.
Change-Id: I5d2a30b89092680dee9d946e1aafd017eaaef8c0
Signed-off-by: Peter Portante <peter.portante@redhat.com>
2013-04-19 00:10:38 -04:00
|
|
|
else:
|
|
|
|
self.fail("Expected an AttributeError raised for 'gzip,identity'")
|
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
class TestStatusMap(unittest.TestCase):
|
|
|
|
def test_status_map(self):
|
|
|
|
response_args = []
|
|
|
|
|
|
|
|
def start_response(status, headers):
|
|
|
|
response_args.append(status)
|
|
|
|
response_args.append(headers)
|
|
|
|
resp_cls = swift.common.swob.status_map[404]
|
|
|
|
resp = resp_cls()
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 404)
|
|
|
|
self.assertEqual(resp.title, 'Not Found')
|
2012-09-04 14:02:19 -07:00
|
|
|
body = ''.join(resp({}, start_response))
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('The resource could not be found.' in body)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(response_args[0], '404 Not Found')
|
2012-09-04 14:02:19 -07:00
|
|
|
headers = dict(response_args[1])
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(headers['Content-Type'], 'text/html; charset=UTF-8')
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue(int(headers['Content-Length']) > 0)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
|
|
|
|
class TestResponse(unittest.TestCase):
|
|
|
|
def _get_response(self):
|
|
|
|
def test_app(environ, start_response):
|
|
|
|
start_response('200 OK', [])
|
|
|
|
return ['hi']
|
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank('/')
|
|
|
|
return req.get_response(test_app)
|
|
|
|
|
|
|
|
def test_properties(self):
|
|
|
|
resp = self._get_response()
|
|
|
|
|
|
|
|
resp.location = 'something'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.location, 'something')
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Location' in resp.headers)
|
2012-09-04 14:02:19 -07:00
|
|
|
resp.location = None
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Location' not in resp.headers)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
resp.content_type = 'text/plain'
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Content-Type' in resp.headers)
|
2012-09-04 14:02:19 -07:00
|
|
|
resp.content_type = None
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('Content-Type' not in resp.headers)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2012-10-12 06:59:14 +02:00
|
|
|
def test_empty_body(self):
|
|
|
|
resp = self._get_response()
|
|
|
|
resp.body = ''
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.body, '')
|
2012-10-12 06:59:14 +02:00
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
def test_unicode_body(self):
|
|
|
|
resp = self._get_response()
|
|
|
|
resp.body = u'\N{SNOWMAN}'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.body, u'\N{SNOWMAN}'.encode('utf-8'))
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2013-01-10 13:08:22 -08:00
|
|
|
def test_call_reifies_request_if_necessary(self):
|
|
|
|
"""
|
|
|
|
The actual bug was a HEAD response coming out with a body because the
|
|
|
|
Request object wasn't passed into the Response object's constructor.
|
|
|
|
The Response object's __call__ method should be able to reify a
|
|
|
|
Request object from the env it gets passed.
|
|
|
|
"""
|
|
|
|
def test_app(environ, start_response):
|
|
|
|
start_response('200 OK', [])
|
|
|
|
return ['hi']
|
|
|
|
req = swift.common.swob.Request.blank('/')
|
|
|
|
req.method = 'HEAD'
|
|
|
|
status, headers, app_iter = req.call_application(test_app)
|
|
|
|
resp = swift.common.swob.Response(status=status, headers=dict(headers),
|
|
|
|
app_iter=app_iter)
|
|
|
|
output_iter = resp(req.environ, lambda *_: None)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(list(output_iter), [''])
|
2013-01-10 13:08:22 -08:00
|
|
|
|
2013-12-03 14:49:57 -08:00
|
|
|
def test_call_preserves_closeability(self):
|
|
|
|
def test_app(environ, start_response):
|
|
|
|
start_response('200 OK', [])
|
|
|
|
yield "igloo"
|
|
|
|
yield "shindig"
|
|
|
|
yield "macadamia"
|
|
|
|
yield "hullabaloo"
|
|
|
|
req = swift.common.swob.Request.blank('/')
|
|
|
|
req.method = 'GET'
|
|
|
|
status, headers, app_iter = req.call_application(test_app)
|
|
|
|
iterator = iter(app_iter)
|
2015-06-15 22:10:45 +05:30
|
|
|
self.assertEqual('igloo', next(iterator))
|
|
|
|
self.assertEqual('shindig', next(iterator))
|
2013-12-03 14:49:57 -08:00
|
|
|
app_iter.close()
|
|
|
|
self.assertRaises(StopIteration, iterator.next)
|
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
def test_location_rewrite(self):
|
|
|
|
def start_response(env, headers):
|
|
|
|
pass
|
2012-11-08 14:17:40 -08:00
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', environ={'HTTP_HOST': 'somehost'})
|
|
|
|
resp = self._get_response()
|
|
|
|
resp.location = '/something'
|
2013-03-26 20:42:26 +00:00
|
|
|
# read response
|
|
|
|
''.join(resp(req.environ, start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.location, 'http://somehost/something')
|
2012-11-08 14:17:40 -08:00
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', environ={'HTTP_HOST': 'somehost:80'})
|
|
|
|
resp = self._get_response()
|
|
|
|
resp.location = '/something'
|
2013-03-26 20:42:26 +00:00
|
|
|
# read response
|
|
|
|
''.join(resp(req.environ, start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.location, 'http://somehost/something')
|
2012-11-08 14:17:40 -08:00
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', environ={'HTTP_HOST': 'somehost:443',
|
|
|
|
'wsgi.url_scheme': 'http'})
|
2012-09-04 14:02:19 -07:00
|
|
|
resp = self._get_response()
|
|
|
|
resp.location = '/something'
|
2013-03-26 20:42:26 +00:00
|
|
|
# read response
|
|
|
|
''.join(resp(req.environ, start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.location, 'http://somehost:443/something')
|
2012-11-08 14:17:40 -08:00
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', environ={'HTTP_HOST': 'somehost:443',
|
|
|
|
'wsgi.url_scheme': 'https'})
|
|
|
|
resp = self._get_response()
|
|
|
|
resp.location = '/something'
|
2013-03-26 20:42:26 +00:00
|
|
|
# read response
|
|
|
|
''.join(resp(req.environ, start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.location, 'https://somehost/something')
|
2012-11-08 14:17:40 -08:00
|
|
|
|
|
|
|
def test_location_rewrite_no_host(self):
|
|
|
|
def start_response(env, headers):
|
|
|
|
pass
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', environ={'SERVER_NAME': 'local', 'SERVER_PORT': 80})
|
|
|
|
del req.environ['HTTP_HOST']
|
|
|
|
resp = self._get_response()
|
|
|
|
resp.location = '/something'
|
2013-03-26 20:42:26 +00:00
|
|
|
# read response
|
|
|
|
''.join(resp(req.environ, start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.location, 'http://local/something')
|
2012-11-08 14:17:40 -08:00
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', environ={'SERVER_NAME': 'local', 'SERVER_PORT': 81})
|
|
|
|
del req.environ['HTTP_HOST']
|
|
|
|
resp = self._get_response()
|
|
|
|
resp.location = '/something'
|
2013-03-26 20:42:26 +00:00
|
|
|
# read response
|
|
|
|
''.join(resp(req.environ, start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.location, 'http://local:81/something')
|
2012-11-08 14:17:40 -08:00
|
|
|
|
|
|
|
def test_location_no_rewrite(self):
|
|
|
|
def start_response(env, headers):
|
|
|
|
pass
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', environ={'HTTP_HOST': 'somehost'})
|
|
|
|
resp = self._get_response()
|
|
|
|
resp.location = 'http://www.google.com/'
|
2013-03-26 20:42:26 +00:00
|
|
|
# read response
|
|
|
|
''.join(resp(req.environ, start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.location, 'http://www.google.com/')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2014-01-22 13:03:51 -08:00
|
|
|
def test_location_no_rewrite_when_told_not_to(self):
|
|
|
|
def start_response(env, headers):
|
|
|
|
pass
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', environ={'SERVER_NAME': 'local', 'SERVER_PORT': 81,
|
|
|
|
'swift.leave_relative_location': True})
|
|
|
|
del req.environ['HTTP_HOST']
|
|
|
|
resp = self._get_response()
|
|
|
|
resp.location = '/something'
|
|
|
|
# read response
|
|
|
|
''.join(resp(req.environ, start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.location, '/something')
|
2014-01-22 13:03:51 -08:00
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
def test_app_iter(self):
|
|
|
|
def start_response(env, headers):
|
|
|
|
pass
|
|
|
|
resp = self._get_response()
|
|
|
|
resp.app_iter = ['a', 'b', 'c']
|
|
|
|
body = ''.join(resp({}, start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(body, 'abc')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2012-11-01 20:45:11 -04:00
|
|
|
def test_multi_ranges_wo_iter_ranges(self):
|
|
|
|
def test_app(environ, start_response):
|
|
|
|
start_response('200 OK', [('Content-Length', '10')])
|
|
|
|
return ['1234567890']
|
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'Range': 'bytes=0-9,10-19,20-29'})
|
|
|
|
|
|
|
|
resp = req.get_response(test_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
resp.content_length = 10
|
|
|
|
|
2013-03-26 20:42:26 +00:00
|
|
|
# read response
|
|
|
|
''.join(resp._response_iter(resp.app_iter, ''))
|
2012-11-01 20:45:11 -04:00
|
|
|
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status, '200 OK')
|
2012-11-01 20:45:11 -04:00
|
|
|
self.assertEqual(10, resp.content_length)
|
|
|
|
|
|
|
|
def test_single_range_wo_iter_range(self):
|
|
|
|
def test_app(environ, start_response):
|
|
|
|
start_response('200 OK', [('Content-Length', '10')])
|
|
|
|
return ['1234567890']
|
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'Range': 'bytes=0-9'})
|
|
|
|
|
|
|
|
resp = req.get_response(test_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
resp.content_length = 10
|
|
|
|
|
2013-03-26 20:42:26 +00:00
|
|
|
# read response
|
|
|
|
''.join(resp._response_iter(resp.app_iter, ''))
|
2012-11-01 20:45:11 -04:00
|
|
|
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status, '200 OK')
|
2012-11-01 20:45:11 -04:00
|
|
|
self.assertEqual(10, resp.content_length)
|
|
|
|
|
|
|
|
def test_multi_range_body(self):
|
|
|
|
def test_app(environ, start_response):
|
|
|
|
start_response('200 OK', [('Content-Length', '4')])
|
|
|
|
return ['abcd']
|
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'Range': 'bytes=0-9,10-19,20-29'})
|
|
|
|
|
|
|
|
resp = req.get_response(test_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
resp.content_length = 100
|
|
|
|
|
2015-11-25 18:49:39 +00:00
|
|
|
resp.content_type = 'text/plain; charset=utf8'
|
2012-11-01 20:45:11 -04:00
|
|
|
content = ''.join(resp._response_iter(None,
|
|
|
|
('0123456789112345678'
|
|
|
|
'92123456789')))
|
|
|
|
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue(re.match(('--[a-f0-9]{32}\r\n'
|
2015-11-25 18:49:39 +00:00
|
|
|
'Content-Type: text/plain; charset=utf8\r\n'
|
2015-07-21 19:23:00 +05:30
|
|
|
'Content-Range: bytes '
|
|
|
|
'0-9/100\r\n\r\n0123456789\r\n'
|
|
|
|
'--[a-f0-9]{32}\r\n'
|
2015-11-25 18:49:39 +00:00
|
|
|
'Content-Type: text/plain; charset=utf8\r\n'
|
2015-07-21 19:23:00 +05:30
|
|
|
'Content-Range: bytes '
|
|
|
|
'10-19/100\r\n\r\n1123456789\r\n'
|
|
|
|
'--[a-f0-9]{32}\r\n'
|
2015-11-25 18:49:39 +00:00
|
|
|
'Content-Type: text/plain; charset=utf8\r\n'
|
2015-07-21 19:23:00 +05:30
|
|
|
'Content-Range: bytes '
|
|
|
|
'20-29/100\r\n\r\n2123456789\r\n'
|
|
|
|
'--[a-f0-9]{32}--'), content))
|
2012-11-01 20:45:11 -04:00
|
|
|
|
|
|
|
def test_multi_response_iter(self):
|
|
|
|
def test_app(environ, start_response):
|
|
|
|
start_response('200 OK', [('Content-Length', '10'),
|
|
|
|
('Content-Type', 'application/xml')])
|
|
|
|
return ['0123456789']
|
|
|
|
|
|
|
|
app_iter_ranges_args = []
|
|
|
|
|
|
|
|
class App_iter(object):
|
|
|
|
def app_iter_ranges(self, ranges, content_type, boundary, size):
|
|
|
|
app_iter_ranges_args.append((ranges, content_type, boundary,
|
|
|
|
size))
|
2015-05-25 18:28:02 +02:00
|
|
|
for i in range(3):
|
2012-11-01 20:45:11 -04:00
|
|
|
yield str(i) + 'fun'
|
|
|
|
yield boundary
|
|
|
|
|
|
|
|
def __iter__(self):
|
2015-05-25 18:28:02 +02:00
|
|
|
for i in range(3):
|
2012-11-01 20:45:11 -04:00
|
|
|
yield str(i) + 'fun'
|
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'Range': 'bytes=1-5,8-11'})
|
|
|
|
|
|
|
|
resp = req.get_response(test_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
resp.content_length = 12
|
|
|
|
|
|
|
|
content = ''.join(resp._response_iter(App_iter(), ''))
|
|
|
|
boundary = content[-32:]
|
|
|
|
self.assertEqual(content[:-32], '0fun1fun2fun')
|
|
|
|
self.assertEqual(app_iter_ranges_args,
|
|
|
|
[([(1, 6), (8, 12)], 'application/xml',
|
|
|
|
boundary, 12)])
|
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
def test_range_body(self):
|
|
|
|
|
|
|
|
def test_app(environ, start_response):
|
|
|
|
start_response('200 OK', [('Content-Length', '10')])
|
|
|
|
return ['1234567890']
|
|
|
|
|
|
|
|
def start_response(env, headers):
|
|
|
|
pass
|
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'Range': 'bytes=1-3'})
|
|
|
|
|
|
|
|
resp = swift.common.swob.Response(
|
|
|
|
body='1234567890', request=req,
|
|
|
|
conditional_response=True)
|
|
|
|
body = ''.join(resp([], start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(body, '234')
|
|
|
|
self.assertEqual(resp.content_range, 'bytes 1-3/10')
|
|
|
|
self.assertEqual(resp.status, '206 Partial Content')
|
2012-10-03 14:20:52 -07:00
|
|
|
|
2012-11-01 20:45:11 -04:00
|
|
|
# syntactically valid, but does not make sense, so returning 416
|
|
|
|
# in next couple of cases.
|
2012-10-03 14:20:52 -07:00
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'Range': 'bytes=-0'})
|
|
|
|
resp = req.get_response(test_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp([], start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(body, '')
|
|
|
|
self.assertEqual(resp.content_length, 0)
|
|
|
|
self.assertEqual(resp.status, '416 Requested Range Not Satisfiable')
|
2012-10-03 14:20:52 -07:00
|
|
|
|
|
|
|
resp = swift.common.swob.Response(
|
|
|
|
body='1234567890', request=req,
|
|
|
|
conditional_response=True)
|
|
|
|
body = ''.join(resp([], start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(body, '')
|
|
|
|
self.assertEqual(resp.content_length, 0)
|
|
|
|
self.assertEqual(resp.status, '416 Requested Range Not Satisfiable')
|
2012-10-03 14:20:52 -07:00
|
|
|
|
|
|
|
# Syntactically-invalid Range headers "MUST" be ignored
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'Range': 'bytes=3-2'})
|
|
|
|
resp = req.get_response(test_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp([], start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(body, '1234567890')
|
|
|
|
self.assertEqual(resp.status, '200 OK')
|
2012-10-03 14:20:52 -07:00
|
|
|
|
|
|
|
resp = swift.common.swob.Response(
|
|
|
|
body='1234567890', request=req,
|
|
|
|
conditional_response=True)
|
|
|
|
body = ''.join(resp([], start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(body, '1234567890')
|
|
|
|
self.assertEqual(resp.status, '200 OK')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
def test_content_type(self):
|
|
|
|
resp = self._get_response()
|
|
|
|
resp.content_type = 'text/plain; charset=utf8'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.content_type, 'text/plain')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
def test_charset(self):
|
|
|
|
resp = self._get_response()
|
|
|
|
resp.content_type = 'text/plain; charset=utf8'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.charset, 'utf8')
|
2012-09-04 14:02:19 -07:00
|
|
|
resp.charset = 'utf16'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.charset, 'utf16')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2013-07-23 14:54:51 -07:00
|
|
|
def test_charset_content_type(self):
|
|
|
|
resp = swift.common.swob.Response(
|
|
|
|
content_type='text/plain', charset='utf-8')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.charset, 'utf-8')
|
2013-07-23 14:54:51 -07:00
|
|
|
resp = swift.common.swob.Response(
|
|
|
|
charset='utf-8', content_type='text/plain')
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.charset, 'utf-8')
|
2013-07-23 14:54:51 -07:00
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
def test_etag(self):
|
|
|
|
resp = self._get_response()
|
|
|
|
resp.etag = 'hi'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.headers['Etag'], '"hi"')
|
|
|
|
self.assertEqual(resp.etag, 'hi')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('etag' in resp.headers)
|
2012-09-04 14:02:19 -07:00
|
|
|
resp.etag = None
|
2015-07-21 19:23:00 +05:30
|
|
|
self.assertTrue('etag' not in resp.headers)
|
2012-09-04 14:02:19 -07:00
|
|
|
|
2012-11-10 16:39:25 +00:00
|
|
|
def test_host_url_default(self):
|
|
|
|
resp = self._get_response()
|
|
|
|
env = resp.environ
|
|
|
|
env['wsgi.url_scheme'] = 'http'
|
|
|
|
env['SERVER_NAME'] = 'bob'
|
|
|
|
env['SERVER_PORT'] = '1234'
|
|
|
|
del env['HTTP_HOST']
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.host_url, 'http://bob:1234')
|
2012-11-10 16:39:25 +00:00
|
|
|
|
|
|
|
def test_host_url_default_port_squelched(self):
|
|
|
|
resp = self._get_response()
|
|
|
|
env = resp.environ
|
|
|
|
env['wsgi.url_scheme'] = 'http'
|
|
|
|
env['SERVER_NAME'] = 'bob'
|
|
|
|
env['SERVER_PORT'] = '80'
|
|
|
|
del env['HTTP_HOST']
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.host_url, 'http://bob')
|
2012-11-10 16:39:25 +00:00
|
|
|
|
|
|
|
def test_host_url_https(self):
|
|
|
|
resp = self._get_response()
|
|
|
|
env = resp.environ
|
|
|
|
env['wsgi.url_scheme'] = 'https'
|
|
|
|
env['SERVER_NAME'] = 'bob'
|
|
|
|
env['SERVER_PORT'] = '1234'
|
|
|
|
del env['HTTP_HOST']
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.host_url, 'https://bob:1234')
|
2012-11-10 16:39:25 +00:00
|
|
|
|
|
|
|
def test_host_url_https_port_squelched(self):
|
|
|
|
resp = self._get_response()
|
|
|
|
env = resp.environ
|
|
|
|
env['wsgi.url_scheme'] = 'https'
|
|
|
|
env['SERVER_NAME'] = 'bob'
|
|
|
|
env['SERVER_PORT'] = '443'
|
|
|
|
del env['HTTP_HOST']
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.host_url, 'https://bob')
|
2012-11-10 16:39:25 +00:00
|
|
|
|
|
|
|
def test_host_url_host_override(self):
|
|
|
|
resp = self._get_response()
|
|
|
|
env = resp.environ
|
|
|
|
env['wsgi.url_scheme'] = 'http'
|
|
|
|
env['SERVER_NAME'] = 'bob'
|
|
|
|
env['SERVER_PORT'] = '1234'
|
|
|
|
env['HTTP_HOST'] = 'someother'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.host_url, 'http://someother')
|
2012-11-10 16:39:25 +00:00
|
|
|
|
|
|
|
def test_host_url_host_port_override(self):
|
|
|
|
resp = self._get_response()
|
|
|
|
env = resp.environ
|
|
|
|
env['wsgi.url_scheme'] = 'http'
|
|
|
|
env['SERVER_NAME'] = 'bob'
|
|
|
|
env['SERVER_PORT'] = '1234'
|
|
|
|
env['HTTP_HOST'] = 'someother:5678'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.host_url, 'http://someother:5678')
|
2012-11-10 16:39:25 +00:00
|
|
|
|
|
|
|
def test_host_url_host_https(self):
|
|
|
|
resp = self._get_response()
|
|
|
|
env = resp.environ
|
|
|
|
env['wsgi.url_scheme'] = 'https'
|
|
|
|
env['SERVER_NAME'] = 'bob'
|
|
|
|
env['SERVER_PORT'] = '1234'
|
|
|
|
env['HTTP_HOST'] = 'someother:5678'
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.host_url, 'https://someother:5678')
|
2012-11-10 16:39:25 +00:00
|
|
|
|
2012-12-18 01:18:57 +00:00
|
|
|
def test_507(self):
|
|
|
|
resp = swift.common.swob.HTTPInsufficientStorage()
|
|
|
|
content = ''.join(resp._response_iter(resp.app_iter, resp._body))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(
|
2012-12-18 01:18:57 +00:00
|
|
|
content,
|
|
|
|
'<html><h1>Insufficient Storage</h1><p>There was not enough space '
|
|
|
|
'to save the resource. Drive: unknown</p></html>')
|
|
|
|
resp = swift.common.swob.HTTPInsufficientStorage(drive='sda1')
|
|
|
|
content = ''.join(resp._response_iter(resp.app_iter, resp._body))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(
|
2012-12-18 01:18:57 +00:00
|
|
|
content,
|
|
|
|
'<html><h1>Insufficient Storage</h1><p>There was not enough space '
|
|
|
|
'to save the resource. Drive: sda1</p></html>')
|
|
|
|
|
2015-01-13 05:34:37 -08:00
|
|
|
def test_200_with_body_and_headers(self):
|
|
|
|
headers = {'Content-Length': '0'}
|
|
|
|
content = 'foo'
|
|
|
|
resp = swift.common.swob.HTTPOk(body=content, headers=headers)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.body, content)
|
|
|
|
self.assertEqual(resp.content_length, len(content))
|
2015-01-13 05:34:37 -08:00
|
|
|
|
|
|
|
def test_init_with_body_headers_app_iter(self):
|
|
|
|
# body exists but no headers and no app_iter
|
|
|
|
body = 'ok'
|
|
|
|
resp = swift.common.swob.Response(body=body)
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.body, body)
|
|
|
|
self.assertEqual(resp.content_length, len(body))
|
2015-01-13 05:34:37 -08:00
|
|
|
|
|
|
|
# body and headers with 0 content_length exist but no app_iter
|
|
|
|
body = 'ok'
|
|
|
|
resp = swift.common.swob.Response(
|
|
|
|
body=body, headers={'Content-Length': '0'})
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.body, body)
|
|
|
|
self.assertEqual(resp.content_length, len(body))
|
2015-01-13 05:34:37 -08:00
|
|
|
|
|
|
|
# body and headers with content_length exist but no app_iter
|
|
|
|
body = 'ok'
|
|
|
|
resp = swift.common.swob.Response(
|
|
|
|
body=body, headers={'Content-Length': '5'})
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.body, body)
|
|
|
|
self.assertEqual(resp.content_length, len(body))
|
2015-01-13 05:34:37 -08:00
|
|
|
|
|
|
|
# body and headers with no content_length exist but no app_iter
|
|
|
|
body = 'ok'
|
|
|
|
resp = swift.common.swob.Response(body=body, headers={})
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.body, body)
|
|
|
|
self.assertEqual(resp.content_length, len(body))
|
2015-01-13 05:34:37 -08:00
|
|
|
|
|
|
|
# body, headers with content_length and app_iter exist
|
|
|
|
resp = swift.common.swob.Response(
|
|
|
|
body='ok', headers={'Content-Length': '5'}, app_iter=iter([]))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.content_length, 5)
|
|
|
|
self.assertEqual(resp.body, '')
|
2015-01-13 05:34:37 -08:00
|
|
|
|
|
|
|
# headers with content_length and app_iter exist but no body
|
|
|
|
resp = swift.common.swob.Response(
|
|
|
|
headers={'Content-Length': '5'}, app_iter=iter([]))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.content_length, 5)
|
|
|
|
self.assertEqual(resp.body, '')
|
2015-01-13 05:34:37 -08:00
|
|
|
|
|
|
|
# app_iter exists but no body and headers
|
|
|
|
resp = swift.common.swob.Response(app_iter=iter([]))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.content_length, None)
|
|
|
|
self.assertEqual(resp.body, '')
|
2015-01-13 05:34:37 -08:00
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
class TestUTC(unittest.TestCase):
|
|
|
|
def test_tzname(self):
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(swift.common.swob.UTC.tzname(None), 'UTC')
|
2012-09-04 14:02:19 -07:00
|
|
|
|
|
|
|
|
Support If-[None-]Match for object HEAD, SLO, and DLO
I moved the checking of If-Match and If-None-Match out of the object
server's GET method and into swob so that everyone can use it. The
interface is similar to the Range handling; make a response with
conditional_response=True, and you get handing of If-Match and
If-None-Match.
Since the only users of conditional_response are object GET, object
HEAD, SLO, and DLO, this has the effect of adding support for If-Match
and If-None-Match to just the latter three places and nowhere
else. This makes object GET and HEAD consistent for any kind of
object, large or small.
This also fixes a bug where various conditional headers (If-*) were
passed through to the object server on segment requests, which could
cause segment requests to fail with a 304 or 412 response. Now only
certain headers are copied to the segment requests, and that doesn't
include the conditional ones, so they can't goof up the segment
retrieval.
Note that I moved SegmentedIterable to swift.common.request_helpers
because it sprouted a transitive dependency on swob, and leaving it in
utils caused a circular import.
Bonus fix: unified the handling of DiskFileQuarantined and
DiskFileNotFound in object server GET and HEAD. Now in either case, a
412 will be returned if the client said "If-Match: *". If not, the
response is a 404, just like before.
Closes-Bug: 1279076
Closes-Bug: 1280022
Closes-Bug: 1280028
Change-Id: Id2ee78346244d516b980202e990aa38ce6812de5
2014-02-12 18:29:12 -08:00
|
|
|
class TestConditionalIfNoneMatch(unittest.TestCase):
|
|
|
|
def fake_app(self, environ, start_response):
|
|
|
|
start_response('200 OK', [('Etag', 'the-etag')])
|
|
|
|
return ['hi']
|
|
|
|
|
|
|
|
def fake_start_response(*a, **kw):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def test_simple_match(self):
|
|
|
|
# etag matches --> 304
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'If-None-Match': 'the-etag'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 304)
|
|
|
|
self.assertEqual(body, '')
|
Support If-[None-]Match for object HEAD, SLO, and DLO
I moved the checking of If-Match and If-None-Match out of the object
server's GET method and into swob so that everyone can use it. The
interface is similar to the Range handling; make a response with
conditional_response=True, and you get handing of If-Match and
If-None-Match.
Since the only users of conditional_response are object GET, object
HEAD, SLO, and DLO, this has the effect of adding support for If-Match
and If-None-Match to just the latter three places and nowhere
else. This makes object GET and HEAD consistent for any kind of
object, large or small.
This also fixes a bug where various conditional headers (If-*) were
passed through to the object server on segment requests, which could
cause segment requests to fail with a 304 or 412 response. Now only
certain headers are copied to the segment requests, and that doesn't
include the conditional ones, so they can't goof up the segment
retrieval.
Note that I moved SegmentedIterable to swift.common.request_helpers
because it sprouted a transitive dependency on swob, and leaving it in
utils caused a circular import.
Bonus fix: unified the handling of DiskFileQuarantined and
DiskFileNotFound in object server GET and HEAD. Now in either case, a
412 will be returned if the client said "If-Match: *". If not, the
response is a 404, just like before.
Closes-Bug: 1279076
Closes-Bug: 1280022
Closes-Bug: 1280028
Change-Id: Id2ee78346244d516b980202e990aa38ce6812de5
2014-02-12 18:29:12 -08:00
|
|
|
|
|
|
|
def test_quoted_simple_match(self):
|
|
|
|
# double quotes don't matter
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'If-None-Match': '"the-etag"'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 304)
|
|
|
|
self.assertEqual(body, '')
|
Support If-[None-]Match for object HEAD, SLO, and DLO
I moved the checking of If-Match and If-None-Match out of the object
server's GET method and into swob so that everyone can use it. The
interface is similar to the Range handling; make a response with
conditional_response=True, and you get handing of If-Match and
If-None-Match.
Since the only users of conditional_response are object GET, object
HEAD, SLO, and DLO, this has the effect of adding support for If-Match
and If-None-Match to just the latter three places and nowhere
else. This makes object GET and HEAD consistent for any kind of
object, large or small.
This also fixes a bug where various conditional headers (If-*) were
passed through to the object server on segment requests, which could
cause segment requests to fail with a 304 or 412 response. Now only
certain headers are copied to the segment requests, and that doesn't
include the conditional ones, so they can't goof up the segment
retrieval.
Note that I moved SegmentedIterable to swift.common.request_helpers
because it sprouted a transitive dependency on swob, and leaving it in
utils caused a circular import.
Bonus fix: unified the handling of DiskFileQuarantined and
DiskFileNotFound in object server GET and HEAD. Now in either case, a
412 will be returned if the client said "If-Match: *". If not, the
response is a 404, just like before.
Closes-Bug: 1279076
Closes-Bug: 1280022
Closes-Bug: 1280028
Change-Id: Id2ee78346244d516b980202e990aa38ce6812de5
2014-02-12 18:29:12 -08:00
|
|
|
|
|
|
|
def test_list_match(self):
|
|
|
|
# it works with lists of etags to match
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'If-None-Match': '"bert", "the-etag", "ernie"'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 304)
|
|
|
|
self.assertEqual(body, '')
|
Support If-[None-]Match for object HEAD, SLO, and DLO
I moved the checking of If-Match and If-None-Match out of the object
server's GET method and into swob so that everyone can use it. The
interface is similar to the Range handling; make a response with
conditional_response=True, and you get handing of If-Match and
If-None-Match.
Since the only users of conditional_response are object GET, object
HEAD, SLO, and DLO, this has the effect of adding support for If-Match
and If-None-Match to just the latter three places and nowhere
else. This makes object GET and HEAD consistent for any kind of
object, large or small.
This also fixes a bug where various conditional headers (If-*) were
passed through to the object server on segment requests, which could
cause segment requests to fail with a 304 or 412 response. Now only
certain headers are copied to the segment requests, and that doesn't
include the conditional ones, so they can't goof up the segment
retrieval.
Note that I moved SegmentedIterable to swift.common.request_helpers
because it sprouted a transitive dependency on swob, and leaving it in
utils caused a circular import.
Bonus fix: unified the handling of DiskFileQuarantined and
DiskFileNotFound in object server GET and HEAD. Now in either case, a
412 will be returned if the client said "If-Match: *". If not, the
response is a 404, just like before.
Closes-Bug: 1279076
Closes-Bug: 1280022
Closes-Bug: 1280028
Change-Id: Id2ee78346244d516b980202e990aa38ce6812de5
2014-02-12 18:29:12 -08:00
|
|
|
|
|
|
|
def test_list_no_match(self):
|
|
|
|
# no matches --> whatever the original status was
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'If-None-Match': '"bert", "ernie"'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 200)
|
|
|
|
self.assertEqual(body, 'hi')
|
Support If-[None-]Match for object HEAD, SLO, and DLO
I moved the checking of If-Match and If-None-Match out of the object
server's GET method and into swob so that everyone can use it. The
interface is similar to the Range handling; make a response with
conditional_response=True, and you get handing of If-Match and
If-None-Match.
Since the only users of conditional_response are object GET, object
HEAD, SLO, and DLO, this has the effect of adding support for If-Match
and If-None-Match to just the latter three places and nowhere
else. This makes object GET and HEAD consistent for any kind of
object, large or small.
This also fixes a bug where various conditional headers (If-*) were
passed through to the object server on segment requests, which could
cause segment requests to fail with a 304 or 412 response. Now only
certain headers are copied to the segment requests, and that doesn't
include the conditional ones, so they can't goof up the segment
retrieval.
Note that I moved SegmentedIterable to swift.common.request_helpers
because it sprouted a transitive dependency on swob, and leaving it in
utils caused a circular import.
Bonus fix: unified the handling of DiskFileQuarantined and
DiskFileNotFound in object server GET and HEAD. Now in either case, a
412 will be returned if the client said "If-Match: *". If not, the
response is a 404, just like before.
Closes-Bug: 1279076
Closes-Bug: 1280022
Closes-Bug: 1280028
Change-Id: Id2ee78346244d516b980202e990aa38ce6812de5
2014-02-12 18:29:12 -08:00
|
|
|
|
|
|
|
def test_match_star(self):
|
|
|
|
# "*" means match anything; see RFC 2616 section 14.24
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'If-None-Match': '*'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 304)
|
|
|
|
self.assertEqual(body, '')
|
Support If-[None-]Match for object HEAD, SLO, and DLO
I moved the checking of If-Match and If-None-Match out of the object
server's GET method and into swob so that everyone can use it. The
interface is similar to the Range handling; make a response with
conditional_response=True, and you get handing of If-Match and
If-None-Match.
Since the only users of conditional_response are object GET, object
HEAD, SLO, and DLO, this has the effect of adding support for If-Match
and If-None-Match to just the latter three places and nowhere
else. This makes object GET and HEAD consistent for any kind of
object, large or small.
This also fixes a bug where various conditional headers (If-*) were
passed through to the object server on segment requests, which could
cause segment requests to fail with a 304 or 412 response. Now only
certain headers are copied to the segment requests, and that doesn't
include the conditional ones, so they can't goof up the segment
retrieval.
Note that I moved SegmentedIterable to swift.common.request_helpers
because it sprouted a transitive dependency on swob, and leaving it in
utils caused a circular import.
Bonus fix: unified the handling of DiskFileQuarantined and
DiskFileNotFound in object server GET and HEAD. Now in either case, a
412 will be returned if the client said "If-Match: *". If not, the
response is a 404, just like before.
Closes-Bug: 1279076
Closes-Bug: 1280022
Closes-Bug: 1280028
Change-Id: Id2ee78346244d516b980202e990aa38ce6812de5
2014-02-12 18:29:12 -08:00
|
|
|
|
|
|
|
|
|
|
|
class TestConditionalIfMatch(unittest.TestCase):
|
|
|
|
def fake_app(self, environ, start_response):
|
|
|
|
start_response('200 OK', [('Etag', 'the-etag')])
|
|
|
|
return ['hi']
|
|
|
|
|
|
|
|
def fake_start_response(*a, **kw):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def test_simple_match(self):
|
|
|
|
# if etag matches, proceed as normal
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'If-Match': 'the-etag'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 200)
|
|
|
|
self.assertEqual(body, 'hi')
|
Support If-[None-]Match for object HEAD, SLO, and DLO
I moved the checking of If-Match and If-None-Match out of the object
server's GET method and into swob so that everyone can use it. The
interface is similar to the Range handling; make a response with
conditional_response=True, and you get handing of If-Match and
If-None-Match.
Since the only users of conditional_response are object GET, object
HEAD, SLO, and DLO, this has the effect of adding support for If-Match
and If-None-Match to just the latter three places and nowhere
else. This makes object GET and HEAD consistent for any kind of
object, large or small.
This also fixes a bug where various conditional headers (If-*) were
passed through to the object server on segment requests, which could
cause segment requests to fail with a 304 or 412 response. Now only
certain headers are copied to the segment requests, and that doesn't
include the conditional ones, so they can't goof up the segment
retrieval.
Note that I moved SegmentedIterable to swift.common.request_helpers
because it sprouted a transitive dependency on swob, and leaving it in
utils caused a circular import.
Bonus fix: unified the handling of DiskFileQuarantined and
DiskFileNotFound in object server GET and HEAD. Now in either case, a
412 will be returned if the client said "If-Match: *". If not, the
response is a 404, just like before.
Closes-Bug: 1279076
Closes-Bug: 1280022
Closes-Bug: 1280028
Change-Id: Id2ee78346244d516b980202e990aa38ce6812de5
2014-02-12 18:29:12 -08:00
|
|
|
|
2014-09-16 18:40:41 -07:00
|
|
|
def test_simple_conditional_etag_match(self):
|
|
|
|
# if etag matches, proceed as normal
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'If-Match': 'not-the-etag'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
resp._conditional_etag = 'not-the-etag'
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 200)
|
|
|
|
self.assertEqual(body, 'hi')
|
2014-09-16 18:40:41 -07:00
|
|
|
|
Support If-[None-]Match for object HEAD, SLO, and DLO
I moved the checking of If-Match and If-None-Match out of the object
server's GET method and into swob so that everyone can use it. The
interface is similar to the Range handling; make a response with
conditional_response=True, and you get handing of If-Match and
If-None-Match.
Since the only users of conditional_response are object GET, object
HEAD, SLO, and DLO, this has the effect of adding support for If-Match
and If-None-Match to just the latter three places and nowhere
else. This makes object GET and HEAD consistent for any kind of
object, large or small.
This also fixes a bug where various conditional headers (If-*) were
passed through to the object server on segment requests, which could
cause segment requests to fail with a 304 or 412 response. Now only
certain headers are copied to the segment requests, and that doesn't
include the conditional ones, so they can't goof up the segment
retrieval.
Note that I moved SegmentedIterable to swift.common.request_helpers
because it sprouted a transitive dependency on swob, and leaving it in
utils caused a circular import.
Bonus fix: unified the handling of DiskFileQuarantined and
DiskFileNotFound in object server GET and HEAD. Now in either case, a
412 will be returned if the client said "If-Match: *". If not, the
response is a 404, just like before.
Closes-Bug: 1279076
Closes-Bug: 1280022
Closes-Bug: 1280028
Change-Id: Id2ee78346244d516b980202e990aa38ce6812de5
2014-02-12 18:29:12 -08:00
|
|
|
def test_quoted_simple_match(self):
|
|
|
|
# double quotes or not, doesn't matter
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'If-Match': '"the-etag"'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 200)
|
|
|
|
self.assertEqual(body, 'hi')
|
Support If-[None-]Match for object HEAD, SLO, and DLO
I moved the checking of If-Match and If-None-Match out of the object
server's GET method and into swob so that everyone can use it. The
interface is similar to the Range handling; make a response with
conditional_response=True, and you get handing of If-Match and
If-None-Match.
Since the only users of conditional_response are object GET, object
HEAD, SLO, and DLO, this has the effect of adding support for If-Match
and If-None-Match to just the latter three places and nowhere
else. This makes object GET and HEAD consistent for any kind of
object, large or small.
This also fixes a bug where various conditional headers (If-*) were
passed through to the object server on segment requests, which could
cause segment requests to fail with a 304 or 412 response. Now only
certain headers are copied to the segment requests, and that doesn't
include the conditional ones, so they can't goof up the segment
retrieval.
Note that I moved SegmentedIterable to swift.common.request_helpers
because it sprouted a transitive dependency on swob, and leaving it in
utils caused a circular import.
Bonus fix: unified the handling of DiskFileQuarantined and
DiskFileNotFound in object server GET and HEAD. Now in either case, a
412 will be returned if the client said "If-Match: *". If not, the
response is a 404, just like before.
Closes-Bug: 1279076
Closes-Bug: 1280022
Closes-Bug: 1280028
Change-Id: Id2ee78346244d516b980202e990aa38ce6812de5
2014-02-12 18:29:12 -08:00
|
|
|
|
|
|
|
def test_no_match(self):
|
|
|
|
# no match --> 412
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'If-Match': 'not-the-etag'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 412)
|
|
|
|
self.assertEqual(body, '')
|
Support If-[None-]Match for object HEAD, SLO, and DLO
I moved the checking of If-Match and If-None-Match out of the object
server's GET method and into swob so that everyone can use it. The
interface is similar to the Range handling; make a response with
conditional_response=True, and you get handing of If-Match and
If-None-Match.
Since the only users of conditional_response are object GET, object
HEAD, SLO, and DLO, this has the effect of adding support for If-Match
and If-None-Match to just the latter three places and nowhere
else. This makes object GET and HEAD consistent for any kind of
object, large or small.
This also fixes a bug where various conditional headers (If-*) were
passed through to the object server on segment requests, which could
cause segment requests to fail with a 304 or 412 response. Now only
certain headers are copied to the segment requests, and that doesn't
include the conditional ones, so they can't goof up the segment
retrieval.
Note that I moved SegmentedIterable to swift.common.request_helpers
because it sprouted a transitive dependency on swob, and leaving it in
utils caused a circular import.
Bonus fix: unified the handling of DiskFileQuarantined and
DiskFileNotFound in object server GET and HEAD. Now in either case, a
412 will be returned if the client said "If-Match: *". If not, the
response is a 404, just like before.
Closes-Bug: 1279076
Closes-Bug: 1280022
Closes-Bug: 1280028
Change-Id: Id2ee78346244d516b980202e990aa38ce6812de5
2014-02-12 18:29:12 -08:00
|
|
|
|
2014-09-16 18:40:41 -07:00
|
|
|
def test_simple_conditional_etag_no_match(self):
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'If-Match': 'the-etag'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
resp._conditional_etag = 'not-the-etag'
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 412)
|
|
|
|
self.assertEqual(body, '')
|
2014-09-16 18:40:41 -07:00
|
|
|
|
Support If-[None-]Match for object HEAD, SLO, and DLO
I moved the checking of If-Match and If-None-Match out of the object
server's GET method and into swob so that everyone can use it. The
interface is similar to the Range handling; make a response with
conditional_response=True, and you get handing of If-Match and
If-None-Match.
Since the only users of conditional_response are object GET, object
HEAD, SLO, and DLO, this has the effect of adding support for If-Match
and If-None-Match to just the latter three places and nowhere
else. This makes object GET and HEAD consistent for any kind of
object, large or small.
This also fixes a bug where various conditional headers (If-*) were
passed through to the object server on segment requests, which could
cause segment requests to fail with a 304 or 412 response. Now only
certain headers are copied to the segment requests, and that doesn't
include the conditional ones, so they can't goof up the segment
retrieval.
Note that I moved SegmentedIterable to swift.common.request_helpers
because it sprouted a transitive dependency on swob, and leaving it in
utils caused a circular import.
Bonus fix: unified the handling of DiskFileQuarantined and
DiskFileNotFound in object server GET and HEAD. Now in either case, a
412 will be returned if the client said "If-Match: *". If not, the
response is a 404, just like before.
Closes-Bug: 1279076
Closes-Bug: 1280022
Closes-Bug: 1280028
Change-Id: Id2ee78346244d516b980202e990aa38ce6812de5
2014-02-12 18:29:12 -08:00
|
|
|
def test_match_star(self):
|
|
|
|
# "*" means match anything; see RFC 2616 section 14.24
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'If-Match': '*'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 200)
|
|
|
|
self.assertEqual(body, 'hi')
|
Support If-[None-]Match for object HEAD, SLO, and DLO
I moved the checking of If-Match and If-None-Match out of the object
server's GET method and into swob so that everyone can use it. The
interface is similar to the Range handling; make a response with
conditional_response=True, and you get handing of If-Match and
If-None-Match.
Since the only users of conditional_response are object GET, object
HEAD, SLO, and DLO, this has the effect of adding support for If-Match
and If-None-Match to just the latter three places and nowhere
else. This makes object GET and HEAD consistent for any kind of
object, large or small.
This also fixes a bug where various conditional headers (If-*) were
passed through to the object server on segment requests, which could
cause segment requests to fail with a 304 or 412 response. Now only
certain headers are copied to the segment requests, and that doesn't
include the conditional ones, so they can't goof up the segment
retrieval.
Note that I moved SegmentedIterable to swift.common.request_helpers
because it sprouted a transitive dependency on swob, and leaving it in
utils caused a circular import.
Bonus fix: unified the handling of DiskFileQuarantined and
DiskFileNotFound in object server GET and HEAD. Now in either case, a
412 will be returned if the client said "If-Match: *". If not, the
response is a 404, just like before.
Closes-Bug: 1279076
Closes-Bug: 1280022
Closes-Bug: 1280028
Change-Id: Id2ee78346244d516b980202e990aa38ce6812de5
2014-02-12 18:29:12 -08:00
|
|
|
|
|
|
|
def test_match_star_on_404(self):
|
|
|
|
|
|
|
|
def fake_app_404(environ, start_response):
|
|
|
|
start_response('404 Not Found', [])
|
|
|
|
return ['hi']
|
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/', headers={'If-Match': '*'})
|
|
|
|
resp = req.get_response(fake_app_404)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 412)
|
|
|
|
self.assertEqual(body, '')
|
Support If-[None-]Match for object HEAD, SLO, and DLO
I moved the checking of If-Match and If-None-Match out of the object
server's GET method and into swob so that everyone can use it. The
interface is similar to the Range handling; make a response with
conditional_response=True, and you get handing of If-Match and
If-None-Match.
Since the only users of conditional_response are object GET, object
HEAD, SLO, and DLO, this has the effect of adding support for If-Match
and If-None-Match to just the latter three places and nowhere
else. This makes object GET and HEAD consistent for any kind of
object, large or small.
This also fixes a bug where various conditional headers (If-*) were
passed through to the object server on segment requests, which could
cause segment requests to fail with a 304 or 412 response. Now only
certain headers are copied to the segment requests, and that doesn't
include the conditional ones, so they can't goof up the segment
retrieval.
Note that I moved SegmentedIterable to swift.common.request_helpers
because it sprouted a transitive dependency on swob, and leaving it in
utils caused a circular import.
Bonus fix: unified the handling of DiskFileQuarantined and
DiskFileNotFound in object server GET and HEAD. Now in either case, a
412 will be returned if the client said "If-Match: *". If not, the
response is a 404, just like before.
Closes-Bug: 1279076
Closes-Bug: 1280022
Closes-Bug: 1280028
Change-Id: Id2ee78346244d516b980202e990aa38ce6812de5
2014-02-12 18:29:12 -08:00
|
|
|
|
|
|
|
|
2014-03-04 11:52:48 -08:00
|
|
|
class TestConditionalIfModifiedSince(unittest.TestCase):
|
|
|
|
def fake_app(self, environ, start_response):
|
|
|
|
start_response(
|
|
|
|
'200 OK', [('Last-Modified', 'Thu, 27 Feb 2014 03:29:37 GMT')])
|
|
|
|
return ['hi']
|
|
|
|
|
|
|
|
def fake_start_response(*a, **kw):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def test_absent(self):
|
|
|
|
req = swift.common.swob.Request.blank('/')
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 200)
|
|
|
|
self.assertEqual(body, 'hi')
|
2014-03-04 11:52:48 -08:00
|
|
|
|
|
|
|
def test_before(self):
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/',
|
|
|
|
headers={'If-Modified-Since': 'Thu, 27 Feb 2014 03:29:36 GMT'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 200)
|
|
|
|
self.assertEqual(body, 'hi')
|
2014-03-04 11:52:48 -08:00
|
|
|
|
|
|
|
def test_same(self):
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/',
|
|
|
|
headers={'If-Modified-Since': 'Thu, 27 Feb 2014 03:29:37 GMT'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 304)
|
|
|
|
self.assertEqual(body, '')
|
2014-03-04 11:52:48 -08:00
|
|
|
|
|
|
|
def test_greater(self):
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/',
|
|
|
|
headers={'If-Modified-Since': 'Thu, 27 Feb 2014 03:29:38 GMT'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 304)
|
|
|
|
self.assertEqual(body, '')
|
2014-03-04 11:52:48 -08:00
|
|
|
|
|
|
|
def test_out_of_range_is_ignored(self):
|
|
|
|
# All that datetime gives us is a ValueError or OverflowError when
|
|
|
|
# something is out of range (i.e. less than datetime.datetime.min or
|
|
|
|
# greater than datetime.datetime.max). Unfortunately, we can't
|
|
|
|
# distinguish between a date being too old and a date being too new,
|
|
|
|
# so the best we can do is ignore such headers.
|
|
|
|
max_date_list = list(datetime.datetime.max.timetuple())
|
|
|
|
max_date_list[0] += 1 # bump up the year
|
|
|
|
too_big_date_header = time.strftime(
|
|
|
|
"%a, %d %b %Y %H:%M:%S GMT", time.struct_time(max_date_list))
|
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/',
|
|
|
|
headers={'If-Modified-Since': too_big_date_header})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 200)
|
|
|
|
self.assertEqual(body, 'hi')
|
2014-03-04 11:52:48 -08:00
|
|
|
|
|
|
|
|
|
|
|
class TestConditionalIfUnmodifiedSince(unittest.TestCase):
|
|
|
|
def fake_app(self, environ, start_response):
|
|
|
|
start_response(
|
|
|
|
'200 OK', [('Last-Modified', 'Thu, 20 Feb 2014 03:29:37 GMT')])
|
|
|
|
return ['hi']
|
|
|
|
|
|
|
|
def fake_start_response(*a, **kw):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def test_absent(self):
|
|
|
|
req = swift.common.swob.Request.blank('/')
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 200)
|
|
|
|
self.assertEqual(body, 'hi')
|
2014-03-04 11:52:48 -08:00
|
|
|
|
|
|
|
def test_before(self):
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/',
|
|
|
|
headers={'If-Unmodified-Since': 'Thu, 20 Feb 2014 03:29:36 GMT'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 412)
|
|
|
|
self.assertEqual(body, '')
|
2014-03-04 11:52:48 -08:00
|
|
|
|
|
|
|
def test_same(self):
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/',
|
|
|
|
headers={'If-Unmodified-Since': 'Thu, 20 Feb 2014 03:29:37 GMT'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 200)
|
|
|
|
self.assertEqual(body, 'hi')
|
2014-03-04 11:52:48 -08:00
|
|
|
|
|
|
|
def test_greater(self):
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/',
|
|
|
|
headers={'If-Unmodified-Since': 'Thu, 20 Feb 2014 03:29:38 GMT'})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 200)
|
|
|
|
self.assertEqual(body, 'hi')
|
2014-03-04 11:52:48 -08:00
|
|
|
|
|
|
|
def test_out_of_range_is_ignored(self):
|
|
|
|
# All that datetime gives us is a ValueError or OverflowError when
|
|
|
|
# something is out of range (i.e. less than datetime.datetime.min or
|
|
|
|
# greater than datetime.datetime.max). Unfortunately, we can't
|
|
|
|
# distinguish between a date being too old and a date being too new,
|
|
|
|
# so the best we can do is ignore such headers.
|
|
|
|
max_date_list = list(datetime.datetime.max.timetuple())
|
|
|
|
max_date_list[0] += 1 # bump up the year
|
|
|
|
too_big_date_header = time.strftime(
|
|
|
|
"%a, %d %b %Y %H:%M:%S GMT", time.struct_time(max_date_list))
|
|
|
|
|
|
|
|
req = swift.common.swob.Request.blank(
|
|
|
|
'/',
|
|
|
|
headers={'If-Unmodified-Since': too_big_date_header})
|
|
|
|
resp = req.get_response(self.fake_app)
|
|
|
|
resp.conditional_response = True
|
|
|
|
body = ''.join(resp(req.environ, self.fake_start_response))
|
2015-08-05 23:58:14 +05:30
|
|
|
self.assertEqual(resp.status_int, 200)
|
|
|
|
self.assertEqual(body, 'hi')
|
2014-03-04 11:52:48 -08:00
|
|
|
|
|
|
|
|
2012-09-04 14:02:19 -07:00
|
|
|
if __name__ == '__main__':
|
|
|
|
unittest.main()
|