Repository: oduwsdl/ipwb

Issue:
Update to test with go-ipfs 0.7.0
Released September 22, 2020: https://github.com/ipfs/go-ipfs/releases/tag/v0.7.0
Daemon CLI argument not implemented
The -d (daemon address) CLI argument isn't implemented, also stated in the help.

I use an external IPFS daemon with an exposed API behind nginx as a reverse proxy. 
Right now it is not possible to use an external Daemon.

Discussion:

Patch:
diff --git a/ipwb/__main__.py b/ipwb/__main__.py
index 8420152..bf8ed43 100644
--- a/ipwb/__main__.py
+++ b/ipwb/__main__.py
@@ -5,10 +5,12 @@ import random  # For generating a temp file for stdin
 import sys
 import tempfile
 
+from multiaddr import Multiaddr
+from multiaddr import exceptions as multiaddr_exceptions
 # ipwb modules
 from ipwb import settings, replay, indexer, util
 from ipwb.error_handler import exception_logger
-from .__init__ import __version__ as ipwb_version
+from ipwb.__init__ import __version__ as ipwb_version
 
 
 @exception_logger(catch=not settings.DEBUG)
@@ -16,7 +18,16 @@ def main():
     check_args(sys.argv)
 
 
-def check_args_index(args):
+def checkArgs_index(args):
+    # args.daemon_address is always set. Either default or by CLI
+    try:
+        # see if it parses
+        daemon = Multiaddr(args.daemon_address)
+    except multiaddr_exceptions.StringParseError as e:
+        print("Daemon address cannot be parsed")
+        raise e
+    settings.App.set("ipfsapi", str(daemon))
+
     util.check_daemon_is_alive()
 
     enc_key = None
@@ -57,6 +68,13 @@ def check_args_replay(args):
     if hasattr(args, 'proxy') and args.proxy is not None:
         print(f'Proxying to {args.proxy}')
         proxy = args.proxy
+    try:
+        # see if it parses
+        daemon = Multiaddr(args.daemon_address)
+    except multiaddr_exceptions.StringParseError as e:
+        print("Daemon address cannot be parsed")
+        raise e
+    settings.App.set("ipfsapi", str(daemon))
 
     port = replay.IPWBREPLAY_PORT
     if hasattr(args, 'port') and args.port is not None:
@@ -151,7 +169,7 @@ def check_args(args_in):
         '-d', '--daemon',
         help=("Multi-address of IPFS daemon "
               "(default /dns/localhost/tcp/5001/http)"),
-        default=util.IPFSAPI_MUTLIADDRESS,
+        default=settings.App.config("ipfsapi"),
         dest='daemon_address')
     parser.add_argument(
         '-v', '--version', help='Report the version of ipwb', action='version',
diff --git a/ipwb/backends.py b/ipwb/backends.py
index f98f3c5..9935e57 100644
--- a/ipwb/backends.py
+++ b/ipwb/backends.py
@@ -5,7 +5,7 @@ from urllib.parse import urlparse
 import ipfshttpclient
 import requests
 
-from ipwb import util
+from ipwb import util, settings
 
 
 @dataclasses.dataclass(frozen=True)
@@ -35,7 +35,7 @@ def fetch_ipfs_index(path: str) -> Optional[str]:
         return None
 
     try:
-        with ipfshttpclient.connect(util.IPFSAPI_MUTLIADDRESS) as client:
+        with ipfshttpclient.connect(settings.App.config("ipfsapi")) as client:
             return client.cat(path).decode('utf-8')
 
     except ipfshttpclient.exceptions.StatusError as err:
diff --git a/ipwb/replay.py b/ipwb/replay.py
index c82715f..acd39b1 100755
--- a/ipwb/replay.py
+++ b/ipwb/replay.py
@@ -52,6 +52,7 @@ import base64
 
 from werkzeug.routing import BaseConverter
 from .__init__ import __version__ as ipwb_version
+from . import settings
 
 
 from flask import flash
@@ -136,20 +137,23 @@ class UnsupportedIPFSVersions(Exception):
 
 @app.route('/ipfsdaemon/<cmd>')
 def command_daemon(cmd):
+    local_daemon = ipwb_utils.is_localhosty(settings.App.config("ipfsapi"))
+
     if cmd == 'status':
         return generate_daemon_status_button()
     elif cmd == 'version':
         return request_daemon_version_via_http()
-    elif cmd == 'start':
+    elif cmd == 'start' and local_daemon:
         subprocess.Popen(['ipfs', 'daemon'])
         return Response('IPFS daemon starting...')
 
-    elif cmd == 'stop':
+    elif cmd == 'stop' and local_daemon:
         try:
             ipfs_version = ipfs_client().version()['Version']
             if ipwb_utils.compare_versions(ipfs_version, '0.4.10') < 0:
                 raise UnsupportedIPFSVersions()
-            ipfs_client().stop()
+
+            ipfs_client().close()
         except (subprocess.CalledProcessError, UnsupportedIPFSVersions) as _:
             if os.name != 'nt':  # Big hammer
                 subprocess.call(['killall', 'ipfs'])
@@ -657,7 +661,7 @@ def all_exception_handler(error):
 @app.route('/ipwbadmin', strict_slashes=False)
 def show_admin():
     status = {'ipwb_version': ipwb_version,
-              'ipfs_endpoint': ipwb_utils.IPFSAPI_MUTLIADDRESS}
+              'ipfs_endpoint': settings.App.config("ipfsapi")}
     index_file = ipwb_utils.get_ipwb_replay_index_path()
 
     memento_info = calculate_memento_info_in_index(index_file)
@@ -706,7 +710,7 @@ def show_landing_page():
 
 def show_uri(path, datetime=None):
     try:
-        ipwb_utils.check_daemon_is_alive(ipwb_utils.IPFSAPI_MUTLIADDRESS)
+        ipwb_utils.check_daemon_is_alive()
 
     except IPFSDaemonNotAvailable:
         err_str = ('IPFS daemon not running. '
diff --git a/ipwb/settings.py b/ipwb/settings.py
index f9d7123..c003043 100644
--- a/ipwb/settings.py
+++ b/ipwb/settings.py
@@ -2,10 +2,15 @@
 
 import os
 from logging import config as logging_config
+from ipwb import util
 
 # Running in debug mode or not?
 DEBUG = os.environ.get('DEBUG', False)
 
+IPFSAPI_MUTLIADDRESS = '/dns/localhost/tcp/5001/http'
+# or '/dns/{host}/tcp/{port}/http'
+# or '/ip4/{ipaddress}/tcp/{port}/http'
+# or '/ip6/{ipaddress}/tcp/{port}/http
 
 LOGGING = {
     'version': 1,
@@ -33,3 +38,21 @@ LOGGING = {
 
 
 logging_config.dictConfig(LOGGING)
+
+
+class App:
+    __conf = {
+        "ipfsapi": IPFSAPI_MUTLIADDRESS
+    }
+    __setters = ["ipfsapi"]
+
+    @staticmethod
+    def config(name):
+        return App.__conf[name]
+
+    @staticmethod
+    def set(name, value):
+        if name in App.__setters:
+            App.__conf[name] = value
+        else:
+            raise NameError("Name not accepted in set() method")
diff --git a/ipwb/util.py b/ipwb/util.py
index 657075c..02e9cea 100644
--- a/ipwb/util.py
+++ b/ipwb/util.py
@@ -20,6 +20,7 @@ from urllib.error import URLError
 
 import json
 from .__init__ import __version__ as ipwb_version
+from . import settings
 
 from ipfshttpclient.exceptions import ConnectionError, AddressError
 from multiaddr.exceptions import StringParseError
@@ -56,8 +57,9 @@ log.setLevel(logging.ERROR)
 dt_pattern = re.compile(r"^(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?$")
 
 
-def create_ipfs_client(daemonMultiaddr=IPFSAPI_MUTLIADDRESS):
+def create_ipfs_client():
     """Create and return IPFS client."""
+    daemonMultiaddr = settings.App.config("ipfsapi")
     try:
         return ipfshttpclient.Client(daemonMultiaddr)
     except Exception as err:
@@ -65,19 +67,20 @@ def create_ipfs_client(daemonMultiaddr=IPFSAPI_MUTLIADDRESS):
 
 
 @functools.lru_cache()
-def ipfs_client(daemonMultiaddr=IPFSAPI_MUTLIADDRESS):
+def ipfs_client():
     """
     Create and cache IPFS client instance.
 
     Caching is the single difference between this and
     `create_ipfs_client()` above.
     """
-    return create_ipfs_client(daemonMultiaddr)
+    return create_ipfs_client()
 
 
-def check_daemon_is_alive(daemonMultiaddr=IPFSAPI_MUTLIADDRESS):
+def check_daemon_is_alive():
     """Ensure that the IPFS daemon is running via HTTP before proceeding"""
     client = ipfs_client()
+    daemonMultiaddr = settings.App.config("ipfsapi")
 
     try:
         # ConnectionError/AttributeError if IPFS daemon not running
@@ -255,14 +258,16 @@ def write_ipfs_config(json_to_write):
         f.write(json.dumps(json_to_write, indent=4, sort_keys=True))
 
 
-def get_ipfsapi_host_and_port(ipfs_json=None):
-    if not ipfs_json:
-        ipfs_json = read_ipfs_config()
+def get_ipfsapi_host_and_port():
+    daemon_address = settings.App.config("ipfsapi")
+    # format right now is "/dns/localhost/tcp/5001/http"
 
-    (scheme, host, protocol, port) = (
-        ipfs_json['Addresses']['API'][1:].split('/')
-    )
-    return host + ':' + port
+    (scheme, host, protocol, port, protocol2) = daemon_address[1:].split('/')
+    if protocol2 == "https" and port == "443":
+        # if https is used, rely on a 301/302 redirect response
+        return host
+    else:
+        return host + ':' + port
 
 
 def get_ipwb_replay_config(ipfs_json=None):
diff --git a/requirements.txt b/requirements.txt
index bb4c347..a767acf 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,3 +6,4 @@ requests>=2.19.1
 beautifulsoup4>=4.6.3
 six==1.11.0
 surt>=0.3.0
+multiaddr >= 0.0.9


Test patch:
diff --git a/tests/test_daemon.py b/tests/test_daemon.py
new file mode 100644
index 0000000..cea2b5a
--- /dev/null
+++ b/tests/test_daemon.py
@@ -0,0 +1,57 @@
+import pytest
+
+
+from ipwb import __main__
+from multiaddr import exceptions as multiaddr_exceptions
+
+
+class dotdict(dict):
+    """dot.notation access to dictionary attributes"""
+    __getattr__ = dict.get
+    __setattr__ = dict.__setitem__
+    __delattr__ = dict.__delitem__
+
+
+daemon = None
+args = {
+        "c": False,
+        "compressFirst": False,
+        "daemon_address": daemon,
+        "debug": False,
+        "e": False,
+        "outfile": None,
+        "update_check": False,
+        "warc_path": ['../samples/warcs/sample-1.warc.gz']
+    }
+
+
+def test_daemon_wrong_scheme():
+    daemon = "/dnswrong/localhost/tcp/5001/http"
+    args['daemon_address'] = daemon
+    with pytest.raises(multiaddr_exceptions.StringParseError):
+        __main__.checkArgs_index(dotdict(args))
+        __main__.checkArgs_replay(dotdict(args))
+
+
+def test_daemon_wrong_ip():
+    daemon = "/ip4/256.999.478.444/tcp/5001/http"
+    args['daemon_address'] = daemon
+    with pytest.raises(multiaddr_exceptions.StringParseError):
+        __main__.checkArgs_index(dotdict(args))
+        __main__.checkArgs_replay(dotdict(args))
+
+
+def test_daemon_wrong_protocol():
+    daemon = "/dns/localhost/tcp/5001/httpwrong"
+    args['daemon_address'] = daemon
+    with pytest.raises(multiaddr_exceptions.StringParseError):
+        __main__.checkArgs_index(dotdict(args))
+        __main__.checkArgs_replay(dotdict(args))
+
+
+def test_daemon_wrong_format():
+    daemon = "localhost:5001"
+    args['daemon_address'] = daemon
+    with pytest.raises(multiaddr_exceptions.StringParseError):
+        __main__.checkArgs_index(dotdict(args))
+        __main__.checkArgs_replay(dotdict(args))