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 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 scan(self, nodes: RpcNodeList, remove_invalid=False): """ Start threads checking known nodes to see if they're alive. :param nodes: :param remove_invalid: only return valid nodes when set to True :return: valid nodes """ if len(nodes) == 0: return nodes now = datetime.now() log_msg( 'Scanning %d node(s) on port %d. This can take several minutes. Let it run.' % (len(nodes), self._m_rpc_port)) pool = Pool(processes=CONFIG['concurrent_scans']) nodes = RpcNodeList.from_list( pool.map(partial(RpcNode.is_valid, self._blockchain_height), nodes)) pool.close() pool.join() log_msg('Scanning %d node(s) done after %d seconds, found %d valid' % (len(nodes), (datetime.now() - now).total_seconds(), len(nodes.valid(valid=True)))) if remove_invalid: nodes = nodes.valid(valid=True) 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 __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 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 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 get_records(self): nodes = RpcNodeList() log_msg('Fetching existing record(s) (%s.%s)' % (self.subdomain_name, self.domain_name)) 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
def monerod_get_peers(self): """Gets the last known peers from monerod""" nodes = RpcNodeList() output = self._daemon_command("print_pl") if not output: return nodes regex = r"(gray|white)\s+(\w+)\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d{1,5})" matches = re.finditer(regex, output) for i, match in enumerate(matches): if match.group(1) != 'white': continue address = match.group(3) nodes.append(RpcNode(address=address)) log_msg('Got peers from RPC: %d node(s)' % len(nodes)) return nodes
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 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)