1 module tagion.gossip.AddressBook;
2 
3 import core.thread : Thread;
4 import std.file : exists;
5 import std.format;
6 import std.path : setExtension;
7 import std.random;
8 import tagion.basic.tagionexceptions;
9 import tagion.crypto.Types : Pubkey;
10 import tagion.dart.DART : DART;
11 import tagion.dart.DARTRim;
12 import tagion.hibon.HiBONFile;
13 import tagion.hibon.HiBONRecord;
14 import tagion.logger.Logger : log;
15 import tagion.utils.Miscellaneous : cutHex;
16 
17 @safe class AddressBookException : TagionException {
18     this(string msg, string file = __FILE__, size_t line = __LINE__) pure {
19         super(msg, file, line);
20     }
21 }
22 
23 /** check function used in the HiBON package */
24 alias check = Check!(AddressBookException);
25 
26 enum lockext = "lock";
27 
28 /**
29  * Lock file
30  * @param filename - file to lock
31  */
32 void lock(string filename) {
33     import std.file : fwrite = write;
34 
35     immutable file_lock = filename.setExtension(lockext);
36     file_lock.fwrite(null);
37 }
38 
39 /**
40  * Unlock file
41  * @param filename - file to unlock
42  */
43 void unlock(string filename) nothrow {
44     import std.file : remove;
45 
46     immutable file_lock = filename.setExtension(lockext);
47     try {
48         file_lock.remove;
49     }
50     catch (Exception e) {
51         // ignore
52     }
53 }
54 
55 /**
56  * Check file is locked or unlocked
57  * @param filename - file to check
58  * @return true if locked
59  */
60 bool locked(string filename) {
61     immutable file_lock = filename.setExtension(lockext);
62     return file_lock.exists;
63 }
64 
65 /** Address book for libp2p */
66 @safe
67 synchronized class AddressBook {
68     import core.time;
69 
70     alias NodeAddresses = NodeAddress[Pubkey];
71     alias NodePair = typeof((() @trusted => (cast(NodeAddresses) addresses).byKeyValue.front)());
72 
73     /** \struct AddressDirectory
74      * Storage for node addresses
75      */
76     static struct AddressDirectory {
77         /* associative array with node addresses 
78          * node address - value, public key - key
79          */
80         NodeAddresses addresses;
81         mixin HiBONRecord;
82     }
83 
84     /** used for lock, unlock file */
85     enum max_count = 3;
86     /** used for lock, unlock file */
87     protected int timeout = 300;
88     /** nodes amount */
89     protected size_t nodes;
90 
91     /**
92      * Set number of active nodes
93      * @param nodes - number of active nodes
94      */
95     void number_of_active_nodes(const size_t nodes) pure nothrow
96     in {
97         debug log.trace("this.nodes %s set to %s", this.nodes, nodes);
98         assert(this.nodes is size_t.init);
99     }
100     do {
101         this.nodes = nodes;
102     }
103 
104     protected {
105         Random rnd;
106     }
107     this() {
108         rnd = shared(Random)(unpredictableSeed);
109     }
110 
111     /** Addresses for node */
112     protected shared(NodeAddresses) addresses;
113 
114     /**
115      * Create associative array with public keys and addresses of nodes
116      * @return addresses of nodes
117      */
118     immutable(NodeAddress[Pubkey]) _data() @trusted {
119         pragma(msg, "fixme(cbr): AddressBook._data This function should be removed when the addressbook has been implemented");
120         NodeAddress[Pubkey] result;
121         foreach (pkey, addr; addresses) {
122             result[pkey] = addr;
123         }
124         return cast(immutable) result;
125     }
126 
127     /**
128      * Overwrite node addresses associative array
129      * @param addrs - array to overwrite
130      */
131     private void overwrite(const(NodeAddress[Pubkey]) addrs) {
132         addresses = null;
133         foreach (pkey, addr; addrs) {
134             addresses[pkey] = addr;
135         }
136     }
137 
138     /**
139      * Load file if it's exist
140      * @param filename - file to load
141      * @param do_unlock - flag for unlock file
142      */
143     void load(string filename, bool do_unlock = true) @trusted {
144         void local_read() @safe {
145             auto dir = filename.fread!AddressDirectory;
146             overwrite(dir.addresses);
147         }
148 
149         if (filename.exists) {
150             int count_down = max_count;
151             while (filename.locked) {
152                 Thread.sleep(timeout.msecs);
153                 count_down--;
154                 check(count_down > 0, format("The bootstrap file is locked. Timeout can't load file %s", filename));
155             }
156             filename.lock;
157             local_read;
158             if (do_unlock) {
159                 filename.unlock;
160             }
161         }
162     }
163 
164     /**
165      * Save addresses to file
166      * @param filename - file to save addresses
167      * @param nonelock - flag tolock file for save operation
168      */
169     void save(string filename, bool nonelock = false) @trusted {
170         void local_write() {
171             AddressDirectory dir;
172             dir.addresses = cast(NodeAddress[Pubkey]) addresses;
173             filename.fwrite(dir);
174         }
175 
176         int count_down = max_count;
177         while (!nonelock && filename.locked) {
178             Thread.sleep(timeout.msecs);
179             count_down--;
180             check(count_down > 0, format("The bootstrap file is locked. Timeout can't save file %s", filename));
181         }
182         filename.lock;
183         local_write;
184         filename.unlock;
185     }
186 
187     /**
188      * Init NodeAddress if public key exist
189      * @param pkey - public key for check
190      * @return initialized node address
191      */
192     immutable(NodeAddress) opIndex(const Pubkey pkey) const pure nothrow {
193         auto addr = pkey in addresses;
194         if (addr) {
195             return cast(immutable)(*addr);
196         }
197         return NodeAddress.init;
198     }
199 
200     /**
201      * Create associative array addresses
202      * @param addr - value
203      * @param pkey - key
204      */
205     void opIndexAssign(const NodeAddress addr, const Pubkey pkey)
206     in ((pkey in addresses) is null, format("Address %s has already been set", pkey.cutHex))
207     do {
208         import std.stdio;
209         import tagion.utils.Miscellaneous : cutHex;
210 
211         addresses[pkey] = addr;
212     }
213 
214     /**
215      * Remove addresses by public key
216      * @param pkey - public key fo remove addresses
217      */
218     void erase(const Pubkey pkey) pure nothrow {
219         addresses.remove(pkey);
220     }
221 
222     /**
223      * Check for a public key in network
224      * @param pkey - public key fo check
225      * @return true if public key exist
226      */
227     bool exists(const Pubkey pkey) const nothrow {
228         return (pkey in addresses) !is null;
229     }
230 
231     /**
232      * Check for an active public key in network
233      * @param pkey - public key fo check
234      * @return true if pkey active
235      */
236     bool isActive(const Pubkey pkey) const pure nothrow {
237         return (pkey in addresses) !is null;
238     }
239 
240     /**
241      * Return active node channels in network
242      * @return active node channels
243      */
244     Pubkey[] activeNodeChannels() @trusted const pure nothrow {
245         auto channels = (cast(NodeAddresses) addresses).keys;
246         return channels;
247     }
248 
249     const(string) getAddress(const Pubkey pkey) const pure nothrow {
250         return addresses[pkey].address;
251     }
252 
253     /**
254      * Return amount of active nodes in network
255      * @return amount of active nodes
256      */
257     size_t numOfActiveNodes() const pure nothrow {
258         return addresses.length;
259     }
260 
261     /**
262      * Return amount of nodes in networt
263      * @return amount of nodes
264      */
265     size_t numOfNodes() const pure nothrow {
266         return addresses.length;
267     }
268 
269     /**
270      * Check that nodes >= 4 and addresses >= nodes
271      * @return true if network ready
272      */
273     bool isReady() const pure nothrow {
274         return (nodes >= 4) && (addresses.length >= nodes);
275     }
276 
277     /**
278      * For random generation node pair
279      * @return node pair
280      */
281     immutable(NodePair) random() @trusted const pure {
282         if (addresses.length) {
283             import std.range : dropExactly;
284 
285             auto _addresses = cast(NodeAddresses) addresses;
286             const random_key_index = uniform(0, addresses.length, cast(Random) rnd);
287             return _addresses.byKeyValue.dropExactly(random_key_index).front;
288         }
289         return NodePair.init;
290     }
291 
292     /**
293      * Select active channel by index
294      * @param index - index to select active channel
295      * @return active channel
296      */
297     const(Pubkey) selectActiveChannel(const size_t index) @trusted const pure {
298         import std.range : dropExactly;
299 
300         auto _addresses = cast(NodeAddresses) addresses;
301         return _addresses.byKey.dropExactly(index).front;
302     }
303 }
304 
305 static shared(AddressBook) addressbook;
306 
307 shared static this() {
308     addressbook = new shared(AddressBook);
309 }
310 
311 /** 
312  * \struct NodeAddress
313  * Struct for node addresses 
314  */
315 @safe
316 @recordType("NNR")
317 struct NodeAddress {
318     enum tcp_token = "/tcp/";
319     enum p2p_token = "/p2p/";
320     enum intrn_token = "/node/";
321     /** node address */
322     string address;
323     /** If true, then struct with node addresses is used as an address
324      * If false, then the local address used 
325      */
326     bool is_marshal;
327     /** node id */
328     string id;
329     /** node port */
330     uint port;
331     /** DART sector */
332     SectorRange sector;
333 
334     mixin HiBONRecord!(
335             q{
336             this(string address) {
337                 pragma(msg, "fixme(pr): addressbook for mode0 should be created instead");
338                 this.address = address;
339             }
340             this(string address, const ulong port_base, bool marshal = false) {
341         import std.string;
342 
343         try {
344             this.address = address;
345             this.is_marshal = marshal;
346             if (!marshal) {
347                 pragma(msg, "fixme(cbr): This code should be done with a regex");
348                 this.id = address[address.lastIndexOf(p2p_token) + 5 .. $];
349                 auto tcpIndex = address.indexOf(tcp_token) + tcp_token.length;
350                 this.port = to!uint(address[tcpIndex .. tcpIndex + 4]);
351 
352                 const node_number = this.port - port_base;
353                 
354                 sector = SectorRange(0, 0);
355                 
356             }
357             else if (address[0..intrn_token.length] != intrn_token) {
358                 import std.json;
359                 auto json = parseJSON(address);
360                 this.id = json["ID"].str;
361                 auto addr = (() @trusted => json["Addrs"].array()[0].str())();
362                 auto tcpIndex = addr.indexOf(tcp_token) + tcp_token.length;
363                 this.port = to!uint(addr[tcpIndex .. tcpIndex + 4]);
364             }
365         }
366         catch (Exception e) {
367             log.fatal(e.msg);
368         }
369     }
370         });
371 
372     /**
373      * Parse node address
374      * @param addr - address to parse
375      * @return parsed address
376      */
377     static string parseAddr(string addr) {
378         import std.string;
379 
380         pragma(msg, "fixme(cbr): change this to a more bust parse (use regex)");
381         string result;
382         const firstpartAddr = addr.indexOf('[') + 1;
383         const secondpartAddr = addr[firstpartAddr .. $].indexOf(' ') + firstpartAddr;
384         const firstpartId = addr.indexOf('{') + 1;
385         const secondpartId = addr.indexOf(':');
386         result = addr[firstpartAddr .. secondpartAddr] ~ p2p_token ~ addr[firstpartId .. secondpartId];
387         return result;
388     }
389 
390     /**
391      * Parse node address to string
392      * @return string address
393      */
394     public string toString() {
395         return address;
396     }
397 }