diff --git a/SHARING.md b/SHARING.md index 0638eed7..42fd2d55 100644 --- a/SHARING.md +++ b/SHARING.md @@ -323,14 +323,14 @@ Create a share by mapping a collection of an `Owner` to a token. curl -u user:$userpw -d "PathMapped=/user/testcalendar1/" -d "Enabled=True" -d "Hidden=False" http://localhost:5232/.sharing/v1/token/create ApiVersion=1 Status='success' -PathOrToken='v1/VQR7AmsVRi2ZlFj_JwGpFx-ES5Goyku-gP_YkLh1zUw=' +PathOrToken='/.token/v1/VQR7AmsVRi2ZlFj_JwGpFx-ES5Goyku-gP_YkLh1zUw0/' ``` * json->json ```bash curl -u user:$userpw -H "Content-Type: application/json" -d '{ "PathMapped": "/user/testcalendar1/", "Enabled": true, "Hidden": false}' http://localhost:5232/.sharing/v1/token/create -{"ApiVersion": 1, "Status": "success", "PathOrToken": "v1/aMsmGqOsRwSH-2-6tEa8EMr4RMYzMU7WvPmjnp5qDnw="} +{"ApiVersion": 1, "Status": "success", "PathOrToken": "/.token/v1/aMsmGqOsRwSH-2-6tEa8EMr4RMYzMU7WvPmjnp5qDnw0/"} ``` ###### API Hook "(map|bday)/create" diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index 7deefa09..068a336b 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -108,7 +108,7 @@ API_TYPES_V1: dict[str, type] = { "Hidden": bool, "Properties": dict} -TOKEN_PATTERN_V1: str = "(v1/[a-zA-Z0-9_=\\-]{44})" +TOKEN_PATTERN_V1: str = "v1/[a-zA-Z0-9_\\-]{44}" PATH_PATTERN: str = "([a-zA-Z0-9/.\\-]+)" # TODO: extend or find better source @@ -432,7 +432,7 @@ class BaseSharing: if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/sharing/token: check path: %r", path) if path.startswith("/.token/"): - pattern = re.compile('^/\\.token/' + TOKEN_PATTERN_V1 + '$') + pattern = re.compile('^(/\\.token/' + TOKEN_PATTERN_V1 + '/)$') match = pattern.match(path) if not match: if logger.isEnabledFor(logging.DEBUG): @@ -753,7 +753,7 @@ class BaseSharing: return httputils.bad_request("Invalid value for Permissions") elif key == "PathOrToken": if ShareType == "token": - if not re.search('^' + TOKEN_PATTERN_V1 + '$', request_data[key]): + if not re.search('^/.token/' + TOKEN_PATTERN_V1 + '/$', request_data[key]): logger.warning(api_info + ": unsupported " + key) return httputils.bad_request("Invalid value for PathOrToken") else: @@ -928,8 +928,8 @@ class BaseSharing: else: User = user - # v1: create uuid token with 2x 32 bytes = 256 bit - token = "v1/" + str(base64.urlsafe_b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes), 'utf-8') + # v1: create uuid token with 2x 32 bytes = 256 bit with base64 encoding but replace '=' with '0' to avoid any additional encoding issues issues + token = "/.token/v1/" + str(base64.urlsafe_b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes), 'utf-8').replace('=', '0') + "/" if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/" + api_info + ": %r (Permissions=%r token=%r)", PathMapped, Permissions, token) diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index e46ac795..50cba39f 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -703,7 +703,6 @@ class TestSharingApiSanity(BaseTest): form_array: Sequence[str] json_dict: dict - path_token = "/.token/" path_base = "/owner/calendar.ics/" path_base2 = "/owner/calendar2.ics/" @@ -761,10 +760,10 @@ class TestSharingApiSanity(BaseTest): assert "Status='success'" in answer logging.info("\n*** fetch collection using invalid token (without credentials)") - _, headers, answer = self.request("GET", path_token + "v1/invalidtoken", check=401) + _, headers, answer = self.request("GET", "/.token/v1/invalidtoken/", check=401) logging.info("\n*** fetch collection using token (without credentials)") - _, headers, answer = self.request("GET", path_token + token, check=200) + _, headers, answer = self.request("GET", token, check=200) assert "UID:event" in answer logging.info("\n*** disable token (form->text)") @@ -773,7 +772,7 @@ class TestSharingApiSanity(BaseTest): assert "Status='success'" in answer logging.info("\n*** fetch collection using disabled token (without credentials)") - _, headers, answer = self.request("GET", path_token + token, check=401) + _, headers, answer = self.request("GET", token, check=401) logging.info("\n*** enable token (form->text)") form_array = ["PathOrToken=" + token] @@ -781,7 +780,7 @@ class TestSharingApiSanity(BaseTest): assert "Status='success'" in answer logging.info("\n*** fetch collection using token (without credentials)") - _, headers, answer = self.request("GET", path_token + token, check=200) + _, headers, answer = self.request("GET", token, check=200) assert "UID:event" in answer logging.info("\n*** delete token#2 (json->json)") @@ -804,7 +803,7 @@ class TestSharingApiSanity(BaseTest): _, headers, answer = self._sharing_api_form("token", "delete", check=404, login="owner:ownerpw", form_array=form_array) logging.info("\n*** fetch collection using deleted token (without credentials)") - _, headers, answer = self.request("GET", path_token + token, check=401) + _, headers, answer = self.request("GET", token, check=401) def test_sharing_api_map_basic(self) -> None: """share-by-map API basic tests."""