From f646fdf2725eafa84f59499172dba0f12db55205 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 16 Apr 2017 16:47:51 +0200 Subject: [PATCH] [fix] Split checkurl into two functions : availability + booking (#267) * Splitting checkurl into two functions, one to check availability, the other for booking * [fix] move import at file's beginning. * Rename bookurl to registerurl * Set registerurl as a PUT request for the api * urlavailable returns a boolean now * Revert moving import to top of file :/ * Have domain and path as separate arguments * Flagging checkurl as deprecated in the actionmap * Adding unit tests for registerurl and related * Using built-in deprectation mechanism of Moulinette * Using - separator in names + moving url-available to domain * Returning directly a bool in url-available --- data/actionsmap/yunohost.yml | 32 +++++++++++++++ locales/en.json | 3 ++ src/yunohost/app.py | 36 +++++++++++++++++ src/yunohost/domain.py | 57 ++++++++++++++++++++++++++ src/yunohost/tests/test_appurl.py | 67 +++++++++++++++++++++++++++++++ 5 files changed, 195 insertions(+) create mode 100644 src/yunohost/tests/test_appurl.py diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 502d0b7d5..f44647ef5 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -368,6 +368,21 @@ domain: help: Use the fake/staging Let's Encrypt certification authority. The new certificate won't actually be enabled - it is only intended to test the main steps of the procedure. action: store_true + ### domain_url_available() + url-available: + action_help: Check availability of a web path + api: GET /domain/urlavailable + configuration: + authenticate: all + authenticator: ldap-anonymous + arguments: + domain: + help: The domain for the web path (e.g. your.domain.tld) + extra: + pattern: *pattern_domain + path: + help: The path to check (e.g. /coffee) + ### domain_info() # info: @@ -563,6 +578,7 @@ app: checkurl: action_help: Check availability of a web path api: GET /tools/checkurl + deprecated: True configuration: authenticate: all authenticator: ldap-anonymous @@ -573,6 +589,22 @@ app: full: --app help: Write domain & path to app settings for further checks + ### app_register_url() + register-url: + action_help: Book/register a web path for a given app + api: PUT /tools/registerurl + configuration: + authenticate: all + authenticator: ldap-anonymous + arguments: + app: + help: App which will use the web path + domain: + help: The domain on which the app should be registered (e.g. your.domain.tld) + path: + help: The path to be registered (e.g. /coffee) + + ### app_initdb() initdb: action_help: Create database and initialize it with optionnal attached script diff --git a/locales/en.json b/locales/en.json index 09a2422ca..41c3c2098 100644 --- a/locales/en.json +++ b/locales/en.json @@ -4,6 +4,7 @@ "admin_password_change_failed": "Unable to change password", "admin_password_changed": "The administration password has been changed", "app_already_installed": "{app:s} is already installed", + "app_already_installed_cant_change_url": "This app is already installed. The url cannot be changed just by this function. Look into `app changeurl` if it's available.", "app_argument_choice_invalid": "Invalid choice for argument '{name:s}', it must be one of {choices:s}", "app_argument_invalid": "Invalid value for argument '{name:s}': {error:s}", "app_argument_required": "Argument '{name:s}' is required", @@ -13,6 +14,7 @@ "app_install_files_invalid": "Invalid installation files", "app_location_already_used": "An app is already installed in this location", "app_location_install_failed": "Unable to install the app in this location", + "app_location_unavailable": "This url is not available or conflicts with an already installed app", "app_manifest_invalid": "Invalid app manifest", "app_no_upgrade": "No app to upgrade", "app_not_correctly_installed": "{app:s} seems to be incorrectly installed", @@ -119,6 +121,7 @@ "hook_name_unknown": "Unknown hook name '{name:s}'", "installation_complete": "Installation complete", "installation_failed": "Installation failed", + "invalid_url_format": "Invalid URL format", "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "ldap_initialized": "LDAP has been initialized", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 622e5fb77..f3f945d1e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -966,6 +966,42 @@ def app_checkport(port): m18n.n('port_unavailable', port=int(port))) +def app_register_url(auth, app, domain, path): + """ + Book/register a web path for a given app + + Keyword argument: + app -- App which will use the web path + domain -- The domain on which the app should be registered (e.g. your.domain.tld) + path -- The path to be registered (e.g. /coffee) + """ + + # This line can't be moved on top of file, otherwise it creates an infinite + # loop of import with tools.py... + from domain import domain_url_available, _normalize_domain_path + + domain, path = _normalize_domain_path(domain, path) + + # We cannot change the url of an app already installed simply by changing + # the settings... + # FIXME should look into change_url once it's merged + + installed = app in app_list(installed=True, raw=True).keys() + if installed: + settings = _get_app_settings(app) + if "path" in settings.keys() and "domain" in settings.keys(): + raise MoulinetteError(errno.EINVAL, + m18n.n('app_already_installed_cant_change_url')) + + # Check the url is available + if not domain_url_available(auth, domain, path): + raise MoulinetteError(errno.EINVAL, + m18n.n('app_location_unavailable')) + + app_setting(app, 'domain', value=domain) + app_setting(app, 'path', value=path) + + def app_checkurl(auth, url, app=None): """ Check availability of a web path diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 37636229e..e869df6d0 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -275,6 +275,45 @@ def domain_cert_renew(auth, domain_list, force=False, no_checks=False, email=Fal return yunohost.certificate.certificate_renew(auth, domain_list, force, no_checks, email, staging) +def domain_url_available(auth, domain, path): + """ + Check availability of a web path + + Keyword argument: + domain -- The domain for the web path (e.g. your.domain.tld) + path -- The path to check (e.g. /coffee) + """ + + domain, path = _normalize_domain_path(domain, path) + + # Abort if domain is unknown + if domain not in domain_list(auth)['domains']: + raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) + + # This import cannot be put on top of file because it would create a + # recursive import... + from yunohost.app import app_map + + # Fetch apps map + apps_map = app_map(raw=True) + + # Loop through all apps to check if path is taken by one of them + available = True + if domain in apps_map: + # Loop through apps + for p, a in apps_map[domain].items(): + if path == p: + available = False + break + # We also don't want conflicts with other apps starting with + # same name + elif path.startswith(p) or p.startswith(path): + available = False + break + + return available + + def get_public_ip(protocol=4): """Retrieve the public IP address from ip.yunohost.org""" if protocol == 4: @@ -300,3 +339,21 @@ def _get_maindomain(): def _set_maindomain(domain): with open('/etc/yunohost/current_host', 'w') as f: f.write(domain) + + +def _normalize_domain_path(domain, path): + + # We want url to be of the format : + # some.domain.tld/foo + + # Remove http/https prefix if it's there + if domain.startswith("https://"): + domain = domain[len("https://"):] + elif domain.startswith("http://"): + domain = domain[len("http://"):] + + # Remove trailing slashes + domain = domain.rstrip("/") + path = "/" + path.strip("/") + + return domain, path diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py new file mode 100644 index 000000000..dc1dbc29b --- /dev/null +++ b/src/yunohost/tests/test_appurl.py @@ -0,0 +1,67 @@ +import pytest + +from moulinette.core import MoulinetteError, init_authenticator + +from yunohost.app import app_install, app_remove +from yunohost.domain import _get_maindomain, domain_url_available, _normalize_domain_path + +# Instantiate LDAP Authenticator +auth_identifier = ('ldap', 'ldap-anonymous') +auth_parameters = {'uri': 'ldap://localhost:389', 'base_dn': 'dc=yunohost,dc=org'} +auth = init_authenticator(auth_identifier, auth_parameters) + + +# Get main domain +maindomain = _get_maindomain() + + +def setup_function(function): + + try: + app_remove(auth, "register_url_app") + except: + pass + +def teardown_function(function): + + try: + app_remove(auth, "register_url_app") + except: + pass + + +def test_normalize_domain_path(): + + assert _normalize_domain_path("https://yolo.swag/", "macnuggets") == ("yolo.swag", "/macnuggets") + assert _normalize_domain_path("http://yolo.swag", "/macnuggets/") == ("yolo.swag", "/macnuggets") + assert _normalize_domain_path("yolo.swag/", "macnuggets/") == ("yolo.swag", "/macnuggets") + + +def test_urlavailable(): + + # Except the maindomain/macnuggets to be available + assert domain_url_available(auth, maindomain, "/macnuggets") + + # We don't know the domain yolo.swag + with pytest.raises(MoulinetteError): + assert domain_url_available(auth, "yolo.swag", "/macnuggets") + + +def test_registerurl(): + + app_install(auth, "./tests/apps/register_url_app_ynh", + args="domain=%s&path=%s" % (maindomain, "/urlregisterapp")) + + assert not domain_url_available(auth, maindomain, "/urlregisterapp") + + # Try installing at same location + with pytest.raises(MoulinetteError): + app_install(auth, "./tests/apps/register_url_app_ynh", + args="domain=%s&path=%s" % (maindomain, "/urlregisterapp")) + + +def test_registerurl_baddomain(): + + with pytest.raises(MoulinetteError): + app_install(auth, "./tests/apps/register_url_app_ynh", + args="domain=%s&path=%s" % ("yolo.swag", "/urlregisterapp"))