Beispiel #1
0
    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()
Beispiel #3
0
    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
Beispiel #4
0
    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))
Beispiel #5
0
    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)))
Beispiel #12
0
    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)