From 38f88a687048c8700dc826c11babac9a21df69a9 Mon Sep 17 00:00:00 2001 From: Shubham Nazare Date: Tue, 27 Jun 2023 11:04:04 +0530 Subject: [PATCH] feat: add support for IPFS Signed-off-by: Shubham Nazare --- tuf/adapter/__init__.py | 0 tuf/adapter/adapter.py | 91 +++++++++++++++++++++++++++++++++++++++++ tuf/api/metadata.py | 2 + tuf/ngclient/updater.py | 26 ++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 tuf/adapter/__init__.py create mode 100644 tuf/adapter/adapter.py diff --git a/tuf/adapter/__init__.py b/tuf/adapter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tuf/adapter/adapter.py b/tuf/adapter/adapter.py new file mode 100644 index 0000000000..6c9a15a395 --- /dev/null +++ b/tuf/adapter/adapter.py @@ -0,0 +1,91 @@ +# Copyright 2020, New York University and the TUF contributors +# SPDX-License-Identifier: MIT OR Apache-2.0 + +"""Implementation of content addressable targets""" +import os +import sys +import inspect +from abc import ABC, abstractmethod +from typing import Optional +import requests + +class Adapter(ABC): + """Abstract class for content addressable systems""" + + @staticmethod + @abstractmethod + def scheme_name() -> str: + """Return the scheme of the URI. + + Used to find out the correct adapter of a target file. + """ + raise NotImplementedError + + @abstractmethod + def fetch_target(self, target_dir: str) -> str: + """Download the target file and return the target directory. + + Different adapters have different methods to fetch a file from its ecosystem. + """ + raise NotImplementedError + + @abstractmethod + def find_target_in_local_cache(self, target_dir: str) -> str: + """Check whether the target file exists in the local cache. + + If found, return the location of the file. + """ + raise NotImplementedError + +class IPFS(Adapter): + """Implements Adapter for IPFS targets""" + + ipfs_gateway_url = 'http://127.0.0.1:8081/ipfs/' + scheme = 'ipfs' + + def __init__(self, cid: str): + self.cid = cid + + @staticmethod + def scheme_name() -> str: + return IPFS.scheme + + def fetch_target(self, target_dir: str) -> str: + file_url = self.ipfs_gateway_url + self.cid + response = requests.get(file_url, timeout=5) + if response.status_code == 200: + filepath = self._generate_target_file_path(target_dir) + with open(filepath, 'wb') as file: + file.write(response.content) + + return filepath + + print('Failed to retrieve file:', response.status_code) + return '' + + def find_target_in_local_cache(self, target_dir: str) -> Optional[str]: + filepath = self._generate_target_file_path(target_dir) + if os.path.exists(filepath): + return filepath + + return None + + def _generate_target_file_path(self, target_dir: str) -> str: + # TODO: Add the correct extension to the file name using Content-Type response header. + file_name = self.cid + return os.path.join(target_dir, file_name) + +def get_adapter_class(scheme: str) -> Adapter: + """Return the class of the provided scheme""" + classType = Adapter + subclasses = [] + classes = inspect.getmembers(sys.modules[__name__], inspect.isclass) + for name, obj in classes: + if (obj is not classType) and (classType in inspect.getmro(obj)): + subclasses.append((obj, name)) + + for cls, name in subclasses: + if cls.scheme_name() == scheme: + return cls + + return None \ No newline at end of file diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 4584051223..5a691d074d 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -62,6 +62,7 @@ MetadataSerializer, SignedSerializer, ) +from tuf.adapter import adapter _ROOT = "root" _SNAPSHOT = "snapshot" @@ -1609,6 +1610,7 @@ def __init__( unrecognized_fields = {} self.unrecognized_fields = unrecognized_fields + self.adapter: Optional[adapter.Adapter] = None @property def custom(self) -> Any: diff --git a/tuf/ngclient/updater.py b/tuf/ngclient/updater.py index ca41b2b566..05817df7f9 100644 --- a/tuf/ngclient/updater.py +++ b/tuf/ngclient/updater.py @@ -56,6 +56,7 @@ from tuf.ngclient._internal import requests_fetcher, trusted_metadata_set from tuf.ngclient.config import UpdaterConfig from tuf.ngclient.fetcher import FetcherInterface +from tuf.adapter import adapter logger = logging.getLogger(__name__) @@ -192,6 +193,10 @@ def find_cached_target( ``None`` if file is not found or it is not up to date. """ + if targetinfo.adapter is not None: + filepath = filepath or self.target_dir + return targetinfo.adapter.find_target_in_local_cache(filepath) + if filepath is None: filepath = self._generate_target_file_path(targetinfo) @@ -229,6 +234,10 @@ def download_target( Local path to downloaded file """ + if targetinfo.adapter is not None: + target_dir = filepath or self.target_dir + return targetinfo.adapter.fetch_target(target_dir) + if filepath is None: filepath = self._generate_target_file_path(targetinfo) @@ -444,6 +453,14 @@ def _preorder_depth_first_walk( if target is not None: logger.debug("Found target in current role %s", role_name) + scheme, identifer = _get_scheme_and_identifer(target_filepath) + if scheme is not None or identifer is not None: + cls = adapter.get_adapter_class(scheme) + if cls is None: + logger.debug("Invalid scheme name. No implementation of scheme %s exists", scheme) + return target + target.adapter = cls(identifer) + return target # After preorder check, add current role to set of visited roles. @@ -483,3 +500,12 @@ def _preorder_depth_first_walk( def _ensure_trailing_slash(url: str) -> str: """Return url guaranteed to end in a slash.""" return url if url.endswith("/") else f"{url}/" + +def _get_scheme_and_identifer(uri): + try: + parsed_uri = parse.urlparse(uri) + scheme = parsed_uri.scheme + identifer = parsed_uri.path + return scheme, identifer + except ValueError: + return None, None \ No newline at end of file