network_tools.py 7.11 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

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

Moul's avatar
Moul committed
34

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

Moul's avatar
Moul committed
54

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

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

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

Moul's avatar
Moul committed
88

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

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

97
    return getinstance
98

99
100
101

@singleton
class EndPoint(object):
102
103
104
    def __init__(self):
        cli_args = Command()
        ep = dict()
Moul's avatar
Moul committed
105
        if cli_args.contains_switches("p"):
106
107
108
109
110
            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
111
        else:
Moul's avatar
Moul committed
112
113
114
115
116
117
            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("]"):
118
            ep["domain"] = ep["domain"][1:-1]
119
        self.ep = ep
120
121
        api = "BMAS" if ep["port"] == "443" else "BASIC_MERKLED_API"
        self.BMA_ENDPOINT = " ".join([api, ep["domain"], ep["port"]])
122
123


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


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

Moul's avatar
Moul committed
155

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

Moul's avatar
Moul committed
166

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

Moul's avatar
Moul committed
173

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

Moul's avatar
Moul committed
186

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

Moul's avatar
Moul committed
203

Moul's avatar
Moul committed
204
205
def best_node(ep, main):
    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
245
    def __init__(self):
        self.head_block = get_request("blockchain/current")