From e5e22ebeba2140b1da2e9d52d6b8ecd92bc75c88 Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Fri, 1 Jun 2018 15:38:10 -0700 Subject: [PATCH] Make symlink work with Unicode account names Also, ensure that the stored symlink headers really *are* url-encoded as we've been assuming. Change-Id: I1f300d69bec43f0deb430294da05a4ec04308040 Related-Bug: 1774238 Closes-Bug: #1821240 --- swift/common/middleware/symlink.py | 13 +++++---- test/functional/test_symlink.py | 36 +++++++++++++++++++----- test/functional/test_versioned_writes.py | 9 +++--- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/swift/common/middleware/symlink.py b/swift/common/middleware/symlink.py index 94e6c8edfe..a0bb56de39 100644 --- a/swift/common/middleware/symlink.py +++ b/swift/common/middleware/symlink.py @@ -161,7 +161,7 @@ from cgi import parse_header from six.moves.urllib.parse import unquote from swift.common.utils import get_logger, register_swift_info, split_path, \ - MD5_OF_EMPTY_STRING, closing_if_possible + MD5_OF_EMPTY_STRING, closing_if_possible, quote from swift.common.constraints import check_account_format from swift.common.wsgi import WSGIContext, make_subrequest from swift.common.request_helpers import get_sys_meta_prefix, \ @@ -208,6 +208,7 @@ def _check_symlink_header(req): req, TGT_OBJ_SYMLINK_HDR, 2, 'X-Symlink-Target header must be of the ' 'form /') + req.headers[TGT_OBJ_SYMLINK_HDR] = quote('%s/%s' % (container, obj)) # Check account format if it exists account = check_account_format( @@ -217,7 +218,9 @@ def _check_symlink_header(req): # Extract request path _junk, req_acc, req_cont, req_obj = req.split_path(4, 4, True) - if not account: + if account: + req.headers[TGT_ACCT_SYMLINK_HDR] = quote(account) + else: account = req_acc # Check if symlink targets the symlink itself or not @@ -378,9 +381,9 @@ class SymlinkObjectContext(WSGIContext): :returns: new request for target path if it's symlink otherwise None """ - version, account, _junk = split_path(req.path, 2, 3, True) + version, account, _junk = req.split_path(2, 3, True) account = self._response_header_value( - TGT_ACCT_SYSMETA_SYMLINK_HDR) or account + TGT_ACCT_SYSMETA_SYMLINK_HDR) or quote(account) target_path = os.path.join( '/', version, account, symlink_target.lstrip('/')) @@ -485,7 +488,7 @@ class SymlinkObjectContext(WSGIContext): if tgt_co: version, account, _junk = req.split_path(2, 3, True) target_acc = self._response_header_value( - TGT_ACCT_SYSMETA_SYMLINK_HDR) or account + TGT_ACCT_SYSMETA_SYMLINK_HDR) or quote(account) location_hdr = os.path.join( '/', version, target_acc, tgt_co) req.environ['swift.leave_relative_location'] = True diff --git a/test/functional/test_symlink.py b/test/functional/test_symlink.py index cfc0031b2c..721b449741 100755 --- a/test/functional/test_symlink.py +++ b/test/functional/test_symlink.py @@ -270,23 +270,45 @@ class TestSymlink(Base): target_obj = 'dealde%2Fl04 011e%204c8df/flash.png' link_obj = uuid4().hex - # Now let's write a new target object and symlink will be able to - # return object + # create target using unnormalized path resp = retry( self._make_request, method='PUT', container=self.env.tgt_cont, obj=target_obj, body=TARGET_BODY) - self.assertEqual(resp.status, 201) + # you can get it using either name + resp = retry( + self._make_request, method='GET', container=self.env.tgt_cont, + obj=target_obj) + self.assertEqual(resp.status, 200) + self.assertEqual(resp.content, TARGET_BODY) + normalized_quoted_obj = 'dealde/l04%20011e%204c8df/flash.png' + self.assertEqual(normalized_quoted_obj, urllib.parse.quote( + urllib.parse.unquote(target_obj))) + resp = retry( + self._make_request, method='GET', container=self.env.tgt_cont, + obj=normalized_quoted_obj) + self.assertEqual(resp.status, 200) + self.assertEqual(resp.content, TARGET_BODY) - # PUT symlink + # create a symlink using the un-normalized target path self._test_put_symlink(link_cont=self.env.link_cont, link_obj=link_obj, tgt_cont=self.env.tgt_cont, tgt_obj=target_obj) - + # and it's normalized self._assertSymlink( self.env.link_cont, link_obj, - expected_content_location="%s/%s" % (self.env.tgt_cont, - target_obj)) + expected_content_location='%s/%s' % ( + self.env.tgt_cont, normalized_quoted_obj)) + + # create a symlink using the normalized target path + self._test_put_symlink(link_cont=self.env.link_cont, link_obj=link_obj, + tgt_cont=self.env.tgt_cont, + tgt_obj=normalized_quoted_obj) + # and it's ALSO normalized + self._assertSymlink( + self.env.link_cont, link_obj, + expected_content_location='%s/%s' % ( + self.env.tgt_cont, normalized_quoted_obj)) def test_symlink_put_head_get(self): link_obj = uuid4().hex diff --git a/test/functional/test_versioned_writes.py b/test/functional/test_versioned_writes.py index 205ba06633..1ae0058401 100644 --- a/test/functional/test_versioned_writes.py +++ b/test/functional/test_versioned_writes.py @@ -18,7 +18,7 @@ from copy import deepcopy import json import time import unittest2 -from six.moves.urllib.parse import quote +from six.moves.urllib.parse import quote, unquote import test.functional as tf @@ -652,7 +652,7 @@ class TestObjectVersioning(Base): tgt_b.write("bbbbb") symlink_name = Utils.create_name() - sym_tgt_header = '%s/%s' % (container.name, tgt_a_name) + sym_tgt_header = quote(unquote('%s/%s' % (container.name, tgt_a_name))) sym_headers_a = {'X-Symlink-Target': sym_tgt_header} symlink = container.file(symlink_name) symlink.write("", hdrs=sym_headers_a) @@ -684,8 +684,9 @@ class TestObjectVersioning(Base): sym_info = symlink.info(parms={'symlink': 'get'}) self.assertEqual("aaaaa", symlink.read()) self.assertEqual(MD5_OF_EMPTY_STRING, sym_info['etag']) - self.assertEqual('%s/%s' % (self.env.container.name, target.name), - sym_info['x_symlink_target']) + self.assertEqual( + quote(unquote('%s/%s' % (self.env.container.name, target.name))), + sym_info['x_symlink_target']) def _setup_symlink(self): target = self.env.container.file('target-object')