From 45a43d187a9c7f6282550949b23d35662267c087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Mond=C3=A9jar=20Rubio?= Date: Tue, 29 Jun 2021 17:55:23 +0200 Subject: [PATCH] Add 'freenom-autorenew' hook --- .bumpversion.cfg | 2 +- .pre-commit-hooks.yaml | 9 +- README.md | 25 ++++- hooks/freenom_autorenew.py | 203 +++++++++++++++++++++++++++++++++++++ setup.cfg | 6 +- 5 files changed, 239 insertions(+), 6 deletions(-) create mode 100644 hooks/freenom_autorenew.py diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 5ad9944..66bb098 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.4.0 +current_version = 1.5.0 [bumpversion:file:setup.cfg] diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 2125e03..3efa1b1 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -9,7 +9,7 @@ pass_filenames: false - id: cloudflare-gh-pages-dns name: cloudflare-gh-pages-dns - entry: cloudflare-gh-pages-dns + entry: cloudflare-gh-pages-dns-hook description: Check that the DNS records of a domain managed by Cloudflare are properly configured to serve a Github Pages site language: python additional_dependencies: @@ -38,6 +38,13 @@ language: python pass_filenames: false always_run: true +- id: freenom-autorenew + name: freenom-autorenew + entry: freenom-autorenew-hook + description: Renews the free domains of your Freenom account + language: python + always_run: true + pass_filenames: false - id: root-editorconfig-required name: root-editorconfig-required entry: root-editorconfig-required-hook diff --git a/README.md b/README.md index cd2a256..3da2a8f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ```yaml - repo: https://github.com/mondeja/pre-commit-hooks - rev: v1.4.0 + rev: v1.5.0 hooks: - id: dev-extras-required - id: root-editorconfig-required @@ -111,6 +111,26 @@ The required DNS records to make it pass are: - `CF_API_KEY`: [Cloudflare API key][cloudflare-apikey-link] of the user that is managing the DNS records of the site using [Cloudflare][cloudflare-link]. +### **`freenom-autorenew`** + +Renews your free [Freenom][freenom-link] domains. + +You must set the environment variables `FREENOM_EMAIL` and `FREENOM_PASSWORD` +to give permissions to this hook for entering in your Freenom account. + +#### Parameters + +- `-domain=DOMAIN`: Domain to renew. This parameter is optional, if you don't + specify it, the hook will renew all of the free domains registered in your + account. +- `-period=DOMAIN`: Period for the new renovation time. This parameter is + optional, if you don't specify it the time will be one year (`12M`). + +#### Environment variables + +- `FREENOM_EMAIL`: Email of your Freenom account. +- `FREENOM_PASSWORD`: Password of your Freenom account. + ### **`root-editorconfig-required`** Check if your repository has an `.editorconfig` file and if this has a `root` @@ -146,8 +166,9 @@ durations... [tests-image]: https://img.shields.io/github/workflow/status/mondeja/pre-commit-hooks/CI?logo=github&label=tests [tests-link]: https://github.com/mondeja/pre-commit-hooks/actions?query=workflow%CI -[setup-py-upgrade-link]: https://github.com/asottile/setup-py-upgrade [cloudflare-link]: https://cloudflare.com [cloudflare-apikey-link]: https://support.cloudflare.com/hc/en-us/articles/200167836-Managing-API-Tokens-and-Keys +[freenom-link]: https://www.freenom.com [gh-pages-link]: https://pages.github.com [pre-commit-po-hooks-link]: https://github.com/mondeja/pre-commit-po-hooks +[setup-py-upgrade-link]: https://github.com/asottile/setup-py-upgrade diff --git a/hooks/freenom_autorenew.py b/hooks/freenom_autorenew.py new file mode 100644 index 0000000..1a67682 --- /dev/null +++ b/hooks/freenom_autorenew.py @@ -0,0 +1,203 @@ +"""Script that auto renews a free Freenom domain.""" + +import argparse +import functools +import os +import re +import sys + +import requests + + +MOZILLA_USER_AGENT = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/79.0.3945.130 Safari/537.36" +) + +LOGIN_URL = "https://my.freenom.com/dologin.php" +DOMAIN_STATUS_URL = "https://my.freenom.com/domains.php?a=renewals" +RENEW_DOMAIN_URL = "https://my.freenom.com/domains.php?submitrenewals=true" + +TOKEN_PTN = re.compile('name="token" value="(.*?)"', re.I) +DOMAIN_INFO_TPN = re.compile( + r"(.*?)[^<]+[^<]+" + r'.*?', + re.I, +) + + +class FreeNom: + """Freenom implementation used to login into Freenom and autorenew free + domains using HTTP requests. + + Extracted from https://github.com/SunYufei/freenom + """ + + def __init__(self, username: str, password: str): + self._u = username + self._p = password + + self._s = requests.Session() + self._s.headers.update( + { + "user-agent": MOZILLA_USER_AGENT, + } + ) + + def _login(self) -> bool: + self._s.headers.update( + { + "content-type": "application/x-www-form-urlencoded", + "referer": "https://my.freenom.com/clientarea.php", + } + ) + r = self._s.post(LOGIN_URL, data={"username": self._u, "password": self._p}) + return r.status_code == 200 + + def renew(self, domain=None, period="12M"): + """Renew the domains of an account.""" + ok = self._login() + if not ok: + sys.stderr.write( + "Failed to login to Freenom.\nPlease, check that you've" + " properly your credentials in 'FREENOM_EMAIL' and" + " 'FREENOM_PASSWORD' environment variables.\n" + ) + return + + self._s.headers.update({"referer": "https://my.freenom.com/clientarea.php"}) + r = self._s.get(DOMAIN_STATUS_URL) + + # page token + match = re.search(TOKEN_PTN, r.text) + if not match: + sys.stderr.write("Failed to get token inside Freenom page\n") + return + token = match.group(1) + + # renew domains + domains = re.findall(DOMAIN_INFO_TPN, r.text) + + for domain_, days, renewal_id in domains: + if domain is not None and domain_ != domain: + continue + + days = int(days) + if days < 14: + self._s.headers.update( + { + "referer": ( + "https://my.freenom.com/domains.php?a=renewdomain" + "&domain={renewal_id}" + ), + "content-type": "application/x-www-form-urlencoded", + } + ) + r = self._s.post( + RENEW_DOMAIN_URL, + data={ + "token": token, + "renewalid": renewal_id, + f"renewalperiod[{renewal_id}]": period, + "paymentmethod": "credit", + }, + ) + if r.text.find("Order Confirmation") != -1: + sys.stdout.write(f"{domain_} -> Successful renew\n") + else: + sys.stderr.write(f"{domain_} -> Error renewing!\n") + sys.stdout.write(f"{domain_} -> {days} days for expiration\n") + + return True + + +def check_freenom_auth(): + authorization = True + + if not os.environ.get("FREENOM_EMAIL"): + sys.stderr.write( + "You must set the environment variable 'FREENOM_EMAIL' with" + " the email used to login into your account.\n" + ) + authorization = False + + if not os.environ.get("FREENOM_PASSWORD"): + sys.stderr.write( + "You must set the environment variable 'FREENOM_PASSWORD' with" + " the password used to login into your account.\n" + ) + authorization = False + + return authorization + + +@functools.lru_cache(maxsize=None) +def freenom_auth_parameters(): + return (os.environ["FREENOM_EMAIL"], os.environ["FREENOM_PASSWORD"]) + + +def autorenew_freenom_domain(domain=None, period="12M", quiet=False): + """Auto renews a free Freenom domain is it's inside the renovation time. + + Parameters + ---------- + + domain : str + Domain to renew. + + period : str, optional + Period for which to renew. As default, the maximum allowed for free domains. + + quiet : bool, optional + If ``True``, don't print messages about what is doing during the process. + """ + if not check_freenom_auth(): + return False + + freenom = FreeNom(*freenom_auth_parameters()) + return freenom.renew(domain=domain, period=period) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("-q", "--quiet", action="store_true", help="Supress output") + parser.add_argument( + "-d", + "-domain", + "--domain", + type=str, + metavar="DOMAIN", + required=False, + default=None, + dest="domain", + help=( + "Freenom domain to renew. By default, all the free domains of your" + " account will be renovated." + ), + ) + parser.add_argument( + "-p", + "-period", + "--period", + type=str, + metavar="PERIOD", + required=False, + default="12M", + dest="period", + help=( + "Period for the renovation. By default, the maximum allowed by" + " Freenom for free domains, 12 months (12M)." + ), + ) + args = parser.parse_args() + + return ( + 0 + if autorenew_freenom_domain( + domain=args.domain, period=args.period, quiet=args.quiet + ) + else 1 + ) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/setup.cfg b/setup.cfg index c6bd184..b07a95d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,11 +1,11 @@ [metadata] name = mondeja_pre_commit_hooks -version = 1.4.0 +version = 1.5.0 description = My own useful pre-commit hooks long_description = file: README.md long_description_content_type = text/markdown url = https://github.com/mondeja/pre-commit-hooks -author = Alvaro Mondejar +author = Alvaro Mondejar Rubio author_email = mondejar1994@gmail.com license = BSD-3-Clause license_file = LICENSE @@ -34,6 +34,7 @@ console_scripts = add-pre-commit-hook = hooks.add_pre_commit_hook:main cloudflare-gh-pages-dns-hook = hooks.cf_gh_pages_dns_records:main dev-extras-required-hook = hooks.dev_extras_required:main + freenom-autorenew-hook = hooks.freenom_autorenew:main nameservers-endswith-hook = hooks.nameservers_endswith:main root-editorconfig-required-hook = hooks.root_editorconfig_required:main wavelint-hook = hooks.wavelint:main @@ -70,6 +71,7 @@ extend-ignore = W503, D103, D104, + D107, D205, D400, D412,