network_tools.py 7.14 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
"""
Copyright  2016-2019 Maël Azimi <m.a@moul.re>

Silkaj is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Silkaj 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 Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
"""

Moul's avatar
Moul committed
18
from __future__ import unicode_literals
19
from ipaddress import ip_address
Moul's avatar
Moul committed
20
import socket
21
import logging
22
from sys import exit, stderr
23
from asyncio import sleep
24
from duniterpy.api.client import Client
25
from duniterpy.api.bma import blockchain, network
Moul's avatar
Moul committed
26

Moul's avatar
Moul committed
27 28 29 30
from silkaj.constants import (
    G1_DEFAULT_ENDPOINT,
    G1_TEST_DEFAULT_ENDPOINT,
    CONNECTION_TIMEOUT,
31
    ASYNC_SLEEP,
Moul's avatar
Moul committed
32 33
)

Moul's avatar
Moul committed
34

35
async def discover_peers(discover):
36 37 38 39 40 41
    """
    From first node, discover his known nodes.
    Remove from know nodes if nodes are down.
    If discover option: scan all network to know all nodes.
        display percentage discovering.
    """
42 43
    client = ClientInstance().client
    endpoints = await get_peers_among_leaves(client)
Moul's avatar
Moul committed
44 45
    if discover:
        print("Discovering network")
46
    for i, endpoint in enumerate(endpoints):
Moul's avatar
Moul committed
47 48
        if discover:
            print("{0:.0f}%".format(i / len(endpoints) * 100))
Moul's avatar
Moul committed
49
        if best_endpoint_address(endpoint, False) is None:
50
            endpoints.remove(endpoint)
Moul's avatar
Moul committed
51
        elif discover:
52
            endpoints = await recursive_discovering(endpoints, endpoint)
53
    return endpoints
54

Moul's avatar
Moul committed
55

56
async def recursive_discovering(endpoints, endpoint):
57 58 59 60
    """
    Discover recursively new nodes.
    If new node found add it and try to found new node from his known nodes.
    """
61 62 63 64
    api = generate_duniterpy_endpoint_format(endpoint)
    sub_client = Client(api)
    news = await get_peers_among_leaves(sub_client)
    await sub_client.close()
65
    for new in news:
Moul's avatar
Moul committed
66
        if best_endpoint_address(new, False) is not None and new not in endpoints:
67
            endpoints.append(new)
68
            await recursive_discovering(endpoints, new)
69
    return endpoints
Moul's avatar
Moul committed
70

Moul's avatar
Moul committed
71

72 73 74 75 76 77 78
async def get_peers_among_leaves(client):
    """
    Browse among leaves of peers to retrieve the other peers’ endpoints
    """
    leaves = await client(network.peers, leaves=True)
    peers = list()
    for leaf in leaves["leaves"]:
79
        await sleep(ASYNC_SLEEP + 0.05)
80 81 82 83 84
        leaf_response = await client(network.peers, leaf=leaf)
        peers.append(leaf_response["leaf"]["value"])
    return parse_endpoints(peers)


Moul's avatar
Moul committed
85 86 87 88 89 90 91
def parse_endpoints(rep):
    """
    endpoints[]{"domain", "ip4", "ip6", "pubkey"}
    list of dictionaries
    reps: raw endpoints
    """
    i, j, endpoints = 0, 0, []
Moul's avatar
Moul committed
92 93
    while i < len(rep):
        if rep[i]["status"] == "UP":
Moul's avatar
Moul committed
94 95
            while j < len(rep[i]["endpoints"]):
                ep = parse_endpoint(rep[i]["endpoints"][j])
Moul's avatar
Moul committed
96
                j += 1
97 98 99
                if ep is None:
                    break
                ep["pubkey"] = rep[i]["pubkey"]
Moul's avatar
Moul committed
100
                endpoints.append(ep)
Moul's avatar
Moul committed
101 102
        i += 1
        j = 0
103
    return endpoints
Moul's avatar
Moul committed
104

Moul's avatar
Moul committed
105

106 107 108 109 110 111 112 113 114
def generate_duniterpy_endpoint_format(ep):
    api = "BASIC_MERKLED_API " if ep["port"] != "443" else "BMAS "
    api += ep.get("domain") + " " if "domain" in ep else ""
    api += ep.get("ip4") + " " if "ip4" in ep else ""
    api += ep.get("ip6") + " " if "ip6" in ep else ""
    api += ep.get("port")
    return api


115 116 117 118 119 120 121
def singleton(class_):
    instances = {}

    def getinstance(*args, **kwargs):
        if class_ not in instances:
            instances[class_] = class_(*args, **kwargs)
        return instances[class_]
122

123
    return getinstance
124

125 126 127

@singleton
class EndPoint(object):
128
    def __init__(self):
129
        ep = dict()
130 131 132 133 134 135 136 137
        try:
            from click.globals import get_current_context

            ctx = get_current_context()
            peer = ctx.obj["PEER"]
            gtest = ctx.obj["GTEST"]
        except (ModuleNotFoundError, RuntimeError):
            peer, gtest = None, None
138
        if peer:
139 140 141 142
            if ":" in peer:
                ep["domain"], ep["port"] = peer.rsplit(":", 1)
            else:
                ep["domain"], ep["port"] = peer, "443"
Moul's avatar
Moul committed
143
        else:
Moul's avatar
Moul committed
144
            ep["domain"], ep["port"] = (
145
                G1_TEST_DEFAULT_ENDPOINT if gtest else G1_DEFAULT_ENDPOINT
Moul's avatar
Moul committed
146 147
            )
        if ep["domain"].startswith("[") and ep["domain"].endswith("]"):
148
            ep["domain"] = ep["domain"][1:-1]
149
        self.ep = ep
150 151
        api = "BMAS" if ep["port"] == "443" else "BASIC_MERKLED_API"
        self.BMA_ENDPOINT = " ".join([api, ep["domain"], ep["port"]])
152 153


154 155 156 157 158 159
@singleton
class ClientInstance(object):
    def __init__(self):
        self.client = Client(EndPoint().BMA_ENDPOINT)


Moul's avatar
Moul committed
160 161 162 163 164 165 166
def parse_endpoint(rep):
    """
    rep: raw endpoint, sep: split endpoint
    domain, ip4 or ip6 could miss on raw endpoint
    """
    ep, sep = {}, rep.split(" ")
    if sep[0] == "BASIC_MERKLED_API":
Moul's avatar
Moul committed
167 168
        if check_port(sep[-1]):
            ep["port"] = sep[-1]
Moul's avatar
Moul committed
169 170 171 172 173 174
        if (
            len(sep) == 5
            and check_ip(sep[1]) == 0
            and check_ip(sep[2]) == 4
            and check_ip(sep[3]) == 6
        ):
Moul's avatar
Moul committed
175 176 177 178 179 180
            ep["domain"], ep["ip4"], ep["ip6"] = sep[1], sep[2], sep[3]
        if len(sep) == 4:
            ep = endpoint_type(sep[1], ep)
            ep = endpoint_type(sep[2], ep)
        if len(sep) == 3:
            ep = endpoint_type(sep[1], ep)
181
        return ep
182
    else:
183
        return None
Moul's avatar
Moul committed
184

Moul's avatar
Moul committed
185

Moul's avatar
Moul committed
186 187
def endpoint_type(sep, ep):
    typ = check_ip(sep)
Moul's avatar
Moul committed
188 189 190 191 192 193
    if typ == 0:
        ep["domain"] = sep
    elif typ == 4:
        ep["ip4"] = sep
    elif typ == 6:
        ep["ip6"] = sep
194
    return ep
Moul's avatar
Moul committed
195

Moul's avatar
Moul committed
196

Moul's avatar
Moul committed
197
def check_ip(address):
Moul's avatar
Moul committed
198
    try:
199
        return ip_address(address).version
Moul's avatar
Moul committed
200 201 202
    except:
        return 0

Moul's avatar
Moul committed
203

Moul's avatar
Moul committed
204
def best_endpoint_address(ep, main):
Moul's avatar
Moul committed
205
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
206
    s.settimeout(CONNECTION_TIMEOUT)
207
    addresses, port = ("domain", "ip6", "ip4"), int(ep["port"])
Moul's avatar
Moul committed
208 209 210 211
    for address in addresses:
        if address in ep:
            try:
                s.connect((ep[address], port))
212
                s.close()
213
                return address
214
            except Exception as e:
Moul's avatar
Moul committed
215 216 217
                logging.debug(
                    "Connection to endpoint %s (%s) failled (%s)" % (ep, address, e)
                )
Moul's avatar
Moul committed
218
    if main:
vincentux's avatar
vincentux committed
219
        print("Wrong node given as argument", file=stderr)
220
        exit(1)
221
    return None
Moul's avatar
Moul committed
222 223


Moul's avatar
Moul committed
224 225 226 227
def check_port(port):
    try:
        port = int(port)
    except:
228
        print("Port must be an integer", file=stderr)
229
        return False
Moul's avatar
Moul committed
230
    if port < 0 or port > 65536:
231
        print("Wrong port number", file=stderr)
232 233
        return False
    return True
234

235

236 237
class HeadBlock(object):
    __instance = None
238

239
    def __new__(cls):
240 241 242
        if HeadBlock.__instance is None:
            HeadBlock.__instance = object.__new__(cls)
        return HeadBlock.__instance
243

244
    def __init__(self):
Moul's avatar
Moul committed
245 246 247 248 249
        self.head_block = self.get_head()

    async def get_head(self):
        client = ClientInstance().client
        return await client(blockchain.current)