def cache_read(path): """ Reads nodes from the nodes cache file. :return: List of RpcNode objects """ log_msg('Reading \'%s\'' % path) try: f = open(path, 'r') blob = json.loads(f.read()) f.close() except Exception as ex: log_err('Reading \'%s\' failed' % path) return RpcNodeList() if not isinstance(blob, list): return RpcNodeList() nodes = RpcNodeList() for node in blob: dt = dateutil_parse(node.pop('dt')) if (datetime.now() - dt).total_seconds( ) < CONFIG['scan_interval'] and 'address' in node: nodes.append(RpcNode(**node)) log_msg('Loaded %d nodes from \'%s\'' % (len(nodes), path)) return nodes
def __init__(self, dns_provider: DnsProvider, md_address: str = '127.0.0.1', md_port: int = 18081, md_auth: str = 'not:used', md_path: str = 'monerod.exe', md_height_discovery_method: str = 'xmrchain', ban_list_path: str = ''): self.dns_provider = dns_provider self.md_path = md_path self.md_daemon_addr = md_address self.md_daemon_port = md_port self.md_daemon_auth = md_auth if md_height_discovery_method not in ['xmrchain', 'monerod', 'compare', 'moneroblocks']: log_err('bad height_discovery_method option', fatal=True) self.md_height_discovery_method = md_height_discovery_method # default Monero RPC port self._m_rpc_port = 18089 self._blockchain_height = None self.last_mass_scan_time = 0 if not os.path.isfile(PATH_CACHE): log_msg("Auto creating \'%s\'" % PATH_CACHE) f = open(PATH_CACHE, 'a') f.write('[]') f.close() if ban_list_path != '': ban_list = parse_ban_list(ban_list_path) log_msg('Load %d nodes from %s'%(len(ban_list), ban_list_path)) self.ban_list = ban_list else: self.ban_list = [] self.monerod_check()
def cache_read(path): """ Reads nodes from the nodes cache file. :return: List of RpcNode objects """ log_msg('Reading \'%s\'' % path) try: f = open(path, 'r') blob = json.loads(f.read()) f.close() except Exception as ex: log_err('Reading \'%s\' failed' % path) return RpcNodeList() if not isinstance(blob, list): return RpcNodeList() nodes = RpcNodeList() for node in blob: dt = dateutil_parse(node['dt']) if 'address' in node: nodes.append(RpcNode(**node)) log_msg('Loaded %d nodes from \'%s\'' % (len(nodes), path)) return nodes
def __init__(self, **kwargs): super(Cloudflare, self).__init__(**kwargs) self.headers = { 'Content-Type': 'application/json', 'X-Auth-Email': kwargs['api_email'], 'X-Auth-Key': kwargs['api_key'], 'User-Agent': random_user_agent() } self.api_base = 'https://api.cloudflare.com/client/v4/zones' self.zone_id = None # zone_id is required and will be detected via Cloudflare API if not self.zone_id: log_msg('Determining zone_id; looking for \'%s\'' % self.domain_name) result = make_json_request(url=self.api_base, headers=self.headers) try: zones = result.get('result') self.zone_id = next( zone.get('id') for zone in zones if zone.get('name') == self.domain_name) except StopIteration: log_err( 'could not determine zone_id. Is your Cloudflare domain correct?', fatal=True) log_msg('Cloudflare zone_id \'%s\' matched to \'%s\'' % (self.zone_id, self.domain_name))
def get_records(self): max_retries = 5 nodes = RpcNodeList() log_msg('Fetching existing record(s) (%s.%s)' % (self.subdomain_name, self.domain_name)) retries = 0 while (True): try: result = make_json_request( '%s/%s/dns_records/?type=A&name=%s.%s' % (self.api_base, self.zone_id, self.subdomain_name, self.domain_name), headers=self.headers) records = result.get('result') # filter on A records / subdomain for record in records: if record.get('type') != 'A' or record.get( 'name') != self.fulldomain_name: continue node = RpcNode(address=record.get('content'), uid=record.get('id')) nodes.append(node) log_msg('> A %s %s' % (record.get('name'), record.get('content'))) return nodes except Exception as ex: log_err("Cloudflare record fetching failed: %s" % (str(ex))) retries += 1 time.sleep(1) if retries > max_retries: return None
def main(self): # get & set the current blockheight height = self.monerod_get_height( method=self.md_height_discovery_method) if not height or not isinstance(height, int): log_err("Unable to fetch the current blockchain height") return self._blockchain_height = height nodes = RpcNodeList() nodes += RpcNodeList.cache_read(PATH_CACHE) # from `cached_nodes.json` if nodes: nodes = self.scan(nodes, remove_invalid=True) if len(nodes.nodes) <= 2: peers = self.monerod_get_peers() # from monerod nodes += self.scan(peers, remove_invalid=True) if nodes.nodes: nodes.cache_write() nodes.shuffle() inserts = nodes.nodes[:self.dns_provider.max_records] dns_nodes = self.dns_provider.get_records() # insert new records for node in inserts: if node.address not in dns_nodes: self.dns_provider.add_record(node) # remove old records for i, node in enumerate(dns_nodes): if node.address not in inserts: self.dns_provider.delete_record(node)
def __init__(self, dns_provider: DnsProvider, md_address: str = '127.0.0.1', md_port: int = 18081, md_auth: str = 'not:used', md_path: str = 'monerod.exe', md_height_discovery_method: str = 'xmrchain'): self.dns_provider = dns_provider self.md_path = md_path self.md_daemon_addr = md_address self.md_daemon_port = md_port self.md_daemon_auth = md_auth if md_height_discovery_method not in [ 'xmrchain', 'monerod', 'compare', 'moneroblocks' ]: log_err('bad height_discovery_method option', fatal=True) self.md_height_discovery_method = md_height_discovery_method # default Monero RPC port self._m_rpc_port = 18089 self._blockchain_height = None if not os.path.isfile(PATH_CACHE): log_msg("Auto creating \'%s\'" % PATH_CACHE) f = open(PATH_CACHE, 'a') f.write('[]') f.close() self.monerod_check()
def monerod_get_height(self, method='compare'): """ Gets the current top block on the chain :param method: 'monerod' will use only monerod to fetch the height. 'xmrchain' will only use xmrchain. 'both' will query both and compare. :return: """ data = {} xmrchain_height = 0 max_retries = 5 if method == ['compare', 'monerod']: output = self._daemon_command(cmd="print_height") if isinstance(output, str) and output.startswith('Error') or not output: log_err("monerod output: %s" % output) elif isinstance(output, str): data['md_height'] = int(re.sub('[^0-9]', '', output.splitlines()[1])) log_msg('monerod height is %d' % data['md_height']) if method == 'monerod': return data['md_height'] if method in ['compare', 'moneroblocks']: retries = 0 while True: if retries > max_retries: break try: blob = make_json_request('https://moneroblocks.info/api/get_stats/', timeout=5, verify=True) data['moneroblocks'] = blob.get('height') break except Exception as ex: log_msg('Fetching moneroblocks JSON has failed. Retrying.') retries += 1 time.sleep(1) if method in ['compare', 'xmrchain']: retries = 0 while True: if retries > max_retries: break try: blob = make_json_request('https://xmrchain.net/api/networkinfo', timeout=5, verify=True) assert blob.get('status') == 'success' data['xmrchain_height'] = blob.get('data', {}).get('height') assert isinstance(data['xmrchain_height'], int) log_msg('xmrchain height is %d' % data['xmrchain_height']) if method == 'xmrchain': return data['xmrchain_height'] break except Exception as ex: log_msg('Fetching xmrchain JSON has failed. Retrying.') retries += 1 time.sleep(1) continue if data: return max(data.values()) log_err('Unable to obtain blockheight.')
def monerod_check(self): url = 'http://%s:%d' % (self.md_daemon_addr, self.md_daemon_port) try: resp = requests.get(url, timeout=2) assert resp.status_code in [401, 403, 404] assert resp.headers.get('Server', '').startswith('Epee') return True except Exception as ex: log_err("monerod not reachable: %s" % url, fatal=True)
def delete_record(self, node: RpcNode): # Delete DNS Record log_msg('Cloudflare record deletion: %s' % node.address) try: url = '%s/%s/dns_records/%s' % (self.api_base, self.zone_id, node.uid) data = make_json_request(url=url, method='DELETE', headers=self.headers) assert data.get('success') is True return data.get('result') except Exception as ex: log_err("Record (%s) deletion failed: %s" % (node.address, str(ex)))
def add_record(self, node: RpcNode): log_msg('Record insertion: %s' % node.address) try: url = '%s/%s/dns_records' % (self.api_base, self.zone_id) make_json_request(url=url, method='POST', headers=self.headers, json={ 'name': self.subdomain_name, 'content': node.address, 'type': 'A', 'ttl': 120 }) except Exception as ex: log_err("Cloudflare record (%s) insertion failed: %s" % (node.address, str(ex)))
def cache_write(self): """Writes a cache file of valid nodes""" now = datetime.now() data = [] for node in self.nodes: if node.valid: data.append({ 'address': node.address, 'port': node.port, 'dt': now.strftime('%Y-%m-%d %H:%M:%S') }) try: f = open(PATH_CACHE, 'w') f.write(json.dumps(data, indent=4)) f.close() except Exception as ex: log_err('Writing \'%s\' failed' % PATH_CACHE) raise log_msg('Written \'%s\' with %d nodes' % (PATH_CACHE, len(data)))
def _daemon_command(self, cmd: str): if not os.path.exists(self.md_path): log_err("monerod not found in path \'%s\'" % self.md_path) return log_msg("Spawning daemon; executing command \'%s\'" % cmd) # build proc args args = [ '--rpc-bind-ip', self.md_daemon_addr, '--rpc-bind-port', str(self.md_daemon_port), ] if self.md_daemon_auth: args.extend(['--rpc-login', self.md_daemon_auth]) args.append(cmd) try: process = Popen([self.md_path, *args], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, bufsize=1) output, err = process.communicate(timeout=10) if not output: log_err("No output from monerod") return output except Exception as ex: log_err('Could not spawn \'%s %s\': %s' % ( self.md_path, ' '.join(args), str(ex) )) finally: # cleanup process.kill()
def main(self): # get & set the current blockheight height = self.monerod_get_height( method=self.md_height_discovery_method) if not height or not isinstance(height, int): log_err("Unable to fetch the current blockchain height") return self._blockchain_height = height nodes = RpcNodeList() nodes += RpcNodeList.cache_read(PATH_CACHE) # from `cached_nodes.json` if nodes: nodes = self.scan(nodes, remove_invalid=True) now = time.time() this_round_uptime = now - self.last_mass_scan_time if len( nodes.nodes ) <= self.dns_provider.max_records or this_round_uptime > CONFIG[ 'scan_interval']: peers = self.monerod_get_peers() # from monerod nodes += self.scan(peers, remove_invalid=True) self.last_mass_scan_time = now if len(nodes.nodes) > 0: nodes.cache_write() nodes.shuffle() inserts = nodes.nodes[:self.dns_provider.max_records] insert_ips = [] for node in inserts: insert_ips.append(node.address) dns_nodes = self.dns_provider.get_records() if dns_nodes != None: # insert new records for node in inserts: if node.address not in dns_nodes: self.dns_provider.add_record(node) # remove old records for node in dns_nodes: if node.address not in insert_ips: self.dns_provider.delete_record(node) else: log_err('Could not fetch DNS records, skipping this update.') else: log_err('Could not get any valid node, skipping this update.')
import os import subprocess import time from functools import partial from multiprocessing import Pool from subprocess import Popen from datetime import datetime from moneriote import PATH_CACHE, CONFIG from moneriote.dns import DnsProvider from moneriote.rpc import RpcNode, RpcNodeList from moneriote.utils import log_msg, log_err, make_json_request, banner, parse_ban_list if sys.version_info[0] != 3 or sys.version_info[1] < 3.5: log_err("please run with python >= 3.5", fatal=True) try: import requests except ImportError: log_err("please install requests: pip install requests", fatal=True) class Moneriote: def __init__(self, dns_provider: DnsProvider, md_address: str = '127.0.0.1', md_port: int = 18081, md_auth: str = 'not:used', md_path: str = 'monerod.exe', md_height_discovery_method: str = 'xmrchain', ban_list_path: str = ''): self.dns_provider = dns_provider self.md_path = md_path self.md_daemon_addr = md_address
def cli(monerod_path, monerod_address, monerod_port, monerod_auth, blockheight_discovery, dns_provider, domain, subdomain, api_key, api_email, max_records, loop_interval, concurrent_scans, scan_interval, ban_list, from_config): from moneriote import CONFIG from moneriote.moneriote import Moneriote from moneriote.utils import log_err, log_msg, banner, parse_ini banner() if from_config: md, dns, ban = parse_ini(from_config) monerod_path = md['path'] monerod_address = md['address'] monerod_auth = md['auth'] monerod_port = md['port'] api_email = dns['api_email'] api_key = dns['api_key'] domain = dns['domain_name'] subdomain = dns['subdomain_name'] max_records = int(dns['max_records']) dns_provider = dns['provider'] ban_list = ban['ban_list_path'] if not api_email: log_err('Parameter api_email is required', fatal=True) if not api_key: log_err('Parameter api_key is required', fatal=True) if not domain: log_err('Parametre domain is required', fatal=True) CONFIG['concurrent_scans'] = concurrent_scans CONFIG['scan_interval'] = scan_interval if dns_provider == 'cloudflare': from moneriote.dns.cloudflare import Cloudflare dns_provider = Cloudflare(domain_name=domain, subdomain_name=subdomain, api_key=api_key, api_email=api_email, max_records=max_records) elif dns_provider == 'transip': from moneriote.dns.transip import TransIP dns_provider = TransIP(api_email=api_email, api_key=api_key, subdomain_name=subdomain, domain_name=domain, max_records=max_records) else: log_err("Unknown DNS provider \'%s\'" % dns_provider, fatal=True) mon = Moneriote(dns_provider=dns_provider, md_path=monerod_path, md_address=monerod_address, md_port=monerod_port, md_auth=monerod_auth, md_height_discovery_method=blockheight_discovery, ban_list_path=ban_list) while True: mon.main() log_msg('Sleeping for %d seconds' % loop_interval) sleep(loop_interval)