commit 9a2bf20eef93798d385e239c23569900bffc5d02 Author: Anon Date: Tue Jul 19 19:41:40 2022 -0700 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..868859c --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Credentials file +/.credentials.secret + +# PyCharm Editor +/.idea/ + +# PyCharm Virtual Environment +/venv* +/src/__pycache__/ diff --git a/default/.credentials.secret b/default/.credentials.secret new file mode 100644 index 0000000..c88aa50 --- /dev/null +++ b/default/.credentials.secret @@ -0,0 +1,10 @@ +# domain_name: Your domain name +# cf_api_key: This should appear when you create or roll your API +# cf_zone_id: This should appear on the dashboard of your API +# cf_record_id: Looking for the A record -> Use the id (not the zone_id, which will be the same as cf_zone_id + +[CLOUDFLARE] +domain_name = yandere.cc +cf_api_key = +cf_zone_id = +cf_record_id = diff --git a/requierments.txt b/requierments.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/requierments.txt @@ -0,0 +1 @@ +requests diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..0404489 --- /dev/null +++ b/run.sh @@ -0,0 +1,35 @@ +#! /usr/bin/env bash + +# Yandere Lewd Bot, an image posting bot for Pleroma +# Copyright (C) 2022 Anon +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Get the runtime path of the bot +ABS_PATH="$(readlink -f "$0")" +RUN_DIR="$(dirname "$ABS_PATH")" + +# Relative paths to the virtual environment and main.py +VENV='./venv/bin/activate' +ENTRY='./src/main.py' + +# cd into the bot's root path, set up the virtual environment, and run +cd "$RUN_DIR" +[ ! -f "$VENV" ] && echo "Virtual environment not found: ${VENV}" && cd - > /dev/null && exit 1 +source "$VENV" +"$ENTRY" "$@" + +# Cleanup +deactivate +cd - > /dev/null diff --git a/src/main.py b/src/main.py new file mode 100755 index 0000000..958a77c --- /dev/null +++ b/src/main.py @@ -0,0 +1,215 @@ +#! /usr/bin/env python + +# Yandere Lewd Bot, an image posting bot for Pleroma +# Copyright (C) 2022 Anon +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import requests +import json +import argparse +import configparser +import contextlib +import logging +from collections import OrderedDict + + +def get_cf_dns_domain(cf_zone_id): + return "https://api.cloudflare.com/client/v4/zones/{}/dns_records".format( + cf_zone_id + ) + + +def get_cf_dns_domain_record_id(cf_zone_id, cf_record_id): + return "/".join([ + get_cf_dns_domain(cf_zone_id), + cf_record_id + ]) + + +# !! Do not use this !! The global Cloudflair API is dangerous. Keeping this here for posterity and info +# def get_headers_global_api(cf_api_key, cf_email): +# return { +# "X-Auth-Key": cf_api_key, +# "X-Auth-Email": cf_email +# } + + +def get_headers(cf_api_key): + return { + "Authorization": "Bearer {}".format(cf_api_key) + } + + +def get_record_id(cf_zone_id, cf_api_key): + resp = requests.get( + url=get_cf_dns_domain(cf_zone_id), + headers=get_headers(cf_api_key) + ) + failed = resp.status_code != 200 + if not failed: + print(json.dumps(resp.json(), indent=4, sort_keys=True)) + return failed + + +# A hack to prevent pycharm from bitching +def dictionary_split(s, delim='='): + k, v = s.split(delim, 1) + return k, v + + +# Get the current ip address of the user +def get_new_ip(): + with contextlib.suppress(Exception): + a = requests.get("https://1.1.1.1/cdn-cgi/trace").text.split("\n") + ip = dict(dictionary_split(item) for item in a if item != "")["ip"] + return ip + return None + + +def get_old_ip(cf_zone_id, cf_record_id, cf_api_key): + resp = requests.get( + url=get_cf_dns_domain_record_id(cf_zone_id, cf_record_id), + headers=get_headers(cf_api_key) + ) + if resp.status_code == 200: + return resp.json()["result"]["content"] + return None + + +def put_new_ip(domain_name, ip, cf_zone_id, cf_record_id, cf_api_key): + json_payload = { + "type": 'A', + "name": domain_name, + "content": ip, + "proxied": True + } + + resp = requests.put( + url=get_cf_dns_domain_record_id(cf_zone_id, cf_record_id), + json=json_payload, + headers=get_headers(cf_api_key) + ) + success = resp.status_code == 200 + return success + + +def config_parser_has_option(config, profile, option): + # 'DEFAULT' is a special value in ConfigParser + config_name = "" if profile == "DEFAULT" else profile + if config.has_option(config_name, option): + return config[profile][option] + return None + + +def get_default_path(filename): + paths = (".", os.environ["HOME"], "/root", "/") + for path in path: + full_path = os.path.join(path, filename) + if os.file.exists(full_path): + return full_path + return filename + + +def main(): + # Logger - set to logging.INFO for production + logging.basicConfig(level=logging.INFO) + + # String constants + default_secret_file = ".credentials.secret" + default_profile = "CLOUDFLARE" + default_fullpath = get_defualt_path(default_secret_file) + + # Find the default path to crdentials file + + # Argument Parser + parser = argparse.ArgumentParser( + description="A utility for updating DNS records for dynamic IP addresses", + epilog="Configuration files should be owned by root with an octal permission of 600 (read/write by root only). Run this program as sudo.", + add_help=True + ) + parser.add_argument("-i", "--get-record-ids", help="Print out (in JSON format) all records for your Cloudflair account", action="store_true") + parser.add_argument("-c", "--config", help="Specify configuration file (DEFAULT: '{}')".format(default_fullpath), default=default_fullpath) + parser.add_argument("-p", "--profile", help="Specify profile in configuration file (DEFAULT: '{}')".format(default_profile), default=default_profile) + parser.add_argument("-s", "--set", help="Set ip address to ip specified") + parser.add_argument("-g", "--get", help="Get old (from Cloudflare DNS records) and current ip address", action="store_true") + arguments = parser.parse_args() + + # Configuration file + config = configparser.ConfigParser() + config.read(arguments.config) + + options = OrderedDict([ + ("domain_name", config_parser_has_option(config, arguments.profile, "domain_name")), + ("cf_api_key", config_parser_has_option(config, arguments.profile, "cf_api_key")), + ("cf_zone_id", config_parser_has_option(config, arguments.profile, "cf_zone_id")), + ("cf_record_id", config_parser_has_option(config, arguments.profile, "cf_record_id")) + ]) + + # Validate configuration file + missing_parameter = next((i for i in options if options[i] is None and i != "cf_record_id"), None) + if missing_parameter is not None: + logging.error("Configuration: {} | Profile: {} | Option: {}".format( + arguments.config, arguments.profile, missing_parameter + )) + return 1 + + # If cf_zone_id is None, print out all record ids. The user will have to update this setting in their config file + # This will have to be done at least once + if arguments.get_record_ids or options["cf_record_id"] is None: + return int(get_record_id(options["cf_zone_id"], options["cf_api_key"])) + + # Get the current dns record from cloudflair (old) + old_ip = get_old_ip(options["cf_zone_id"], options["cf_record_id"], options["cf_api_key"]) + + # Get the current ip address of the server (new) + new_ip = None + + if arguments.set: + new_ip = arguments.set + else: + new_ip = get_new_ip() + + if new_ip is None: + logging.error("Could not determine new IP") + return 1 + + # Print beginning of status line + if old_ip is None: + logging.warn("Could not determine old IP") + + # Update cloudflair's dns record if necessary and print out the end of the status line + if arguments.get: + logging.info("Current IP address: {}".format(new_ip)) + elif old_ip == new_ip: + logging.info("New IP matches old IP: {}".format(new_ip)) + else: + if put_new_ip(options["domain_name"], new_ip, options["cf_zone_id"], options["cf_record_id"], options["cf_api_key"]): + logging.info("IP updated to: {} from: {}".format(old_ip, new_ip)) + else: + logging.error("Failed to update to: {} from: {}".format(old_ip, new_ip)) + return 1 + return 0 + + +# Exit codes: +# 0 - Success +# 1 - An error occurred +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as e: + print("Exception!!", e) + sys.exit(1)