network_tools.py 7.25 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
20
from ipaddress import ip_address
from json import loads
Moul's avatar
Moul committed
21
22
import socket
import urllib.request
23
import logging
24
from sys import exit, stderr
25
from commandlines import Command
26
from duniterpy.api.client import Client
Moul's avatar
Moul committed
27
from duniterpy.api.bma import blockchain
Moul's avatar
Moul committed
28

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

Moul's avatar
Moul committed
35

36
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
    endpoints = parse_endpoints(get_request("network/peers")["peers"])
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))
49
50
        if best_node(endpoint, False) is None:
            endpoints.remove(endpoint)
Moul's avatar
Moul committed
51
        elif discover:
52
            endpoints = recursive_discovering(endpoints, endpoint)
53
    return endpoints
54

Moul's avatar
Moul committed
55

56
def recursive_discovering(endpoints):
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
    news = parse_endpoints(get_request("network/peers")["peers"])
62
    for new in news:
Moul's avatar
Moul committed
63
        if best_node(new, False) is not None and new not in endpoints:
64
65
            endpoints.append(new)
            recursive_discovering(endpoints, new)
66
    return endpoints
Moul's avatar
Moul committed
67

Moul's avatar
Moul committed
68
69
70
71
72
73
74
75

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
76
77
    while i < len(rep):
        if rep[i]["status"] == "UP":
Moul's avatar
Moul committed
78
79
            while j < len(rep[i]["endpoints"]):
                ep = parse_endpoint(rep[i]["endpoints"][j])
Moul's avatar
Moul committed
80
                j += 1
81
82
83
                if ep is None:
                    break
                ep["pubkey"] = rep[i]["pubkey"]
Moul's avatar
Moul committed
84
                endpoints.append(ep)
Moul's avatar
Moul committed
85
86
        i += 1
        j = 0
87
    return endpoints
Moul's avatar
Moul committed
88

Moul's avatar
Moul committed
89

90
91
92
93
94
95
96
def singleton(class_):
    instances = {}

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

98
    return getinstance
99

100
101
102

@singleton
class EndPoint(object):
103
104
105
    def __init__(self):
        cli_args = Command()
        ep = dict()
Moul's avatar
Moul committed
106
        if cli_args.contains_switches("p"):
107
108
109
110
111
            peer = cli_args.get_definition("p")
            if ":" in peer:
                ep["domain"], ep["port"] = peer.rsplit(":", 1)
            else:
                ep["domain"], ep["port"] = peer, "443"
Moul's avatar
Moul committed
112
        else:
Moul's avatar
Moul committed
113
114
115
116
117
118
            ep["domain"], ep["port"] = (
                G1_TEST_DEFAULT_ENDPOINT
                if cli_args.contains_switches("gtest")
                else G1_DEFAULT_ENDPOINT
            )
        if ep["domain"].startswith("[") and ep["domain"].endswith("]"):
119
            ep["domain"] = ep["domain"][1:-1]
120
        self.ep = ep
121
122
        api = "BMAS" if ep["port"] == "443" else "BASIC_MERKLED_API"
        self.BMA_ENDPOINT = " ".join([api, ep["domain"], ep["port"]])
123
124


125
126
127
128
129
130
@singleton
class ClientInstance(object):
    def __init__(self):
        self.client = Client(EndPoint().BMA_ENDPOINT)


Moul's avatar
Moul committed
131
132
133
134
135
136
137
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
138
139
        if check_port(sep[-1]):
            ep["port"] = sep[-1]
Moul's avatar
Moul committed
140
141
142
143
144
145
        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
146
147
148
149
150
151
            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)
152
        return ep
153
    else:
154
        return None
Moul's avatar
Moul committed
155

Moul's avatar
Moul committed
156

Moul's avatar
Moul committed
157
158
def endpoint_type(sep, ep):
    typ = check_ip(sep)
Moul's avatar
Moul committed
159
160
161
162
163
164
    if typ == 0:
        ep["domain"] = sep
    elif typ == 4:
        ep["ip4"] = sep
    elif typ == 6:
        ep["ip6"] = sep
165
    return ep
Moul's avatar
Moul committed
166

Moul's avatar
Moul committed
167

Moul's avatar
Moul committed
168
def check_ip(address):
Moul's avatar
Moul committed
169
    try:
170
        return ip_address(address).version
Moul's avatar
Moul committed
171
172
173
    except:
        return 0

Moul's avatar
Moul committed
174

175
def get_request(path, ep=EndPoint().ep):
Moul's avatar
Moul committed
176
    address = best_node(ep, False)
Moul's avatar
Moul committed
177
178
    if address is None:
        return address
179
    url = "http://" + ep[address] + ":" + ep["port"] + "/" + path
180
181
    if ep["port"] == "443":
        url = "https://" + ep[address] + "/" + path
Moul's avatar
Moul committed
182
    request = urllib.request.Request(url)
183
    response = urllib.request.urlopen(request, timeout=CONNECTION_TIMEOUT)
Moul's avatar
Moul committed
184
    encoding = response.info().get_content_charset("utf8")
185
    return loads(response.read().decode(encoding))
Moul's avatar
Moul committed
186

Moul's avatar
Moul committed
187

188
def post_request(path, postdata, ep=EndPoint().ep):
Moul's avatar
Moul committed
189
    address = best_node(ep, False)
Moul's avatar
Moul committed
190
191
    if address is None:
        return address
Tortue95's avatar
Tortue95 committed
192
193
194
    url = "http://" + ep[address] + ":" + ep["port"] + "/" + path
    if ep["port"] == "443":
        url = "https://" + ep[address] + "/" + path
Moul's avatar
Moul committed
195
    request = urllib.request.Request(url, bytes(postdata, "utf-8"))
Tortue95's avatar
Tortue95 committed
196
    try:
197
        response = urllib.request.urlopen(request, timeout=CONNECTION_TIMEOUT)
Tortue95's avatar
Tortue95 committed
198
    except urllib.error.URLError as e:
199
200
        print(e, file=stderr)
        exit(1)
Moul's avatar
Moul committed
201
    encoding = response.info().get_content_charset("utf8")
202
    return loads(response.read().decode(encoding))
Tortue95's avatar
Tortue95 committed
203

Moul's avatar
Moul committed
204

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


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

236

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

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

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

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