network_tools.py 6.97 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 click import pass_context
24
from asyncio import sleep
25
from duniterpy.api.client import Client
26
from duniterpy.api.bma import blockchain, network
Moul's avatar
Moul committed
27

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

Moul's avatar
Moul committed
35

36
async def discover_peers(discover):
37
38
39
40
41
42
    """
    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.
    """
43
44
    client = ClientInstance().client
    endpoints = await get_peers_among_leaves(client)
Moul's avatar
Moul committed
45
46
    if discover:
        print("Discovering network")
47
    for i, endpoint in enumerate(endpoints):
Moul's avatar
Moul committed
48
49
        if discover:
            print("{0:.0f}%".format(i / len(endpoints) * 100))
Moul's avatar
Moul committed
50
        if best_endpoint_address(endpoint, False) is None:
51
            endpoints.remove(endpoint)
Moul's avatar
Moul committed
52
        elif discover:
53
            endpoints = await recursive_discovering(endpoints, endpoint)
54
    return endpoints
55

Moul's avatar
Moul committed
56

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

Moul's avatar
Moul committed
72

73
74
75
76
77
78
79
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"]:
80
        await sleep(ASYNC_SLEEP + 0.05)
81
82
83
84
85
        leaf_response = await client(network.peers, leaf=leaf)
        peers.append(leaf_response["leaf"]["value"])
    return parse_endpoints(peers)


Moul's avatar
Moul committed
86
87
88
89
90
91
92
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
93
94
    while i < len(rep):
        if rep[i]["status"] == "UP":
Moul's avatar
Moul committed
95
96
            while j < len(rep[i]["endpoints"]):
                ep = parse_endpoint(rep[i]["endpoints"][j])
Moul's avatar
Moul committed
97
                j += 1
98
99
100
                if ep is None:
                    break
                ep["pubkey"] = rep[i]["pubkey"]
Moul's avatar
Moul committed
101
                endpoints.append(ep)
Moul's avatar
Moul committed
102
103
        i += 1
        j = 0
104
    return endpoints
Moul's avatar
Moul committed
105

Moul's avatar
Moul committed
106

107
108
109
110
111
112
113
114
115
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


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

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

124
    return getinstance
125

126
127
128

@singleton
class EndPoint(object):
129
130
    @pass_context
    def __init__(ctx, self):
131
        ep = dict()
132
133
        peer = ctx.obj["PEER"]
        if peer:
134
135
136
137
            if ":" in peer:
                ep["domain"], ep["port"] = peer.rsplit(":", 1)
            else:
                ep["domain"], ep["port"] = peer, "443"
Moul's avatar
Moul committed
138
        else:
Moul's avatar
Moul committed
139
            ep["domain"], ep["port"] = (
140
                G1_TEST_DEFAULT_ENDPOINT if ctx.obj["GTEST"] else G1_DEFAULT_ENDPOINT
Moul's avatar
Moul committed
141
142
            )
        if ep["domain"].startswith("[") and ep["domain"].endswith("]"):
143
            ep["domain"] = ep["domain"][1:-1]
144
        self.ep = ep
145
146
        api = "BMAS" if ep["port"] == "443" else "BASIC_MERKLED_API"
        self.BMA_ENDPOINT = " ".join([api, ep["domain"], ep["port"]])
147
148


149
150
151
152
153
154
@singleton
class ClientInstance(object):
    def __init__(self):
        self.client = Client(EndPoint().BMA_ENDPOINT)


Moul's avatar
Moul committed
155
156
157
158
159
160
161
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
162
163
        if check_port(sep[-1]):
            ep["port"] = sep[-1]
Moul's avatar
Moul committed
164
165
166
167
168
169
        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
170
171
172
173
174
175
            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)
176
        return ep
177
    else:
178
        return None
Moul's avatar
Moul committed
179

Moul's avatar
Moul committed
180

Moul's avatar
Moul committed
181
182
def endpoint_type(sep, ep):
    typ = check_ip(sep)
Moul's avatar
Moul committed
183
184
185
186
187
188
    if typ == 0:
        ep["domain"] = sep
    elif typ == 4:
        ep["ip4"] = sep
    elif typ == 6:
        ep["ip6"] = sep
189
    return ep
Moul's avatar
Moul committed
190

Moul's avatar
Moul committed
191

Moul's avatar
Moul committed
192
def check_ip(address):
Moul's avatar
Moul committed
193
    try:
194
        return ip_address(address).version
Moul's avatar
Moul committed
195
196
197
    except:
        return 0

Moul's avatar
Moul committed
198

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


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

230

231
232
class HeadBlock(object):
    __instance = None
233

234
    def __new__(cls):
235
236
237
        if HeadBlock.__instance is None:
            HeadBlock.__instance = object.__new__(cls)
        return HeadBlock.__instance
238

239
    def __init__(self):
Moul's avatar
Moul committed
240
241
242
243
244
        self.head_block = self.get_head()

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