network_tools.py 6.89 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 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
31
32
from silkaj.constants import (
    G1_DEFAULT_ENDPOINT,
    G1_TEST_DEFAULT_ENDPOINT,
    CONNECTION_TIMEOUT,
)

Moul's avatar
Moul committed
33

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

Moul's avatar
Moul committed
54

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

Moul's avatar
Moul committed
70

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


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

Moul's avatar
Moul committed
103

104
105
106
107
108
109
110
111
112
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


113
114
115
116
117
118
119
def singleton(class_):
    instances = {}

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

121
    return getinstance
122

123
124
125

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


146
147
148
149
150
151
@singleton
class ClientInstance(object):
    def __init__(self):
        self.client = Client(EndPoint().BMA_ENDPOINT)


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

Moul's avatar
Moul committed
177

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

Moul's avatar
Moul committed
188

Moul's avatar
Moul committed
189
def check_ip(address):
Moul's avatar
Moul committed
190
    try:
191
        return ip_address(address).version
Moul's avatar
Moul committed
192
193
194
    except:
        return 0

Moul's avatar
Moul committed
195

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


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

227

228
229
class HeadBlock(object):
    __instance = None
230

231
    def __new__(cls):
232
233
234
        if HeadBlock.__instance is None:
            HeadBlock.__instance = object.__new__(cls)
        return HeadBlock.__instance
235

236
    def __init__(self):
Moul's avatar
Moul committed
237
238
239
240
241
        self.head_block = self.get_head()

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